fix(battle-node): push BattleFinish on Scripted TurnEndFinal so the client doesn't park on the disconnect-checker

Before: when the player declared their final winning turn (TurnEndFinal),
Scripted mode forwarded it to the bot — which fired a useless 3-frame
TurnStart/TurnEnd/Judge burst as if the game were continuing. No
BattleFinish was ever pushed, so the client's
BattleFinishToOpponentDisConnectChecker (NetworkBattleManagerBase.cs:1640
+ BattleFinishToOpponentDisConnectChecker.cs) parked the player on a
"waiting for opponent" dialog for 128 seconds, eventually falling through
to a synthetic OnDisConnectWin. The user could see "opponent defeated"
animations but couldn't proceed to the post-battle screen.

After: Scripted TurnEndFinal pushes BattleFinish with result=LifeLose=102
to the player (matches the RESULT_CODE the client expects per
NetworkBattleReceiver.cs:963-986; client maps LifeLose → "opponent's life
ran out, PLAYER WIN" UI per NetworkBattleManagerBase.cs:1450-1459). Phase
transitions to Terminal so RunAsync's PvP-disconnect cascade doesn't
synthesize a second BattleFinish on top. No bot burst — the game is
over.

Wire reference: prod TK2 capture battle-traffic_tk2_regular.ndjson:273-274
shows server pushing TurnEndFinal followed immediately by BattleFinish
result:102.

BattleResult enum gets the LifeWin=101 / LifeLose=102 values and a
corrected docstring. The pre-existing Lose=0 / Win=1 / Consistency=2
values stay (Retire/Kill flow ships them today and works as "no contest"
end-of-battle), but their docstring no longer claims they're the WS
shape — they were always the HTTP /finish shape, mislabeled.

TurnEnd (regular, not final) keeps the existing forward-to-bot behavior
in Scripted mode — that's a normal turn boundary, not game end.

PvP TurnEndFinal still broadcasts the same TurnEnd+Judge as regular
TurnEnd; the actual game-end BattleFinish push in PvP rides the loser's
Retire/Kill or the disconnect cascade in RunAsync.

177 battle-node tests passing (was 176; +1 covering the new dispatch arm).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-02 17:48:20 -04:00
parent c7e61c6f8d
commit ee23985055
3 changed files with 107 additions and 13 deletions

View File

@@ -220,12 +220,11 @@ public sealed class BattleSession
break;
}
// Regular TurnEnd: continues the game. Scripted forwards to bot for the 3-frame
// burst; PvP broadcasts; Bot stays silent.
case NetworkBattleUri.TurnEnd when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
case NetworkBattleUri.TurnEndFinal when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
if (Type == BattleType.Pvp && BothAfterReady())
{
// Broadcast TurnEnd + Judge to BOTH. Each client's JudgeOperation ->
// ControlTurnStartPlayer advances the active-player state machine.
var turnEndBroadcast = BuildTurnEndBroadcast();
var judgeBroadcast = BuildJudgeBroadcast();
result.Add((from, turnEndBroadcast, false));
@@ -235,10 +234,40 @@ public sealed class BattleSession
}
else if (Type == BattleType.Scripted)
{
// Phase 1 Scripted: forward to bot; bot fires three-frame burst back.
result.Add((other, env, false));
}
// For Bot type, no-op (NoOpBot swallows; client handles its own turn end).
// Bot type: no-op (NoOpBot swallows; client handles its own turn end).
break;
// TurnEndFinal: client signals the player's FINAL turn is over (they hit a
// game-end condition — usually killed opponent's leader). The client computes
// win/loss locally; the server's job is just to push back a wire BattleFinish
// so the client's BattleFinishToOpponentDisConnectChecker doesn't fire.
// Reference: prod TK2 capture battle-traffic_tk2_regular.ndjson:273-274 shows
// TurnEndFinal followed immediately by BattleFinish.
case NetworkBattleUri.TurnEndFinal when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
if (Type == BattleType.Pvp && BothAfterReady())
{
// PvP: same broadcast as regular TurnEnd. The actual game-end push (a
// separate BattleFinish frame to the survivor) is driven by the loser's
// Retire/Kill or the disconnect cascade in RunAsync — not here.
var turnEndBroadcast = BuildTurnEndBroadcast();
var judgeBroadcast = BuildJudgeBroadcast();
result.Add((from, turnEndBroadcast, false));
result.Add((other, turnEndBroadcast, false));
result.Add((from, judgeBroadcast, false));
result.Add((other, judgeBroadcast, false));
}
else if (Type == BattleType.Scripted)
{
// Push BattleFinish with LifeLose=102 — client interprets as "opponent's
// life ran out, player WIN". DON'T forward to bot (no next turn) and DON'T
// re-fire the 3-frame burst — the game is over. Phase → Terminal so the
// RunAsync cascade doesn't synthesize a follow-up BattleFinish.
result.Add((from, BuildBattleFinish(BattleResult.LifeLose), true));
Phase = BattleSessionPhase.Terminal;
}
// Bot type: no-op (rank-battle finish is HTTP-driven via /ai_*/finish).
break;
case NetworkBattleUri.Retire: