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:
@@ -82,6 +82,35 @@ public class BattleSessionDispatchTests
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Scripted_TurnEndFinal_pushes_BattleFinish_LifeLose_to_player_and_terminates()
|
||||
{
|
||||
// Regression for the BattleFinishToOpponentDisConnectChecker softlock — when the
|
||||
// player declares their final winning turn (TurnEndFinal), the server must push a
|
||||
// wire BattleFinish so the client doesn't park on "waiting for opponent" for 128s.
|
||||
// LifeLose=102 = "opponent's life ran out" → client UI shows player WIN.
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.TurnEndFinal));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1),
|
||||
"Scripted TurnEndFinal must push exactly one frame (BattleFinish); no bot 3-frame burst.");
|
||||
Assert.That(routes[0].Target, Is.SameAs(a),
|
||||
"BattleFinish goes to the player who declared the final turn, not the bot.");
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
Assert.That(routes[0].NoStock, Is.True,
|
||||
"BattleFinish is a no-stock control push (matches the Retire/Kill arm).");
|
||||
Assert.That(routes[0].Frame.Body, Is.InstanceOf<SVSim.BattleNode.Protocol.Bodies.BattleFinishBody>());
|
||||
var body = (SVSim.BattleNode.Protocol.Bodies.BattleFinishBody)routes[0].Frame.Body;
|
||||
Assert.That(body.Result, Is.EqualTo(BattleResult.LifeLose),
|
||||
"Wire result must be RESULT_CODE.LifeLose (102); the client maps that to player WIN.");
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal),
|
||||
"Session must transition to Terminal so the RunAsync cascade doesn't synthesize a second BattleFinish.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Handshake_dispatch_reads_per_participant_Phase_not_session_Phase()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user