diff --git a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs
index 0b9e6d6..478d6fc 100644
--- a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs
+++ b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs
@@ -117,6 +117,16 @@ public static class ScriptedLifecycle
public static MsgEnvelope BuildOpponentTurnEnd() =>
EnvelopeForPush(NetworkBattleUri.TurnEnd, new TurnEndBody(TurnState: 0));
+ ///
+ /// Server-pushed Judge frame that follows the opponent's TurnEnd and unblocks the
+ /// client's JudgeOperation → ControlTurnStartPlayer, transitioning to the
+ /// player's next turn. Without this frame the client hangs on "Opponent's turn…" —
+ /// see data_dumps/captures/battle-traffic.ndjson line 14 (client emits its own
+ /// Judge then waits forever).
+ ///
+ public static MsgEnvelope BuildOpponentJudge() =>
+ EnvelopeForPush(NetworkBattleUri.Judge, new JudgeBody(Spin: ScriptedProfiles.OpponentJudgeSpin));
+
private static IReadOnlyList BuildPosIdxList(IReadOnlyList hand)
{
var list = new List(hand.Count);
diff --git a/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs b/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs
index 0958b85..386a474 100644
--- a/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs
+++ b/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs
@@ -51,4 +51,11 @@ internal static class ScriptedProfiles
// display state. v1 doesn't simulate the opponent — once this lands,
// the client sits there indefinitely.
public const int OpponentTurnStartSpin = 100;
+
+ ///
+ /// Server-pushed Judge frame spin value. Prod varies per push (55, 175, 73, ...) — it's
+ /// an animation seed, not a stateful value. Fixed at 100 here for test stability;
+ /// the client's JudgeOperation doesn't read it.
+ ///
+ public const int OpponentJudgeSpin = 100;
}
diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs
index c4c3881..1cd5ed0 100644
--- a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs
+++ b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs
@@ -166,6 +166,19 @@ public class ScriptedLifecycleTests
Assert.That(body.ResultCode, Is.EqualTo(1));
}
+ [Test]
+ public void BuildOpponentJudge_emits_Judge_uri_with_spin_and_default_result_code()
+ {
+ var env = ScriptedLifecycle.BuildOpponentJudge();
+
+ Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.Judge));
+ Assert.That(env.ViewerId, Is.EqualTo(ScriptedLifecycle.FakeOpponentViewerId));
+ Assert.That(env.Cat, Is.EqualTo(EmitCategory.Battle));
+ var body = (JudgeBody)env.Body;
+ Assert.That(body.Spin, Is.EqualTo(ScriptedProfiles.OpponentJudgeSpin));
+ Assert.That(body.ResultCode, Is.EqualTo(1));
+ }
+
private static MatchContext FixtureCtx(IReadOnlyList? deck = null) => new(
SelfDeckCardIds: deck ?? Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(),
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",