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, fromAssets/Resources/Character/Soldier.asset. - Starting weapon:
Soldier_GunatAssets/Prefab/Player/Soldier/Soldier_Gun.prefab, GUID52b0d040dc8b2bf478cadb6aea14369c. - Normal projectile:
Soldier_BulletatAssets/Prefab/Player/Soldier/Soldier_Bullet.prefab, GUID5eb121939ac384146ab1f6e36fd83848. - The weapon is ranged and automatic. There is no input-trigger check in
PlayerWeapon.CheckReadyFireWeapon(); firing is driven eachUpdate()when target, range, timer, dash/pause, reload, and ammo gates pass. - Base magazine/capacity is 10.
PlayerWeapon.Start()briefly initializesCurrentAmmoto 1, but completed player setup andSetWeaponInStartAfterSpawned()overwrite it withfinalWeaponAmmo, 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:
secondaryProjectileis 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 triggerCircleCollider2D. - 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 tagUntagged. - Hit acceptance:
OnTriggerEnter2Drequires the other GameObject's tag to beEnemy, requires that exact GameObject not already be in the activation's hit list, requires the damaging window to be active, and ignoresEnemy.IsInvulnerable. maxEnemyHit = 1, so the projectile has one total valid hit and zero conventional pierces. After that hit it stops, disables its collider, triggersHit, 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, soEnemy.TakeDamage()receives the firingPlayeras the knockback source and derives direction from source/target positions. For that reason the schema's vector-shapedknockbackremains[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, GUIDc736b737fb94f074da18f2529d653a95. It is spawned only when the excluded Exploding Bullet ability setsIsExplodingBullet; it is inactive for this handoff.hitEffect:Assets/Prefab/Player/Soldier/Bullet_HitEffect.prefab, GUIDf303d69cf74d3a240ab6d1302112af66. 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
Enemytag 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
isFlip180-degree correction, rotate the muzzle offset with aim, and account for the left-facing Y mirror.