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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user