shockwave_fire spell combo can never trigger — un-normalized case in GetComboType #794

Open
opened 2026-07-22 07:19:26 +00:00 by spikerj · 1 comment
Owner

Found by the coverage wave (spikersoft-backend PR #454) while testing ProjectileCollisionSystem.

ProjectileCollisionSystem.GetComboType normalizes every spell pair with string.CompareOrdinal so the ordinally-smaller id always comes first, then matches:

return (first, second) switch
{
    ...
    ("laserBeam", "plasmaBolt") => "laser_plasma",
    ("shockwave", "fireball") => "shockwave_fire",   // ← dead arm
    _ => null
};

Since "fireball" < "shockwave" ordinally, the normalized tuple for that pair is always ("fireball", "shockwave") — the ("shockwave", "fireball") pattern can never match, so the shockwave_fire combo (6m radius, 150 damage per SpellComboResult.GetComboProperties) is unreachable in gameplay.

Fix: flip the arm to ("fireball", "shockwave").

ProjectileCollisionSystemTests.DetectCombos_ShockwaveFireball_CurrentlyNeverCombos pins the current behavior and will fail when this is fixed (then move the pair back into the pairing-table Theory).

Found by the coverage wave (spikersoft-backend PR #454) while testing `ProjectileCollisionSystem`. `ProjectileCollisionSystem.GetComboType` normalizes every spell pair with `string.CompareOrdinal` so the ordinally-smaller id always comes first, then matches: ```csharp return (first, second) switch { ... ("laserBeam", "plasmaBolt") => "laser_plasma", ("shockwave", "fireball") => "shockwave_fire", // ← dead arm _ => null }; ``` Since `"fireball" < "shockwave"` ordinally, the normalized tuple for that pair is always `("fireball", "shockwave")` — the `("shockwave", "fireball")` pattern can never match, so the `shockwave_fire` combo (6m radius, 150 damage per `SpellComboResult.GetComboProperties`) is unreachable in gameplay. Fix: flip the arm to `("fireball", "shockwave")`. `ProjectileCollisionSystemTests.DetectCombos_ShockwaveFireball_CurrentlyNeverCombos` pins the current behavior and will fail when this is fixed (then move the pair back into the pairing-table Theory).
Author
Owner

Audited against origin/masterSTILL BROKEN, and there is a second fix site the ticket doesn't mention.

The named defect is unchanged. SpikerSoft.GameServer/Physics/ProjectileCollisionSystem.cs:518 still matches ("shockwave", "fireball") after the ordinal normalization at :504-506, so the arm is unreachable. Description and line numbers are accurate. This one is liveGetComboType is called at :126.

The addition: there is a second, independent GetComboType. SpikerSoft.GameServer/Zones/DungeonZone.cs:765, called from :744 and :790. The ticket names only the Physics copy.

I did not verify whether the DungeonZone copy carries the same dead arm — flagging it rather than claiming it. But whoever fixes this should check, because a one-line fix to the Physics copy would leave the dungeon path behaving differently from the camp path, and the divergence would be invisible until a player noticed the combo working in one zone and not the other.

That duplication is worth a look on its own terms: two independent implementations of the same combo-resolution logic is how they drift. If they're meant to be identical, extracting one shared implementation while fixing this would prevent the next instance.

Priority context from the same eleven-ticket sweep: this is on a live path, alongside #811 (broken AES on a real network path — the highest severity of the set), #793, #795, #797 and #799. #792, #808 and #840 turned out to be latent with no production caller at all.

Audited against `origin/master` — **STILL BROKEN, and there is a second fix site the ticket doesn't mention.** **The named defect is unchanged.** `SpikerSoft.GameServer/Physics/ProjectileCollisionSystem.cs:518` still matches `("shockwave", "fireball")` *after* the ordinal normalization at `:504-506`, so the arm is unreachable. Description and line numbers are accurate. This one is **live** — `GetComboType` is called at `:126`. **The addition: there is a second, independent `GetComboType`.** `SpikerSoft.GameServer/Zones/DungeonZone.cs:765`, called from `:744` and `:790`. The ticket names only the Physics copy. I did **not** verify whether the DungeonZone copy carries the same dead arm — flagging it rather than claiming it. But whoever fixes this should check, because a one-line fix to the Physics copy would leave the dungeon path behaving differently from the camp path, and the divergence would be invisible until a player noticed the combo working in one zone and not the other. That duplication is worth a look on its own terms: two independent implementations of the same combo-resolution logic is how they drift. If they're meant to be identical, extracting one shared implementation while fixing this would prevent the next instance. **Priority context** from the same eleven-ticket sweep: this is on a live path, alongside **#811** (broken AES on a real network path — the highest severity of the set), **#793**, **#795**, **#797** and **#799**. **#792**, **#808** and **#840** turned out to be latent with no production caller at all.
Sign in to join this conversation.