using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; namespace SVSim.BattleNode.Lifecycle; /// /// v1 hand-rolled scripted opponent. Static frame builders for the five lifecycle uris /// (Matched / BattleStart / Deal / Swap response / Ready) plus a trivial opponent TurnStart /// the dispatch pushes after the player's TurnEnd. The values are templated from the TK2 /// captures at data_dumps/captures/battle-traffic_tk2_regular.ndjson — anything /// hardcoded here came from a real prod frame, with names + provenance in /// . The player-half of Matched/BattleStart now reads from /// instead of . /// public static class ScriptedLifecycle { /// /// Viewer id we present as the opponent on every scripted push. Out-of-range vs. real /// viewer ids so it can't collide with a real account in the auth pipeline. /// public const long FakeOpponentViewerId = 999_999_999L; public static MsgEnvelope BuildMatched(MatchContext ctx, long playerViewerId, long opponentViewerId, string battleId) => EnvelopeForPush(NetworkBattleUri.Matched, new MatchedBody( SelfInfo: new MatchedSelfInfo( CountryCode: ctx.CountryCode, UserName: ctx.UserName, SleeveId: ctx.SleeveId, EmblemId: ctx.EmblemId, DegreeId: ctx.DegreeId, FieldId: ctx.FieldId, IsOfficial: ctx.IsOfficial, OppoId: opponentViewerId, Seed: ScriptedProfiles.BattleSeed), OppoInfo: ScriptedProfiles.OpponentMatchedProfile with { OppoId = playerViewerId }, SelfDeck: BuildPlayerDeck(ctx.SelfDeckCardIds)), bid: battleId); public static MsgEnvelope BuildBattleStart(long playerViewerId) => EnvelopeForPush(NetworkBattleUri.BattleStart, new BattleStartBody( TurnState: 0, // player goes first BattleType: 11, // TK2 NetworkBattleType SelfInfo: ScriptedProfiles.PlayerBattleStartProfile, OppoInfo: ScriptedProfiles.OpponentBattleStartProfile)); public static MsgEnvelope BuildDeal() => EnvelopeForPush(NetworkBattleUri.Deal, new DealBody( Self: new[] { new PosIdx(0, 1), new PosIdx(1, 2), new PosIdx(2, 3) }, Oppo: new[] { new PosIdx(0, 1), new PosIdx(1, 2), new PosIdx(2, 3) })); /// /// 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) => EnvelopeForPush(NetworkBattleUri.Swap, new SwapResponseBody(Self: BuildPosIdxList(hand))); public static MsgEnvelope BuildReady(IReadOnlyList hand) => EnvelopeForPush(NetworkBattleUri.Ready, new ReadyBody( Self: BuildPosIdxList(hand), Oppo: BuildPosIdxList(InitialHand), IdxChangeSeed: ScriptedProfiles.ReadyIdxChangeSeed, Spin: ScriptedProfiles.ReadySpin)); /// /// 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() => EnvelopeForPush(NetworkBattleUri.TurnStart, new OpponentTurnStartBody(Spin: ScriptedProfiles.OpponentTurnStartSpin)); private static IReadOnlyList BuildPosIdxList(IReadOnlyList hand) { var list = new List(hand.Count); for (var pos = 0; pos < hand.Count; pos++) { list.Add(new PosIdx(Pos: pos, Idx: (int)hand[pos])); } return list; } private static IReadOnlyList BuildPlayerDeck(IReadOnlyList cardIds) { var deck = new List(cardIds.Count); for (var i = 0; i < cardIds.Count; i++) { deck.Add(new DeckCardRef(Idx: i + 1, CardId: cardIds[i])); } return deck; } private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, IMsgBody body, string? bid = null) => new(uri, ViewerId: FakeOpponentViewerId, Uuid: WireConstants.ServerUuid, Bid: bid, Try: 0, Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null, Body: body); }