using SVSim.BattleNode.Protocol.Bodies; namespace SVSim.BattleNode.Sessions.Dispatch; /// Pure transforms from the active player's RawBody sub-structures to the opponent-facing /// shapes. No session state, no wire I/O — unit-testable in isolation. RawBody nested values arrive /// as Dictionary<string,object?> / List<object?> with numeric leaves boxed /// as long/int/double (see MsgEnvelope.FromJson). internal static class KnownListBuilder { /// The played card's knownList entry, or null when its identity can't be synthesized /// (token idx not in the deck map, or no matching move op). spellboost/attachTarget default to /// 0/"" for the vanilla slice; cost/clan/tribe are deferred (receiver re-derives from cardId). public static KnownCardEntry? BuildPlayedCard( IReadOnlyDictionary deckMap, int playIdx, object? orderList) { if (!deckMap.TryGetValue(playIdx, out var cardId)) return null; var to = ExtractMoveTo(orderList, playIdx); if (to is null) return null; return new KnownCardEntry(Idx: playIdx, CardId: cardId, To: to.Value, Spellboost: 0, AttachTarget: ""); } /// The to place-state of the FIRST move op whose idx list contains /// (the played card's own move; later add/alter ops are the deferred /// token slice), or null if absent. NOTE: the sender-side to is passed through verbatim — /// for the vanilla slice we assume send-side and recv-side place-state codes match, pending /// recv-capture confirmation. public static int? ExtractMoveTo(object? orderList, int playIdx) { if (orderList is not IEnumerable ops) return null; foreach (var op in ops) { if (op is not IDictionary opDict) continue; if (!opDict.TryGetValue("move", out var moveRaw) || moveRaw is not IDictionary move) continue; if (move.TryGetValue("idx", out var idxRaw) && idxRaw is IEnumerable idxList) { foreach (var i in idxList) if (AsLong(i) == playIdx && move.TryGetValue("to", out var toRaw)) return (int)AsLong(toRaw); } } return null; } /// Rename targetList -> oppoTargetList; isSelf is actor-relative /// and passes through unchanged (F2). Null for a missing/empty list. public static IReadOnlyList? RenameTargets(object? targetList) { if (targetList is not IEnumerable entries) return null; var result = new List(); foreach (var e in entries) { if (e is not IDictionary d) continue; d.TryGetValue("targetIdx", out var targetIdxRaw); d.TryGetValue("isSelf", out var isSelfRaw); result.Add(new OppoTargetEntry( TargetIdx: (int)AsLong(targetIdxRaw), IsSelf: (int)AsLong(isSelfRaw))); } return result.Count == 0 ? null : result; } /// Coerce a boxed RawBody numeric leaf (long/int/double/decimal/string) to long; 0 for /// null/unparseable. public static long AsLong(object? value) => value switch { long l => l, int i => i, double d => (long)d, decimal m => (long)m, string s when long.TryParse(s, out var p) => p, _ => 0, }; }