Skip to content

Archived on 2026-10-24. The Godot rewrite is cancelled. This handoff is historical evidence, not an active Unity task or verified runtime behavior. See the current documentation.

Phase 5 Soldier Weapon Import Notes

Confirmed base facts

  • Character: SOLDIER, Alex, from Assets/Resources/Character/Soldier.asset.
  • Starting weapon: Soldier_Gun at Assets/Prefab/Player/Soldier/Soldier_Gun.prefab, GUID 52b0d040dc8b2bf478cadb6aea14369c.
  • Normal projectile: Soldier_Bullet at Assets/Prefab/Player/Soldier/Soldier_Bullet.prefab, GUID 5eb121939ac384146ab1f6e36fd83848.
  • The weapon is ranged and automatic. There is no input-trigger check in PlayerWeapon.CheckReadyFireWeapon(); firing is driven each Update() when target, range, timer, dash/pause, reload, and ammo gates pass.
  • Base magazine/capacity is 10. PlayerWeapon.Start() briefly initializes CurrentAmmo to 1, but completed player setup and SetWeaponInStartAfterSpawned() overwrite it with finalWeaponAmmo, which is 10 in the requested base state.
  • Base shot count is one with no spread. The only spread code is the excluded Multishoot ability (two additional shots at -10 and +10 degrees in its ordinary form).
  • There is no active secondary projectile: secondaryProjectile is null on this prefab.

Formulas and timing semantics

With Frenzy inactive and all shop, upgrade, ability, and temporary values at their neutral defaults:

Quantity Unity evidence Compact result
Damage Soldier_Gun.weaponDamage = 10; PlayerModifier.SetupPlayer() copies it to baseDamage; Calculate() multiplies only neutral factors finalDamage = (10 + 0 + 0) * 1 = 10
Normal hit PlayerWeapon.DamageCalculation(false) and neutral Enemy.TakeDamage() resistance 10 - 10 * 0 / 100 = 10
Critical hit Soldier criticalDamage = 50; DamageCalculation(true) treats it as a percent added to base 10 + 10 * 50 / 100 = 15
Critical chance Soldier criticalChance = 1; AttackModify() uses integer Random.Range(0, 100) and rollCrit < finalCritChance exactly 1 outcome out of 100: 1%
Fire interval attackSpeed = 1.5; PlayerModifier.Calculate() stores the reciprocal in finalAttackSpeed; FireWeapon() uses that as the timer duration 1 / 1.5 = 0.6666667 s
Displayed attacks/s Status UI displays 1 / finalAttackSpeed 1.5 attacks/s
Reload reloadTime = 0.4; Calculate() takes its reciprocal; ReloadAmmo() waits that duration 1 / 0.4 = 2.5 s
Projectile velocity projectileSpeed = 25; no bonus and multiplier 1; SpawnAttack() assigns linear velocity 25 * normalized_direction units/s
Range Soldier attackRange = 12; no bonusFindRange finalAttackRange = 12; gate is distance_to_hitpoint - hitpoint_size <= 12

expected_neutral_hit_damage is 10 for the ordinary non-critical hit. The actual first shot is rolled at fire time and can instead be a 15-damage critical hit with the confirmed 1% chance; no deterministic single value can describe both outcomes. The statistical mean would be 10.05, but it is intentionally not placed in the receiving field because Unity does not serialize or directly deliver that averaged value.

Damage and crit are calculated at fire time in PlayerWeapon.AttackModify(), after the pooled projectile is obtained and before it travels. They are not recalculated on collision. The projectile passes the stored Damage, DamageType, and Player into Enemy.TakeDamage().

Aim, origin, and no-target behavior

Player.UpdateEnemyLocations() sorts enemy hitpoints by effective distance: raw player-to-hitpoint distance minus each hitpoint's serialized size. targetPriority = 0 therefore chooses the nearest valid hitpoint. Invulnerable enemies are deprioritized/removed by the target accessors.

PlayerWeapon.HandAimCursor() does have a cursor/last-horizontal fallback when no target exists, so the weapon visual can continue to orient. However, CheckReadyFireWeapon() separately calls GetEnemyByPriority(0) and returns immediately when it gets null. The normal Soldier weapon does not fire at the cursor without a valid enemy.

The initial travel direction is normalized from the weapon prefab root position to the selected hitpoint. The aiming transform's local +X (right) is aligned to that direction. Spawn rotation is the aiming parent's world rotation; velocity uses the separately calculated normalized direction. Because the projectile has isFlip: 1, RotateSprite() then adds 180 degrees to the projectile transform for its visual orientation without changing velocity.

The muzzle's exact default root-local position is (0.684, 0.034): weapon child (0.188, 0) plus BulletSpawnPoint child (0.496, 0.034). At runtime this offset rotates with the aiming parent. When aiming left, the parent local Y scale is -1, so the local Y component is mirrored.

Projectile collision and knockback

  • Physics: dynamic Rigidbody2D, gravity scale 0, discrete collision detection, no damping, and trigger CircleCollider2D.
  • Collider: serialized local radius 0.3 and offset (0, 0). The projectile root scale is uniformly 0.65, so the default effective world radius is 0.195 unless an external parent or runtime scale changes it.
  • Projectile GameObject: Unity layer 0 (Default) and tag Untagged.
  • Hit acceptance: OnTriggerEnter2D requires the other GameObject's tag to be Enemy, requires that exact GameObject not already be in the activation's hit list, requires the damaging window to be active, and ignores Enemy.IsInvulnerable.
  • maxEnemyHit = 1, so the projectile has one total valid hit and zero conventional pierces. After that hit it stops, disables its collider, triggers Hit, and releases 0.45 seconds later rather than releasing in the same frame.
  • Knockback is not a fixed XY vector. The serialized scalar strength is 1 and knockbackFromPlayer = true, so Enemy.TakeDamage() receives the firing Player as the knockback source and derives direction from source/target positions. For that reason the schema's vector-shaped knockback remains [null, null].

There is no simple serialized per-projectile layer mask. Collider include/exclude override bitfields are both zero, while effective contacts also depend on the project-wide Physics2DSettings collision matrix. The exact receiving mask therefore remains null; use the confirmed Enemy-tag rule as the required gameplay filter and manually verify the intended Godot physics layers.

Pooling and reset requirements

The pool key is Soldier_Bullet, derived by PoolManager.GetObjectNameWithoutClone(). PoolManager.Start() creates an empty dictionary and performs no prewarm. The first request lazily registers the prefab and calls Instantiate; later requests also instantiate when the queue is empty. Thus the weapon itself has no direct Instantiate fallback, but the pool manager dynamically instantiates on a missing/empty pool. Prewarm count is 0.

PlayerAttack.Reset() runs on enable and restores its timers, collider, active flags, duplicate-hit list, bonus hit count, and knockback-origin default. ReturnPool() guards duplicate release, clears the list and trail, enqueues/deactivates, and explicitly clears only IsVampiric and IsConcussiveImpact. Position, rotation, velocity, speed, owner, spell amp, damage, and damage type are overwritten for each normal shot.

For Godot, reset all projectile state on checkout/release, especially every ability/status boolean. Unity leaves many optional flags uncleared, which creates a potential stale pooled-state hazard if the same prefab was previously modified by an ability. Also cancel or generation-guard old lifetime timers and zero velocity during release.

Direct references that are not part of the base data import

Soldier_Bullet directly serializes:

  • explosion: Assets/Prefab/Player/Skill/ExplodingBullet.prefab, GUID c736b737fb94f074da18f2529d653a95. It is spawned only when the excluded Exploding Bullet ability sets IsExplodingBullet; it is inactive for this handoff.
  • hitEffect: Assets/Prefab/Player/Soldier/Bullet_HitEffect.prefab, GUID f303d69cf74d3a240ab6d1302112af66. It is pooled after a valid hit, but it is a visual effect and is excluded from the requested data-only package.

No art, audio, VFX, animation, material, or these referenced prefabs are copied into the package.

Unresolved and manual verification

  • No stable serialized weapon id or projectile id exists in the traced assets. The explicit unresolved strings must be replaced only if the Godot project defines its own stable ids.
  • The effective Unity layer collision set should be confirmed in the Unity Physics 2D settings UI before choosing Godot collision layers/masks. The gameplay code's Enemy tag requirement is confirmed.
  • Confirm in a clean base-game run that game-session shop fields are truly zero for the intended new-game profile. The formulas here deliberately impose the request's no-shop-modifier state even though SetupPlayer() reads persistent session modifiers.
  • Confirm a clean run's player setup order if reproducing frame-zero presentation. The stable post-setup ammo is 10, but PlayerWeapon.Start() contains a transient initialization to 1.
  • Godot 2D uses +X as a conventional sprite-forward choice but has a screen-down +Y axis by default. Preserve the source's +X velocity convention, decide whether imported visuals need the Unity isFlip 180-degree correction, rotate the muzzle offset with aim, and account for the left-facing Y mirror.