using SVSim.BattleNode.Protocol; namespace SVSim.BattleNode.Lifecycle; /// /// v1 Path-A scripted opponent. Hand-rolled static frames good enough to land the client on /// the mulligan screen and let them play turn 1. Templates derived from /// data_dumps/captures/battle-traffic_tk2_regular.ndjson. /// public static class ScriptedLifecycle { /// 30 dummy cardIds — repeats of a stable neutral card. public static readonly long DummyCardId = 100011010; public const long FakeOpponentViewerId = 999_999_999L; public static MsgEnvelope BuildMatched(long playerViewerId, long opponentViewerId, string battleId) { var body = new Dictionary { ["selfInfo"] = new Dictionary { ["country_code"] = "KOR", ["userName"] = "Player", ["sleeveId"] = "3000011", ["emblemId"] = "701441011", ["degreeId"] = "300003", ["fieldId"] = 43, ["isOfficial"] = 0, ["oppoId"] = opponentViewerId, ["seed"] = 17548138L, }, ["oppoInfo"] = new Dictionary { ["country_code"] = "JPN", ["userName"] = "Opponent", ["sleeveId"] = "704141010", ["emblemId"] = "400001100", ["degreeId"] = "120027", ["fieldId"] = 5, ["isOfficial"] = 0, ["oppoId"] = playerViewerId, ["seed"] = 17548138L, ["oppoDeckCount"] = 30, }, ["selfDeck"] = BuildDummyDeck(), }; return EnvelopeForPush(NetworkBattleUri.Matched, body, bid: battleId); } public static MsgEnvelope BuildBattleStart(long playerViewerId) { var body = new Dictionary { ["turnState"] = 0, // player goes first ["battleType"] = 11, // TK2 NetworkBattleType ["selfInfo"] = new Dictionary { ["rank"] = "10", ["battlePoint"] = "6270", ["classId"] = "1", ["charaId"] = "1", ["cardMasterName"] = "card_master_node_10015", }, ["oppoInfo"] = new Dictionary { ["rank"] = "1", ["isMasterRank"] = "0", ["battlePoint"] = 0, ["masterPoint"] = "0", ["classId"] = "8", ["charaId"] = "8", ["cardMasterName"] = "card_master_node_10015", }, }; return EnvelopeForPush(NetworkBattleUri.BattleStart, body); } public static MsgEnvelope BuildDeal() { var body = new Dictionary { ["self"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, ["oppo"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, }; return EnvelopeForPush(NetworkBattleUri.Deal, body); } /// /// Initial 3-card hand idxs from . Each position in this array /// is one card; the value is the card's deck idx. /// private static readonly long[] InitialHand = { 1, 2, 3 }; /// /// Compute the player's hand after a mulligan. For every idx in /// that is currently in the hand, replace it with the next unused deck idx (starting at 4, /// since 1..3 were dealt). Positions of kept cards are preserved. /// public static long[] ComputeHandAfterSwap(IReadOnlyList swapIndices) { var hand = (long[])InitialHand.Clone(); var nextDeckIdx = 4L; for (var pos = 0; pos < hand.Length; pos++) { if (swapIndices.Contains(hand[pos])) { hand[pos] = nextDeckIdx++; } } return hand; } public static MsgEnvelope BuildSwapResponse(IReadOnlyList hand) { var body = new Dictionary { ["self"] = BuildPosIdxList(hand), }; return EnvelopeForPush(NetworkBattleUri.Swap, body); } public static MsgEnvelope BuildReady(IReadOnlyList hand) { var body = new Dictionary { ["self"] = BuildPosIdxList(hand), // Opponent hand stays at the static 3 cards for v1. ["oppo"] = BuildPosIdxList(InitialHand), ["idxChangeSeed"] = 771335280, ["spin"] = 243, }; return EnvelopeForPush(NetworkBattleUri.Ready, body); } /// /// Generic TurnStart push used to transition the client into "Opponent's turn…" state /// after the player's TurnEnd. v1 doesn't simulate the opponent — once this lands the /// client sits at the opponent-turn display indefinitely. /// public static MsgEnvelope BuildOpponentTurnStart() { var body = new Dictionary { ["spin"] = 100, }; return EnvelopeForPush(NetworkBattleUri.TurnStart, body); } private static List BuildPosIdxList(IReadOnlyList hand) { var list = new List(hand.Count); for (var pos = 0; pos < hand.Count; pos++) { list.Add(new Dictionary { ["pos"] = pos, ["idx"] = (int)hand[pos] }); } return list; } private static List BuildDummyDeck() { var deck = new List(30); for (var i = 1; i <= 30; i++) { deck.Add(new Dictionary { ["idx"] = i, ["cardId"] = DummyCardId, }); } return deck; } private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, Dictionary body, string? bid = null) { // Synchronize-push routing in the client's OnReceived drops any frame whose // resultCode != Success (1). Absent counts as 0 (None) and is also dropped — so we // MUST include it on every scripted push, not just InitNetwork ack / BattleFinish. // See server-to-client.md §"Routing in OnReceived" and the matching prod captures. body["resultCode"] = (int)ReceiveNodeResultCode.Success; return new MsgEnvelope(uri, ViewerId: FakeOpponentViewerId, Uuid: "node-stub", Bid: bid, Try: 0, Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null, // OutboundSequencer.AssignAndArchive stamps this Body: body); } }