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

@@ -279,6 +279,44 @@ internal sealed class SessionBattleEngine
return card?.SpellChargeCount ?? fallback;
}
/// <summary>The engine-RESOLVED clan of the card whose engine <c>Index</c> == <paramref name="idx"/> on
/// <paramref name="playerSeat"/> (M-HC-4e), as the int <c>ClanType</c> ordinal prod sends on the
/// knownList entry (e.g. <c>clan:8</c> in the tk2 capture). Reads <see cref="BattleCardBase.Clan"/>, whose
/// getter returns the skill-applied clan (<c>SkillApplyInformation.ClanSkinInfo.Last()</c> when a skill
/// changed it, else <c>BaseParameter.Clan</c>) — so a <c>change_affiliation</c> is reflected, which is WHY
/// the engine value (not the static card-master clan) is the faithful one to emit.
/// <para>Same post-resolution zone search + degrade-to-<paramref name="fallback"/> contract as
/// <see cref="PlayedCardCost"/>: no engine / no card → fallback, so a non-engine session never crashes.</para></summary>
public int PlayedCardClan(bool playerSeat, int idx, int fallback = 0)
{
if (_mgr is null) return fallback;
var card = FindByIndex(Seat(playerSeat), idx);
return card is null ? fallback : (int)card.Clan;
}
/// <summary>The engine-RESOLVED tribe of the card whose engine <c>Index</c> == <paramref name="idx"/> on
/// <paramref name="playerSeat"/> (M-HC-4e), in the EXACT wire string form prod sends: the comma-joined
/// int <c>TribeType</c> ordinals (e.g. <c>tribe:"7,16"</c> for MACHINE+SCHOOL in the tk2 capture), and
/// <c>"0"</c> when the card has no tribe (== <c>TribeType.ALL == 0</c> — prod never sends empty/omitted;
/// the client reads it via <c>item.Value.ToString()</c>, NetworkBattleReceiver.cs:2382). Reads
/// <see cref="BattleCardBase.Tribe"/>, whose getter folds in any skill-applied tribe CHANGE/ADD over
/// <c>BaseParameter.Tribe</c> (and drops ALL when the resolved list has ≥2 entries) — so the wire carries
/// the LIVE tribe, the faithful value over the static card-master one.
/// <para>Same post-resolution zone search as <see cref="PlayedCardCost"/>; no engine / no card → "" (an
/// engine that isn't owned this session emits no card, so the caller's BuildPlayedCard never fires).</para></summary>
public string PlayedCardTribe(bool playerSeat, int idx)
{
if (_mgr is null) return string.Empty;
var card = FindByIndex(Seat(playerSeat), idx);
if (card is null) return string.Empty;
var tribe = card.Tribe;
// Prod's no-tribe form is the single "0" (TribeType.ALL == 0), never an empty string; an empty list
// (defensive) renders the same "0".
return tribe is null || tribe.Count == 0
? "0"
: string.Join(",", tribe.Select(t => (int)t));
}
// Locate the card with the given engine Index across the seat's post-resolution zones. Order matters
// only for disambiguation; Index is unique per card so the first hit is the card. In-play (followers)
// and cemetery (spells) are where a just-resolved play lands; hand is the pre-resolution fallback.