Commit Graph

396 Commits

Author SHA1 Message Date
gamer147
9130d6de11 test(inventory): pack-open shape produces acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:59:39 -04:00
gamer147
d01294ebb4 test(item-acquire-history): literal wire-shape round-trip 2026-06-09 14:57:18 -04:00
gamer147
f9a971a546 feat(item-acquire-history): controller + DTOs
Add ItemAcquireHistoryController (POST /item_acquire_history/info) with its
three DTOs and two integration tests (ordering + empty-viewer). The endpoint
reads ViewerAcquireHistory rows written by InventoryTransaction.CommitAsync,
ordered newest-first, capped at 300. Tests access doc.RootElement.histories
directly (no envelope wrapper in the test path — middleware skips non-UnityPlayer UA).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:49:43 -04:00
gamer147
77ad614258 feat(inventory): prune acquire history above 300-row cap
Adds PruneAcquireHistoryAsync to InventoryTransaction.CommitAsync; runs
inside the open DB transaction after history rows are flushed, keeping at
most 300 rows per viewer (oldest discarded). Adds a covering test that
verifies the cap and per-viewer isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:41:04 -04:00
gamer147
fb1e6829b7 refactor(inventory): consolidate IsCurrency, skip num=0 grants in history
- Drop IsWalletCurrency (duplicate of IsCurrency); use IsCurrency in WriteAcquireHistory.
- Add comment on first SaveChangesAsync in CommitAsync explaining the two-phase flush.
- Guard WriteAcquireHistory loop with grant.Num == 0 check so synthetic DebitItem post-state ops do not produce history rows.
- Add InventoryHistoryTests.Commit_writes_no_history_row_for_item_debit to lock in the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:37:50 -04:00
gamer147
bea5a1efd4 feat(inventory): write acquire history rows on commit
CommitAsync now calls WriteAcquireHistory() between the two SaveChanges
calls: iterates _ops, skips SpendOps, writes one ViewerAcquireHistoryEntry
per GrantOp. Cascade rows get GrantSource.CardCosmeticCascade; wallet
currencies zero RewardDetailId; all rows in a single commit share one
DateTime.UtcNow timestamp. Closes _source plumbing from Task 5.

5 new tests added (46 total inventory, 0 failures).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:32:51 -04:00
gamer147
0d036e1bff feat(inventory): add ViewerAcquireHistoryEntry entity + DbSet
Adds the ViewerAcquireHistoryEntry model (8 fields: Id, ViewerId,
RewardType, RewardDetailId, RewardCount, AcquireType, Message,
AcquireTime), registers DbSet<ViewerAcquireHistoryEntry> on
SVSimDbContext, configures model (PK, FK cascade to Viewer, MaxLength
64 on Message, composite index on ViewerId/AcquireTime/Id), and adds a
DbSet round-trip integration test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:19:00 -04:00
gamer147
82dc877639 feat(inventory): add Source to InventoryLoadConfig
Adds a `GrantSource Source { get; set; }` property (defaults to
`GrantSource.Unknown`) to `InventoryLoadConfig`. Plumbing-only — no
behavior change; callers that don't set `Source` get Unknown rows,
greppable via `acquire_type=0` in dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:13:56 -04:00
gamer147
9f65326449 feat(inventory): add GrantSource enum + message lookup
Introduces GrantSource (17 values, DB-persisted) and GrantSourceMessages.For()
for the item-acquire-history feature. Values 1/2 mirror prod captures;
coverage test verifies every enum value has a non-empty message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:08:01 -04:00
gamer147
998402ebc3 refactor(tutorial-presents): promote static catalogue to seed-driven TutorialPresentEntries table
The five tutorial gifts every fresh viewer is given at signup used to live as a
static C# array in SVSim.Database/SeedData/TutorialPresents.cs — outside the
seed-JSON pipeline used by all 40+ other globals tables. Editing a gift required
a code change + rebuild instead of a JSON edit + bootstrap re-run.

Now authored in SVSim.Bootstrap/Data/seeds/tutorial-presents.json and loaded into
a new TutorialPresentEntries table via TutorialPresentsImporter (clear-and-rewrite,
mirroring HomeDialogs). ViewerRepository.RegisterAnonymousViewer reads the table
at signup and projects each row into a ViewerPresent with Source="tutorial".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 09:53:10 -04:00
gamer147
7118b92522 refactor(pack): type PackChildGachaEntry.TypeDetail as CardPackType enum 2026-06-09 08:48:16 -04:00
gamer147
57d231cd56 feat(pack): /pack/open supports type_detail=10 FREE_PACKS with per-campaign daily quota 2026-06-08 21:43:04 -04:00
gamer147
6c7e8ae8ad feat(pack): /pack/info filters spent free-pack children and emits campaign metadata 2026-06-08 21:38:26 -04:00
gamer147
d762c5766f feat(pack): persist daily_free_gacha_count on PackChildGachaEntry 2026-06-08 21:31:02 -04:00
gamer147
feee6e7c09 test(tutorial-e2e): seed tutorial presents — RegisterViewer no longer auto-seeds 2026-06-08 20:51:42 -04:00
gamer147
83e89455e2 test(signup): assert tutorial presents seeded by RegisterAnonymousViewer 2026-06-08 20:46:27 -04:00
gamer147
7a582f310e test(gift): prod-URL coverage and state=3 delete behavior 2026-06-08 20:45:49 -04:00
gamer147
f1d881b26a fix(gift): drop RowVersion (SQLite incompatible) + restore wire reward_type map
[Timestamp] byte[] doesn't work under SQLite (the test backend) — EF
expects the DB to populate it on insert, but SQLite has no equivalent
of Postgres's xmin. The WHERE Status = Unclaimed filter plus
IInventoryService's viewer-level concurrency is the practical defense;
RowVersion was only a backstop. Regenerated the migration without the
RowVersion column.

Wire reward_type on the gift endpoint uses a gift-specific scheme that
diverges from UserGoodsType for currencies: wire 1 = Crystal (enum=2),
wire 9 = Rupy (enum=9), wire 4 = Item (enum=4). A naked cast resolves
wire 1 to UserGoodsType.RedEther and silently grants the wrong wallet
— restored the explicit WireRewardTypeToUserGoodsType map from the old
tutorial controller.

Retrofits existing GiftControllerTests to call SeedTutorialPresentsAsync
on the new helper (RegisterViewer doesn't auto-seed; only the prod
signup path does). All 7 existing tests pass.
2026-06-08 20:44:52 -04:00
gamer147
fafd7ea183 test(gift): add SeedTutorialPresentsAsync helper 2026-06-08 20:37:26 -04:00
gamer147
ce32a9c6b7 feat(home-dialog): seed file + importer + bootstrap hookup
Mirrors banners pattern: clear-and-rewrite from per-table JSON seed.
Ships one entry pointing at parent_gacha_id 80032 to match the
2026-06-03 prod capture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:57:38 -04:00
gamer147
9d6a5cc3b9 feat(home-dialog): populate home_dialog_list on /mypage/index
Walk-down behavior: each call emits the highest-priority unfired
active dialog; subsequent calls walk to the next-priority entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:55:48 -04:00
gamer147
7e757ebcd2 feat(home-dialog): per-session suppression tracker
Singleton keyed by ShortUdid; lock on per-viewer set to avoid
cross-viewer contention. Process lifetime — restart re-fires.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:53:33 -04:00
gamer147
6d60edaa2a feat(home-dialog): IGlobalsRepository.GetActiveHomeDialogsAsync
Window is [begin, end) — exclusive upper bound. Ordered priority-DESC
then Id-ASC so the controller can break on the first match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:52:32 -04:00
gamer147
9e93a7b198 refactor(engine-ambient): wrap residual UnitTests + delete EngineSessionGate
Step 7 of multi-instancing migration. Residual SVSim.UnitTests that touch
engine code directly are wrapped in TestBattleScope. EngineSessionGate is
deleted along with the _engineOwned bookkeeping in BattleSession; engine
setup is unconditional now that per-battle state is isolated on the ambient.
Gate-specific fallback branches in BattleSession.ShadowIngest are simplified.

Suite fully green (SVSim.UnitTests, SVSim.BattleEngine.Tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 22:43:18 -04:00
gamer147
addeb021d2 fix(battlenode): shadow engine tracks live PvP wire-truth (full battle, multiple bid regressions)
Six distinct fixes accumulated over live-test iterations against four bids
(654473755566, 806245601092, 283192092460, 131549100204, 799755786270) — together
they take the shadow engine from "throws on the first non-mulligan play" to
"survives a full PvP battle, only weird-edge-case Unity touches still left to whack".

1. Engine StableRandom seed aligned with clients' Matched.seed
   (BattleSession.EnsureEngineSetup, NodeNativeBattleHarness.Create). Clients seed
   _stableRandom with BattleSeeds.Stable(masterSeed) (the value the node ships in
   Matched.seed); we were passing the RAW masterSeed to engine.Setup, so every
   StableRandom call diverged from call #1 onward — every turn-1+ draw picked a
   different deck position than the clients. Verified Stable(1184631275)=1543475792
   matches the wire on bid 654473755566.

2. SeedDeck advances cardTotalNum to deck.Count+1 + pins BattleStartDeckCardList.
   Mirrors SBattleLoad.InitPlayer's tail (SBattleLoad.cs:1292). Without it,
   skill-generated tokens auto-assigned Index 0,1,... and COLLIDED with deck-loaded
   indices 1..40 — silent until something addressed the deck card with the
   colliding Index (Hoverboarder at deck idx 1 + a token at engine Index 1 made
   GetBattleCardIdx's SingleOrDefault throw on bid 806245601092).

3. BattleCardView.GameObject lazily non-null in the shim (ViewUiTouchStubs.cs).
   The IsRecovery card-create delegate (NetworkBattleManagerBase.cs:379) passes
   null cardGameObject; Skill_metamorphose.cs:147 in the in-play branch then NRE'd
   on `metamorphosedCard.BattleCardView.GameObject.transform.rotation = identity`,
   a purely cosmetic touch with no game-state implication. Bid 283192092460:
   Petrification on a board follower.

4. TranslateChoiceKeyAction unwraps wrapped selectCard on shadow ingest
   (SessionBattleEngine.cs, sibling to TranslateTargetOwners). Live sender-send
   wires Choice plays as selectCard:{cardId:[...], open:0}; engine's
   ConvertToListInt does `value as List<object>` — a Dict casts to null and
   foreach NREs. The receiver's swallow-all catch (NetworkBattleReceiver.cs:1255)
   logs to Debug.LogError + LocalLog — both shimmed/no-op'd headlessly — and
   returns false, but Receive calls ReceivedMessage with checkBreakData:false so
   the false isn't propagated. The play continues with choiceIdList=[], the chosen
   branch never resolves, the played card stays in hand; a later targeted play
   (A's bounce on B's "board" idx 20) then can't find the target → NRE on null in
   ActionProcessor.PlayCard:407. Bid 131549100204: B's Resonance + A's bounce.
   Opponent-relay path is unaffected — node strips selectCard from broadcasts.

5. HeadlessHandViewStub overrides HandUnfocus/HandFocus/FocusRearrangeHandHand
   to return NullVfx. CreateHandControl returns null in headless; the base
   methods unconditionally deref `_handControl.SetHandState(...)`. A follower
   with a when_spell_play Heal trigger fired on its leader for amount 0 — even
   a 0-heal drives ApplyHealing → CreatePullHandInVfx → HandUnfocus → NRE.
   Bid 799755786270: two consecutive spell plays both crashed this stack.
   Added InternalsVisibleTo("SVSim.BattleEngine.Tests") so the shim-level
   regression tests can pin the no-op contracts directly.

Plus the previous-session fixes carried in this same uncommitted state
(see docs/superpowers/plans/2026-06-07-shadow-engine-desync-handoff.md):
  - doesPlayerGoFirst:true + mgr.IsFirst:true (turn-1 draw count correct
    per seat)
  - RecoveryOperationCollection.PlayHandCardOperation routes all type:30
    through PlaySkillSelectHandCardOperation (skips the two-phase user-select
    guard that aborts targeted spells in recovery)
  - ShadowFeed + ToRawBody: server-generated typed bodies (DealBody, etc.)
    converted to RawBody before engine.Receive (`env.Body as RawBody`
    returned null for typed bodies)
  - Ready idxChangeSeed seeds A's XorShift via the receiver; B's seed is
    injected via SeedOppoIdxChange (BattleSeeds.IdxChange + viewerId)
  - ReadySpin defaulted to 0 (was 243) — non-zero double-cranks the shadow
    which ingests BOTH sides' Ready frames on one stream

Test counts: SVSim.UnitTests 1054/1054, SVSim.BattleEngine.Tests 34/34.

Open: known-residual Unity touches are individual whack-a-mole now (per-card
skill edge cases), not the structural divergences fixed here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 19:05:07 -04:00
gamer147
25751462f4 fix(battlenode): translate live isSelf target frames to engine vid shape on ingest (live PvP fidelity)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:44:53 -04:00
gamer147
8bd8d1db2f docs(battlenode): correct EVOLUTION_SELECT deferral rationale — skill data is present (M-HC-4) 2026-06-07 00:52:22 -04:00
gamer147
f1c96ed37d refactor(battlenode): M-HC-4 cleanup — EpCount rename, dedupe evolve-ramp, drop tautological guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:47:33 -04:00
gamer147
a30a496265 refactor(battlenode): engine-first token identity (cardId); keep wire-mining fallback (M-HC-4f, partial)
Source the played card's opponent-facing knownList[].cardId off the shadow engine
(SessionBattleEngine.PlayedCardId -> BattleCardBase.CardId), engine-first with the
wire-mined idx->cardId map as the fallback. PROVEN engine-resolved (each backed by a
HeadlessConductorTests PlayedCardId_* test): deck cards and receive-path substituted/
revealed tokens (engine seats the wire id at the wire idx).

PARTIAL retirement: the wire-mining bookkeeping (MineAddOps/MineChoicePicks/MineCopyTokens
+ Record*From) is KEPT as the load-bearing fallback. The choice/Discover, copy/clone and
cross-side (isSelf:0) token cases are NOT proven to resolve at a wire idx headless — the
autonomous token_draw path seats a chosen token at engine Index 0 (would collide with the
leader), and copy/cross-side aren't cheaply fixturable. Deleting their mining on faith
would silently corrupt opponent reveals, so it stays behind a TODO(M-HC-4f) gate.

- SessionBattleEngine.PlayedCardId: new accessor mirroring PlayedCardClan/Tribe.
- BuildPlayedCard: signature deckMap->explicit cardId; null on cardId==0 (no engine id AND
  no mined/deck-map fallback).
- PlayActionsHandler: cardId = engine.PlayedCardId(seat, idx, fallback: mapped) ; mining retained.
- Tests: PlayedCardId_* (deck/substituted/degrade pass; choice-gap [Explicit] documents the
  Index-0 finding). KnownListBuilder + CaptureConformance call-sites updated to new signature.

Full BattleNode suite 263/263 green; HeadlessConductorTests 27/27; drift clean; no Engine edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:36:49 -04:00
gamer147
d3508d7bd4 fix(battlenode): PlayedCardTribe degrades to 0 not empty; clan/tribe builder tests (M-HC-4e review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:23:07 -04:00
gamer147
693fba5003 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>
2026-06-07 00:11:28 -04:00
gamer147
daaec20afb test(battlenode): board-dependent when_evolve_other cost validated headless (M-HC-4d)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:54:13 -04:00
gamer147
3285097d1b test(battlenode): target-discriminating + documented choice shape (M-HC-4c review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:46:59 -04:00
gamer147
3add58f939 feat(battlenode): target/choice ops resolve on engine state via view-untangle (M-HC-4c)
A targeted hand-play (PLAY_HAND_SELECT, opcode 31) and a choice play (PLAY_HAND +
keyAction type Choice) both resolve headless through the recovery receive conductor
with NO new shim/view fills — the 4a/4b view seeds (DetailPanelControl, _inPlayFrameEffect,
_playerInfoPair, HeadlessConductorVfxMgr) already cover the target/choice surface, because
the recovery path resolves targets/choices from the wire frame without the interactive
select UI, and the damage/token VFX execute through the existing top-level InstantVfx path.

Fixtures (cards.json, full skill mechanics):
- single-target: 100414020 (cost-1 Dragoncraft spell, when_play damage=2 to a selected
  enemy unit). Asserts the enemy 1/4 (101411060) drops to life 2 — exact magnitude, survives.
- choice: 127011010 (cost-1 Neutral choice follower, choose 1 of 2 tokens to add to hand).
  Asserts the chosen token (B) lands in hand and the un-chosen token (A) does not — decisive
  about WHICH branch resolved. Wire keyAction shape cross-checked against a real capture of
  this exact card (battle_test/rng/battle-traffic_cl1.ndjson); the receiver consumes a flat
  selectCard list (ConvertToListInt).

Drivers: NodeNativeBattleHarness.TargetedPlayBody (reuses the {targetIdx,vid,selectSkillIndex}
target shape proven by AttackBody) + ChoicePlayBody. Zero Engine/*.cs edits (drift clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:34:39 -04:00
gamer147
2e8f9ab64e feat(battlenode): evolve resolves on engine state via view-untangle (M-HC-4b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:08:59 -04:00
gamer147
c5a511e4fe feat(battlenode): attack resolves on engine state via view-untangle (M-HC-4a)
Drive ATTACK frames through the headless receive conductor and assert on engine
board state (node-native harness). Two cases: follower -> enemy leader (leader
life drops by atk, attacker spent) and a lethal follower-vs-follower trade (both
removed). ATTACK opcode confirmed = 10 (NetworkBattleDefine.PlayActionType).

Headless view-untangle (no Engine logic edits; drift clean):
- IBattlePlayerView.AttackSelectControl -> non-null HeadlessAttackSelectControl
  (no-op RegisterAttackPair/ResetCardAfterAttack); IsCardTranslatable left to base.
- IBattleCardView.CardInfo -> backing card via BuildInfo (so IsCardTranslatable
  reads authentic IsClass); class/null view ctors now chain : base(buildInfo).
- IBattleCardView._inPlayFrameEffect -> non-null no-op control.
- Seed Certification.viewer_id headless so the IsRecovery target parse
  (vid != UserViewerID) does not throw inside SavedataManager and silently drop
  the parsed targetList.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 22:48:26 -04:00
gamer147
0d7136787a refactor(battlenode): retire spellboost bookkeeping, engine owns cost+spellboost (M-HC-3)
The headless engine accumulates spell-charge for real on the receive path
(each spell play runs the played card's own AddSpellChargeCount) and resolves
the discounted cost by construction, so the wire-derived spellboost-count
bookkeeping is redundant. Engine-source the knownList spellboost COUNT too
(prod-faithful) via a new SessionBattleEngine.PlayedCardSpellboost, using the
same persist-post-play zone search as PlayedCardCost (SpellChargeCount survives
PlayCard; only ctor/ReturnCard zero it).

- Delete IdxToSpellboost/SpellboostMap/GetSpellboostMap/RecordSpellboostFrom
  (BattleSessionState) and MineAlterSpellboosts (KnownListBuilder); token/choice/
  copy identity maps are untouched.
- BuildPlayedCard takes an engine-sourced spellboost int (drops spellboostMap).
- Seed BattleLogManager fusion lists headless (the per-frame filter cleanup
  NREs on null EnemyFusionCard when a fanfare card registers a CalledCreateFilter)
  so real spell-charge grantor plays resolve.
- Add committed real-charge regression tests (no SeedHandCardSpellboostCost seam):
  one grantor play accumulates +1 on the reducer -> cost 5->4, count 1, persisting
  post-play; handler emits cost 4 + spellboost 1 engine-sourced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:48:50 -04:00
gamer147
51419d15cd feat(battlenode): emit engine-resolved cost on every knownList entry (M-HC-3)
The opponent-facing PlayActions knownList now carries the engine-RESOLVED
play-time cost (KnownCardEntry.cost), sourced from the headless shadow engine's
PlayedCost on the just-resolved card. This closes the spellboost cost-desync BY
CONSTRUCTION: the engine already knows the true discounted cost (spellboost +
board modifiers folded in), so no bookkeeping is needed.

- DTO: add non-nullable cost to KnownCardEntry (prod emits cost 45/45).
- SessionBattleEngine.PlayedCardCost(seat, idx, fallback): finds the resolved
  card by engine Index across in-play/cemetery/hand zones and returns PlayedCost
  (captured by PlayCard at resolution == discounted Cost), degrading to fallback
  when the engine is not owned/ready.
- PlayActionsHandler sources the played card's cost from ctx.Engine (ShadowIngest
  already resolved the play before the handler runs). Spellboost-map plumbing
  stays for now; Task 6 (M-HC-3b) retires it.
- Validation: engine-read test (charge-seeded reducer 101314020: base 5, cost
  5/1/0 at charge 0/4/5) + handler-emit test asserting knownList[0].cost == 1
  (discounted, not base 5) with non-vacuity. Board-dependent (when_evolve_other)
  case deferred to M-HC-4 (evolve not yet headless); cost is read off the resolved
  engine so board modifiers are captured by construction once their ops resolve.
- Harness: promote alt vanilla follower id (101211120) to AltVanillaFollowerId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:18:29 -04:00
gamer147
b73f0f7157 test(battlenode): reveal test stresses cardId substitution with mismatched seed (M-HC-2 review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:01:01 -04:00
gamer147
07ffc8906d feat(battlenode): opponent reveal resolves on engine state via ReplaceReceivedCards (M-HC-2)
Drive a node-native battle to seat B's turn, then ingest an opponent
PlayActions reveal frame (knownList[{idx,cardId,to:Field}], isPlayerSeat:false)
matching battle_test_cl2's wire shape. The engine's ReplaceReceivedCard.ReplaceCard
-> CreateActualCard -> CreateBattleCardWithGameObject path resolves headless and
seats the substituted card on seat B's board with the wire cardId. No Engine/ logic
edits and no new view shims were needed — the card-creation view surface is fully
covered by the BackGround/icon-anim/play-queue/hand stubs from Tasks 2/3.

Adds InPlayCardId(seat, boardPos) accessor (SessionBattleEngine + harness) to read a
seated in-play follower's true identity, leader-excluded like BoardCount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:51:55 -04:00
gamer147
b1d17fb97d test(battlenode): unify DealBody helper + assert seat-B deck (M-HC-1 review)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:43:04 -04:00
gamer147
f0977ab45c feat(battlenode): mulligan+turn ops track on engine state (M-HC-1)
Task 2's WireMulliganPhase already installed the full mulligan delegate
set (Swap/Ready, not just Deal) via MulliganEventSetting, and the
mulligan + turn-draw mutations flow through VfxMgr.RegisterSequentialVfx
— which HeadlessConductorVfxMgr runs for InstantVfx. So Swap/Ready/
TurnStart/TurnEnd resolve headless with ZERO new shim/seed/view fills.

Adds the M-HC-1 milestone assertions: a mulligan-swap test (post-swap
hand holds deck idx 1,2,4 — idx-3 swapped for the next unused idx) and a
two-turn test (Deal->Swap->Ready->TurnStart/TurnEnd x2) asserting the
engine's deterministic node-native progression on both seats
(hand/deck/PP/turn/leader-life) at each boundary. Frame shapes mirror the
captured battle_test_cl1 receive stream (self/oppo pos-idx lists, spin).

Harness/node: +DeckCount/Turn board-state pass-throughs (test reads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:30:39 -04:00
gamer147
35e9847911 feat(battlenode): receive conductor resolves self Deal+Play headless via view-untangle (M-HC-0)
The engine's receive CONDUCTOR fuses each authoritative mutation behind a view
call: the play mutation is an InstantVfx registered to VfxMgr, and the deal hand
is seated by MulliganPhaseBase.StartDeal wired to OperateReceive.OnReceiveDeal.
Headless, the shared VfxMgr no-op'd registration (correct for the direct
ActionProcessor path the M2-M12 oracles use) and OnReceiveDeal was never wired,
so the receive path resolved nothing.

Untangle (Candidate B, zero Engine logic edits):
- InstantVfx.Run() opt-in executor (authored shim).
- HeadlessConductorVfxMgr : VfxMgr runs registered InstantVfx; wired only via the
  node's SessionContentsCreator.CreateVfxMgr (verified the receive mgr's VfxMgr
  comes from there — BattleManagerBase.cs:768). M2-M12 use HeadlessContentsCreator,
  so they're isolated by construction.
- WireMulliganPhase: construct NetworkMulliganPhase + MulliganEventSetting() to
  install OnReceiveDeal -> StartDeal (the node never pumps the phase machine).

View no-op surface (the 7 from the probe, minus 1 not hit; +1 emergent):
- Deal wiring (NetworkMulliganPhase) [node seed]
- MulliganInfoControl._partsPlayer/_partsOpponent._exchangeMark/_keepZone/_abandonZone [node seed: prefab + SeedMulliganInfoControl]
- Data.BattleRecoveryInfo (IsMulliganEnd=false) [EngineGlobalInit seed]
- IBattlePlayerView.PlayQueueView -> HeadlessPlayQueueViewStub [_IfaceImpl.g.cs, both getters]
- DetailMgr.DetailPanelControl/SubDetailPanelControl [node seed]
- BattleCardIconAnimations.collection (emergent: UpdateInPlayBattleCardIconLabel) -> HeadlessIconAnimations empty SkillCollectionBase [_IfaceImpl.g.cs]
- BattleMenuBtn (probe item 7): NOT hit on the vanilla path; not seeded.

Oracle (HeadlessConductorTests): node Deal seats 3-card hand; a vanilla
hand-card Play leaves hand (-1), adds board (+1), drops PP by cost.

Regression: 24/24 BattleEngine.Tests oracles (M2-M12) green; 241/241
SVSim.UnitTests BattleNode green. The 2 SessionEngine capture-replay shadow
tests are marked Ignore (superseded): they passed VACUOUSLY when the receive
path resolved nothing; with resolution live they hit the documented
capture-replay draw-misalignment artifact. Node-native battles are the oracle.
Drift: no drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:08:53 -04:00
gamer147
50294c10b1 test(battlenode): harness stub fails loud + non-parallelizable (M-HC-0 review)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 19:42:55 -04:00
gamer147
ca91fca028 test(battlenode): node-native battle harness for headless conductor (M-HC-0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:37:41 -04:00
gamer147
13f902ce58 fix(battlenode): emit real spellboost count in played-card knownList
The node hardcoded knownList.spellboost=0 on every played card. Prod sends
the true accumulated count, which the client reads straight into the card's
cost model; with 0 the opponent computes the card at full price and silently
rejects the play in OperateReceiveChecker.IsPlayCard (PP-over -> ConductError
-> NullOperationCollection -> no render/echo), desyncing the board.

Mine spellboost-count changes from the sender''s orderList alter ops
(MineAlterSpellboosts: a/s/h ops), accumulate per-side idx->count in
BattleSessionState (RecordSpellboostFrom), and surface the current count on
the played card via BuildPlayedCard. Recorded from the authoritative
PlayActions only (never the Echo) and folded in AFTER the played card is
built, since a card''s cost is fixed as it leaves hand and a play that grants
spellboost targets the rest of the hand.

Also adds a [sio-in-body] full-body inbound log to RealParticipant to capture
both clients'' re-simulated responses for PvP RNG verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:51:40 -04:00
gamer147
1007cf24d2 refactor(battlenode): type MatchContext.ClassId as CardClass enum (§C)
Behavior-preserving; full solution builds, 1013 tests green.

ClassId is the one genuinely-closed set of the three flagged stringly fields, so it
becomes a CardClass enum (1..8). Wire stays "1".."8": producer casts
(CardClass)run.ClassId, ServerBattleFrames renders via CardClassWire.ToWireValue().
RankBattleController's AI-start path drops a fragile int.TryParse(...)?:-1 for (int)cast.

CharaId (free-form leader/skin id, e.g. "5000123") and CountryCode (open-ended account
data) stay string with proper XML docs; CountryCodes.Korea/Japan name the captured values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:04:49 -04:00
gamer147
9b8a7f1e37 refactor(battlenode): name sender-only vs both-sides handshake checks (§D)
Behavior-preserving; 231 BattleNode tests green.

FrameDispatchContext.BothAfterReady() -> BothSidesAfterReady() (7 call sites). The
4 inline `SenderPhase == AfterReady` checks in TurnEndHandler/TurnEndFinalHandler now
read a new SenderIsAfterReady property. Both carry cross-referencing docs so the
Bot-arm (sender-only) vs PvP-arm (both-sides) distinction is explicit at the type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:49:27 -04:00
gamer147
578d0a75ef refactor(battlenode): rename mode-id field off BattleType, add BattleModes (§D)
Behavior-preserving; 271 BattleNode/Matching/Services tests green, full solution builds.

"BattleType" meant two things: the Sessions.BattleType enum (Pvp/Bot) and an int
"mode id" field. Renamed the int field on MatchContext AND the BattleStartBody wire
DTO to BattleModeId (wire key stays "battleType" via JsonPropertyName), so BattleType
now means only the enum project-wide.

New Bridge/BattleModes.cs (TakeTwo = 11) replaces every 11 literal — both prod
MatchContextBuilder sites and the test fixtures/assertions. The arbitrary-passthrough
42 and bot 0 stay literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:44:02 -04:00
gamer147
3e8901eec3 refactor(battlenode): split BattleSessionPhase into HandshakePhase + SessionLifecycle
Behavior-preserving; 231 BattleNode tests green.

One enum conflated two axes. Split:
- HandshakePhase (per participant): AwaitingInitNetwork..AfterReady. On
  IHasHandshakePhase.Phase, FrameDispatchContext.SenderPhase, the handler gates.
- SessionLifecycle (per battle): Active | Terminal. On the renamed
  BattleSessionState.Lifecycle (was SessionPhase, defaulting to a handshake value)
  and BattleSession.Lifecycle (was Phase). Reads are only != Terminal, so the
  Active default is behavior-identical.

OpponentTurn was dead (never assigned) -> dropped. BattleSessionPhase deleted; the
two axes can no longer be cross-assigned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:21:59 -04:00