feat(battlenode): emit engine-resolved clan/tribe on knownList entries (M-HC-4e)

Prod always emits clan (int ClanType) + tribe (comma-joined int TribeType
string, "0" for none) on every knownList entry (battle-traffic_tk2_regular
.ndjson). Source both off the resolved engine (SessionBattleEngine.PlayedCardClan/
PlayedCardTribe -> BattleCardBase.Clan/Tribe), so skill-applied clan/tribe
changes ride the wire rather than the static card-master value. Thread through
KnownListBuilder.BuildPlayedCard + PlayActionsHandler; add clan/tribe to the
KnownCardEntry DTO (always present, non-null). Node-side only; no engine edits,
drift clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 00:11:28 -04:00
parent daaec20afb
commit 693fba5003
7 changed files with 228 additions and 21 deletions

View File

@@ -294,7 +294,10 @@ public class CaptureConformanceTests
var ourEntry = ourDoc.RootElement.GetProperty("knownList")[0];
using var prodDoc = JsonDocument.Parse(prodEntry);
// We are responsible for idx/cardId/to (+ spellboost/attachTarget). cost/clan/tribe are deferred.
// This is a pure-BUILDER shape check: idx/cardId/to are synthesized here; cost/clan/tribe are
// ENGINE-sourced at the handler (PlayActionsHandler reads them off the resolved engine and passes them
// to BuildPlayedCard — covered by HeadlessConductorTests), so this builder-only call leaves them at
// their defaults and we assert only the structural keys.
foreach (var key in new[] { "idx", "cardId", "to" })
{
Assert.That(ourEntry.TryGetProperty(key, out var ours), Is.True, $"knownList entry missing '{key}'");
@@ -342,7 +345,8 @@ public class CaptureConformanceTests
var ourEntry = ourDoc.RootElement.GetProperty("knownList")[0];
using var prodDoc = JsonDocument.Parse(prodEntry);
// We own idx/cardId/to; cost/clan/tribe are deferred (receiver re-derives from cardId).
// Pure-BUILDER shape check: idx/cardId/to are synthesized here; cost/clan/tribe are ENGINE-sourced at
// the handler (covered by HeadlessConductorTests), so this builder-only call leaves them at defaults.
foreach (var key in new[] { "idx", "cardId", "to" })
{
Assert.That(ourEntry.TryGetProperty(key, out var ours), Is.True, $"knownList entry missing '{key}'");
@@ -424,7 +428,8 @@ public class CaptureConformanceTests
// keyAction is {type,cardId} only (selectCard stripped for the hidden open:0 choice); knownList
// reveals the generating DECK card. The choiceAdd lands a hidden token at idx 46 (candidates).
// Subset check covers playIdx/type/keyAction — the parts we own; knownList idx/cardId/to are
// asserted explicitly below (cost/clan/tribe are deferred, re-derived by the receiver from cardId).
// asserted explicitly below (cost/clan/tribe are ENGINE-sourced at the handler, covered by
// HeadlessConductorTests, so this builder-only call leaves them at defaults — not checked here).
const string prodFrame = """
{ "playIdx": 18, "type": 30,
"keyAction": [ { "type": 1, "cardId": 810014030 } ] }
@@ -471,8 +476,8 @@ public class CaptureConformanceTests
Assert.That(ourKa.GetProperty("cardId").GetInt64(), Is.EqualTo(810014030L));
// The generating deck card reveals on its own play (idx 18 -> 810014030, to 30). cost/clan/tribe
// are deferred (receiver re-derives from cardId), so only idx/cardId/to are checked — as in the
// sibling SynthesizedKnownList_* tests.
// are ENGINE-sourced at the handler (covered by HeadlessConductorTests); this builder-only call
// leaves them at defaults, so only idx/cardId/to are checked — as in the sibling SynthesizedKnownList_* tests.
var ourKnown = ourDoc.RootElement.GetProperty("knownList")[0];
Assert.That(ourKnown.GetProperty("idx").GetInt32(), Is.EqualTo(18));
Assert.That(ourKnown.GetProperty("cardId").GetInt64(), Is.EqualTo(810014030L));