using System.Text.Json.Serialization; namespace SVSim.BattleNode.Protocol.Bodies; /// Opponent-facing PlayActions frame the node synthesizes from the active player's /// send. KnownList reveals the played card's identity (null = token reveal deferred, see /// the deterministic-turn slice). OppoTargetList is the renamed targetList /// (independent of KnownList — a targeted hand play carries both). KeyAction forwards a /// choice/Discover play's {type,cardId} so the opponent renders the choice-token generation; /// the pick (selectCard) is stripped for a hidden (open:0) draw-to-hand choice. UList /// forwards the sender's unapproved-movement list (deck-sourced summons/fetches) verbatim. All are /// omitted when null via the envelope's WhenWritingNull policy (a vanilla play carries none). public sealed record PlayActionsBroadcastBody( [property: JsonPropertyName("playIdx")] int PlayIdx, [property: JsonPropertyName("type")] int Type, [property: JsonPropertyName("knownList")] IReadOnlyList? KnownList, [property: JsonPropertyName("oppoTargetList")] IReadOnlyList? OppoTargetList, [property: JsonPropertyName("uList")] IReadOnlyList? UList = null, [property: JsonPropertyName("keyAction")] IReadOnlyList? KeyAction = null) : IMsgBody; /// Opponent-facing keyAction entry for a choice/Discover play. type/cardId /// (the GENERATING card) pass through so the opponent re-derives the candidate pool from that card's /// skill; selectCard is stripped (null) for a hidden (open:0) choice — the pick stays secret /// until the chosen card is played — and passed through for a visible (open:1) board choice (§6, /// provisional pending live confirmation). public sealed record KeyActionEntry( [property: JsonPropertyName("type")] [property: JsonConverter(typeof(JsonNumberEnumConverter))] KeyActionType Type, [property: JsonPropertyName("cardId")] long CardId, [property: JsonPropertyName("selectCard")] SelectCardEntry? SelectCard); /// A visible choice's revealed pick: the chosen cardId(s) and the open flag. /// Only emitted for the open:1 pass-through case (open:0 strips the whole selectCard). public sealed record SelectCardEntry( [property: JsonPropertyName("cardId")] IReadOnlyList CardId, [property: JsonPropertyName("open")] [property: JsonConverter(typeof(JsonNumberEnumConverter))] ChoiceVisibility Open); /// One revealed card in a knownList. cardId from the sender's deck map; cost /// is the ENGINE-RESOLVED play-time cost (M-HC-3a) — the discounted cost the headless engine actually /// charged (spellboost + board modifiers folded in by construction), emitted on EVERY entry (prod sends /// cost 45/45 in captures, so it is NOT omitted). spellboost is now ALSO engine-sourced (M-HC-3b) — /// the played card's accumulated spell-charge count read straight off the resolved engine /// (SessionBattleEngine.PlayedCardSpellboost); the wire-derived spellboost bookkeeping is retired. /// Cost already folds the discount in by construction; the count rides the entry only to stay prod-faithful /// (prod sends the real count). /// clan and tribe are likewise ENGINE-SOURCED (M-HC-4e) — read off the resolved card's /// BattleCardBase.Clan/BattleCardBase.Tribe getters (via /// SessionBattleEngine.PlayedCardClan/PlayedCardTribe), which fold in /// any skill-applied clan/tribe CHANGE/ADD (e.g. change_affiliation), so the wire carries the LIVE /// clan/tribe the engine resolved, not the static card-master value. PROD ALWAYS EMITS BOTH on every /// knownList entry (tk2 capture battle-traffic_tk2_regular.ndjson, e.g. /// {idx:17,cardId:128821011,...,clan:8,tribe:"7,16",...}): clan is the int ClanType /// ordinal (present even when 0); tribe is the comma-joined int TribeType ordinals as a /// STRING, "0" when the card has no tribe (== ClanType/TribeType.ALL == 0, never empty/omitted — /// the client reads it via item.Value.ToString(), NetworkBattleReceiver.cs:2382). Both are always /// present (non-null string for tribe), so neither is null-omitted. attachTarget stays "". public sealed record KnownCardEntry( [property: JsonPropertyName("idx")] int Idx, [property: JsonPropertyName("cardId")] long CardId, [property: JsonPropertyName("to")] int To, [property: JsonPropertyName("spellboost")] int Spellboost, [property: JsonPropertyName("attachTarget")] string AttachTarget, [property: JsonPropertyName("cost")] int Cost, [property: JsonPropertyName("clan")] int Clan, [property: JsonPropertyName("tribe")] string Tribe); /// Renamed targetList entry. isSelf is actor-relative and passes through /// verbatim — no perspective flip (bullet-3 audit F2). public sealed record OppoTargetEntry( [property: JsonPropertyName("targetIdx")] int TargetIdx, [property: JsonPropertyName("isSelf")] [property: JsonConverter(typeof(JsonNumberEnumConverter))] CardOwner IsSelf); /// One entry in a relayed uList (the unapproved-movement list) — a skill-driven /// card movement (fetch / search / summon-from-deck / discard-reveal) the node forwards VERBATIM /// (bullet-3 audit F1; the node makes no reveal decision — cardId presence is the sender's /// call). The first five fields are always emitted; the rest are conditional in /// SendCardDataMaker.MakeUList (cardId when revealed, clan/cost when set, etc.) and omit when /// null. isSelf is actor-relative and passes through unchanged (F2). public sealed record UnapprovedCardEntry( [property: JsonPropertyName("idxList")] IReadOnlyList IdxList, [property: JsonPropertyName("from")] int From, [property: JsonPropertyName("to")] int To, [property: JsonPropertyName("isSelf")] [property: JsonConverter(typeof(JsonNumberEnumConverter))] CardOwner IsSelf, [property: JsonPropertyName("skill")] string Skill, [property: JsonPropertyName("cardId")] long? CardId = null, [property: JsonPropertyName("clan")] int? Clan = null, [property: JsonPropertyName("cost")] int? Cost = null, [property: JsonPropertyName("skillKeyCardIdx")] IReadOnlyList? SkillKeyCardIdx = null, [property: JsonPropertyName("randomTargetIdx")] IReadOnlyList? RandomTargetIdx = null, [property: JsonPropertyName("isInvoke")] [property: JsonConverter(typeof(NumericBoolJsonConverter))] bool? IsInvoke = null, [property: JsonPropertyName("attachTarget")] string? AttachTarget = null);