Commit Graph

409 Commits

Author SHA1 Message Date
gamer147
366a71688d feat(gift): add GiftRewardTypes supported-set helper
Centralizes which UserGoodsType values IInventoryTransaction.GrantAsync
can handle in the gift-inbox flow; both GiftController (post-fix) and
CampaignController will consult this instead of duplicating the set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:39:10 -04:00
gamer147
0d32d2167b feat(campaign): CampaignController.RegisterSerialCode + DTOs + tests
Adds POST /campaign/regist_serial_code with 9 integration tests covering
success path, all disqualifier conditions (unknown/disabled/expired/future/
already-redeemed/unsupported-reward-type), 401 on missing auth, and case-
sensitivity. IsSupportedGiftRewardType uses gift-wire literals (1/4/9) not
UserGoodsType enum values, matching GiftController.WireRewardTypeToUserGoodsType.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:51:03 -04:00
gamer147
206be77a86 feat(serial-code): add SerialCode + SerialCodeReward + ViewerSerialCodeRedemption entities
Three new EF entities for /campaign/regist_serial_code: SerialCodeEntry (code, message,
window, enabled flag), SerialCodeRewardEntry (FK child, per-slot reward), and
ViewerSerialCodeRedemption (composite-PK redemption record). Registered in SVSimDbContext
with unique index on Code and cascade FK constraints. 3/3 persistence tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:42:10 -04:00
gamer147
b117fe825c test(profile): literal wire-shape round-trip for UserClass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:41:11 -04:00
gamer147
11215bd69f feat(profile): ProfileController + DTOs + integration tests
Add /profile/index endpoint that returns user_rank_match_total_win (stubbed 0)
and user_class_list built from viewer Classes + owned LeaderSkins. Six NUnit
integration tests cover zero wins, all classes present, level/exp/default skin,
leader_skin_id_list population, is_random_leader_skin round-trip, and 401 on
unauthenticated access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:37:35 -04:00
gamer147
2f4420bf15 feat(viewer): add ViewerClassData.IsRandomLeaderSkin column
Adds the bool column (defaults false) to the [Owned] ViewerClassData
entity and a two-test persistence suite that verifies round-trip and
default-value behaviour via SQLite/EnsureCreated.
2026-06-09 17:24:32 -04:00
gamer147
b50d69d3a5 test(user-mypage): assert /user_mypage/update returns 401 without auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:04:12 -04:00
gamer147
483cc1c1e0 feat(mypage): /mypage/index reflects persisted bg selection
Wires MyPageController.Index to read Viewer.MyPageBgId/SelectType/BgRotation
instead of emitting an empty MyPageBgSetting placeholder; adds Include for
MyPageBgRotation in ViewerRepository.GetViewerByShortUdid; adds fresh-viewer
zero-defaults test and write→read round-trip test (9/9 controller tests pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:53:34 -04:00
gamer147
6123a64b37 test(user-mypage): explain Clear() + cover unparseable rotation entries
Add a comment above MyPageBgRotation.Clear() explaining the EF OwnsMany
delete-on-clear semantics. Add a 7th test covering garbage/"" entries in
mypage_id_list falling back to BgId=0 while adjacent valid entries are
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:50:38 -04:00
gamer147
9ff6c70faf feat(user-mypage): UserMyPageController + DTOs + persistence tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:46:49 -04:00
gamer147
b447f5032d fix(mypage): wire mypage_id/select_type/mypage_id_list as strings
Prod capture (traffic_prod_misc_clicking.ndjson) shows all three
MyPageBgSetting fields arrive as decimal strings, not ints.  Update the
DTO from int/int/List<int> to string/string/List<string> with "0"/empty
defaults, and add a literal wire-shape round-trip test pinning the
exact JSON against the capture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:40:38 -04:00
gamer147
ee808a60a2 feat(viewer): add MyPageBgSelectType + MyPageBgId scalars + MyPageBgRotation owned collection
Adds BGType persistence (0=Deck/1=CustomBG/2=RandomBG) to Viewer via two scalar
columns and an owned collection keyed (ViewerId, Slot). Two persistence tests
confirm round-trip and zero-defaults on fresh viewers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:26:48 -04:00
gamer147
8de78ba7ed test(inventory): lock Unknown-source fallthrough behaviour
Adds Commit_writes_history_row_with_Unknown_source_when_caller_omits_Source
to InventoryHistoryTests — asserts that BeginAsync without a configure callback
writes AcquireType=0 / Message="Unknown", per spec §10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:33:04 -04:00
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