Compare commits

...

131 Commits

Author SHA1 Message Date
gamer147
20ddba4c5f Merge battle-engine-extraction: engine port + multi-instancing
Lands the Phase 1 + Phase 2 + multi-instancing migration as one mergepoint.

Phase 1 — engine extraction (M1-M14): client's BattleManagerBase ported
headless under SVSim.BattleEngine; deterministic emit; authoritative RNG
seam (Q-RNG/F2); single-card resolution set proven.

Phase 2 — headless conductor (M-HC-0..4 + M-HC-exit): receive conductor
runs headless inside the battle node via no-op view shims + InstantVfx
rule (zero Engine/*.cs edits, check_drift.py clean). Six structural
shadow-vs-wire divergences resolved against live PvP traffic.

Multi-instancing: per-battle engine statics now AsyncLocal-scoped via
BattleAmbientContext (Mgr, GameMgr, ViewerId, IsForecast, IsRandomDraw,
RealTimeNetworkAgent, BattleRecoveryInfo). EngineSessionGate deleted;
SessionBattleEngine wraps all entry points; MultiInstanceEngineTests is
the regression oracle.

Parallelism hardening: Resources._loaded + GameObject._components shim
Dictionaries -> ConcurrentDictionary; Wizard.LocalLog mutations gated by
single static lock. SVSim.BattleEngine.Tests now runs under
ParallelScope.Fixtures; StressN parallelizes Setup AND Drive.

Pending: live two-pair PvP smoke (Task 10 of the multi-instancing plan).

Suite: SVSim.UnitTests 1054/1054; SVSim.BattleEngine.Tests 59/2 (skips
pre-existing). Audit script tools/engine-port/audit-static-writes.ps1
green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-08 10:21:09 -04:00
gamer147
5a23f93152 docs(engine-ambient): explain why _components GetOrAdd factory is contention-safe
Reviewer noted the factory may be invoked more than once under contention.
Document the analysis inline so a future reader doesn't have to redo it:
the discarded instance's mutations land on private fields of a soon-unreachable
object, and the only shared sentinel (_noopViewMaterial) is read-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-08 08:25:34 -04:00
gamer147
fbac66fd0b chore(engine-ambient): harden shim + LocalLog statics for fixture parallelism
Follow-up to the multi-instancing migration. Wraps the process-shared engine
statics that aren't ambient-fronted but race between concurrent battles:

- UnityEngine.Resources._loaded: Dictionary -> ConcurrentDictionary.GetOrAdd
  (the shared prefab cache keyed by path; concurrent first-misses produced
  duplicate GameObjects + Dictionary corruption)
- UnityEngine.GameObject._components: Dictionary -> ConcurrentDictionary with
  Interlocked.CompareExchange init (Resources.Load returns SHARED prefab
  GameObjects, so two engines' Setup() can race on the same _components map
  — surfaced as "Operations that change non-concurrent collections" crashes
  during BattleManagerBase ctor's GetComponent<T>() chain)
- Wizard.LocalLog: single static lock around all mutating entry points
  (StringBuilder _lastTraceLogStringBuilder + ~12 mutable string/bool/int
  scratch fields; serializing the trace-log surface is cheap since logging
  is not the hot path)

Flips SVSim.BattleEngine.Tests assembly Parallelizable scope from Self to
Fixtures and restructures MultiInstanceEngineTests.StressN_BaselineMatches so
Setup runs INSIDE Task.Run (was previously serialized as a workaround for the
LocalLog races). The fixture is also lifted to ParallelScope.All so the
two-engines and stress tests can run alongside each other.

Suite fully green under fixture parallelism (59/0/2 across 3 consecutive runs);
SVSim.UnitTests still 1054/0/0 — true multi-instance correctness is now proved
end-to-end in tests rather than gated behind a serial workaround.

Manifest sha refresh + new patch artifact for the LocalLog edit (decomp-origin);
the two shim files are authored, so no metadata update is needed for them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-08 08:02:49 -04:00
gamer147
45344e6d83 chore(engine-ambient): audit script for static-write regressions
Step 9 of multi-instancing migration. PowerShell audit fails CI if anything
references the deleted BattleManagerBase.main field or introduces a new
Thread() outside the LeanThreadPool allowlist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 23:31:09 -04:00
gamer147
ab4545b274 test(engine-ambient): tighten MultiInstanceEngineTests post-setup assertions
Replace trivially-true Pp>=0 with concrete post-Setup pins (LeaderLife=20,
Pp=0, HandCount=3). Drop the unused seed parameter from SampleDeck - every
call already returned the same vanilla deck, and the StressN test name 'Random
Decks' overpromised. The cross-contamination property the test pins (parallel
LeaderLife[] equals sequential LeaderLife[]) holds with identical decks +
distinct masterSeeds, which is what's actually being verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 23:28:21 -04:00
gamer147
c789d836f1 feat(engine-ambient): delete static fallbacks; add MultiInstanceEngineTests
Step 8 (final) of multi-instancing migration. All per-battle statics now
require a BattleAmbient scope — unwrapped writes throw InvalidOperationException
(fail-fast forcing function). MultiInstanceEngineTests proves correctness:
two parallel battles resolve independently, N=4/8/16 stress matches sequential
baseline, GameMgr.GetIns throws without scope.

Migration complete. EngineSessionGate gone. Suite fully green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 23:19:37 -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
8af1be6555 test(engine-ambient): TestBattleScope + HeadlessFixture split for multi-instance
Step 6 of multi-instancing migration. HeadlessEngineEnv.EnsureInitialized
is split into EnsureProcessGlobals (idempotent, process-once) +
SeedCharaIdsOnCurrentAmbient (per-test). New TestBattleScope IDisposable
sets up a fresh BattleAmbientContext per test. NonParallelizable removed
from converted classes; assembly-level Parallelizable(Fixtures) enabled.

SVSim.BattleEngine.Tests fully green under parallel test execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 22:24:21 -04:00
gamer147
1ba75c565a refactor(engine-ambient): GameMgr.GetIns throws Require; wrap SessionBattleEngine entry points
Step 5 of multi-instancing migration. GameMgr.GetIns() now resolves through
BattleAmbient.Require() (throws when no scope active — fail-fast since engine
callers unconditionally dereference). SessionBattleEngine now owns a single
BattleAmbientContext, pushed via BattleAmbient.Enter at Setup/Receive/all
~30 read accessors and Debug* seams.

EngineGlobalInit.WirePerSessionGameMgr extracted out of the _done-gated block:
GameMgr is now per-session (ctx.GameMgr is a fresh `new()` per SessionBattleEngine),
so the DataMgr chara ids + NetworkUserInfoData seeding must run every Setup, not
process-once. The wiring itself is already idempotent. Without this, second-or-
later sessions in a process NRE in NetworkBattleManagerBase.CreateBackgroundId.

Expected state: SVSim.BattleEngine.Tests have known-failing tests that don't
go through SessionBattleEngine (Task 6 wraps HeadlessFixture). SVSim.UnitTests
mostly recover; residual failures (deal-frame Accepted:false in conductor
integration tests) are captured in
data_dumps/task5-test-output/failing-tests-after-task5-node-postwrap.txt for
Task 7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:56:34 -04:00
gamer147
18da7fd19e test(engine-ambient): cover BattleRecoveryInfo setter ambient write-through
Setter is the asymmetric one (write-through inside scope, unlike ViewerId's
no-op-in-scope) — adding parity with the SetRealTimeNetworkBattle ambient
setter test to catch future regressions if the routing branch is touched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:42:55 -04:00
gamer147
fe146fde50 refactor(engine-ambient): ViewerId/RealTimeNetworkAgent/BattleRecoveryInfo read ambient first
Step 4 of multi-instancing migration. Three additional per-battle statics
front-fronted by BattleAmbient.Current, each with a static fallback for
unwrapped callers. ViewerId's SavedataManager-persisting setter is preserved
on the fallback path; inside a scope, the setter is a no-op (the per-battle
perspective is fixed at scope entry).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:37:58 -04:00
gamer147
4e756a6c46 refactor(engine-ambient): BattleManagerBase.GetIns reads ambient first, static fallback
Step 3 of multi-instancing migration. The dominant per-battle singleton now
resolves through BattleAmbient.Current.Mgr when a scope is active. The legacy
'main' field is renamed _mainFallback and retained for unwrapped callers
(tests, anything not yet scope-wrapped). GetIns() still returns null when
neither is set, preserving the '?.Foo ?? default' patterns in engine code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:23:27 -04:00
gamer147
92da7819f4 chore(engine-ambient): refresh BattleManagerBase manifest sha + add patch artifact
Hygiene fixup for the IsForecast/IsRandomDraw ambient conversion in 3b5f2e1.
The manifest sha was stale (pointed at the pre-ambient RNG-virtual-patched
contents) and the change had no companion .patch artifact alongside
BattleManagerBase.rng-virtual.patch. Follow established convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:16:57 -04:00
gamer147
3b5f2e18b3 refactor(engine-ambient): IsForecast/IsRandomDraw read ambient first, static fallback
Step 2 of multi-instancing migration. Both flags now resolve through
BattleAmbient.Current when a scope is active, otherwise hit a static fallback
that preserves today's behavior unchanged for unwrapped callers.

Suite green: SVSim.BattleEngine.Tests pass; SVSim.UnitTests baseline holds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:11:49 -04:00
gamer147
4829e8c263 feat(engine-ambient): add BattleAmbientContext + AsyncLocal scope
Step 1 of the engine multi-instancing migration. Standalone infrastructure;
no engine static reads/writes through it yet. Scope is reentrant (restores
prior on dispose), AsyncLocal flows across awaits, and isolated between
concurrent Task.Run flows.

See docs/superpowers/specs/2026-06-07-engine-multi-instancing-design.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 21:04:21 -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
2a8c44a6d7 build(battleengine): pin LangVersion 12.0 (was 'latest') so C# 14's 'field' keyword doesn't break the decompiled engine under newer SDKs 2026-06-07 08:03:01 -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
97e4664cc4 docs(battlenode): regen-guard banners on hand-edited .g.cs + accessor-band null-policy invariant (M-HC-4 final review) 2026-06-07 01:02:00 -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
7a02cb3626 docs(battlenode): note _playerInfoPair seeded ahead for evolve (M-HC-4a review) 2026-06-06 22:59:16 -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
e96cc3363c refactor(battlenode): guard generated iface-impl against regen + stub visibility (M-HC-0 review)
- _IfaceImpl.g.cs: extend header to warn about hand-edits; tag all bare
  // HEADLESS-FIX lines with their milestone (M13 on GetSideLogControl ×2)
  so `grep HEADLESS-FIX` reliably surfaces every block before a regen.
- HeadlessHandViewStub / HeadlessPlayQueueViewStub: narrow from public to
  internal sealed — both stubs are consumed only within SVSim.BattleEngine
  (via the generated partial impls); no public surface exposes the concrete
  type, so internal is correct and aligns with HeadlessIconAnimations.
- SessionBattleEngine.SeedMulliganInfoControl: add one-line comment on the
  GetComponent<MulliganInfoControl>() call explaining the shim's lazy
  materialisation behaviour (otherwise reads like a guaranteed NRE).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:20:34 -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
fcc30ffe5e refactor(battlenode): drop obsolete pre-ingest spellboost peek (Phase 2 revised, O-HC-5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 19:28:21 -04:00
gamer147
fcd64c8c11 feat(battlenode): engine read surface for played-card spellboost (Phase 2 N2 Task 3 — oracle BLOCKED)
Adds SessionBattleEngine.PlayedCardSpellboost + PeekPlayedCardSpellboost (pre-resolve
read of the acting seat's hand card by Index==playIdx) and a CaptureReplay.InterleavedSends
helper. The non-circular capture oracle (engine-derived spellboost vs prod's independent
emission to cl2: idx2->1, idx14->2) is added but [Ignore]'d: the headless receive path does
not apply the wire's authoritative orderList (Deal/Swap don't seat the mulligan hand, draws
follow the seeded deck top instead of the wire move ops, plays never remove the card, alter
spellboost never accumulates), so the engine cannot yet DERIVE the count. Closing this needs
an Engine/*.cs + VfxMgr-execution logic change (escalation per the N2 playbook), not a
mechanical no-op fill. Read surface, node + engine builds, drift, and the rest of the
SessionEngine suite are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:55:47 -04:00
gamer147
eb52890251 feat(battlenode): per-session charaId + single-active-engine gate (Phase 2 N2 carried-risk B)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:35:42 -04:00
gamer147
6e8af4e68b fix(battlenode): EngineGlobalInit guarantees full-master postcondition (Phase 2 N2 review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:29:04 -04:00
gamer147
5e0723c182 feat(battlenode): host-owned engine global init (Phase 2 N2 carried-risk A)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:19:21 -04:00
gamer147
e982300c6d feat(battlenode): inject SessionBattleEngine into BattleSession in pure shadow (Phase 2 N1 exit)
The engine is constructed per session, seated once from the master seed + both
shuffled decks (F-N-5), and fed each frame via ShadowIngest — all inside a
try/catch in ComputeFrames so a shadow failure can never break live dispatch
(ND1/ND6). Routes still come from the existing handlers: wire output is
byte-for-byte unchanged. FrameDispatchContext gains the Engine ref for N2+.

csproj: PrivateAssets=compile on the engine ref so its global-namespace type
surface (MessagePackSerializer, UserConfig, UserCard, ChallengeConfig, ...) does
not leak transitively into SVSim.EmulatedEntrypoint (which references BattleNode)
and collide with that project's own types; the runtime DLL still flows.

All 238 BattleNode unit tests pass; EmulatedEntrypoint builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:35:35 -04:00
gamer147
fa86739ac2 test(battlenode): N1 shadow replay tracks captured battle state (Phase 2 N1)
Full single-client capture replay (cl1 send=player seat, receive=opponent seat,
ts-ordered) ingests end-to-end: 33 frames, 0 rejects, 0 invariant violations at
turn boundaries (leader life/PP/board/hand).

Headless gaps filled per playbook (no Engine/ drift):
- IsRecovery=true after construction: the engine's own headless replay mode gates
  the live view/UI layer off (BattleUIContainer, turn-control UI, VFX waits) while
  keeping the live NetworkBattleReceiver (ND4) and authoritative state.
- Seed ToolboxGame.RealTimeNetworkAgent, BattleUIContainer, _backGround, and
  per-player NullPlayerEmotion no-ops the receive/turn cycle dereferences.
- _IfaceImpl.g.cs (shim, not Engine/): BattleCardView.BattleCardIconAnimations
  returns a lazy non-null no-op so the opponent card-reveal icon-init (deferred
  VFX) doesn't NRE.
- HeadlessCardMaster.Load made cumulative: it replaced the global CardMaster each
  call, so a Load(deck) evicted the oracle card set and broke tests run after.

Adds board-state accessors (LeaderLife/Pp/HandCount/BoardCount) and CaptureReplay
ts ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:28:08 -04:00
gamer147
6740313446 feat(battlenode): Receive ingests a captured PlayActions headless (Phase 2 N0)
Receive feeds the decoded frame into the mgr's own NetworkBattleReceiver
(isHaveSequence:true, checkBreakData:false — mirroring the engine's
RecoveryDataHandler frame replay), reboxing object?->object for nested data.
No engine gaps surfaced; the only fix was a test-harness one (load all deck ids
in a single HeadlessCardMaster.Load — per-id calls each replace the master).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:05:36 -04:00
gamer147
eaa7b4d85c test(battlenode): capture-replay helper + battle_test fixtures (Phase 2 N1)
CaptureReplay normalizes the capture's send/receive envelope asymmetry (send
frames carry uri at top level + bare payload body; receive frames carry a full
envelope body) and extracts selfDeck + master seed from Matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 14:59:48 -04:00
gamer147
c9841c012b feat(battlenode): Setup builds two-seat network battle headless (Phase 2 N0)
Mirrors HeadlessFixture.NewNetworkEmitBattle wiring (opponent seating, leader
life, card templates, deck seeding) minus the emit-only RealTimeNetworkAgent
scaffolding (shadow only receives). Probe passed first run — M13 already filled
the network-mgr construction gaps. No Engine/ edits; drift clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 14:51:51 -04:00
gamer147
f6cbde723b feat(battlenode): SessionBattleEngine skeleton + types (Phase 2 N0)
SessionContentsCreator mirrors the test HeadlessContentsCreator fully (all
IBattleMgrContentsCreator members) so it compiles; Setup/Receive throw pending
the Task 3/4 probes. New files use the 'engine' extern alias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 14:49:18 -04:00
gamer147
83f82efe1b feat(battlenode): reference SVSim.BattleEngine (Phase 2 N0 wire-up)
Aliased (extern alias 'engine') to confine the decompiled engine's large
global-namespace type surface, which would otherwise collide with node types
(BattlePlayer, MessagePackSerializer). Also expose internals to
SVSim.BattleEngine.Tests for the upcoming N0/N1 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 14:46:37 -04:00
gamer147
e6a561b30f test(battle-engine M13): shared NetworkEmitFixtureBase teardown — close IsForecast/agent global leak
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:03:07 -04:00
gamer147
bfd99c4829 docs(battle-engine M13): note _notEmit precondition on TryReadStockedEmitData (review polish)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:42:45 -04:00
gamer147
feb47f6437 test(battle-engine M13): best-effort emit-payload presence (Inconclusive => deferred to structural validation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:37:24 -04:00
gamer147
73286ba78b chore(battle-engine M13): align OnEmit line-cite + HEADLESS marker spelling (review polish)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:34:53 -04:00
gamer147
ac0886389a feat(battle-engine M13): M3 spell emits PlayActions headless via OperateMgr -> NetworkBattleSender (O1 read = GO)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:23:51 -04:00
gamer147
25e9ae9573 test(battle-engine M13): NewNetworkEmitBattle harness + OnEmit capture seam
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:00:04 -04:00
gamer147
6b2c825eb8 chore(battle-engine M13): drop unused using + complete shim comment (review polish)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:56:23 -04:00
gamer147
2f6bc5b6c0 test(battle-engine M13): HeadlessNetworkBattleMgr constructs headless (construction probe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:48:52 -04:00
gamer147
0fe45517da test(rng-seam): reset IsRandomDraw in RandomDrawOracleTests teardown (avoid cross-fixture leak) 2026-06-06 10:57:39 -04:00
gamer147
ffc0fcaa43 test(rng-seam): M12 oracle — scripted RNG draws a known deck card (genuine multi-outcome roll) 2026-06-06 10:46:35 -04:00
gamer147
2fd0aac5b6 test(rng-seam): M12 constants + NewAuthoritativeBattle harness factory 2026-06-06 10:40:59 -04:00
gamer147
f6e3b67be1 docs(rng-seam): note stableRandomCount divergence in HeadlessBattleMgr 2026-06-06 10:38:58 -04:00
gamer147
c47f8d9fa7 feat(rng-seam): HeadlessBattleMgr override + decoupling/parity tests (F2 resolved)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:33:59 -04:00
gamer147
201158db5d patch(rng-seam): make StableRandomDouble/StableRandomOnlySelf virtual (DP5, zero logic change)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:25:35 -04:00
gamer147
1a108fa393 feat(rng-seam): ScriptedRandomSource (throw-on-overrun deterministic source) 2026-06-06 10:21:44 -04:00
gamer147
2fd42c10cf feat(rng-seam): SeededRandomSource mirrors the engine's two System.Random streams 2026-06-06 10:18:19 -04:00
gamer147
c77d789558 feat(rng-seam): IRandomSource interface + RandomSourceBridge arithmetic
Adds the RNG seam skeleton (Task 1 of M12): IRandomSource (NextUnit/NextSelf)
and RandomSourceBridge.Range mirroring BattleManagerBase.StableRandom exactly
(`(int)Math.Floor(val * unit)`). RngSeamTests pins the floor arithmetic (1 test, passing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:14:50 -04:00
gamer147
7370a35e9c test(battle-engine-port): M11 — gated conditional resolves headless (the GATE is the oracle)
Card 103111050 (ELF cost-1 self-buff follower) carries skill_condition
`character=me&target=self&play_count>2`. New GatedConditionalOracleTests asserts
BOTH branches of the SAME card in one fixture, varying only the seeded per-turn
play count via the public AddCurrentTrunPlayCount seam (M4/M10):

  * gate TRUE  (seed 5 > 2)  -> when_play powerup fires -> 1/1 -> 2/2
  * gate FALSE (seed 0 <= 2) -> powerup is a NO-OP (stays 1/1), BUT the card
    still pays its cost and still moves hand -> board.

This proves the engine SUPPRESSES an effect when a skill_condition is false (the
dual of "effect fires" — no prior milestone proved this), and that the gate
suppresses the EFFECT, not the PLAY. Jointly satisfiable only by a correctly-
gating engine: an always-buffs engine fails FALSE, a never-buffs engine fails
TRUE. Reuses the M4-proven buff dimension so the only new thing under test is the
conditional itself.

11/11 green; engine 0 errors; check_drift clean; ZERO new Engine copies / ZERO
shim / ZERO manifest changes — a clean milestone like M4/M6/M8/M10 (condition
evaluation is pure logic on copied engine code).

Load-bearing proof (M4/M6/M8/M10 discipline; the test passed on its first run,
which proves nothing alone): swapped the two seeds -> exactly the 4 stat
assertions failed for the right reason (formerly-TRUE branch seeded 0 took no
buff [1/1, expected 2/2]; formerly-FALSE branch seeded 5 buffed [2/2, expected
1/1]), while the cost-paid + hand->board assertions stayed green in both branches
— confirming the gate drives ONLY the effect and the card resolves regardless.
Reverted -> 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:10:45 -04:00
gamer147
c3590e9c9b test(battle-engine-port): M10 — first dynamic {}-value card resolves headless
DynamicValueSpellOracleTests proves the engine COMPUTES an effect magnitude
from live game state (the value the wire can't carry). Card 112134010's
`when_play damage={me.play_count}-1` resolves via the proven IsForecast/
IsRecovery + ActionProcessor.PlayCard (DP4) path; the oracle asserts the
damage equals the engine's own live GetCurrentTurnPlayCount() - 1, not a
literal. Seeds play_count via M4's AddCurrentTrunPlayCount seam; lone
surviving enemy 13/13 gives a clean life-delta; selectedCards: null
(auto-target AoE). 10/10 green; zero Engine/shim/manifest changes; drift
clean. First-unknown resolved by the first RED: the per-play +1 lives in
OnBeforePlayCard (wired only via OperateMgr/Prediction), so the direct-
ActionProcessor harness reads exactly the seeded count (damage == seeded-1);
load-bearing proven by varying the seed 4->7 and watching damage track 3->6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:02:59 -04:00
gamer147
eee8450144 feat(battle-engine-port): M9 COMPLETE — when_play draw resolves headless (hand/deck-delta oracle)
Proves the deck->hand transfer dimension (design §5 draw oracle) — the last
deterministic, non-RNG card-effect class no prior milestone touched (M3/M4/M6/M8
moved stats, M2/M5/M7 the board, M3 the leader).

Card 800114010 (clan-1 ELF cost-1 when_play draw 1 from own deck, ungated, no
evo/preprocess). The resume-guide's skill_target=none/no-RNG shape does not exist
in cards.json — EVERY draw selects from the deck via a random_count filter
(skill_option is always literally 'none'). RNG neutralized structurally: seed the
deck with EXACTLY ONE known card so random_count=1 is deterministic regardless of
seed. New primitive HeadlessEngineEnv.SeedDeck (create via the null-view seam +
engine AddToDeck). Oracle DrawSpellOracleTests asserts: seeded card moves deck->hand
(by id + by reference), deck -1, drawn card IsInHand, spell pays cost + leaves hand
+ resolves to cemetery, board/opponent untouched. Load-bearing confirmed the M7 way
(seed a different id -> the by-id assertion fails).

Shim gap fixed (the predicted M9 cost): Skill_draw's BattleLog tail
(UpdateFusionedCardSkillDrewCard, unguarded; + the IsBattleLog AddLogSkillDrawCard
calls) dereferences BattleLogManager.GetInstance(), an M1 'default!' null singleton
-> NRE after the draw already committed. One-line HEADLESS-FIX (M9) in
BattleLogManager.g.cs returns the existing _instance singleton (all its methods are
no-ops), per the M2/M7 Null*-singleton playbook. No Engine/ edit (drift clean).

9/9 green; check_drift.py clean; engine still 0 Error(s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:47:04 -04:00
gamer147
4f76fb21f0 feat(battle-engine-port): M8 COMPLETE — lethal damage proves follower death via combat math
A when_play damage=5 spell (the M6 card 800134020) played at a select_count=1
enemy follower with life <= 5 kills it as a consequence of damage -> life <= 0 ->
the dead-check + the same RemoveInplayCard/cemetery path M7 lit up (the dominant
real-card removal mechanic), reached through combat math rather than `destroy`.

Oracle LethalDamageSpellOracleTests: selected follower (1/2) removed (board -1 +
cemetery +1, the M7 dimension); un-selected control (6/7, life > 5) untouched and
still on board (M6 routing; select_count=1 hits only the selected target). 8/8
green; engine 0 errors; check_drift clean; ZERO new Engine/shim/manifest work —
the death path inherited M7's death-voice fix; the predicted damage-VFX shadow
never materialized.

Load-bearing (M4/M6 discipline): swapping the selection to the 6/7 -> it survives
at 2 and nobody dies, proving removal is gated on the SELECTED follower's life
reaching <= 0, not on selection (M7's destroy) or a blanket wipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:35:02 -04:00
gamer147
9fc97abee7 feat(battle-engine-port): M7 COMPLETE — targeted destroy resolves headless (follower death / board-removal)
First proof that follower DEATH / board-removal commits in the authoritative
part of PlayCard headless (not the cosmetic post-Process tail). Card 800144120
(cost-0 when_play destroy of a select_count=1 enemy follower) resolves via the
M6 selectedCards path: selected enemy follower removed (board -1 + cemetery +1),
un-selected untouched (routing confirmed load-bearing by swapping the selection).

Shim gap fixed (the predicted M7 cost): SkillProcessor.SelectCardToHaveDestroyVoicePlay's
cosmetic death-voice tail NRE'd on three M1 default!/Null* shadows
(IBattleCardView.VoiceInfo, CardVoiceInfoCache.GetCardVoiceInfoForBattle,
ReadOnlyVoiceInfo.GetDestroyVoice — the last unusable as the interface since
m1_stub_gen dropped its : IReadOnlyVoiceInfo base). Fix = one hand shim
HeadlessVoiceInfo : IReadOnlyVoiceInfo returning the engine's own
VoiceAndWaitTime._nullVoice sentinel, wired into the two generated seams with
// HEADLESS-FIX markers. No Engine/ edit (drift clean).

dotnet test SVSim.BattleEngine.Tests -> 7/7 green; check_drift.py clean; engine 0 Error(s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:23:53 -04:00
gamer147
c8314bd3c0 test(battle-engine-port): M6 COMPLETE — targeted when_play damage spell resolves headless (selection-routing oracle)
First card to exercise the selectedCards path of ActionProcessor.PlayCard
(dormant through M2-M5, all of which played selectedCards: null). Spell
800134020 (clan-1 cost-1, when_play damage=5 to a select_count=1 enemy
follower) resolves headless: with two vanilla followers on the enemy board
and one passed as selectedCards, the damage hits ONLY the selected follower
(13->8) and the un-selected one is untouched (7).

New oracle dimension: SELECTION ROUTING via a differential life-delta on two
surviving targets (selected -5, un-selected 0) — reads the authoritative
damage path M3 proved, with no dependence on follower death/board-removal
timing. Load-bearing confirmed (M4 discipline): swapping which follower is
selected makes the damage follow the selection (assertions fail for the right
reason), then reverted to green.

Like M4, a clean milestone: NO new engine/shim work — the selectedCards path
resolved on the existing shim surface. The only authoring was test-side: the
M6 card constants, a shared HeadlessEngineEnv.PutFollowerInPlay primitive
(create via the null-view seam + drive HandCardToField), and the oracle.

Engine still 0 errors; check_drift clean; dotnet test -> 6/6 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:08:01 -04:00
gamer147
62a28fe2d4 feat(battle-engine-port): M5 COMPLETE — summon_token spell resolves headless (board-count delta oracle)
Card 800134010 (clan-1 cost-1 ungated spell, summon_token=100011020): a when_play
summon places one new neutral 2/2 follower token on the caster board. New oracle
dimension = board-count + token-identity delta from a SKILL-CREATED card. 5/5 green;
engine 0 errors; check_drift clean; zero new Engine copies.

This is the first headless run of the PUBLIC prefab card-creation path
(CardCreatorBase.CreateCard, createNullView:false) — engine-internal card creation
(summon/draw/token) has no null-view path in solo mode, unlike the M2-M4 hand-card
seam. Built that path headless:
- Self-consistent no-op Unity object graph (UnityShim.cs): Component.gameObject/
  transform, GameObject.transform, Transform.parent/Find now lazily non-null +
  cached; GetComponent routed through the GameObject component model.
- Targeted NGUI material backing-field wiring (UIFont.mMat / UILabel.mMaterial) so
  the copied material getters return non-null via their simple branch (blanket/deep
  wiring would make them delegate down a re-nulling chain).
- getUIBase_CardManager() default! -> field-wired no-op via new ShimView.Create<T>().
- Test-side seeds: SBattleLoad card templates + 3D scene GameObjects (InitCardTemplates).

Load-bearing proof: swapping to the M3 non-summoning spell fails the board-count
(Expected 2, was 1) + token-not-found assertions; reverted to green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 03:19:47 -04:00
gamer147
b13cfa0fad test(battle-engine-port): M4 COMPLETE — when_play self-buff follower resolves headless (4/4 green)
Fold SetupCardEvent into a shared HeadlessEngineEnv.CreateHeadlessHandCard primitive
(consolidating the duplicated M2/M3 helpers), then add the M4 oracle: card 103111050
(ELF cost-1 1/1, when_play powerup add_offense=1&add_life=1 to target=self). New oracle
dimension = the played card's OWN stat delta (1/1 -> 2/2). Gate play_count>2 seeded via
the public AddCurrentTrunPlayCount; proven load-bearing (without the seed the fanfare
gates out and Atk stays 1). No new shim/data gaps were needed — only harness seeding.
Engine still 0 errors; check_drift clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 02:36:02 -04:00
gamer147
c47ae93027 feat(battle-engine-port): M3 COMPLETE — fixed-damage spell resolves headless (leader-life-delta oracle passes)
Card 900124030 (ELF cost-3, when_play damage=3 to enemy leader) resolves to
correct authoritative state headless via the IsForecast/IsRecovery +
ActionProcessor.PlayCard path. New oracle dimension (opponent leader-life delta)
passes; 3/3 tests green; engine still 0 errors; check_drift clean.

Four headless gaps, each mechanical (no logic/Unity wall):
- Data seam: InitLeaderLife (SetupInitialGameState->InitializeClassLife subset);
  leader BaseMaxLife was 0 => game-over => play silently rejected. M2 missed it
  (only asserted leader life unchanged: 0==0).
- Runtime cast: re-attach IClassBattleCardView on the generated
  NullClassBattleCardView stub (members already present; base-clause recovery
  stripped the decl). Compiled fine -> M1 loop never surfaced it.
- M1 mis-cut: copy NullVfxWithLoading verbatim (its GetInstance() lazy singleton
  was stubbed to default!/null). Same pattern as M2 NullCardVfxCreator.
- Card events: CreateHeadlessHandCard now calls SetupCardEvent so a spell's
  OnPlay->RemoveSpellCardFromHand / OnFinishWhenPlaySkill->AddSpellCardToCemetery
  fire (the bare CreateCardWithoutResources seam skips them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 02:19:54 -04:00
gamer147
171f07ec74 feat(battle-engine-port): M2 COMPLETE — vanilla follower resolves headless (go/no-go = GO)
First green: a zero-skill vanilla follower (100011010, neutral 1/2) resolves to
correct authoritative state HEADLESS via IsForecast/IsRecovery + ActionProcessor.
PlayCard (DP4), no Unity runtime. §5 oracle passes (PP-cost; hand->in-play;
atk/health == CardCSVData base; opponent unchanged; no exception). VERDICT: the
port approach is validated through the resolution path, not just M1's compile path.

VanillaFollowerOracleTests.Vanilla_follower_resolves_to_correct_state — GREEN.
HeadlessCardMaster now loads the follower's real id from cards.json.

Resolution-path shim/engine gaps closed (all mechanical no-op fills or data seams,
never a Unity/logic wall):
- M1 mis-cut copies (DP1/DP3 — pure no-op logic wrongly stubbed to null):
  Engine/Wizard.Battle.View.Vfx/NullCardVfxCreator.cs (its GetInstance() singleton
  was nulled) + its dep NotEmptyNullVfx.cs. Deleted the generated NullCardVfxCreator
  stub + its _IfaceImpl block; both manifested, check_drift clean.
- _IfaceImpl explicit-impl shadow: interface-typed view/mgr calls dispatch to the
  explicit impls (which returned default!), shadowing public stubs. Fixed
  IBattlePlayerView.GetSideLogControl (SkillProcessor side-log tail) to return a
  non-null no-op. KEY M3+ learning: fix _IfaceImpl.g.cs for interface-typed NREs.

(GameMgr/component-model/Resources/IClassBattleCardView shim fills + CardIconControl
copy + the SVSim.BattleEngine.Tests project landed in the prior commit 2b50657.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:57:15 -04:00
gamer147
2b506574e7 feat(battle-engine-port): M2 step 1 — SingleBattleMgr constructs headless
First green of the M2 go/no-go probe: `new SingleBattleMgr(StandardBattleMgr-
ContentsCreator)` now builds the two-player pair fully headless against the shim,
no Unity runtime. Verdict: headless construction is feasible; every blocker was a
mechanical no-op shim fill or data seam, not a Unity/logic wall.

Shim fills (authored):
- GameMgr: lazy non-null DataMgr/PrefabMgr/InputMgr/SoundMgr/BattleControl.
- GameObject: lazy cached component model so GetComponent<T>/AddComponent<T> return
  non-null no-op instances for Component-derived T (F1: unguarded view touches).
- Resources.Load(string): cached non-null GameObject so the prefab->Instantiate->
  GetComponent chain (UnityEventAgent) yields a real object.
- ClassBattleCardViewBase: re-attach dropped IClassBattleCardView (no-op members);
  ClassBattleCardBase.Setup casts the created view to it.

Engine copy (DP1/DP3 mis-cut fix):
- CardIconControl.cs copied verbatim (manifested) + generated null-stub deleted.
  SplitAndCompleteIconStr is pure string logic on the resolution path that M1 had
  wrongly stubbed as "View" -> null deref in SkillCreator.CreateBuildInfo.

Test harness (SVSim.BattleEngine.Tests, authored fixture):
- HeadlessContentsCreator/HeadlessPhaseCreator: deterministic replica of the solo
  practice init (StandardBattleMgrContentsCreator + SingleBattlePhaseCreator) with
  no-op recovery/replay managers.
- HeadlessCardMaster: reflects the loader cards.json dump into CardMaster.
- HeadlessMasterData: minimal Data.Master (class-character list, empty collections)
  + Data.Load + player/enemy chara ids.
- ConstructionProbeTests.SingleBattleMgr_constructs_headless — GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:36:22 -04:00
gamer147
1078b1ef50 port(m1): wave 7k — M1 COMPLETE: 0 compile errors, headless engine builds (12->0)
Final three clusters:
- RoomParamKey: copy Wizard.RoomMatch/RoomParamKey.cs verbatim (UriNames/WatchUriNames
  static dicts keyed by PlayerController.ROOM_URI + PlayerControllerForWatching.
  SEND_PARAMETER — both now real enums).
- CardChooseTask: copy the TwoPick/CardChooseTask.cs (TaskManager `using`s .Arena.TwoPick,
  not .Competition — copy_loop had only landed the Competition twin).
- SetCardNumLabel CS1739: decompiler param-name artifact — the local fn's 3rd param was
  recovered as `flag` but call sites pass it named `isRed:`. First DP5 tracked patch:
  Engine/UICardList.cs edited (flag->isRed, zero logic change), recorded in
  Patches/ + manifest patched=1 (drift-clean).

M1 exit criteria met: `dotnet build SVSim.BattleEngine` = 0 errors, no Unity ref in csproj,
check_drift clean. Session 7: 198 -> 0 across waves 7a-7k.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:03:58 -04:00
gamer147
f63d1cc2e2 port(m1): wave 7j — Material/Plane/Socket overloads + IDictionary extension (24->12)
- Material.SetVector(int nameID, Vector4); Plane(Vector3, float d) ctor; Socket.On
  (SocketIOEventTypes, callback) overload.
- Global GetValueOrDefault(this IDictionary<,>) extension — the BCL form only binds to
  IReadOnlyDictionary, so the IDictionary call mis-resolved to JsonDataExtension.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:00:12 -04:00
gamer147
ad58994b8e port(m1): wave 7i — RoomMatch/Story/Effect app members + ROOM_URI enum (40->24)
- PlayerController.ROOM_URI nested enum (verbatim-generated).
- RoomRoot.CreateChangeSceneDialog, StoryRecoveryData.ToJsonData,
  BattlePlayerViewBase.IsSelecting, StorySelectionWorldScene.RedirectSectionId,
  EffectMgr.LoadAndInstantiate2dEffectCoroutine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:57:51 -04:00
gamer147
59cb089c97 port(m1): wave 7h — Unity overloads + SDK return types + RoomRoot:UIBase (56->40)
- Resources.LoadAsync(string) non-generic, LayerMask implicit-from-int, Animation
  string indexer.
- SDK return-type fixes: RedShellSDK.MarkConversion/LogEvent return IEnumerator
  (StartCoroutine arg), Packsize.Test() returns bool (!Test()).
- RoomRoot : UIBase (decomp base) so TopBar/Footer's (RoomRoot) casts succeed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:54:47 -04:00
gamer147
3a88b27752 port(m1): wave 7g — Unity coroutine/overload + app-member tail (88->56)
- MonoBehaviour.StopCoroutine(string) (iTween/NGUI StopCoroutine("name")),
  Object.DestroyImmediate(o, bool), GetComponentInParent<T>(bool includeInactive).
- App members: TitlePanelBase (:MonoBehaviour + IsFinishInit), PlayerController.Target,
  DialogManager.CreateDialogBaseOpenCardDetail, BattleLogWindow.HideCardListPanel,
  DetailPanelTouchProcessor.StopAttackTarget, StoryRecoveryData.ChapterCharaId +
  (SelectedStoryInfo) ctor overload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:52:18 -04:00
gamer147
5c5a58af3c port(m1): wave 7f — VFX containers / Create factories / dropped event / ctor cascade (112->88)
- VFX: SequentialVfxPlayer.GetAllVfxAsList, ParallelVfxPlayer.GetVfxList,
  VfxMgr.CheckAndAddEffectVfxList; point our own ShowBattleUIImmediatelyVfx stub at
  NullVfx.GetInstance() (it called a non-existent NullVfx.Create).
- Static factories: SkillTargetSelectTouchProcessor.Create (10-arg),
  DialogReportToManagement.Create(long).
- Re-add StartSkillSelectVfx.OnStart event (m1_stub_gen drops `event` decls — the
  recurring session-6 gap; generator event-capture fix still pending).
- Stop the BattleCardView/GameObjMgr ctor cascade: parameterless ctors on the no-op
  BattleCardView and GameObjMgr hand shims so non-chaining subclass/field stubs satisfy
  their implicit base() call.
- Copy Cute/ListExtensions.cs (FisherYatesShuffle extension) verbatim into Engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:49:07 -04:00
gamer147
981f903504 port(m1): wave 7e — Unity/NGUI/Spine member tail (142->112)
- isActiveAndEnabled onto Behaviour (real Unity location) — clears it on all 5
  MonoBehaviour-derived NGUI types (MyPageCardPanel/WizardUIButton/UITweenAlpha/
  UIScrollView/UICardList) in one edit.
- Touch.tapCount, Rigidbody2D.isKinematic, AnimatorStateInfo.fullPathHash,
  AnimationClip.frameRate.
- Spine: SkeletonData.Skins (List<Skin>) + Skin.Name, for SpineObject's
  Data.Skins.Any(s => s.Name == ...).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:44:04 -04:00
gamer147
7d3d92981e port(m1): wave 7d — LoginBonus/Story data ctors + nested BuildInfo/FileNamePair (158->142)
- Add the (JsonData) ctor to the LoginBonus data hand stubs (Continuous/Normal/
  Special/FreeCardPackBox) and StoryRecoveryData (LitJson.JsonData is copied).
- Full-surface the two nested View types that only the parent's empty stub covered:
  BattleCardView.BuildInfo (14-arg ctor) and DestroyVfx.FileNamePair (ctors +
  ObjectFileName/SeFileName); add BattleCardView(BuildInfo) to the hand shim.
- Regenerate Field/Spell/UnitBattleCardView: stale stubs whose ctors had dropped the
  decomp `: base(buildInfo)` chain, exposed (CS7036) once BattleCardView lost its
  implicit default ctor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:42:03 -04:00
gamer147
57f1f0c25e port(m1): wave 7c — SelectionProcessing Parameter + Touch-processor ctors (174->158)
- Generate both SelectionProcessing Parameter classes namespace-aware (full-surface
  captures the 8-arg Main / 6-arg BattleResult ctors); drop the empty hand stubs.
- Add the missing decomp ctors to the 5 empty Touch-processor hand stubs
  (SetCard/EvolutionSimple/Emotion/ClassBuff/DetailPanel) — compile-only ballast,
  empty bodies.
- Regenerate FusionWaitProcessor.g.cs: it was a stale stub whose ctor had dropped
  its decomp `: base(...)` initializer; harmless while SetCardProcessor had an
  implicit default ctor, exposed (CS7036) once the parameterized ctor landed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:38:58 -04:00
gamer147
38ab33a765 port(m1): wave 7b — Main-namespace dialog dupes + IReplayRecordManager (190->174)
Generate the Main-namespace versions of the four colliding SelectionProcessing
dialog classes (ChapterCharaDecider/DownloadInfoGetter/DeckSelectionDialogDisplay/
DeckSelectionConfirmDialogDisplay) via the new --ns path — AreaSelectUI uses the
Main module and constructs them into an IProcessing[]. baseclauses binds each to
Main.ProcessingBase; iface_reattach (regenerated full) attaches Main.IProcessing.

Also fill IReplayRecordManager with its 3 real members (SetupRecording/
SetupBattleInfoFilter/SetupOperateMgrEvents); both implementors already had them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:34:12 -04:00
gamer147
fc54dac081 port(m1): wave 7a — namespace-aware ProcessingBase collapses Story SelectionProcessing cluster (198->190)
The Story chapter-selection processing subsystem is duplicated across two
namespaces (…SelectionProcessing.Main and .BattleResult), each with its own
ProcessingBase : IProcessing + Parameter. m1_genstub keyed output by bare type
name, so only ONE ProcessingBase.g.cs was emitted (BattleResult), and
m1_baseclauses cross-qualified the Main leaves to BattleResult.ProcessingBase —
making it impossible to give IProcessing its real members (Execute(Main.Parameter)
≠ inherited Execute(BattleResult.Parameter) → CS0535).

Now both ProcessingBase variants are generated via the namespace-aware tooling
(<Type>__<Namespace>.g.cs), baseclauses resolves each leaf to its same-namespace
ProcessingBase, and both IProcessing interfaces carry NextProcessing + Execute.
8 IProcessing CS1061 cleared, no CS0535 introduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:31:29 -04:00
gamer147
6e9c5c059f port(m1): wave 6i — Networking/Facebook/BestHTTP CS0103 statics (210->198)
- UnityEngine.Networking: UnityWebRequestTexture, DownloadHandlerTexture, DownloadHandlerAssetBundle.
- Facebook.Unity.AccessToken (CurrentAccessToken/TokenString/UserId).
- BestHTTP.Decompression.Zlib.GZipStream.UncompressBuffer; global DllCheck.Test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:20:22 -04:00
gamer147
8bb392dcd6 port(m1): wave 6h — CS0246 app types + Unity members/enums (236->210)
- Generated app types: ChapterCharaDecider, DownloadInfoGetter, DeckSelection(Confirm)DialogDisplay,
  SubChapterStorySectionBtn, EvolutionHideMessageVfx; nested OpeningShowCharacterPanelVfx on OpeningVfx.
- EffectMgr.MoveType: full 47-value decomp enum (was 4).
- MonoBehaviour.print, Debug.isDebugBuild, LayerMask.LayerToName,
  SystemLanguage.Chinese, RuntimePlatform.XBOX360/BlackBerryPlayer/+console values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:15:46 -04:00
gamer147
d4364ae4b1 port(m1): wave 6g — CS1061 member cluster (304->236)
- Friend.PlayerData full-surface generated (18 members) + hand stub -> partial.
- Wizard.RoomMatch.Player: 7 friend-info props (ViewerId/Name/Rank/Emblem/Degree/Country/IsFriend).
- GameMgr: HasAuthAdmin, ChangeAspectRatio(float), Update().
- Cute.SceneManager: ChangeScene overloads + SceneChangeParameter (+SceneChangeParameter stub).
- UnityEngine.SceneManagement.SceneManager + Scene; RedShellSDK.RedShellSDK statics.
- Packet.Attachments; Vector2.Dot/Angle/Normalize.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:11:43 -04:00
gamer147
e5e05deadb port(m1): wave 6f — Unity primitive operators (362->304)
CS0019 operator gaps on value-type shims:
- Vector2: ==/!=, Vector2*Vector2, Equals/GetHashCode.
- Vector4: *float, +/-, ==/!=, Equals/GetHashCode.
- Color: ==/!= (Color==Color32 via existing implicit conv), Equals/GetHashCode.
- Rect: ==/!=; Matrix4x4: *, GetColumn/GetRow/indexer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:08:11 -04:00
gamer147
8c9fe7a1b9 port(m1): wave 6e — Unity ctors + Unity/ZXing/SFB type stubs (444->362)
CS1729 Unity ctors: Plane(3pt), Texture2D(w,h,fmt,mip), Keyframe(4),
  AnimationCurve(params Keyframe[]), WebCamTexture(name,w,h,fps), UnityWebRequest(url,method).
CS0246 Unity: AnimationState, GUIContent, TextEditor, WebCamDevice, Display,
  WaitForSecondsRealtime, AnimationBlendMode + WebCamTexture.devices.
CS0246 SDK: SFB ExtensionFilter/StandaloneFileBrowser; ZXing BarcodeFormat/Result/
  BarcodeWriter/BarcodeReader/QrCodeEncodingOptions/ErrorCorrectionLevel.
NullBattleCardView parameterless ctor (CS7036).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:06:03 -04:00
gamer147
629ae6bf98 port(m1): wave 6d — Unity method/ctor overloads (572->444)
CS1501 overload gaps (Unity):
- Transform.TransformPoint/InverseTransformPoint(float,float,float); LookAt(.,worldUp) x2.
- Object.FindObjectsOfType(Type)/(bool); Instantiate<T>(.,pos,rot,parent).
- Component/GameObject.BroadcastMessage(string,object[,opts]).
- Animator.Play(string/int, layer[, normalizedTime]).
- Mathf.Min/Max(params float[]/int[]).
- MonoBehaviour.CancelInvoke(string).
CS1729: NullBattleCardView(BuildInfo) ctor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:00:11 -04:00
gamer147
9376b35db2 port(m1): wave 6c — Unity + Steam/FB/Adjust static-class shims (696->572)
Off-battle-path static surfaces (CS0103 cluster):
- UnityStatics: Gizmos, Physics2D, Caching, GUIUtility, Cursor, ColorUtility,
  ScreenCapture, RenderSettings, JsonUtility, Social + CursorLockMode enum.
- RaycastHit2D implicit-bool operator; ILocalUser in SocialPlatforms.
- Steamworks: Callback<T>.Create, AppId_t/CSteamID/HAuthTicket/SteamNetworkingIdentity,
  MicroTxn/GetAuthSessionTicket response structs, SteamAPI/User/Utils/Client statics.
  Removed empty dup GetAuthSessionTicketResponse_t from ThirdParty.cs.
- Facebook.Unity: FB + ILoginResult + FacebookDelegate<T>; com.adjust.sdk.Adjust;
  global TimeNativePlugin/Packsize native-plugin stubs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:56:52 -04:00
gamer147
755f7fd148 port(m1): wave 6b — View base members + app-type stubs (772->696)
- BattleCardView shim: GameObject, HandFrameEffect, GetCurrentIconLayout (fixes Player/EnemyClassBattleCardView.GameObject inheritance).
- BattlePlayerViewBase.AlwaysShowStatusPanel; NullBattleCardView.ReleaseSharedDummy.
- EvolutionTouchProcessor: 4 events (OnFocus/Unfocus/Select/NotSelect Target) hand-added — m1_stub_gen drops `event` decls.
- Generated full-surface stubs: StoryWorldDataManager, GenerateDeckCode, GameSetup, CommonPrefabContainer, ApplicationFinishManager, EvolutionConfirmation, ReplayDataHandler (hand stubs -> partial).
- Closure pulled by StoryWorldDataManager full-surface: 4 verbatim copies (StoryChapter/Summary/LeaderSelect dialogs, ClassIconName) + empty stubs StoryWorldData/BattleRecovery/ResourceDownloader/TemporaryAssetDeleter (non-battle, signature-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:50:10 -04:00
gamer147
67f91e230e port(m1): wave 6a — GameMgr setters, Tab MonoBehaviour, OpeningVfx members, Vfx stubs (838->772)
- GameMgr.Is{Network,AINetwork,Watch,Replay,Puzzle,AdminWatch}Battle: read-only => settable (CS0200, Matching/NetworkBattleManagerBase assign them).
- Tab : MonoBehaviour (inherits Object.name; CS1061 x8).
- OpeningVfx: static OpenningLogStep, ShowBattleUIImmediatelyVfx (NullVfx, F1 contract), nested WaitVoiceEndVfx (CS0117/0426 x14).
- Generated no-op stubs EffectBattleVfxBase/SkillEffectBattleVfx/FallToGroundVfx/ThinkIconShowVfx (: SequentialVfxPlayer chain), baseclauses reattached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:43:47 -04:00
gamer147
db76808e64 feat(battle-engine): re-attach interfaces dropped by base-clause recovery (958->838)
base-clause recovery strips interfaces (to dodge CS0535), but copied code converts
the stubs to those interfaces -> ~120 CS0266/CS1503. Two mechanisms:
- _IfaceImpl.g.cs: explicit no-op impls of the FULL (copied) interfaces, layered
  onto each hierarchy base (BattleCardView/CardVfxCreatorBase/BattlePlayerView/
  BattleEnemyView/ClassInfomationUIBase + NullCardVfxCreator). Explicit form never
  collides with existing members; leaves inherit. Walks base-interface chains
  (IPlayerView : IBattlePlayerView) and emits events.
- _InterfaceReattach.g.cs: plain ': IFoo' for the empty stub interfaces
  (IProcessing, IReplayRecordManager).
- ClassBattleCardViewBase/NullBattleCardView: restore dropped BattleCardView base
  so they inherit its IBattleCardView impl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:33:14 -04:00
gamer147
be10425819 feat(battle-engine): VfxWith ctor arg order + Unity conversions + ITouchProcessor reattach (1102->958)
- VfxWith<T> ctor params were swapped ((T,VfxBase) vs decomp (VfxBase,T)) -> ~38
  CS1503 across SkillCollectionBase/BattleCardBase skill-processing (ProcessInfo
  <-> VfxBase). These are on the resolution path. Fixed to match decomp.
- UnityEngine primitives: implicit Vector3/Vector2<->Vector4 + Color->Color32
  conversions; Transform.Translate/Rotate(Vector3,Space) overloads (iTween).
- ITouchProcessor was dropped from touch-processor stubs by base-clause recovery;
  re-attach via Shim/View/TouchProcessorIfaces.cs (interface-only for generated
  full-surface stubs, interface+no-op members for the empty hand stubs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:18:35 -04:00
gamer147
795f7a6bc8 feat(battle-engine): preserve ctor base-initializers + Event/Reward shims (1386->1226)
- Regenerate 31 VFX/View/UI/Touch stubs to keep their decomp ': base(...)' /
  ': this(...)' ctor initializers (m1_stub_gen was dropping them -> CS7036/CS1729
  when the copied base has no parameterless ctor). Whole base-ctor cluster cleared.
- UnityEngine.Event: add rawType/keyCode/modifiers/Use() + EventType enum (NGUI
  UIInput/UIInputOnGUI legacy IMGUI path).
- Reward: copy the real Wizard.Scripts.Network.Data.TaskData.Arena.Reward verbatim
  (was an empty ambiguous-name shim in LooseEnds); deps (UserGoods/JsonData) present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:10:52 -04:00
gamer147
7e5ff0a58f feat(battle-engine): ParticleSystem/Collider2D/Quaternion + SocketOptions members (1462->1386)
ParticleSystem.MainModule (playOnAwake/simulationSpeed/startColor + MinMaxGradient),
ParticleSystemRenderer (maskInteraction/trailMaterial + SpriteMaskInteraction), BoxCollider2D
(isTrigger/offset/size), Quaternion.FromToRotation/Inverse. SocketOptions (AutoConnect/
ConnectWith/AdditionalQueryParams) + PlatformSupport ObservableDictionary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:43:39 -04:00
gamer147
7ac13f73f2 feat(battle-engine): BestHTTP SocketIO + Spine SDK member shims (1556->1462)
BestHTTP.SocketIO: Socket.On/Off/Emit + SocketIOCallback delegate, SocketManager ctors/
State/Socket/indexer/Open/Close/SettingRealtimeNetworkAgent, SocketOptions. Spine: Skeleton
(Data/Skin/Scale/FindBone/SetSkin/Update), Bone (WorldX/Y/RotationX), SkeletonMecanim
(MonoBehaviour + skeleton). All minimal hand shims (no full-surface -> no SDK closure pull);
node-socket path is Phase-2, off the battle path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:41:06 -04:00
gamer147
a1c0c2d312 feat(battle-engine): CRI/Unity overload + generated base-ctor fixes (1586->1556)
CRI: CriAtomExPlayer.AttachFader, CriAtomCueSheet.acb, CriAtomExCategory static.
Unity overload gaps (CS7036): Transform.Translate/Rotate(float,float), Vector4(3/2-arg)
ctors, Vector3 instance Scale. Parameterless ctors for generated Vfx bases (ForecastIcon
VfxBase/ShowChantCountVfx/EvolveVfx) whose derived stubs' implicit base() failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:38:02 -04:00
gamer147
4be630bd09 feat(battle-engine): full-surface app-type god-object/manager stubs (1692->1586 true)
Make the minimal hand shims partial + generate full member surface for the manager/
task/controller god-objects (LoadingViewManager/DeckUpdateTask/MyPageTask/ReplayController/
PlayerControllerForWatching/WatchDataHandler/EvolutionTouchProcessor/StoryChapterSelection
Utility/NonDialogPopup). NonDialogPopup given MonoBehaviour base + hand Close() removed
(superseded by full surface). LoadTask dup deleted (already copied verbatim). RoomMatch
watch/replay closure types stubbed. Copied 8 more closure files.

CS0246-in-generated-signature masking note: 4 such errors were hiding ~1582 — generated
CS0246 masks as hard as header CS0246; the real frontier is 1586 (CS7036 base-ctor +
member-level), 0 structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:33:37 -04:00
gamer147
fce02a6250 feat(battle-engine): VFX container Create overloads (2202->1692)
The hand-shim VFX containers only had no-arg Create(); the engine calls them with
collection/params/loading-main args (510 CS1501). Add the real decomp Create overloads
to SequentialVfxPlayer/ParallelVfxPlayer/VfxWithLoading/VfxWithLoadingSequential.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:27:51 -04:00
gamer147
2ddc86943e feat(battle-engine): re-establish dropped base clauses for net-new stubs (2704->2202 true)
The stub generator emits net-new types as base-LESS partials, so generated Vfx/View
types weren't actually VfxBase/etc. -> hundreds of CS1503/CS0029 'cannot convert to
VfxBase' at every polymorphic call site. m1_baseclauses.py recovers each generated
type's decomp base CLASS (interfaces dropped to avoid CS0535) into _BaseClauses.g.cs,
cross-namespace bases fully qualified. Generated the intermediate Vfx/processing base
types (SpreadOutVfx/OpenCardVfx/ProcessingBase/DamageVfxBase/ForecastIconVfxBase/...).
DefaultOpeningVfx regenerated WITH override (its base OpeningVfx is copied+abstract).

Clearing the polymorphism cascade + the masking base-type CS0246s unmasked the true
member-level frontier: 2202 (CS1501/CS1061/CS1503), 0 structural errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:24:22 -04:00
gamer147
d01e3da869 feat(battle-engine): UnityEngine member + static-class shims (3526->2706)
Extend the UnityEngine value/component shims with no-op members surfaced by the compile
loop (UnityWebRequest/Font/Mesh/LODGroup/AudioSource/Rigidbody/Camera/Sprite/Animation/
Transform/Material/Texture2D/Light/Input/Resources + CharacterInfo/Vector4), via partial
declarations + UnityShimExt.cs. Add the missing UnityEngine static classes (PlayerPrefs/
Physics/GUI/SystemInfo/Graphics/QualitySettings/StackTraceUtility) + enums (TextureFormat/
ColorSpace/EventModifiers/RenderTextureReadWrite) + Experimental.Rendering.GraphicsFormat*
in UnityStatics.cs. All cosmetic, off the battle path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:14:23 -04:00
gamer147
9cd3f40a2f feat(battle-engine): CRI Atom/Mana audio+movie shim (3916->3526)
Hand-model the CRI ADX2 (audio) + CRI Mana (movie) SDK surface exercised by the copied
audio/movie engine files (AudioManager/Voice/Se/Effect/MoviePlayer). No decomp source
exists; signatures mirror the real CRI API as called at the sites (arg counts/types from
the call sites). All no-op, cosmetic, off the battle path. Reconciled with the empty CRI
stubs already in SdkStubs (CriAtomExAcb/CriAtomExPlayback/CriManaMovieMaterial).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:08:36 -04:00
gamer147
4b9a603cd4 feat(battle-engine): full View/VFX/UI/Touch/Story type closure (4254->3916, unmasked)
Generate no-op shells for the entire stop-listed View/Vfx/UI/Touch/Story missing-
type closure (~180 types) + 5 copyable engine files. Net-new shells emitted base-less,
so override members are stripped via the new --no-override generator flag. SDK/BCL
over-reach (Adjust/GZipStream/Socket*) and non-battle Story-world clusters reduced to
minimal/empty stubs instead of full-surface. Nested-type closure (BuildInfo/BattleDialog/
ROOM_URI/FuncGetCantAttackText) placed top-level in their decomp namespaces.

Clearing the last View CS0115 unmasked the true member-level frontier: 3916 errors,
0 generated/structural errors, now dominated by Unity-type + god-object members.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:01:37 -04:00
gamer147
a28e3ba334 feat(battle-engine): Unity shim members — BoxCollider/RenderTexture/AnimationCurve/Animator (4572->4254)
Add no-op members + supporting types (FilterMode/TextureWrapMode/WrapMode/Keyframe/
AnimatorStateInfo/AnimatorClipInfo) to the UnityEngine shim. Standard Unity API surface,
inferred from frontier member names — Unity types aren't in the decomp so they're
hand-extended, not generated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:40:37 -04:00
gamer147
f32492b6c9 feat(battle-engine): app-type wave (RoomBase/Avatar/BossRush/tasks) 4850->4572
Full-surface stubs for RoomBase, Avatar/BossRush/MyRotation battle-log items (MonoBehaviour),
GetDeckDataFromCode, MailTopTask, AccountTransferHelper, CanNotTouchCardVfx. EXCLUDE
inherited overrides (CanNotTouchCardVfx.IsEnd, MailTopTask.Parse). ClosureStubs for the
RoomMatch subsystem bleed (~11 Room* types) + AppleLogin(+Error). Reward/Event deferred
(ambiguous common names resolve to wrong SDK files).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:37:23 -04:00
gamer147
70a2c3e8ed feat(battle-engine): View/Room/Vfx type wave (5600->4850)
Full-surface stubs for ICardVfxCreator(iface), SelectedStoryInfo, ImageSelection,
IReadOnlyVoiceInfo, RoomConnectController(+InitializeParameter/enums), RoomRuleSetting,
VideoHostingHUD(+HUDMode), TabList, BattleCardView.AttackTargetSelectInfo, ProtectionColorType.
Wired hand shims partial + MonoBehaviour bases; let generated supersede hand-written
nested enums (decomp-authoritative values). SocketManager kept as minimal SDK hand shim.
ClosureStubs for ~14 referenced empties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:33:00 -04:00
gamer147
b47741d2a5 feat(battle-engine): full-surface god-object stubs (UIManager/GameObjMgr/BattleLog) 7532->5600
Generate the COMPLETE decomp member surface (not frontier-subset, which silently drops
already-provided members) for UIManager(+ViewScene/ChangeViewSceneParam), GameObjMgr,
BattleLogManager/Item, InPlayCardFrameEffectControl. UIManager/Footer base fixes:
UIManager is MonoBehaviour (singleton kept by hand via --exclude); Footer is the copied
Engine type (removed the conflicting global shim). Add HUDMode enum, Wizard manager
return-type stubs, and a closure-stubs file for 7 referenced empties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:23:52 -04:00
gamer147
de1b7362c9 feat(battle-engine): BattleLog cluster via generated no-op stubs (7852->7532)
Stub-generate BattleLogManager(45)/BattleLogItem(17)/InPlayCardFrameEffectControl(4)
member surfaces from decomp signatures; declare BattleLogWindow+nested enum; make
BattleLogItem a MonoBehaviour so inherited gameObject/transform resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:05:57 -04:00
gamer147
f9982f5249 feat(battle-engine): copy-loop closure to 3282 files (member-surface frontier) 2026-06-05 20:41:25 -04:00
gamer147
3dcd53933a feat(battle-engine): AOT/SFB/Steam/DisallowMultiple + FriendDataBase.SetPlayerData
Clears the last type+header frontier (RoomInviteFriendColum override). Per F3 this
unmasks the remaining View/UI/god-object member bodies (~8k) -- the next grind is
pure member-surface growth, closure (~3242 files) now essentially complete.
2026-06-05 20:40:33 -04:00
gamer147
0455ff649e feat(battle-engine): EffectType full enum + collection/card/vfx extension copies
Replaces partial EffectMgr.EffectType with all 226 decomp values; copies the
IsNotNullOrEmpty/EquelsID/FindFromCardId/GetAllFuncVfxResults extension files +
UI extensions; adds Renderer/MeshFilter shared-material/mesh/sortingOrder. Compile
loop then closed the revealed deps (3242 files). 9.1k -> 18 errors.
2026-06-05 20:38:56 -04:00
gamer147
c3bd39f2cb feat(battle-engine): final type-frontier residual (Story/Title/Friend stubs, SDK anchors, Unity AndroidJavaObject/WebCamTexture)
Clears the last CS0246/CS0234 type frontier; per F3 this unmasks the AI-subsystem
member bodies (~9k member-level errors) -- next grind is extension copies + god-object
member growth.
2026-06-05 20:34:49 -04:00
gamer147
824309ec44 feat(battle-engine): close the AI-simulation subsystem (verbatim)
Copied the 89 uncopied AI*SimulationUtility/extension files defining the
AIVirtualCard/AIVirtualField extension methods; the compile loop then auto-closed
the revealed type deps (~3049 files total, drift-clean). 10.0k -> 62 errors.
2026-06-05 20:30:59 -04:00
gamer147
78f310c2b3 feat(battle-engine): grow god-object + VFX-container shim surface
GameMgr (managers/setting/flags), UIManager (GetInstance + scene/dialog/loading
surface), EffectMgr (Start/effect lifecycle), VfxMgr + VfxWithLoading(Sequential)
register methods -- signatures mirrored from decomp. 15.9k -> 10.0k errors.
2026-06-05 20:27:00 -04:00
gamer147
4491c6c7f3 feat(battle-engine): full Unity primitive/runtime surface + game extension copies
Grows Vector2/3, Mathf, Color, Quaternion, GameObject, Transform, Camera, Material,
ParticleSystem, Rect, Bounds, Time to the surface the engine references; adds Input/
Random/Resources statics + full KeyCode enum. Copies the verbatim extension files that
collapse thousands of CS1061s at once (ContentKeywordExt, JsonData/LitJson extensions,
EventExtension.Call, GameObjectExtension(s)). 26.5k -> 15.9k errors; residual now
dominated by god-object member surface (GameMgr/UIManager/EffectMgr/Vfx*).
2026-06-05 20:22:43 -04:00
gamer147
a00e90c74a feat(battle-engine): clear header frontier (Item/ErrorDialog/SDK shims + infra copies)
Resolves the 268-error header frontier: settings Item base, ErrorDialog.Data,
RoomConnectController nested types, Unity asset/light/collider types, CriWare/
CodeStage/Spine SDK surface, and copies INetworkLogger + SingletonMonoBehaviour
verbatim. Per F3 this unmasks the type bodies (~26.5k member-level errors now
visible) -- the real M1 bulk, attacked in following waves.
2026-06-05 20:11:08 -04:00
gamer147
957af3d1ec feat(battle-engine): full Unity/VFX/god-object shims + expanded copy closure (2570 files)
Authored Unity primitive/object-model shim, VFX layer (control-flow-preserving, InstantVfx never invokes its action -- headless suppression), god-object stubs (GameMgr/EffectMgr/UIManager with faithfully-extracted nested enums), View/UI/Touch tree, LitJson+BetterList+Tuple copied, third-party stubs. Discovered Roslyn header-error masking: fixing class-header type errors unmasks body references, so the true copy closure is ~2570 files (was 782 under masking). Errors: masked-25720 -> 268; our shim files compile clean. Remaining: ~50 residual shim/external types, 24 NGUI UI-base overrides, static-type fixes, plus likely 1-2 more unmask waves.
2026-06-05 17:22:20 -04:00
gamer147
0d9d8acae0 feat(battle-engine): M1 auto-copy closure (782 battle-logic files)
Compile-driven bulk-copy loop (tools/engine-port/m1_copy_loop.py) pulled the precise reference closure of the battle-core roots, stopping at the classify god-object/View-VFX-UI boundary. 782 files; no re-explosion (M0 had estimated ~order 1000). Residual frontier = 52 shim-classified + 80 external (Unity/BCL) types to author next.
2026-06-05 16:57:20 -04:00
gamer147
23a6596558 fix(battle-engine): Quaternion.identity w=1 to match Unity semantics 2026-06-05 16:49:02 -04:00
gamer147
550cedbf1e feat(battle-engine): seed copy roots + UnityEngine primitive shim
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 16:45:12 -04:00
gamer147
bb80815b01 feat(battle-engine): scaffold empty SVSim.BattleEngine library 2026-06-05 16:34:46 -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
3669 changed files with 445768 additions and 51 deletions

View File

@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Bootstrap", "SVSim.Bo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleNode", "SVSim.BattleNode\SVSim.BattleNode.csproj", "{F4549DD3-566A-4155-8D52-3A4D2A7072F7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleNode", "SVSim.BattleNode\SVSim.BattleNode.csproj", "{F4549DD3-566A-4155-8D52-3A4D2A7072F7}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine", "SVSim.BattleEngine\SVSim.BattleEngine.csproj", "{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -37,5 +41,13 @@ Global
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.Build.0 = Release|Any CPU {F4549DD3-566A-4155-8D52-3A4D2A7072F7}.Release|Any CPU.Build.0 = Release|Any CPU
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.Build.0 = Release|Any CPU
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -0,0 +1,7 @@
// Each engine-state fixture wraps its tests in a TestBattleScope, so AsyncLocal ambient
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). The
// residual process-globals (Unity Resources shim cache, Wizard.LocalLog accumulators) are
// now thread-safe (ConcurrentDictionary / static lock), so fixtures can run in parallel.
using NUnit.Framework;
[assembly: Parallelizable(ParallelScope.Fixtures)]

View File

@@ -0,0 +1,246 @@
#nullable enable
using SVSim.BattleEngine.Ambient;
using NUnit.Framework;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace SVSim.BattleEngine.Tests;
[TestFixture, Parallelizable(ParallelScope.Self)]
public class BattleAmbientTests
{
[Test]
public void Current_IsNull_WhenNoScope()
{
Assert.That(BattleAmbient.Current, Is.Null);
}
[Test]
public void Require_Throws_WhenNoScope()
{
Assert.Throws<System.InvalidOperationException>(() => BattleAmbient.Require());
}
[Test]
public void Enter_SetsCurrent_RestoresOnDispose()
{
var ctx = new BattleAmbientContext { ViewerId = 42 };
Assert.That(BattleAmbient.Current, Is.Null);
using (var _ = BattleAmbient.Enter(ctx))
{
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
Assert.That(BattleAmbient.Require().ViewerId, Is.EqualTo(42));
}
Assert.That(BattleAmbient.Current, Is.Null);
}
[Test]
public void Enter_Nested_RestoresPriorOnDispose()
{
var outer = new BattleAmbientContext { ViewerId = 1 };
var inner = new BattleAmbientContext { ViewerId = 2 };
using (var _o = BattleAmbient.Enter(outer))
{
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
using (var _i = BattleAmbient.Enter(inner))
{
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(2));
}
Assert.That(BattleAmbient.Current!.ViewerId, Is.EqualTo(1));
}
}
[Test]
public async Task Enter_FlowsAcrossAwait()
{
var ctx = new BattleAmbientContext { ViewerId = 99 };
using (var _ = BattleAmbient.Enter(ctx))
{
await Task.Yield();
Assert.That(BattleAmbient.Current, Is.SameAs(ctx));
}
}
[Test]
public async Task Enter_IsolatedBetweenConcurrentTasks()
{
var ctxA = new BattleAmbientContext { ViewerId = 100 };
var ctxB = new BattleAmbientContext { ViewerId = 200 };
var taskA = Task.Run(async () => {
using var _ = BattleAmbient.Enter(ctxA);
await Task.Delay(20);
return BattleAmbient.Current!.ViewerId;
});
var taskB = Task.Run(async () => {
using var _ = BattleAmbient.Enter(ctxB);
await Task.Delay(20);
return BattleAmbient.Current!.ViewerId;
});
var results = await Task.WhenAll(taskA, taskB);
Assert.That(results[0], Is.EqualTo(100));
Assert.That(results[1], Is.EqualTo(200));
}
[Test]
public void IsForecast_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext { IsForecast = false };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(BattleManagerBase.IsForecast, Is.False);
ctx.IsForecast = true;
Assert.That(BattleManagerBase.IsForecast, Is.True);
}
[Test]
public void IsForecast_WriteInsideScope_WritesAmbient_NotFallback()
{
var ctx = new BattleAmbientContext { IsForecast = false };
using (var _ = BattleAmbient.Enter(ctx))
{
BattleManagerBase.IsForecast = true;
Assert.That(ctx.IsForecast, Is.True);
}
}
[Test]
public void IsForecast_OutsideScope_GetAndSetThrow()
{
// Post-Task-8: fallback is gone. Both get and set go through BattleAmbient.Require(),
// which throws when no scope is active. This is the forcing function — any unwrapped
// engine code that touches IsForecast fails fast instead of silently writing a static.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsForecast; });
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsForecast = true);
}
[Test]
public void IsRandomDraw_OutsideScope_GetAndSetThrow_InsideScope_Roundtrips()
{
// Post-Task-8: get/set both Require() a scope. Inside a scope, writes land on the ctx.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
Assert.Throws<System.InvalidOperationException>(() => BattleManagerBase.IsRandomDraw = true);
var ctx = new BattleAmbientContext { IsRandomDraw = false };
using (var _ = BattleAmbient.Enter(ctx))
{
Assert.That(BattleManagerBase.IsRandomDraw, Is.False);
BattleManagerBase.IsRandomDraw = true;
Assert.That(ctx.IsRandomDraw, Is.True);
}
// Scope disposed -> back to throwing on access.
Assert.Throws<System.InvalidOperationException>(() => { var _ = BattleManagerBase.IsRandomDraw; });
}
[Test]
public void GetIns_ReadsAmbient_WhenScopeActive()
{
var fakeMgr = (BattleManagerBase)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(BattleManagerBase));
var ctx = new BattleAmbientContext { Mgr = fakeMgr };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(BattleManagerBase.GetIns(), Is.SameAs(fakeMgr));
}
[Test]
public void GetIns_OutsideScope_ReturnsNull()
{
// Post-Task-8: fallback is gone. GetIns() reads Current?.Mgr (soft, kept null-tolerant so
// engine call sites that pattern `GetIns()?.Foo ?? default` still compose). With no scope
// active, Current is null, so GetIns() returns null.
Assert.That(BattleAmbient.Current, Is.Null);
Assert.That(BattleManagerBase.GetIns(), Is.Null);
}
[Test]
public void ViewerId_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext { ViewerId = 12345 };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Cute.Certification.ViewerId, Is.EqualTo(12345));
}
[Test]
public void RealTimeNetworkAgent_ReadsAmbient_WhenScopeActive()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Null);
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
ctx.NetworkAgent = agent;
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.SameAs(agent));
}
[Test]
public void SetRealTimeNetworkBattle_InsideScope_WritesAmbient()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(RealTimeNetworkAgent));
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
Assert.That(ctx.NetworkAgent, Is.SameAs(agent));
}
[Test]
public void BattleRecoveryInfo_ReadsAmbient_WhenScopeActive()
{
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
var ctx = new BattleAmbientContext { RecoveryInfo = info };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(Wizard.Data.BattleRecoveryInfo, Is.SameAs(info));
}
[Test]
public void BattleRecoveryInfo_SetInsideScope_WritesAmbient()
{
var ctx = new BattleAmbientContext();
using var _ = BattleAmbient.Enter(ctx);
var info = (Wizard.BattleRecoveryInfo)System.Runtime.Serialization
.FormatterServices.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo));
Wizard.Data.BattleRecoveryInfo = info;
Assert.That(ctx.RecoveryInfo, Is.SameAs(info));
}
[Test]
public void GameMgr_GetIns_InsideScope_ReturnsScopeInstance()
{
var mgr = new GameMgr();
var ctx = new BattleAmbientContext { GameMgr = mgr };
using var _ = BattleAmbient.Enter(ctx);
Assert.That(GameMgr.GetIns(), Is.SameAs(mgr));
}
[Test]
public void GameMgr_GetIns_OutsideScope_Throws()
{
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
}
[Test]
public async Task GameMgr_GetIns_IsolatedBetweenConcurrentTasks()
{
var mgrA = new GameMgr();
var mgrB = new GameMgr();
var taskA = Task.Run(async () => {
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrA });
await Task.Delay(20);
return GameMgr.GetIns();
});
var taskB = Task.Run(async () => {
using var _ = BattleAmbient.Enter(new BattleAmbientContext { GameMgr = mgrB });
await Task.Delay(20);
return GameMgr.GetIns();
});
var results = await Task.WhenAll(taskA, taskB);
Assert.That(results[0], Is.SameAs(mgrA));
Assert.That(results[1], Is.SameAs(mgrB));
}
}

View File

@@ -0,0 +1,56 @@
using NUnit.Framework;
using UnityEngine;
using Wizard.Battle.View;
namespace SVSim.BattleEngine.Tests
{
// Regression for the in-play metamorphose NRE diagnosed 2026-06-07 (bid 283192092460).
//
// The IsRecovery card-create delegate (NetworkBattleManagerBase.cs:379) passes null for the
// cardGameObject, which left BattleCardView.GameObject null. Skill_metamorphose.cs:147 in the
// IsInplay branch then NRE'd on the unguarded
// metamorphosedCard.BattleCardView.GameObject.transform.rotation = Quaternion.identity
// — a purely cosmetic transform reset that has no corresponding state mutation, but tripped over
// null-GameObject before the surrounding mutations (ReplaceInPlay, SetUpInplay,
// FlagCardAsDestroyedBySkill, RemoveFromInPlay) could complete.
//
// Fix: ViewUiTouchStubs.cs's BattleCardView.GameObject is now lazily non-null (matches the
// existing Component.gameObject pattern at UnityShim.cs:94). The shim materializes a no-op
// GameObject on first read; the cosmetic touch resolves to a no-op assignment instead of NRE.
[TestFixture]
public class BattleCardViewShimTests
{
[Test]
public void GameObject_is_lazily_non_null_so_unguarded_recovery_touches_no_op()
{
var view = new BattleCardView();
Assert.That(view.GameObject, Is.Not.Null,
"BattleCardView.GameObject must be lazily non-null in the shim so unguarded " +
"Unity touches on the IsRecovery card-create path (which passes null cardGameObject) " +
"resolve to no-ops instead of NRE-ing.");
Assert.That(view.GameObject.transform, Is.Not.Null,
"GameObject.transform must follow the shim's lazy non-null Component pattern (UnityShim.cs:94).");
Assert.DoesNotThrow(() => view.GameObject.transform.rotation = Quaternion.identity,
"Skill_metamorphose.cs:147's cosmetic transform.rotation reset on the in-play branch must " +
"not throw in the headless IsRecovery path (live bid 283192092460: A's Petrification " +
"on B's in-play card).");
}
[Test]
public void GameObject_is_stable_across_reads_so_a_set_followed_by_read_returns_the_same_instance()
{
// Lazy materialization caches the GameObject on first read, so subsequent reads return
// the same instance — required for any code path that reads .GameObject, mutates it,
// and reads again (e.g. follower position/rotation/scale set in sequence).
var view = new BattleCardView();
var first = view.GameObject;
var second = view.GameObject;
Assert.That(second, Is.SameAs(first),
"lazy GameObject must cache; otherwise the second read returns a fresh instance " +
"and any mutation on the first read is lost.");
}
}
}

View File

@@ -0,0 +1,105 @@
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M4 (next-hardest deterministic card): a when_play SELF-BUFF follower resolves to correct
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the M2
// vanilla follower and M3 fixed-damage spell proved (design §5 / DP4 + M3 resume recipe). The new
// oracle dimension over M2/M3 is the PLAYED CARD'S OWN STAT DELTA: the fanfare `powerup`
// `add_offense=1&add_life=1` to `target=self` must raise the follower's Atk and Life by exactly
// those amounts over its CardCSVData base — a self-buff, so no target selection is involved.
[TestFixture]
public class BuffFollowerOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Self_buff_fanfare_raises_own_atk_and_life()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2/M3 oracles): opponent refs + active turn flag. The
// self-buff's target resolver (`character=me&target=self`) reads the active player's own
// in-play card, so the turn flag must be set before the fanfare sweeps.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over state that silently blocks
// the play (M3 learning); this card deals no damage but the play-legality gate still checks it.
HeadlessEngineEnv.InitLeaderLife(mgr);
// The card's fanfare is gated on `play_count>2` (cards.json skill_condition for 103111050).
// The engine reads this from BattlePlayerBase.GetCurrentTurnPlayCount(); seed it past the
// threshold via the public AddCurrentTrunPlayCount so the powerup actually fires. (Without
// this the card resolves to the board but takes no buff — the delta-vs-base oracle is what
// distinguishes "buff applied" from "fanfare silently gated out".)
player.AddCurrentTrunPlayCount(5);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.BuffFollowerId);
// Place the self-buff follower in the active player's hand with PP to spare; empty board.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.BuffFollowerId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int inplayBefore = player.ClassAndInPlayCardList.Count;
int enemyHandBefore = enemy.HandCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a self-buff fanfare follower");
// Oracle: the own-stat delta is the new M4 dimension; the rest are the §5 follower invariants.
Assert.Multiple(() =>
{
// Primary M4 assertion: the fanfare powerup raised the follower's own stats by exactly
// the buff amounts over its CardCSVData base (1/1 -> 2/2).
Assert.That(card.Atk, Is.EqualTo(cardParam.Atk + HeadlessEngineEnv.BuffAddOffense),
"follower atk != base + fanfare add_offense");
Assert.That(card.Life, Is.EqualTo(cardParam.Life + HeadlessEngineEnv.BuffAddLife),
"follower life != base + fanfare add_life");
// Cost paid.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// Follower moved hand -> board.
Assert.That(player.HandCardList, Does.Not.Contain(card), "card still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Contains.Item(card), "card not in play");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(inplayBefore + 1), "in-play count not +1");
// Opponent unchanged (the buff targets self, not the opponent).
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
});
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using NUnit.Framework;
namespace SVSim.BattleEngine.Tests
{
// M2 probe (go/no-go step 1): can BattleManagerBase / the two-player pair be constructed
// HEADLESS at all? This drives the real practice init path
// (`new SingleBattleMgr(StandardBattleMgrContentsCreator)`), which internally builds the
// BattlePlayer + BattleEnemy pair, against the M1 shim — with NO Unity runtime.
//
// The point of this test is diagnostic: if construction throws, the stack trace tells us the
// first shim gap on the *resolution* path (vs the compile path M1 already proved). We assert
// success, but a failure here is the informative outcome we want surfaced.
[TestFixture]
public class ConstructionProbeTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
[Test]
public void SingleBattleMgr_constructs_headless()
{
// Mirror the forecast flags the design pins (DP4 / §3): suppress VFX registration and
// collapse wait delays. TestBattleScope already sets ctx.IsForecast=true; this line is a
// belt-and-suspenders write through the ambient setter.
BattleManagerBase.IsForecast = true;
SingleBattleMgr mgr = null;
try
{
mgr = new SingleBattleMgr(new HeadlessContentsCreator());
}
catch (Exception ex)
{
Assert.Fail(
"Headless construction of SingleBattleMgr threw — first shim gap on the " +
"resolution path:\n" + ex);
}
Assert.That(mgr, Is.Not.Null);
Assert.That(mgr.BattlePlayer, Is.Not.Null, "BattlePlayer (self) not created");
Assert.That(mgr.BattleEnemy, Is.Not.Null, "BattleEnemy (opponent) not created");
_scope.Ctx.Mgr = mgr;
}
}
}

View File

@@ -0,0 +1,123 @@
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M9 (the §5 draw oracle): a when_play DRAW spell resolves to correct authoritative state HEADLESS
// via the same IsForecast/IsRecovery + ActionProcessor path M2-M8 proved. The NEW oracle dimension
// is the HAND/DECK DELTA — the deck->hand transfer no prior milestone read: M3/M4/M6/M8 moved
// stats, M2/M5/M7 the board, M3 the leader. The spell's `draw 1` must pull the single seeded deck
// card into the caster's hand (deck -1, that exact card now in hand) while the spell itself pays
// its cost and leaves to the cemetery.
//
// RNG is neutralized structurally (see HeadlessEngineEnv.DrawSpellId): every real draw selects from
// the deck via a `random_count` filter, so the deck is seeded with EXACTLY ONE known card — a
// single-card pool makes `random_count=1` deterministic regardless of the RandomSeed. This rides
// the M5 prefab card-creation path (the deck card is engine-created off the null-view seam) the same
// way the summon-token milestone did.
[TestFixture]
public class DrawSpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Draw_spell_moves_the_seeded_deck_card_into_hand()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M8 oracles): opponent refs + active turn flag. The
// draw resolves onto the active player's own hand/deck (the skill filter is character=me).
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life: this spell deals no damage, but the play-legality gate still rejects a
// play when a leader reads as a 0-life game-over state (M3 learning).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Seed the card-template prefabs the internal (createNullView:false) creation path clones —
// the draw VFX touches the drawn card's view layer, so keep the M5 prefab surface available.
HeadlessEngineEnv.InitCardTemplates(mgr);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DrawSpellId);
// Seed EXACTLY ONE known card on the caster's deck (forces the random_count=1 selection),
// and place the draw spell in hand with PP to spare.
var deckCard = HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.DeckSeedCardId, index: 2, isPlayer: true);
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DrawSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int deckBefore = player.DeckCardList.Count;
int cemeteryBefore = player.CemeteryList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
// Sanity: the to-be-drawn card starts in the deck, not the hand.
Assert.That(player.DeckCardList, Does.Contain(deckCard), "seeded card not in deck pre-play");
Assert.That(player.HandCardList, Does.Not.Contain(deckCard), "seeded card already in hand pre-play");
// Resolve the play through the real engine.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a draw spell");
// Oracle: the deck->hand transfer is the new M9 dimension; the rest are the §5 spell-shaped
// invariants proven by M3.
Assert.Multiple(() =>
{
// Primary M9 assertion: the seeded deck card moved into the caster's hand...
Assert.That(player.HandCardList, Does.Contain(deckCard),
"drawn card did not land in hand");
Assert.That(player.HandCardList.Any(c => c.CardId == HeadlessEngineEnv.DeckSeedCardId), Is.True,
"no card with the seeded id is in hand");
// ...and left the deck (deck -1, down to empty here).
Assert.That(player.DeckCardList, Does.Not.Contain(deckCard), "drawn card still in deck");
Assert.That(player.DeckCardList.Count, Is.EqualTo(deckBefore - 1), "deck count not -1");
// The drawn card is the engine's OWN seeded deck object, not a fresh creation.
Assert.That(deckCard.IsInHand, Is.True, "drawn card not marked in-hand");
// The spell itself: pays exactly its cost...
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// ...leaves the hand (it is consumed, the drawn card replaces it -> net hand count flat)...
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore), "hand count changed (spell -1 + draw +1 should net flat)");
// ...resolves to the cemetery (a spell is not a follower; it never occupies the board).
Assert.That(player.CemeteryList, Does.Contain(card), "spell did not resolve to the cemetery");
Assert.That(player.CemeteryList.Count, Is.EqualTo(cemeteryBefore + 1), "cemetery count not +1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(deckCard), "drawn card wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
// Opponent untouched (the draw is character=me).
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
});
}
}
}

View File

@@ -0,0 +1,142 @@
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M10 (the first DYNAMIC `{}`-VALUE card — the first deliberate step beyond the four §5-named
// oracle dimensions M2-M9 closed): a when_play spell whose effect MAGNITUDE is COMPUTED by the
// engine from live game state, not carried as a literal. 112134010's sole skill is
// `when_play damage={me.play_count}-1` to units; the `{}` resolves
// (SkillOptionValue.ParseInt -> SkillFilterVariable.Parse -> SkillEnvironmentalPlayCount.Filtering)
// to `GetCurrentTurnPlayCount() - 1`. That GetCurrentTurnPlayCount() is the SAME per-turn counter
// M4 seeded via the public AddCurrentTrunPlayCount to drive a play_count GATE — M10 proves the seam
// also feeds the effect VALUE.
//
// The new oracle dimension over every prior milestone is the ENGINE-COMPUTED VALUE: the asserted
// damage is derived from the engine's OWN live play-count accessor (GetCurrentTurnPlayCount() - 1),
// never a hardcoded literal. Per memory project_battle_relay_nontargeted_effects, a state-derived
// value that the wire could NOT carry (spellboost cost) is exactly what desynced the PvP relay;
// proving the engine resolves a `{}` value headless is the direct validation that the port (not a
// relay) is the necessary path.
//
// Timing note (the M10 first-unknown, RESOLVED empirically by the first RED): the per-play
// auto-increment AddCurrentTrunPlayCount(1) lives in ActionProcessor's OnBeforePlayCard
// (BattlePlayerBase.cs:1400), which is subscribed by SetupActionProcessorEvent — and that is only
// called on the OperateMgr / Prediction / OperationSimulator paths, NOT on the direct
// `new ActionProcessor(pair).PlayCard` (DP4) path this harness uses. So the headless play does NOT
// self-bump the per-turn play count: the skill reads EXACTLY the seeded GetCurrentTurnPlayCount()
// and the damage == seeded - 1. (The first RED expected a +1 that this path never applies; the
// state-derived primary assertion below was right regardless, and the concrete pins were corrected
// to the observed no-bump behavior.)
[TestFixture]
public class DynamicValueSpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Dynamic_damage_spell_deals_engine_computed_play_count_value()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M6 oracles): opponent refs + active turn flag. The
// spell's target resolver walks player -> opponent -> opponent's in-play units; the
// `{me.play_count}` read keys on the active player's current turn.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Put ONE vanilla follower on the ENEMY board. The spell is `character=both` (AoE over both
// boards' units), but with no player-side units the only matched target is this enemy
// follower; its base life (13) exceeds any seeded play count so it SURVIVES -> clean
// life-delta read (no dependence on death/removal). card_type=unit excludes both leaders.
var target = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DynamicDamageTargetFollowerId, 0, isPlayer: false);
// Seed the live game state the `{}` value reads: the active player's current-turn play
// count. This is the M4 seam (AddCurrentTrunPlayCount), here driving the VALUE not a gate.
player.AddCurrentTrunPlayCount(HeadlessEngineEnv.DynamicSeededPlayCount);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DynamicDamageSpellId);
// Place the dynamic-value spell in the active player's hand with PP to spare.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DynamicDamageSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int targetLifeBefore = target.Life;
int playerLeaderLifeBefore = player.ClassAndInPlayCardList[0].Life;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine (auto-targeted AoE -> selectedCards: null).
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a dynamic {}-value damage spell");
// The engine-computed value, derived from the engine's OWN live play-count accessor (the
// direct-ActionProcessor path does not self-bump it, so this reads the seeded value) —
// exactly the value the skill's `{me.play_count}-1` resolved against. NOT a hardcoded
// literal: this is the M10 dimension (effect magnitude computed from state the wire can't
// carry).
int playCountAtResolution = player.GetCurrentTurnPlayCount();
int expectedDamage = playCountAtResolution - 1;
int actualDamage = targetLifeBefore - target.Life;
Assert.Multiple(() =>
{
// PRIMARY M10 assertion: the damage dealt equals the engine-COMPUTED {me.play_count}-1,
// read from live state — proving the engine resolved the `{}` expression, not a literal.
Assert.That(actualDamage, Is.EqualTo(expectedDamage),
"damage dealt did not equal the engine-computed {me.play_count}-1 value");
// Concrete pins (catch a silent state-read failure where play_count would default to 0,
// making damage -1 -> 0): the direct-ActionProcessor path applies no self-play bump, so
// the resolution-time count is exactly the seeded value and the damage is seeded - 1.
Assert.That(playCountAtResolution, Is.EqualTo(HeadlessEngineEnv.DynamicSeededPlayCount),
"play count was not read as the seeded current-turn value");
Assert.That(actualDamage, Is.EqualTo(HeadlessEngineEnv.DynamicSeededPlayCount - 1),
"net damage did not equal seeded play_count - 1 ({me.play_count}-1 mis-resolved)");
// Target survives (life > damage) and stays on the board; both leaders untouched
// (card_type=unit excludes class cards).
Assert.That(target.Life, Is.EqualTo(targetLifeBefore - expectedDamage), "target life delta wrong");
Assert.That(enemy.ClassAndInPlayCardList, Does.Contain(target), "target unexpectedly left the board");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "enemy board count changed");
Assert.That(player.ClassAndInPlayCardList[0].Life, Is.EqualTo(playerLeaderLifeBefore), "player leader damaged (unit-only AoE hit a leader)");
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "enemy leader damaged (unit-only AoE hit a leader)");
// §5 spell-shaped invariants: cost paid, spell leaves hand, does NOT occupy the board.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
});
}
}
}

View File

@@ -0,0 +1,69 @@
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M13 (hub O1, deterministic): the first headless observation of the EMIT path. Drive the proven M3
// fixed-damage spell (900124030) through mgr.OperateMgr.PlayCard on a NetworkBattleManagerBase-derived
// mgr and confirm the engine reaches its emission path (RealTimeNetworkAgent.OnEmit fires PlayActions)
// without crashing, while the committed state still matches the M3 direct-ActionProcessor oracle.
// Liveness only (E4); structural frame decoding + the RNG rand-list (M14) are deferred.
[TestFixture]
public class EmitPathReadOracleTests : NetworkEmitFixtureBase
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
// The process-global reset (IsForecast=true + clear injected agent) now lives in the shared
// NetworkEmitFixtureBase.ResetNetworkEmitGlobals [TearDown], inherited here — see that file
// for why the leak matters.
[Test]
public void M3_spell_driven_via_OperateMgr_reaches_emit_without_crashing()
{
var (mgr, emitted) = HeadlessEngineEnv.NewNetworkEmitBattle();
_scope.Ctx.Mgr = mgr;
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
int leaderLifeBefore = enemy.Class.Life;
var spell = HeadlessEngineEnv.CreateHeadlessHandCard(
HeadlessEngineEnv.SpellId, index: 1, isPlayer: true, mgr);
player.HandCardList.Add(spell);
int cost = spell.Cost;
player.Pp = 10;
Assert.DoesNotThrow(
() => mgr.OperateMgr.PlayCard(spell, isPlayer: true, selectCards: null),
"OperateMgr.PlayCard threw driving the M3 spell through the emit path");
Assert.Multiple(() =>
{
// Emit reached: OnEmit fired with PlayActions (the O1 liveness signal).
Assert.That(emitted, Does.Contain(NetworkBattleDefine.NetworkBattleURI.PlayActions),
"the engine did not reach a PlayActions emit");
// State intact vs the M3 direct-path oracle.
Assert.That(enemy.Class.Life, Is.EqualTo(leaderLifeBefore - 3), "enemy leader should take 3");
Assert.That(player.Pp, Is.EqualTo(10 - cost), "PP should be paid");
Assert.That(player.HandCardList, Does.Not.Contain(spell), "spell should leave the hand");
Assert.That(player.CemeteryList, Does.Contain(spell), "spell should land in the cemetery");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(spell), "a spell does not occupy the board");
});
// Best-effort (F-E-7): with CurrentMatchingStatus seeded non-Disconnected (NewNetworkEmitBattle),
// the flow reaches stockEmitMessageMgr.StockData(info); read it back. If the stock machinery is
// not drivable headless this milestone, this assertion is DEFERRED to structural validation
// (spec §6) — the OnEmit + no-throw + state checks above are the decisive O1 read on their own.
var agent = Wizard.ToolboxGame.RealTimeNetworkAgent;
var stocked = HeadlessEngineEnv.TryReadStockedEmitData(agent); // returns null if unreachable
if (stocked != null)
Assert.That(stocked, Is.Not.Empty, "the emitted dict should be stocked non-empty");
else
Assert.Inconclusive("payload-presence DEFERRED: stock-sequencer not drivable headless (spec §6)");
}
}
}

View File

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M3 (next-hardest deterministic card): a FIXED-DAMAGE SPELL resolves to correct authoritative
// state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the M2 vanilla
// follower proved (design §5 / DP4 + M3 resume recipe). The new oracle dimension over M2 is the
// OPPONENT LEADER-LIFE DELTA: the spell's when_play `damage=3` to the enemy leader must reduce
// that leader's Life by exactly 3, with the spell consuming its cost and NOT entering the board.
[TestFixture]
public class FixedDamageSpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
// The spell's sole skill is `damage=3` to the enemy leader (cards.json skill_option for 900124030).
private const int ExpectedLeaderDamage = 3;
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Fixed_damage_spell_reduces_opponent_leader_life()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2 oracle): opponent refs + active turn flag. The
// spell's target resolver walks player -> opponent -> opponent's class card (the leader).
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life (engine's InitializeClassLife subset) so the enemy leader is a live,
// damageable target rather than a 0-life game-over state that blocks the play.
HeadlessEngineEnv.InitLeaderLife(mgr);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.SpellId);
// Place the spell in the active player's hand with PP to spare; empty board otherwise.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.SpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a fixed-damage spell");
// Oracle: the leader-life delta is the new M3 dimension; the rest are the §5 spell-shaped invariants.
Assert.Multiple(() =>
{
// Primary M3 assertion: opponent leader takes exactly the spell's fixed damage.
Assert.That(enemy.ClassAndInPlayCardList[0].Life,
Is.EqualTo(enemyLeaderLifeBefore - ExpectedLeaderDamage),
"opponent leader life not reduced by the spell's fixed damage");
// Cost paid.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// Spell leaves hand.
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
// A spell is not a follower: it must NOT occupy the board (resolves to graveyard).
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
// Opponent board (leader card only) count unchanged — only its life moved.
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board count changed");
});
}
}
}

View File

@@ -0,0 +1,40 @@
{"ts":"2026-06-05T16:36:19.3503474Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
{"ts":"2026-06-05T16:36:19.3573466Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100314010},{"idx":2,"cardId":100314020},{"idx":3,"cardId":102324040},{"idx":4,"cardId":101324050},{"idx":5,"cardId":101024010},{"idx":6,"cardId":101314020},{"idx":7,"cardId":101311050},{"idx":8,"cardId":101311010},{"idx":9,"cardId":100314020},{"idx":10,"cardId":101321040},{"idx":11,"cardId":101024010},{"idx":12,"cardId":127011010},{"idx":13,"cardId":100314040},{"idx":14,"cardId":101314020},{"idx":15,"cardId":102331010},{"idx":16,"cardId":102324040},{"idx":17,"cardId":101334040},{"idx":18,"cardId":100321010},{"idx":19,"cardId":101324040},{"idx":20,"cardId":100314030},{"idx":21,"cardId":101324040},{"idx":22,"cardId":101311050},{"idx":23,"cardId":701341011},{"idx":24,"cardId":101324050},{"idx":25,"cardId":100314030},{"idx":26,"cardId":101311010},{"idx":27,"cardId":101321070},{"idx":28,"cardId":101024010},{"idx":29,"cardId":100314040},{"idx":30,"cardId":127011010},{"idx":31,"cardId":127011010},{"idx":32,"cardId":100314010},{"idx":33,"cardId":102334020},{"idx":34,"cardId":101334030},{"idx":35,"cardId":101341010},{"idx":36,"cardId":101321040},{"idx":37,"cardId":101314020},{"idx":38,"cardId":101321070},{"idx":39,"cardId":100321010},{"idx":40,"cardId":101334020}],"resultCode":1}}
{"ts":"2026-06-05T16:36:21.2805258Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":1,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"resultCode":1}}
{"ts":"2026-06-05T16:36:21.2820523Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-05T16:36:45.4884447Z","direction":"send","uri":"Swap","body":{"idxList":[3]}}
{"ts":"2026-06-05T16:36:45.4909435Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"resultCode":1}}
{"ts":"2026-06-05T16:36:46.8360545Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"idxChangeSeed":857671914,"spin":243,"resultCode":1}}
{"ts":"2026-06-05T16:36:46.9530582Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":5,"playSeq":6,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:36:49.0622004Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-05T16:36:53.9257769Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":6,"playSeq":7}}
{"ts":"2026-06-05T16:36:53.9473080Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
{"ts":"2026-06-05T16:36:54.4348349Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":8,"turnState":0,"resultCode":1}}
{"ts":"2026-06-05T16:36:54.4458360Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"17","key3":"0","key4":"141","key5":"170","key6":"0"}}}
{"ts":"2026-06-05T16:36:54.4643354Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":9,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:36:54.5198350Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"actionSeq":2}}
{"ts":"2026-06-05T16:36:59.8031059Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"type":30}}
{"ts":"2026-06-05T16:37:02.5213012Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-05T16:37:03.0188508Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"141","key5":"170","key6":"0"},"type":0,"actionSeq":5,"cemetery":[1,0]}}
{"ts":"2026-06-05T16:37:03.1346446Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":12,"playSeq":10,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:03.1561609Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-05T16:37:07.8849014Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":13,"playSeq":11,"playIdx":37,"type":30,"knownList":[{"idx":37,"cardId":101121020,"to":20,"spellboost":0,"attachTarget":""}]}}
{"ts":"2026-06-05T16:37:08.1357329Z","direction":"send","uri":"Echo","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":0,"from":10,"to":20}},{"playerParam":{"isSelf":0,"buffUnit":1}}],"type":30}}
{"ts":"2026-06-05T16:37:09.1078628Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":14,"playSeq":12}}
{"ts":"2026-06-05T16:37:09.6087702Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":13,"turnState":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:11.0449391Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
{"ts":"2026-06-05T16:37:11.4765571Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"143","key5":"170","key6":"101121070"}}}
{"ts":"2026-06-05T16:37:11.4925578Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":14,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:11.5190593Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":8}}
{"ts":"2026-06-05T16:37:25.1553015Z","direction":"send","uri":"PlayActions","body":{"playIdx":2,"targetList":[{"targetIdx":37,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[2],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"type":31}}
{"ts":"2026-06-05T16:37:26.1829531Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-05T16:37:26.6838102Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"145","key5":"170","key6":"0"},"type":0,"actionSeq":11,"cemetery":[2,1]}}
{"ts":"2026-06-05T16:37:28.3338739Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":15,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:28.3556277Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-05T16:37:33.2699751Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":16}}
{"ts":"2026-06-05T16:37:33.2873251Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
{"ts":"2026-06-05T16:37:33.7738440Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":22,"playSeq":17,"turnState":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:33.7898440Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"147","key5":"265","key6":"0"}}}
{"ts":"2026-06-05T16:37:33.8063464Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":24,"playSeq":18,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:33.8323438Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":13}}
{"ts":"2026-06-05T16:37:38.6691412Z","direction":"send","uri":"PlayActions","body":{"playIdx":14,"orderList":[{"move":{"idx":[14],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,8,24,15,37],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"6"}},{"move":{"idx":[36,18],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"type":30}}

View File

@@ -0,0 +1,38 @@
{"ts":"2026-06-05T16:36:19.3388464Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
{"ts":"2026-06-05T16:36:19.3458471Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100114010},{"idx":2,"cardId":101121080},{"idx":3,"cardId":101114010},{"idx":4,"cardId":113011010},{"idx":5,"cardId":101121020},{"idx":6,"cardId":100111010},{"idx":7,"cardId":102141010},{"idx":8,"cardId":102111060},{"idx":9,"cardId":100111070},{"idx":10,"cardId":113011010},{"idx":11,"cardId":101131050},{"idx":12,"cardId":101121080},{"idx":13,"cardId":100111010},{"idx":14,"cardId":102121010},{"idx":15,"cardId":701141011},{"idx":16,"cardId":100114010},{"idx":17,"cardId":101114050},{"idx":18,"cardId":102131020},{"idx":19,"cardId":102111060},{"idx":20,"cardId":100114010},{"idx":21,"cardId":102121030},{"idx":22,"cardId":102121030},{"idx":23,"cardId":101114050},{"idx":24,"cardId":100111070},{"idx":25,"cardId":100111020},{"idx":26,"cardId":101121110},{"idx":27,"cardId":102131030},{"idx":28,"cardId":113011010},{"idx":29,"cardId":102131010},{"idx":30,"cardId":100111020},{"idx":31,"cardId":101131020},{"idx":32,"cardId":101114050},{"idx":33,"cardId":101121010},{"idx":34,"cardId":101121080},{"idx":35,"cardId":101121110},{"idx":36,"cardId":101114010},{"idx":37,"cardId":101121020},{"idx":38,"cardId":100111020},{"idx":39,"cardId":102121010},{"idx":40,"cardId":101121010}],"resultCode":1}}
{"ts":"2026-06-05T16:36:21.2050506Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":0,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"resultCode":1}}
{"ts":"2026-06-05T16:36:21.2065539Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-05T16:36:46.8260552Z","direction":"send","uri":"Swap","body":{"idxList":[]}}
{"ts":"2026-06-05T16:36:46.8285526Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-05T16:36:46.8295526Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"idxChangeSeed":224055814,"spin":243,"resultCode":1}}
{"ts":"2026-06-05T16:36:46.9460536Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":0}}
{"ts":"2026-06-05T16:36:53.9137786Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
{"ts":"2026-06-05T16:36:54.4108350Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"143","key5":"17","key6":"0"},"type":0,"actionSeq":2,"cemetery":[0,0]}}
{"ts":"2026-06-05T16:36:54.5258347Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":8,"playSeq":6,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:36:54.5523350Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"avarice":1}}]}}
{"ts":"2026-06-05T16:36:59.8136078Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":9,"playSeq":7,"playIdx":1,"type":30,"knownList":[{"idx":1,"cardId":100314010,"to":30,"spellboost":0,"attachTarget":""}]}}
{"ts":"2026-06-05T16:37:00.0026151Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}],"type":30}}
{"ts":"2026-06-05T16:37:02.5313002Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":10,"playSeq":8}}
{"ts":"2026-06-05T16:37:02.5503289Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-05T16:37:03.0339655Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":9,"turnState":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:03.0510647Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"141","key5":"175","key6":"0"}}}
{"ts":"2026-06-05T16:37:03.0670774Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":10,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:03.1321443Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":5}}
{"ts":"2026-06-05T16:37:07.8809043Z","direction":"send","uri":"PlayActions","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":1,"from":10,"to":20}},{"playerParam":{"isSelf":1,"buffUnit":1}}],"type":30}}
{"ts":"2026-06-05T16:37:09.0943648Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
{"ts":"2026-06-05T16:37:09.5927718Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"143","key2":"170","key3":"101121070","key4":"141","key5":"175","key6":"0"},"type":0,"actionSeq":8,"cemetery":[0,1]}}
{"ts":"2026-06-05T16:37:11.5305571Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":16,"playSeq":11,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:11.5519635Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-05T16:37:25.1769841Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":12,"playIdx":2,"type":31,"knownList":[{"idx":2,"cardId":100314020,"to":30,"spellboost":1,"attachTarget":""}],"oppoTargetList":[{"targetIdx":37,"isSelf":0}]}}
{"ts":"2026-06-05T16:37:25.3675799Z","direction":"send","uri":"Echo","body":{"playIdx":2,"orderList":[{"move":{"idx":[2],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":31}}
{"ts":"2026-06-05T16:37:26.1899527Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":13}}
{"ts":"2026-06-05T16:37:26.6913132Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":14,"turnState":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:28.1438230Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-05T16:37:28.2597994Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"145","key2":"170","key3":"0","key4":"142","key5":"334","key6":"0"}}}
{"ts":"2026-06-05T16:37:28.2755229Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":15,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:28.3213347Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":11}}
{"ts":"2026-06-05T16:37:33.2604742Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
{"ts":"2026-06-05T16:37:33.7603450Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"147","key2":"265","key3":"0","key4":"142","key5":"334","key6":"0"},"type":0,"actionSeq":13,"cemetery":[1,2]}}
{"ts":"2026-06-05T16:37:33.8438435Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":25,"playSeq":16,"spin":0,"resultCode":1}}
{"ts":"2026-06-05T16:37:33.8648584Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-05T16:37:38.6786420Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":26,"playSeq":17,"playIdx":14,"type":30,"knownList":[{"idx":14,"cardId":101314020,"to":30,"spellboost":2,"attachTarget":""}]}}

View File

@@ -0,0 +1,109 @@
{"ts":"2026-06-07T12:05:10.0824442Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
{"ts":"2026-06-07T12:05:10.1134456Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"907324319325","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":742186477},"oppoInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":742186477,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":101324040},{"idx":2,"cardId":101321070},{"idx":3,"cardId":101321040},{"idx":4,"cardId":101324050},{"idx":5,"cardId":101334030},{"idx":6,"cardId":102334020},{"idx":7,"cardId":101024010},{"idx":8,"cardId":102331010},{"idx":9,"cardId":101324040},{"idx":10,"cardId":101314020},{"idx":11,"cardId":127011010},{"idx":12,"cardId":100314020},{"idx":13,"cardId":101024010},{"idx":14,"cardId":701341011},{"idx":15,"cardId":101311010},{"idx":16,"cardId":101311050},{"idx":17,"cardId":102324040},{"idx":18,"cardId":101341010},{"idx":19,"cardId":127011010},{"idx":20,"cardId":101311010},{"idx":21,"cardId":101314020},{"idx":22,"cardId":100321010},{"idx":23,"cardId":101321070},{"idx":24,"cardId":100314030},{"idx":25,"cardId":101314020},{"idx":26,"cardId":101311050},{"idx":27,"cardId":101024010},{"idx":28,"cardId":100314010},{"idx":29,"cardId":127011010},{"idx":30,"cardId":100314040},{"idx":31,"cardId":100321010},{"idx":32,"cardId":101334020},{"idx":33,"cardId":100314030},{"idx":34,"cardId":100314040},{"idx":35,"cardId":101321040},{"idx":36,"cardId":102324040},{"idx":37,"cardId":100314020},{"idx":38,"cardId":101334040},{"idx":39,"cardId":100314010},{"idx":40,"cardId":101324050}],"resultCode":1}}
{"ts":"2026-06-07T12:05:13.3684415Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":0,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"resultCode":1}}
{"ts":"2026-06-07T12:05:13.3699431Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-07T12:05:34.8570706Z","direction":"send","uri":"Swap","body":{"idxList":[2,3]}}
{"ts":"2026-06-07T12:05:34.8895711Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"resultCode":1}}
{"ts":"2026-06-07T12:05:34.8905684Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"idxChangeSeed":1430655717,"spin":243,"resultCode":1}}
{"ts":"2026-06-07T12:05:36.6990699Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[8],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":0}}
{"ts":"2026-06-07T12:05:42.2485694Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,8],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
{"ts":"2026-06-07T12:05:42.7450678Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"56","key3":"0","key4":"143","key5":"14","key6":"0"},"type":0,"actionSeq":2,"cemetery":[0,0]}}
{"ts":"2026-06-07T12:05:42.8775704Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":8,"playSeq":6,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:42.9050694Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[10,16],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"avarice":1}}]}}
{"ts":"2026-06-07T12:05:46.4670675Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":9,"playSeq":7}}
{"ts":"2026-06-07T12:05:46.4855683Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,10,16],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-07T12:05:46.9690709Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":10,"playSeq":8,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:46.9860711Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"56","key3":"0","key4":"142","key5":"134","key6":"0"}}}
{"ts":"2026-06-07T12:05:47.0020697Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":10,"playSeq":9,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:47.4990684Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[29],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":4}}
{"ts":"2026-06-07T12:05:54.6460692Z","direction":"send","uri":"PlayActions","body":{"playIdx":8,"orderList":[{"move":{"idx":[8],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:05:55.7140680Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:05:56.2210693Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"143","key2":"140","key3":"102331036","key4":"142","key5":"134","key6":"0"},"type":0,"actionSeq":7,"cemetery":[0,0]}}
{"ts":"2026-06-07T12:05:57.0875698Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":15,"playSeq":10,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:57.1090694Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[15],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:06:12.6924224Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":16,"playSeq":11,"playIdx":1,"type":30,"knownList":[{"idx":1,"cardId":102131030,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":1,"tribe":"0"}]}}
{"ts":"2026-06-07T12:06:12.9394251Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:16.5024225Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":17,"playSeq":12}}
{"ts":"2026-06-07T12:06:16.5194264Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,10,16,15],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:16.9874227Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":18,"playSeq":13,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:17.0039250Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"140","key3":"102331036","key4":"144","key5":"177","key6":"102131049"}}}
{"ts":"2026-06-07T12:06:17.0209229Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":18,"playSeq":14,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:17.0494250Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[3],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":10}}
{"ts":"2026-06-07T12:06:28.8094232Z","direction":"send","uri":"PlayActions","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:29.8539237Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:30.3519249Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"145","key2":"140","key3":"203652104","key4":"144","key5":"177","key6":"102131049"},"type":0,"actionSeq":13,"cemetery":[0,0]}}
{"ts":"2026-06-07T12:06:31.2029243Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":23,"playSeq":15,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:31.2239242Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:06:36.0499227Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":25,"playSeq":16,"playIdx":1,"type":10,"knownList":[{"idx":1,"cardId":102131030,"to":30,"spellboost":0,"attachTarget":"","cost":2,"clan":1,"tribe":"0"}],"oppoTargetList":[{"targetIdx":8,"isSelf":0}]}}
{"ts":"2026-06-07T12:06:36.0879224Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[8],"isSelf":1,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:06:36.7079231Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":26,"playSeq":17}}
{"ts":"2026-06-07T12:06:37.1924235Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":27,"playSeq":18,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:38.0604227Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,10,16,15,24],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
{"ts":"2026-06-07T12:06:38.1769227Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"147","key2":"140","key3":"101321058","key4":"148","key5":"321","key6":"0"}}}
{"ts":"2026-06-07T12:06:38.1919253Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":26,"playSeq":19,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:38.2194225Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":2}},{"move":{"idx":[19],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":16}}
{"ts":"2026-06-07T12:06:46.5499241Z","direction":"send","uri":"PlayActions","body":{"playIdx":29,"keyAction":[{"type":1,"cardId":127011010,"selectCard":{"cardId":[121011010],"open":0}}],"orderList":[{"move":{"idx":[29],"isSelf":1,"from":10,"to":20}},{"add":{"idx":[41],"isSelf":1,"card":{"cardId":121011010},"isChoice":"1"}},{"move":{"idx":[41],"isSelf":1,"from":50,"to":10}}],"type":30}}
{"ts":"2026-06-07T12:06:50.3119230Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,19,41],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:50.8109234Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"149","key2":"305","key3":"228332150","key4":"148","key5":"321","key6":"0"},"type":0,"actionSeq":19,"cemetery":[1,1]}}
{"ts":"2026-06-07T12:06:50.9109252Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":32,"playSeq":20,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:50.9319252Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[11],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:06:55.3344248Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":33,"playSeq":21,"playIdx":10,"type":30,"knownList":[{"idx":10,"cardId":101121080,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:06:55.5239284Z","direction":"send","uri":"Echo","body":{"playIdx":10,"orderList":[{"move":{"idx":[10],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:56.0979233Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":34,"playSeq":22}}
{"ts":"2026-06-07T12:06:56.5964232Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":35,"playSeq":23,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:57.4474248Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}}]}}
{"ts":"2026-06-07T12:06:57.5634280Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"149","key2":"305","key3":"228332150","key4":"150","key5":"302","key6":"101121116"}}}
{"ts":"2026-06-07T12:06:57.5794253Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":36,"playSeq":24,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:57.6139259Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":22}}
{"ts":"2026-06-07T12:07:02.6699249Z","direction":"send","uri":"PlayActions","body":{"playIdx":39,"orderList":[{"move":{"idx":[39],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[1,4,5,19,41],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"7"}},{"move":{"idx":[17],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}},{"trigger":{"isSelf":1,"avarice":1}}],"type":30}}
{"ts":"2026-06-07T12:07:10.2104225Z","direction":"send","uri":"PlayActions","body":{"playIdx":41,"orderList":[{"move":{"idx":[41],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[6],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"type":30}}
{"ts":"2026-06-07T12:07:17.7444250Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,4,5,19,17,6],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:18.2599231Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"147","key2":"221","key3":"349343345","key4":"150","key5":"302","key6":"101121116"},"type":0,"actionSeq":26,"cemetery":[2,1]}}
{"ts":"2026-06-07T12:07:18.3594228Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":41,"playSeq":25,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:18.3874231Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[6],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:07:22.0834250Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":42,"playSeq":26,"playIdx":6,"type":30,"knownList":[{"idx":6,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"uList":[{"idxList":[34],"from":0,"to":10,"isSelf":1,"skill":"6|19|0"}]}}
{"ts":"2026-06-07T12:07:22.2814232Z","direction":"send","uri":"Echo","body":{"playIdx":6,"orderList":[{"move":{"idx":[6],"isSelf":0,"from":10,"to":20}},{"target":{"isSelf":0,"group":["g1"],"conditions":[{"state":0,"tribe":"eq7"}],"rand":[[0.739030951046865]]}},{"move":{"idx":"g1","isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
{"ts":"2026-06-07T12:07:25.6384231Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":43,"playSeq":27}}
{"ts":"2026-06-07T12:07:25.6554259Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11,34],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:26.1384241Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":44,"playSeq":28,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:26.1544243Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"147","key2":"221","key3":"349343345","key4":"149","key5":"540","key6":"214132162"}}}
{"ts":"2026-06-07T12:07:26.1709251Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":45,"playSeq":29,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:26.2184224Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[32],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":29}}
{"ts":"2026-06-07T12:07:34.2019228Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"targetList":[{"targetIdx":6,"isSelf":1,"selectSkillIndex":[1],"skillIndex":[1]}],"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,5,19,17,6,32],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"20"}},{"alter":{"idx":[6],"isSelf":1,"type":"add","spellboost":"a2","attachTarget":"21"}},{"move":{"idx":[23],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"type":31}}
{"ts":"2026-06-07T12:07:41.2306722Z","direction":"send","uri":"PlayActions","body":{"playIdx":17,"orderList":[{"move":{"idx":[17],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,5,19,6,32,23],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"23"}},{"add":{"idx":[42],"isSelf":1,"card":{"cardId":900311050}}},{"move":{"idx":[42],"isSelf":1,"from":50,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:07:46.6846799Z","direction":"send","uri":"PlayActions","body":{"playIdx":41,"targetList":[{"targetIdx":10,"isSelf":0}],"type":10}}
{"ts":"2026-06-07T12:07:48.2356829Z","direction":"send","uri":"PlayActions","body":{"playIdx":29,"targetList":[{"targetIdx":10,"isSelf":0}],"orderList":[{"move":{"idx":[29],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[10],"isSelf":0,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:07:49.9904200Z","direction":"send","uri":"PlayActions","body":{"playIdx":3,"targetList":[{"targetIdx":6,"isSelf":0}],"orderList":[{"move":{"idx":[3],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[6],"isSelf":0,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:07:51.8734061Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[4,5,19,6,32,23],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":5}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:52.3726572Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"154","key2":"393","key3":"1021322270","key4":"153","key5":"540","key6":"0"},"type":0,"actionSeq":36,"cemetery":[6,3]}}
{"ts":"2026-06-07T12:07:52.4729369Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":53,"playSeq":30,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:52.4946960Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":6}},{"move":{"idx":[18],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:07:57.1776003Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":54,"playSeq":31,"playIdx":34,"type":30,"knownList":[{"idx":34,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"uList":[{"idxList":[5],"from":0,"to":10,"isSelf":1,"skill":"34|28|0"}]}}
{"ts":"2026-06-07T12:07:57.2503917Z","direction":"send","uri":"Echo","body":{"playIdx":34,"orderList":[{"move":{"idx":[34],"isSelf":0,"from":10,"to":20}},{"target":{"isSelf":0,"group":["g1"],"conditions":[{"state":0,"tribe":"eq7"}],"rand":[[0.668529128501438]]}},{"move":{"idx":"g1","isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
{"ts":"2026-06-07T12:07:58.2623261Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":55,"playSeq":32,"playIdx":18,"type":30,"knownList":[{"idx":18,"cardId":100111010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:08:00.2645722Z","direction":"send","uri":"Echo","body":{"playIdx":18,"orderList":[{"move":{"idx":[18],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:02.7695981Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":56,"playSeq":33,"playIdx":5,"type":30,"knownList":[{"idx":5,"cardId":113011010,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":0,"tribe":"7"}]}}
{"ts":"2026-06-07T12:08:02.8451199Z","direction":"send","uri":"Echo","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":0,"from":10,"to":20}},{"scan":{"idx":[4,7,8,9,12,13,14,17,19,20,21,22,23,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40],"conditions":[{"tribe":"7"}]}}],"type":30}}
{"ts":"2026-06-07T12:08:05.7442862Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":57,"playSeq":34}}
{"ts":"2026-06-07T12:08:05.7667846Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":1}},{"move":{"idx":[42],"isSelf":1,"from":20,"to":30,"hasGuard":[42]}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-07T12:08:06.2448192Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":58,"playSeq":35,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:06.2608181Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"156","key2":"393","key3":"121011060","key4":"152","key5":"302","key6":"326133205"}}}
{"ts":"2026-06-07T12:08:06.2778185Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":64,"playSeq":36,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:06.3228189Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[38],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":41}}
{"ts":"2026-06-07T12:08:17.8343721Z","direction":"send","uri":"PlayActions","body":{"playIdx":19,"keyAction":[{"type":1,"cardId":127011010,"selectCard":{"cardId":[120011010],"open":0}}],"orderList":[{"move":{"idx":[19],"isSelf":1,"from":10,"to":20}},{"add":{"idx":[43],"isSelf":1,"card":{"cardId":120011010},"isChoice":"1"}},{"move":{"idx":[43],"isSelf":1,"from":50,"to":10}}],"type":30}}
{"ts":"2026-06-07T12:08:21.3291075Z","direction":"send","uri":"PlayActions","body":{"playIdx":4,"targetList":[{"targetIdx":5,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[4],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[5,6,32,23,38,43],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"33"}},{"metamorphose":{"idx":[5],"isSelf":0,"after":{"cardId":900311020}}}],"type":31}}
{"ts":"2026-06-07T12:08:25.9578557Z","direction":"send","uri":"PlayActions","body":{"playIdx":5,"targetList":[{"targetIdx":34,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[5],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[6,32,23,38,43],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"36"}},{"move":{"idx":[34],"isSelf":0,"from":20,"to":30}},{"add":{"idx":[44],"isSelf":1,"card":{"cardId":900334010}}},{"move":{"idx":[44],"isSelf":1,"from":50,"to":10}}],"type":31}}
{"ts":"2026-06-07T12:08:29.5860517Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[6,32,23,38,43,44],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}}]}}
{"ts":"2026-06-07T12:08:30.0854894Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"162","key2":"770","key3":"248022140","key4":"154","key5":"302","key6":"1000422107"},"type":0,"actionSeq":46,"cemetery":[9,4]}}
{"ts":"2026-06-07T12:08:30.1853353Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":65,"playSeq":37,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:30.2078357Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[35],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:08:37.7255447Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":66,"playSeq":38,"playIdx":15,"type":30,"knownList":[{"idx":15,"cardId":101121110,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:08:37.9275599Z","direction":"send","uri":"Echo","body":{"playIdx":15,"orderList":[{"move":{"idx":[15],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:38.5997627Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":67,"playSeq":39}}
{"ts":"2026-06-07T12:08:39.0994174Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":68,"playSeq":40,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:39.8688009Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,3,16,24,11,35],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":5}}]}}
{"ts":"2026-06-07T12:08:39.9995393Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"162","key2":"770","key3":"248022140","key4":"156","key5":"417","key6":"1101543355"}}}
{"ts":"2026-06-07T12:08:40.0160656Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":80,"playSeq":41,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:40.0427529Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[20],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":49}}
{"ts":"2026-06-07T12:08:44.0590977Z","direction":"send","uri":"PlayActions","body":{"playIdx":20,"orderList":[{"move":{"idx":[20],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:49.2798814Z","direction":"send","uri":"PlayActions","body":{"playIdx":23,"orderList":[{"move":{"idx":[23],"isSelf":1,"from":10,"to":20}},{"playerParam":{"isSelf":1,"buffUnit":1}}],"type":30}}

View File

@@ -0,0 +1,118 @@
{"ts":"2026-06-07T12:05:10.0764449Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
{"ts":"2026-06-07T12:05:10.1264431Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"907324319325","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":742186477},"oppoInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":742186477,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":102131030},{"idx":2,"cardId":101121080},{"idx":3,"cardId":101131050},{"idx":4,"cardId":101114010},{"idx":5,"cardId":113011010},{"idx":6,"cardId":113011010},{"idx":7,"cardId":101121020},{"idx":8,"cardId":101121010},{"idx":9,"cardId":102141010},{"idx":10,"cardId":101121080},{"idx":11,"cardId":101114010},{"idx":12,"cardId":102111060},{"idx":13,"cardId":102131020},{"idx":14,"cardId":102131010},{"idx":15,"cardId":101121110},{"idx":16,"cardId":101121110},{"idx":17,"cardId":100111020},{"idx":18,"cardId":100111010},{"idx":19,"cardId":102121030},{"idx":20,"cardId":100111020},{"idx":21,"cardId":101121080},{"idx":22,"cardId":101121020},{"idx":23,"cardId":100111070},{"idx":24,"cardId":102111060},{"idx":25,"cardId":101131020},{"idx":26,"cardId":101114050},{"idx":27,"cardId":101114050},{"idx":28,"cardId":101121010},{"idx":29,"cardId":701141011},{"idx":30,"cardId":102121010},{"idx":31,"cardId":100111010},{"idx":32,"cardId":100114010},{"idx":33,"cardId":101114050},{"idx":34,"cardId":113011010},{"idx":35,"cardId":100114010},{"idx":36,"cardId":100111020},{"idx":37,"cardId":102121030},{"idx":38,"cardId":102121010},{"idx":39,"cardId":100114010},{"idx":40,"cardId":100111070}],"resultCode":1}}
{"ts":"2026-06-07T12:05:13.3624432Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":1,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"resultCode":1}}
{"ts":"2026-06-07T12:05:13.3644442Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-07T12:05:29.7550686Z","direction":"send","uri":"Swap","body":{"idxList":[]}}
{"ts":"2026-06-07T12:05:29.7695695Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
{"ts":"2026-06-07T12:05:34.8895711Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":4},{"pos":2,"idx":5}],"idxChangeSeed":661650374,"spin":243,"resultCode":1}}
{"ts":"2026-06-07T12:05:36.7840686Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":5,"playSeq":6,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:37.9140709Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[8],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:05:42.3100693Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":6,"playSeq":7}}
{"ts":"2026-06-07T12:05:42.3835692Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,8],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
{"ts":"2026-06-07T12:05:42.7575705Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":7,"playSeq":8,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:42.7750675Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"14","key3":"0","key4":"141","key5":"56","key6":"0"}}}
{"ts":"2026-06-07T12:05:42.7905712Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":7,"playSeq":9,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:42.8590737Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[10,16],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"actionSeq":2}}
{"ts":"2026-06-07T12:05:46.4565675Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,10,16],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-07T12:05:46.9540693Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"142","key2":"134","key3":"0","key4":"141","key5":"56","key6":"0"},"type":0,"actionSeq":4,"cemetery":[0,0]}}
{"ts":"2026-06-07T12:05:47.5195696Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":11,"playSeq":10,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:47.5415707Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[29],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:05:54.7275709Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":12,"playSeq":11,"playIdx":8,"type":30,"knownList":[{"idx":8,"cardId":102331010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:05:54.9510692Z","direction":"send","uri":"Echo","body":{"playIdx":8,"orderList":[{"move":{"idx":[8],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:05:55.7230693Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":13,"playSeq":12}}
{"ts":"2026-06-07T12:05:56.2255669Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":14,"playSeq":13,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:56.9100687Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:05:57.0275696Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"142","key2":"134","key3":"0","key4":"143","key5":"140","key6":"102331036"}}}
{"ts":"2026-06-07T12:05:57.0415684Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":14,"playSeq":14,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:05:57.0740682Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[15],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":7}}
{"ts":"2026-06-07T12:06:12.6129250Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:16.4794226Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,10,16,15],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:16.9789227Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"144","key2":"177","key3":"102131049","key4":"143","key5":"140","key6":"102331036"},"type":0,"actionSeq":10,"cemetery":[0,0]}}
{"ts":"2026-06-07T12:06:17.0619236Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":19,"playSeq":15,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:17.0839228Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[3],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:06:28.8204242Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":20,"playSeq":16,"playIdx":3,"type":30,"knownList":[{"idx":3,"cardId":101321040,"to":20,"spellboost":0,"attachTarget":"","cost":2,"clan":3,"tribe":"0"}]}}
{"ts":"2026-06-07T12:06:29.0409223Z","direction":"send","uri":"Echo","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:29.8804238Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":21,"playSeq":17}}
{"ts":"2026-06-07T12:06:30.3639243Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":22,"playSeq":18,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:30.9664239Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,29],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:31.1154246Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"144","key2":"177","key3":"102131049","key4":"145","key5":"140","key6":"203652104"}}}
{"ts":"2026-06-07T12:06:31.1309231Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":22,"playSeq":19,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:31.1914245Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":13}}
{"ts":"2026-06-07T12:06:36.0239226Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"targetList":[{"targetIdx":8,"isSelf":0}],"orderList":[{"move":{"idx":[1],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[8],"isSelf":0,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:06:36.6854243Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,10,16,15,24],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
{"ts":"2026-06-07T12:06:37.1859231Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"148","key2":"321","key3":"0","key4":"147","key5":"140","key6":"101321058"},"type":0,"actionSeq":16,"cemetery":[1,1]}}
{"ts":"2026-06-07T12:06:38.2359235Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":27,"playSeq":20,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:38.2569229Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":2}},{"move":{"idx":[19],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:06:46.5794252Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":30,"playSeq":21,"playIdx":29,"type":30,"knownList":[{"idx":29,"cardId":127011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"keyAction":[{"type":1,"cardId":127011010}]}}
{"ts":"2026-06-07T12:06:47.0374244Z","direction":"send","uri":"Echo","body":{"playIdx":29,"orderList":[{"move":{"idx":[29],"isSelf":0,"from":10,"to":20}},{"add":{"idx":[41],"isSelf":0,"card":{"candidates":[121011010,120011010]},"isChoice":"1"}},{"move":{"idx":[41],"isSelf":0,"from":50,"to":10}}],"type":30}}
{"ts":"2026-06-07T12:06:50.3279267Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":31,"playSeq":22}}
{"ts":"2026-06-07T12:06:50.3444241Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,19,41],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
{"ts":"2026-06-07T12:06:50.8274230Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":32,"playSeq":23,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:50.8434224Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"148","key2":"321","key3":"0","key4":"149","key5":"305","key6":"228332150"}}}
{"ts":"2026-06-07T12:06:50.8594230Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":31,"playSeq":24,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:50.9024228Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[11],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":19}}
{"ts":"2026-06-07T12:06:55.3169242Z","direction":"send","uri":"PlayActions","body":{"playIdx":10,"orderList":[{"move":{"idx":[10],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:06:56.0779247Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}}]}}
{"ts":"2026-06-07T12:06:56.5774224Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"150","key2":"302","key3":"101121116","key4":"149","key5":"305","key6":"228332150"},"type":0,"actionSeq":22,"cemetery":[1,1]}}
{"ts":"2026-06-07T12:06:57.6284227Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":37,"playSeq":25,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:06:57.6504253Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:07:02.6859240Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":38,"playSeq":26,"playIdx":39,"type":30,"knownList":[{"idx":39,"cardId":100314010,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:07:02.8454236Z","direction":"send","uri":"Echo","body":{"playIdx":39,"orderList":[{"move":{"idx":[39],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[1,4,5,19,41],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"7"}},{"move":{"idx":[17],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}},{"trigger":{"isSelf":0,"avarice":1}}],"type":30}}
{"ts":"2026-06-07T12:07:10.2264230Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":39,"playSeq":27,"playIdx":41,"type":30,"knownList":[{"idx":41,"cardId":121011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:07:10.3164226Z","direction":"send","uri":"Echo","body":{"playIdx":41,"orderList":[{"move":{"idx":[41],"isSelf":0,"from":10,"to":20}},{"move":{"idx":[6],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}],"type":30}}
{"ts":"2026-06-07T12:07:17.7599274Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":40,"playSeq":28}}
{"ts":"2026-06-07T12:07:17.7789256Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,4,5,19,17,6],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:18.2769237Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":41,"playSeq":29,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:18.2949243Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"150","key2":"302","key3":"101121116","key4":"147","key5":"221","key6":"349343345"}}}
{"ts":"2026-06-07T12:07:18.3089265Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":40,"playSeq":30,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:18.3409222Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[6],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":26}}
{"ts":"2026-06-07T12:07:22.0604232Z","direction":"send","uri":"PlayActions","body":{"playIdx":6,"orderList":[{"move":{"idx":[6],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[34],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"uList":[{"idxList":[34],"from":0,"to":10,"isSelf":1,"skill":"6|19|0"}],"type":30}}
{"ts":"2026-06-07T12:07:25.6229734Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11,34],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":3}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:26.1224220Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"149","key2":"540","key3":"214132162","key4":"147","key5":"221","key6":"349343345"},"type":0,"actionSeq":29,"cemetery":[1,2]}}
{"ts":"2026-06-07T12:07:26.2219233Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":46,"playSeq":31,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:26.2444230Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[32],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:07:34.2504226Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":49,"playSeq":32,"playIdx":1,"type":31,"knownList":[{"idx":1,"cardId":101324040,"to":30,"spellboost":0,"attachTarget":"","cost":3,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":6,"isSelf":1}]}}
{"ts":"2026-06-07T12:07:34.4124257Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,5,19,17,6,32],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"20"}},{"move":{"idx":[23],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":31}}
{"ts":"2026-06-07T12:07:41.2491729Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":50,"playSeq":33,"playIdx":17,"type":30,"knownList":[{"idx":17,"cardId":102324040,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:07:41.4554809Z","direction":"send","uri":"Echo","body":{"playIdx":17,"orderList":[{"move":{"idx":[17],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,5,19,6,32,23],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"23"}},{"add":{"idx":[42],"isSelf":0,"card":{"cardId":900311050}}},{"move":{"idx":[42],"isSelf":0,"from":50,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:07:46.6891818Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":52,"playSeq":34,"playIdx":41,"type":10,"oppoTargetList":[{"targetIdx":10,"isSelf":0}]}}
{"ts":"2026-06-07T12:07:46.7161815Z","direction":"send","uri":"Echo","body":{"playIdx":41,"type":10}}
{"ts":"2026-06-07T12:07:48.2401820Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":54,"playSeq":35,"playIdx":29,"type":10,"knownList":[{"idx":29,"cardId":127011010,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"oppoTargetList":[{"targetIdx":10,"isSelf":0}]}}
{"ts":"2026-06-07T12:07:48.3302904Z","direction":"send","uri":"Echo","body":{"playIdx":29,"orderList":[{"move":{"idx":[29],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[10],"isSelf":1,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:07:50.0089639Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":56,"playSeq":36,"playIdx":3,"type":10,"knownList":[{"idx":3,"cardId":101321040,"to":30,"spellboost":0,"attachTarget":"","cost":2,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":6,"isSelf":0}]}}
{"ts":"2026-06-07T12:07:50.2322631Z","direction":"send","uri":"Echo","body":{"playIdx":3,"orderList":[{"move":{"idx":[3],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[6],"isSelf":1,"from":20,"to":30}}],"type":10}}
{"ts":"2026-06-07T12:07:51.8934054Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":57,"playSeq":37}}
{"ts":"2026-06-07T12:07:52.0776073Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[4,5,19,6,32,23],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":5}},{"trigger":{"isSelf":0,"avarice":0}}]}}
{"ts":"2026-06-07T12:07:52.3771546Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":58,"playSeq":38,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:52.3931550Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"153","key2":"540","key3":"0","key4":"154","key5":"393","key6":"1021322270"}}}
{"ts":"2026-06-07T12:07:52.4097475Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":52,"playSeq":39,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:07:52.4689367Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":6}},{"move":{"idx":[18],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":36}}
{"ts":"2026-06-07T12:07:57.1625968Z","direction":"send","uri":"PlayActions","body":{"playIdx":34,"orderList":[{"move":{"idx":[34],"isSelf":1,"from":10,"to":20}},{"move":{"idx":[5],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"uList":[{"idxList":[5],"from":0,"to":10,"isSelf":1,"skill":"34|28|0"}],"type":30}}
{"ts":"2026-06-07T12:07:58.2473269Z","direction":"send","uri":"PlayActions","body":{"playIdx":18,"orderList":[{"move":{"idx":[18],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:02.7615951Z","direction":"send","uri":"PlayActions","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:05.7352832Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,15,24,11],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":1}},{"move":{"idx":[42],"isSelf":0,"from":20,"to":30,"hasGuard":[42]}},{"trigger":{"isSelf":1,"avarice":0}}]}}
{"ts":"2026-06-07T12:08:06.2301214Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"152","key2":"302","key3":"326133205","key4":"156","key5":"393","key6":"121011060"},"type":0,"actionSeq":41,"cemetery":[3,7]}}
{"ts":"2026-06-07T12:08:06.3303197Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":65,"playSeq":40,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:06.3513355Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[38],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
{"ts":"2026-06-07T12:08:17.8463749Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":68,"playSeq":41,"playIdx":19,"type":30,"knownList":[{"idx":19,"cardId":127011010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"keyAction":[{"type":1,"cardId":127011010}]}}
{"ts":"2026-06-07T12:08:17.9224312Z","direction":"send","uri":"Echo","body":{"playIdx":19,"orderList":[{"move":{"idx":[19],"isSelf":0,"from":10,"to":20}},{"add":{"idx":[43],"isSelf":0,"card":{"candidates":[121011010,120011010]},"isChoice":"1"}},{"move":{"idx":[43],"isSelf":0,"from":50,"to":10}}],"type":30}}
{"ts":"2026-06-07T12:08:21.3856074Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":71,"playSeq":42,"playIdx":4,"type":31,"knownList":[{"idx":4,"cardId":101324050,"to":30,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}],"oppoTargetList":[{"targetIdx":5,"isSelf":0}]}}
{"ts":"2026-06-07T12:08:21.5844099Z","direction":"send","uri":"Echo","body":{"playIdx":4,"orderList":[{"move":{"idx":[4],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[5,6,32,23,38,43],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"33"}},{"metamorphose":{"idx":[5],"isSelf":1,"after":{"cardId":900311020}}}],"type":31}}
{"ts":"2026-06-07T12:08:25.9743530Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":74,"playSeq":43,"playIdx":5,"type":31,"knownList":[{"idx":5,"cardId":101334030,"to":30,"spellboost":2,"attachTarget":"","cost":3,"clan":3,"tribe":"0"}],"oppoTargetList":[{"targetIdx":34,"isSelf":0}]}}
{"ts":"2026-06-07T12:08:26.1638091Z","direction":"send","uri":"Echo","body":{"playIdx":5,"orderList":[{"move":{"idx":[5],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[6,32,23,38,43],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"36"}},{"move":{"idx":[34],"isSelf":1,"from":20,"to":30}},{"add":{"idx":[44],"isSelf":0,"card":{"cardId":900334010}}},{"move":{"idx":[44],"isSelf":0,"from":50,"to":10}}],"type":31}}
{"ts":"2026-06-07T12:08:29.6025555Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":75,"playSeq":44}}
{"ts":"2026-06-07T12:08:29.6190527Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[6,32,23,38,43,44],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":3}}]}}
{"ts":"2026-06-07T12:08:30.1015223Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":76,"playSeq":45,"turnState":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:30.1180409Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"154","key2":"302","key3":"1000422107","key4":"162","key5":"770","key6":"248022140"}}}
{"ts":"2026-06-07T12:08:30.1345601Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"907324319325","pubSeq":64,"playSeq":46,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:30.1768361Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[35],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":46}}
{"ts":"2026-06-07T12:08:37.7130477Z","direction":"send","uri":"PlayActions","body":{"playIdx":15,"orderList":[{"move":{"idx":[15],"isSelf":1,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:38.5902629Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,3,16,24,11,35],"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":5}}]}}
{"ts":"2026-06-07T12:08:39.0894170Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"156","key2":"417","key3":"1101543355","key4":"162","key5":"770","key6":"248022140"},"type":0,"actionSeq":49,"cemetery":[4,9]}}
{"ts":"2026-06-07T12:08:40.0572510Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":81,"playSeq":47,"spin":0,"resultCode":1}}
{"ts":"2026-06-07T12:08:40.0784574Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"canEvolve":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[20],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
{"ts":"2026-06-07T12:08:44.0705950Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":82,"playSeq":48,"playIdx":20,"type":30,"knownList":[{"idx":20,"cardId":101311010,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:08:44.2734716Z","direction":"send","uri":"Echo","body":{"playIdx":20,"orderList":[{"move":{"idx":[20],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:08:49.2868793Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"907324319325","pubSeq":83,"playSeq":49,"playIdx":23,"type":30,"knownList":[{"idx":23,"cardId":101321070,"to":20,"spellboost":0,"attachTarget":"","cost":0,"clan":0,"tribe":"0"}]}}
{"ts":"2026-06-07T12:08:49.5008504Z","direction":"send","uri":"Echo","body":{"playIdx":23,"orderList":[{"move":{"idx":[23],"isSelf":0,"from":10,"to":20}}],"type":30}}
{"ts":"2026-06-07T12:09:11.1269227Z","direction":"receive","uri":null,"body":{"uri":"BattleFinish","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"result":201,"resultCode":1}}

View File

@@ -0,0 +1,151 @@
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M11 (the GATE itself is the oracle): every prior milestone either had no skill_condition or
// seeded its gate TRUE so the effect fires (M4 seeded play_count>2; M10 seeded a play_count
// VALUE). None proved the engine SUPPRESSES an effect when a skill_condition evaluates FALSE —
// the dual of "effect fires". M11 proves conditional BRANCHING resolves headless by asserting
// BOTH directions of the SAME gated card in ONE fixture (design "M11 — NEXT" resume guide):
//
// * gate TRUE (play_count > 2, seeded via the public AddCurrentTrunPlayCount seam M4/M10 use)
// -> the when_play powerup fires -> the follower is buffed over its base stats.
// * gate FALSE (play_count <= 2, the bare-construction default)
// -> the powerup is a NO-OP: zero stat delta, BUT the card still pays its cost
// and still leaves hand -> board (the gate suppresses the EFFECT, not the PLAY).
//
// Card: 103111050 — the M4 self-buff follower (ELF clan-1 cost-1 base 1/1, sole non-evo skill
// `when_play` `powerup` `add_offense=1&add_life=1` to `character=me&target=self`), whose
// skill_condition is `character=me&target=self&play_count>2` (verified in cards.json). The gate
// reads BattlePlayerBase.GetCurrentTurnPlayCount(), seedable past/below the threshold via the
// public AddCurrentTrunPlayCount. Reusing the M4-proven buff DIMENSION means the only NEW thing
// under test is the CONDITIONAL — exactly the resume-guide's "proven effect dimension, gate is
// the oracle" prescription.
//
// Why one fixture, both branches, ONE card is decisive: the two assertions are jointly
// satisfiable ONLY by a correctly-gating engine. An "always-buffs" engine fails the FALSE branch
// (would buff with play_count=0); a "never-buffs" engine fails the TRUE branch (M4's gate seed
// wouldn't fire). M4 already demonstrated this split as a manual load-bearing probe (remove the
// seed -> buff vanishes); M11 promotes it to the PRIMARY assertion.
[TestFixture]
public class GatedConditionalOracleTests
{
// A clearly super-threshold seed (play_count 5 > 2): the gate evaluates TRUE, fanfare fires.
private const int GateTrueSeed = 5;
// The bare-construction default is play_count 0 (<= 2 -> gate FALSE); we seed nothing for the
// FALSE branch, exactly as M4's load-bearing probe did when it removed its seed.
private const int GateFalseSeed = 0;
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
// Resolve the gated self-buff follower on a FRESH battle with the per-turn play count seeded
// to `seededPlayCount`, and report the play's outcome. A fresh mgr per branch is required:
// play_count is per-mgr state and a resolved play mutates the board, so the two branches must
// not share a battle. Mirrors the M4 BuffFollowerOracleTests setup verbatim, parameterized on
// the seed (which is the only thing M11 varies between branches).
private (BattleCardBase card, CardParameter param, int ppBefore, int ppAfter,
int handBefore, bool inHandAfter, int inplayBefore, bool onBoardAfter, int inplayAfter)
PlayGatedSelfBuff(int seededPlayCount)
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr; // route GetIns() to this branch's mgr
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (M2/M3/M4 oracles): opponent refs + active turn flag. The
// self-buff target resolver (`character=me&target=self`) reads the active player's own
// in-play card, so the turn flag must be set before the fanfare sweeps.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over that silently blocks the
// play (M3 learning). This card deals no damage but the play-legality gate still checks it.
HeadlessEngineEnv.InitLeaderLife(mgr);
// THE GATE SEED — the one knob M11 turns between branches. The skill_condition
// `play_count>2` reads BattlePlayerBase.GetCurrentTurnPlayCount(); seed it via the public
// AddCurrentTrunPlayCount (M4/M10 seam). For the FALSE branch we leave the bare default 0.
if (seededPlayCount > 0) player.AddCurrentTrunPlayCount(seededPlayCount);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.BuffFollowerId);
// Place the gated self-buff follower in the active player's hand with PP to spare; empty board.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.BuffFollowerId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int inplayBefore = player.ClassAndInPlayCardList.Count;
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
$"ActionProcessor.PlayCard threw on the gated self-buff (seed={seededPlayCount})");
return (card, cardParam, ppBefore, player.Pp,
handBefore, player.HandCardList.Contains(card),
inplayBefore, player.ClassAndInPlayCardList.Contains(card),
player.ClassAndInPlayCardList.Count);
}
[Test]
public void Gated_fanfare_fires_when_seeded_true_and_is_suppressed_when_false()
{
// ----- Branch 1: gate TRUE (play_count 5 > 2) -> the fanfare FIRES (M4 dimension). -----
var t = PlayGatedSelfBuff(GateTrueSeed);
// ----- Branch 2: gate FALSE (play_count 0 <= 2) -> the fanfare is SUPPRESSED. -----
var f = PlayGatedSelfBuff(GateFalseSeed);
Assert.Multiple(() =>
{
// PRIMARY M11 assertion — the gate itself: SAME card, opposite stat outcomes driven
// ONLY by the seeded condition.
// TRUE -> buffed: base 1/1 + 1/1 = 2/2.
Assert.That(t.card.Atk, Is.EqualTo(t.param.Atk + HeadlessEngineEnv.BuffAddOffense),
"[gate TRUE] atk != base + add_offense (fanfare should have fired)");
Assert.That(t.card.Life, Is.EqualTo(t.param.Life + HeadlessEngineEnv.BuffAddLife),
"[gate TRUE] life != base + add_life (fanfare should have fired)");
// FALSE -> unbuffed: stays at the CardCSVData base 1/1 (effect suppressed).
Assert.That(f.card.Atk, Is.EqualTo(f.param.Atk),
"[gate FALSE] atk != base (fanfare should have been gated out)");
Assert.That(f.card.Life, Is.EqualTo(f.param.Life),
"[gate FALSE] life != base (fanfare should have been gated out)");
// The gate suppresses the EFFECT, not the PLAY: in BOTH branches the card still pays
// its cost and still moves hand -> board like any follower.
// TRUE branch:
Assert.That(t.ppAfter, Is.EqualTo(t.ppBefore - t.param.Cost), "[gate TRUE] PP not reduced by cost");
Assert.That(t.inHandAfter, Is.False, "[gate TRUE] card still in hand");
Assert.That(t.onBoardAfter, Is.True, "[gate TRUE] card not on board");
Assert.That(t.inplayAfter, Is.EqualTo(t.inplayBefore + 1), "[gate TRUE] in-play count not +1");
// FALSE branch — the M11 crux: cost STILL paid + card STILL resolves despite the no-op effect.
Assert.That(f.ppAfter, Is.EqualTo(f.ppBefore - f.param.Cost), "[gate FALSE] PP not reduced by cost");
Assert.That(f.inHandAfter, Is.False, "[gate FALSE] card still in hand");
Assert.That(f.onBoardAfter, Is.True, "[gate FALSE] card not on board");
Assert.That(f.inplayAfter, Is.EqualTo(f.inplayBefore + 1), "[gate FALSE] in-play count not +1");
});
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using Wizard;
namespace SVSim.BattleEngine.Tests
{
// Populates the engine's static CardMaster headless, from the loader's cards.json dump
// (serialized CardCSVData objects). We bypass the network/Resources init path
// (CardMaster.InitializeCardMaster) and the private ctor/field via reflection — CardMaster
// exposes no public injection seam. Class cards (id < 100) resolve via the ctor's
// _classCardParam, so an empty load still satisfies construction; pass real ids for the oracle.
public static class HeadlessCardMaster
{
private static readonly string CardsJsonPath =
Path.Combine(AppContext.BaseDirectory, "Data", "cards.json");
// Every id ever requested this process. Load is CUMULATIVE: each call rebuilds the master from
// the union, so a later Load(subset) never evicts cards an earlier Load (e.g. EnsureProcessGlobals's
// oracle set) installed. Without this, the static CardMaster is shared mutable state across the
// whole NUnit run and a Load(deck) in one test silently breaks an oracle test that runs after.
private static readonly HashSet<int> _everLoaded = new();
// Serialise Load: assembly-level Parallelizable(Fixtures) means concurrent fixtures race here,
// and HashSet<int>.Add + the static CardMaster install are not thread-safe.
private static readonly object _loadGate = new object();
// Load the given card ids (empty = none) into a CardMaster registered as Default, MERGED with all
// previously-loaded ids.
public static void Load(params int[] cardIds)
{
lock (_loadGate)
{
LoadCore(cardIds);
}
}
private static void LoadCore(int[] cardIds)
{
foreach (var id in cardIds) _everLoaded.Add(id);
var want = new HashSet<int>(_everLoaded);
var rows = new List<CardCSVData>();
if (want.Count > 0)
{
using var doc = JsonDocument.Parse(File.ReadAllText(CardsJsonPath));
int sort = 0;
foreach (var el in doc.RootElement.EnumerateArray())
{
if (!el.TryGetProperty("card_id", out var idEl)) continue;
if (!int.TryParse(idEl.GetString(), out var id) || !want.Contains(id)) continue;
rows.Add(BuildCardCsvData(el, sort++));
}
var missing = want.Except(rows.Select(r => int.Parse(r.card_id))).ToArray();
if (missing.Length > 0)
throw new InvalidOperationException(
"cards.json missing requested ids: " + string.Join(",", missing));
}
var cm = NewCardMaster(rows);
InjectAsDefault(cm);
}
// Construct a CardCSVData without running its CSV ctor; set each member from the JSON object
// by exact name match (cards.json keys == CardCSVData member names).
private static CardCSVData BuildCardCsvData(JsonElement el, int sortIndex)
{
var c = (CardCSVData)FormatterServices.GetUninitializedObject(typeof(CardCSVData));
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
foreach (var prop in el.EnumerateObject())
{
string val = prop.Value.ValueKind == JsonValueKind.Null ? null : prop.Value.ToString();
var f = typeof(CardCSVData).GetField(prop.Name, bf);
if (f != null) { SetMember(f.FieldType, val, v => f.SetValue(c, v)); continue; }
var p = typeof(CardCSVData).GetProperty(prop.Name, bf);
if (p != null && p.CanWrite) SetMember(p.PropertyType, val, v => p.SetValue(c, v));
}
// SortIndex is normally set by the ctor; mirror it.
var si = typeof(CardCSVData).GetProperty("SortIndex", bf);
if (si != null && si.CanWrite) si.SetValue(c, sortIndex);
return c;
}
private static void SetMember(Type t, string val, Action<object> set)
{
if (t == typeof(string)) set(val);
else if (t == typeof(int)) set(int.TryParse(val, out var i) ? i : 0);
else if (t == typeof(bool)) set(val == "1" || string.Equals(val, "true", StringComparison.OrdinalIgnoreCase));
// other types left at default
}
private static CardMaster NewCardMaster(List<CardCSVData> rows)
{
var ctor = typeof(CardMaster).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic, null,
new[] { typeof(List<CardCSVData>) }, null);
if (ctor == null) throw new InvalidOperationException("CardMaster(List<CardCSVData>) ctor not found");
return (CardMaster)ctor.Invoke(new object[] { rows });
}
private static void InjectAsDefault(CardMaster cm)
{
var idType = typeof(CardMaster).GetNestedType("CardMasterId");
var defaultId = Enum.Parse(idType, "Default");
var dictType = typeof(Dictionary<,>).MakeGenericType(idType, typeof(CardMaster));
var dict = (System.Collections.IDictionary)Activator.CreateInstance(dictType);
dict[defaultId] = cm;
var fld = typeof(CardMaster).GetField("_dictCardMaster",
BindingFlags.Static | BindingFlags.NonPublic);
fld.SetValue(null, dict);
}
}
}

View File

@@ -0,0 +1,557 @@
using System.Reflection;
using SVSim.BattleEngine.Rng;
using UnityEngine;
using Wizard;
using Wizard.Battle;
using Wizard.Battle.Phase;
using Wizard.Battle.Recovery;
using Wizard.Battle.Replay;
using Wizard.Battle.Resource;
using Wizard.Battle.View.Vfx;
using Wizard.BattleMgr;
namespace SVSim.BattleEngine.Tests
{
// Initializes the global engine state a headless battle assumes exists. In the real client this
// is populated from /load/index at login; here we author the minimum the resolution path reads.
public static class HeadlessEngineEnv
{
// Simplest zero-skill vanilla follower in cards.json: neutral (clan 0), cost 1, 1/2, no skill.
public const int FollowerId = 100011010;
// M3 next-hardest deterministic card: a fixed-damage spell. 900124030 is an ELF (clan 1, matches
// PlayerClassId) cost-3 spell whose sole skill is `when_play` `damage=3` to `card_type=class`
// (the enemy leader) — auto-targeted (no select_count), no RNG. Deterministic burn to the face.
public const int SpellId = 900124030;
// M4 next-hardest deterministic card: a when_play SELF-BUFF follower. 103111050 is an ELF
// (clan 1) cost-1 1/1 whose sole non-evo skill is `when_play` `powerup` `add_offense=1&add_life=1`
// with skill_target `character=me&target=self` — it buffs ITSELF, so no target selection (the
// fanfare auto-resolves). Fixed +1/+1 => a deterministic stat-delta oracle. The skill is gated on
// `play_count>2`; the headless harness seeds that via the public AddCurrentTrunPlayCount (see the
// oracle test). Base 1/1 -> 2/2 after the fanfare.
public const int BuffFollowerId = 103111050;
public const int BuffAddOffense = 1;
public const int BuffAddLife = 1;
// M5 next-hardest deterministic card: a when_play SUMMON_TOKEN spell. 800134010 is an ELF
// (clan 1) cost-1 spell whose sole skill is `when_play` `summon_token=100011020` with
// `skill_target=none` and an UNGATED condition (`character=me`, trivially the caster): it
// summons exactly ONE neutral 2/2 follower TOKEN onto the caster's board — no target
// selection, no RNG (Skill_summon_token's random branch is `num >= 0 && !IsForecast`, and
// this option carries no `random_count`, so num=-1 => the deterministic literal-id path).
// The new oracle dimension over M2/M3/M4 is a BOARD-COUNT DELTA from a SKILL-CREATED card:
// a token that was never in the hand/deck appears in play. This is also the first headless
// exercise of the PUBLIC prefab card-creation path (CardCreatorBase.CreateCard,
// createNullView:false, via BattlePlayerBase.CreateNextIndexCard) — class-card construction
// hits `default: return null` and the M2-M4 hand cards used the private null-view seam, so
// the view-building creation path is genuinely new here.
public const int TokenSpellId = 800134010;
public const int SummonedTokenId = 100011020; // neutral 2/2 follower token
public const int SummonedTokenAtk = 2;
public const int SummonedTokenLife = 2;
// M6 next milestone: the first card requiring TARGET SELECTION — exercises the selectedCards
// path of ActionProcessor.PlayCard (dormant through M2-M5, all of which played
// selectedCards: null). 800134020 is an ELF (clan 1) cost-1 SPELL whose sole skill is
// `when_play` `damage=5` to a SELECTED enemy follower
// (skill_target=character=op&target=inplay&card_type=unit&select_count=1), ungated
// (character=me), no RNG, no dynamic `{}` value. The new oracle dimension is SELECTION
// ROUTING: with TWO followers on the enemy board and ONE passed as selectedCards, only the
// selected follower takes the 5 damage and the un-selected one is untouched.
public const int TargetSpellId = 800134020;
public const int TargetSpellDamage = 5;
// Two zero-skill vanilla NEUTRAL followers placed on the ENEMY board. Both have life > the
// 5 damage so they SURVIVE — this gives a differential life-delta oracle (selected -5,
// un-selected -0) that reads the authoritative damage path M3 already proved, without
// depending on follower death/board-removal timing (a separate, unproven mechanic). Distinct
// base life (13 vs 7) so the two post-states can't coincidentally match.
public const int SelectTargetFollowerId = 900041010; // neutral 13/13
public const int UnselectTargetFollowerId = 102011010; // neutral 6/7
// M7 next milestone: targeted DESTROY — the first card proving follower DEATH / board-removal
// resolves in the AUTHORITATIVE (committed) part of PlayCard headless, not the cosmetic
// post-Process tail. 800144120 is an ELF (clan 1) cost-0 SPELL whose sole skill is `when_play`
// `destroy` of a SELECTED enemy follower
// (skill_target=character=op&target=inplay&card_type=unit&select_count=1), ungated
// (skill_condition=character=me), no RNG, no dynamic value. `destroy` is UNCONDITIONAL removal
// (vs `damage` needing a >=life amount), so the oracle is the cleanest possible "card left the
// board": selected follower gone + enemy board count -1 + selected card in CemeteryList, while
// the un-selected follower stays (routing, M6's lesson, confirmed load-bearing by swapping the
// selection). Reuses the two M2/M6 vanilla followers as the target board (destroy is
// unconditional so their stats are irrelevant — distinct ids only so selected vs un-selected
// can't be confused). InitCardTemplates is NOT needed (destroy creates no card).
public const int DestroySpellId = 800144120;
public const int DestroyTargetFollowerId = FollowerId; // neutral 1/2 (the selected, destroyed one)
public const int DestroyOtherFollowerId = UnselectTargetFollowerId; // neutral 6/7 (the un-selected survivor)
// M8 next milestone: LETHAL damage — proves follower DEATH VIA COMBAT MATH (damage >= life ->
// 0 life -> the same RemoveInplayCard/cemetery death path M7 lit up via `destroy`, but reached
// through the dominant real-card mechanic: "deal N damage"). Reuses the M6 damage=5 spell
// (800134020) but with target followers STRADDLING 5 life so the SAME spell kills one and merely
// chips the other in a single oracle: the SELECTED target has life <= 5 and dies (board -1 +
// cemetery +1, the M7 assertions), while the UN-SELECTED control has life > 5 and survives at
// reduced life (the M6 life-delta assertion). This combines M7's removal dimension with M6's
// life-delta + routing, and distinguishes death-via-damage from the unconditional `destroy`.
public const int LethalDamageSpellId = TargetSpellId; // 800134020, when_play damage=5
public const int LethalDamage = TargetSpellDamage; // 5
public const int LethalTargetFollowerId = FollowerId; // neutral 1/2 (life 2 <= 5 -> dies)
public const int SurvivorTargetFollowerId = UnselectTargetFollowerId; // neutral 6/7 (life 7 > 5 -> survives at 2)
// M9 next milestone: when_play DRAW — proves the HAND/DECK DELTA dimension (design §5's draw
// oracle): the last deterministic, non-RNG card-effect class no prior milestone touched (M3/M4/
// M6/M8 moved stats, M2/M5/M7 the board, M3 the leader — none read the deck->hand transfer).
// 800114010 is an ELF (clan 1) cost-1 SPELL whose sole skill is `when_play` `draw` of ONE card
// from the caster's own deck (skill_target=character=me&target=deck&card_type=all&random_count=1),
// ungated (skill_condition=character=me), no evo skill, no preprocess, no dynamic `{}` value.
//
// ADAPTATION FROM THE RESUME-GUIDE SHAPE: the guide asked for a `skill_target=none` draw with
// "no RNG", but no such card exists in cards.json — EVERY draw selects from the deck via a
// `random_count=N` target filter (skill_option is always literally `none`; the count lives in
// skill_target). The RNG is neutralized structurally instead: seed the deck with EXACTLY ONE
// known card, so `random_count=1` over a single-card pool is deterministic regardless of the
// RandomSeed. This keeps the oracle decisive (drawn id is forced) while exercising the real
// draw path. Like the summon token, a drawn card is engine-CREATED off the deck the M5 prefab
// way; unlike summon, the card already exists (we seed it) and the skill only MOVES it deck->hand.
public const int DrawSpellId = 800114010;
public const int DeckSeedCardId = FollowerId; // the single known deck card (neutral 1/2 vanilla)
// M10 next milestone: the first DYNAMIC `{}`-VALUE card — proves the engine COMPUTES an effect
// magnitude from live game state (a value the wire can't carry; per memory
// project_battle_relay_nontargeted_effects this state-derived-value problem is exactly what
// broke the PvP relay, so proving the engine resolves it headless is the direct validation that
// the port — not a relay — is the necessary path). Still non-RNG: a seeded state makes the value
// deterministic. 112134010 is an ELF (clan 1) cost-2 SPELL whose sole skill is `when_play`
// `damage={me.play_count}-1` to `character=both&target=inplay&card_type=unit` (with a
// `base_card_id!=900111010|900111020` exclusion) — an AoE over BOTH boards' units, auto-targeted
// (no select_count, so selectedCards: null like M2-M5), ungated (skill_condition=character=me).
//
// The `{}` value resolves (SkillOptionValue.ParseInt) as
// `_filterVariable.Parse("me.play_count") - 1`, where Parse routes to
// SkillEnvironmentalPlayCount.Filtering -> playerInfo.GetCurrentTurnPlayCount() (the
// `isPrePlay=false` resolution path). That is the SAME per-turn counter the public
// AddCurrentTrunPlayCount feeds (M4 proved this seam drove the play_count>2 GATE; M10 proves it
// also feeds the `{}` VALUE). The per-play auto-increment AddCurrentTrunPlayCount(1) lives in
// ActionProcessor's OnBeforePlayCard (BattlePlayerBase.cs:1400), subscribed by
// SetupActionProcessorEvent — which is ONLY called on the OperateMgr/Prediction/OperationSimulator
// paths, NOT on the direct `new ActionProcessor(pair).PlayCard` (DP4) path this harness uses. So
// the headless play does NOT self-bump the per-turn count: the skill reads EXACTLY the seeded
// GetCurrentTurnPlayCount() and the damage == seeded - 1. The oracle derives the expected
// magnitude from the engine's OWN live GetCurrentTurnPlayCount(), not from a hardcoded literal,
// which is the M10 dimension (engine-computed value, not a wire-carried constant).
//
// The target is the M6 vanilla NEUTRAL 13/13 follower (SelectTargetFollowerId, already loaded):
// life 13 > any reasonable seeded count, so it SURVIVES for a clean life-delta read (reusing the
// M3/M6/M8 damage->life path), and `card_type=unit` excludes both leaders (asserted untouched).
public const int DynamicDamageSpellId = 112134010;
public const int DynamicDamageTargetFollowerId = SelectTargetFollowerId; // neutral 13/13 (survives, clean delta)
// A deliberately non-trivial seeded per-turn play count so the computed damage (== this value)
// is an obvious state read, not a coincidence with a small literal. The load-bearing probe
// (M4/M6/M8 discipline) varies this and watches the damage track it.
public const int DynamicSeededPlayCount = 4;
// M12 (the design §5 RNG oracle): reuse the M9 draw spell (800114010, when_play `draw` 1 from the
// caster's deck via a random_count=1 filter) but over a MULTI-card deck with IsRandomDraw=true.
// M9 passed only because IsRandomDraw=false takes BattlePlayerBase.LotteryRandomDrawCard's
// top-of-deck `else` branch (BattlePlayerBase.cs:3174-3185) — a 1-card pool made index 0 the only
// card. With IsRandomDraw=true the selection runs through SkillRandomSelectFilter.Filtering, which
// calls BattleManagerBase.GetIns().StableRandom(poolCount) per pick (SkillRandomSelectFilter.cs:42,
// gated on IsRandomDraw) — the chokepoint HeadlessBattleMgr overrides. So the scripted source picks
// exactly which deck card is drawn, proving a GENUINE multi-outcome roll (the dimension M9's
// one-card pool deliberately avoided).
//
// Three distinguishable deck cards seeded at consecutive indices; SkillRandomSelectFilter orders
// the pool by Index (line 34), so the pick index maps to position in this order:
// index 0 -> RngDeckCardA (100011010), index 1 -> RngDeckCardB (103111050), index 2 -> RngDeckCardC (100011020)
// All three are already loaded by HeadlessCardMaster.Load via EnsureInitialized (FollowerId,
// BuffFollowerId, SummonedTokenId), so no Load change is needed.
public const int RngDrawSpellId = DrawSpellId; // 800114010, when_play draw 1 (random_count=1)
public const int RngDeckCardA = FollowerId; // neutral 1/2 -> Index-order position 0
public const int RngDeckCardB = BuffFollowerId; // ELF 1/1 -> Index-order position 1
public const int RngDeckCardC = SummonedTokenId; // neutral 2/2 -> Index-order position 2
private static bool _done;
private static readonly object _processGlobalsGate = new object();
// Process-globals only: load card master, install master data, seed LoadDetail/Crossover,
// seed Certification.udid. Per-battle/per-test state (IsForecast, chara ids on the DataMgr,
// NetworkUserInfoData) is now seeded inside TestBattleScope's ctor against the per-scope
// GameMgr — calling it here would crash because GameMgr.GetIns() Requires an ambient scope.
// Thread-safe (assembly-level Parallelizable(Fixtures) means many fixtures' [SetUp] race here).
public static void EnsureProcessGlobals()
{
if (_done) return;
lock (_processGlobalsGate)
{
if (_done) return;
EnsureProcessGlobalsCore();
_done = true;
}
}
private static void EnsureProcessGlobalsCore()
{
// Wizard.Data.Load: static /load/index snapshot. The ctor's CreateBackgroundId reads
// Data.Load.data._userTutorial (LoadDetail self-inits _userTutorial). Suppress VFX too.
Wizard.Data.Load = new Load { data = new LoadDetail() };
// CardParameter(CardCSVData) reads Data.Crossover.RestrictedCard for deck-limit calc;
// an empty Crossover returns the default count (no restriction). Private setter -> reflect.
typeof(Wizard.Data).GetProperty("Crossover",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
.SetValue(null, new Wizard.Crossover());
// CardMaster must be non-null before construction (the leader/class card looks up id 0).
// Load the M2 vanilla follower + the M3 fixed-damage spell + the M4 self-buff follower +
// the M5 summon-token spell AND the token it summons so each oracle can create + look up
// real stats. The summoned token id must be present: Skill_summon_token resolves it
// through CardMaster.GetCardParameterFromId during creation.
HeadlessCardMaster.Load(FollowerId, SpellId, BuffFollowerId, TokenSpellId, SummonedTokenId,
TargetSpellId, SelectTargetFollowerId, UnselectTargetFollowerId, DestroySpellId, DrawSpellId,
DynamicDamageSpellId);
// Master reference data (class-character list) for leader/class card resolution.
HeadlessMasterData.Install();
// The network emit path's payload builder (RealTimeNetworkAgent.CreateEmitData) reads
// Cute.Certification.Udid (RealTimeNetworkAgent.cs:1407). The Udid getter lazily decodes from
// Toolbox.SavedataManager (Certification.cs:35), which is null headless. Seed the private static
// backing field with a non-empty placeholder so the getter short-circuits before touching the
// savedata manager. The value is opaque to the engine (it's just echoed into the emit dict).
typeof(Cute.Certification)
.GetField("udid", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
.SetValue(null, "headless-udid");
}
// Simple deterministic 40-card deck for multi-instance tests: every slot is the same vanilla
// FollowerId. Card 100011010 is loaded as part of EnsureProcessGlobals' HeadlessCardMaster.Load
// batch so SessionBattleEngine.Setup resolves each entry without re-loading. Kept a single
// shape — the multi-instance property being verified (per-session ambient isolation across
// parallel battles) is driven by distinct masterSeeds on the engines, not by deck variation.
public static long[] SampleDeck()
{
var deck = new long[40];
for (int i = 0; i < 40; i++) deck[i] = FollowerId;
return deck;
}
// Per-ambient seeder: writes the player/enemy chara ids onto the AMBIENT GameMgr's DataMgr.
// Called by TestBattleScope after the scope is entered so GameMgr.GetIns() routes to the
// per-test GameMgr, not whichever one happened to be ambient last.
public static void SeedCharaIdsOnCurrentAmbient()
{
// Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master).
// Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/
// AvatarBattle info (more null statics) which the resolution path doesn't need (the
// TryGet* accessors are null-tolerant).
var dm = GameMgr.GetIns().GetDataMgr();
SetField(dm, "_playerCharaId", HeadlessMasterData.PlayerCharaId);
SetField(dm, "_enemyCharaId", HeadlessMasterData.EnemyCharaId);
}
// Per-ambient seeder: installs a no-op NetworkUserInfoData on the AMBIENT GameMgr so
// NetworkBattleManagerBase.CreateBackgroundId()'s GetNetworkUserInfoData().GetFieldId() call
// resolves (M13). Field id 1 == ForestField, a valid background.
public static void SeedNetUserOnCurrentAmbient()
{
// NetworkBattleManagerBase.CreateBackgroundId() (M13) reads
// GameMgr.GetIns().GetNetworkUserInfoData().GetFieldId() when the RecoveryManager yields no
// bg id (NullRecoveryManager.BackGroundId == -1). In production RealTimeNetworkAgent seeds
// this NetworkUserInfoData at match start; the bare construction path leaves GameMgr's
// _netUser null (no lazy init, unlike the other GodObject getters). Seed a no-op instance
// whose _selfInfo carries just "fieldId" (GetFieldId reads _selfInfo["fieldId"]); field id 1
// == ForestField, a valid background. Nothing here drives game state — it only satisfies the
// network mgr's background lookup, a background lookup the single-battle path
// (`SingleBattleMgr`) never performs.
var netUser = new NetworkUserInfoData();
netUser.SetSelfInfo(
new System.Collections.Generic.Dictionary<string, object> { ["fieldId"] = 1 },
isWatchReplayRecovery: false);
GameMgr.GetIns().SetNetworkUserInfoData(netUser);
}
// Seed each leader's starting life on a freshly-constructed mgr. The engine does this in
// BattleManagerBase.SetupInitialGameState -> InitializeClassLife (InitBaseMaxLife per leader),
// but the full SetupInitialGameState also cascades into rotation/avatar/turn-panel UI init
// that is irrelevant (and hostile) to a headless resolution test, so apply just the
// InitializeClassLife subset. Without this a leader's BaseMaxLife defaults to 0 — which reads
// as already-dead/game-over and silently blocks any card play (the M2 follower oracle never
// noticed because it only asserted leader life *unchanged*, and 0 == 0).
public const int DefaultLeaderLife = 20;
public static void InitLeaderLife(BattleManagerBase mgr, int life = DefaultLeaderLife)
{
((ClassBattleCardBase)mgr.BattlePlayer.Class).InitBaseMaxLife(life);
((ClassBattleCardBase)mgr.BattleEnemy.Class).InitBaseMaxLife(life);
}
// The PUBLIC prefab card-creation path (CardCreatorBase.CreateCard, createNullView:false) —
// used by anything the engine creates INTERNALLY (summons, token-draws, etc.), as opposed to
// the test's direct private null-view seam for hand cards — clones card-template prefabs held
// on BattleManagerBase.SBattleLoad. The real async battle load (CoLoad) builds these; the bare
// `new SingleBattleMgr(...)` construction path leaves SBattleLoad null (the M2 NRE was here).
// Seed it with non-null no-op CardTemplates: their `.gameObject` is a lazy shim no-op, and the
// shim's CloneObjectToParent + self-consistent object graph carry the rest. Nothing here
// computes game state — the token's authoritative stats come from CardCSVData, not the view.
public static void InitCardTemplates(BattleManagerBase mgr)
{
mgr.SBattleLoad = new SBattleLoad
{
UnitCardTemplate = new CardTemplate(),
SpellCardTemplate = new CardTemplate(),
FieldCardTemplate = new CardTemplate(),
};
// The created card's transform is positioned/parented under the battle's 3D scene-graph
// containers (CardCreatorBase.CreateCardTypeBuildInfo reads ins.CardHolder/ECardHolder/
// PCardPlace/Battle3DContainer). The real battle load instantiates these; seed non-null
// no-op GameObjects so the positioning resolves (no-op transforms; nothing rendered).
mgr.Battle3DContainer = new GameObject();
mgr.CardHolder = new GameObject();
mgr.ECardHolder = new GameObject();
mgr.PCardPlace = new GameObject();
mgr.ChoiceCardHolder = new GameObject();
mgr.EvolveCardHolder = new GameObject();
}
// The shared headless card-creation primitive. CardCreatorBase.CreateCardWithoutResources is
// the engine's own null-view creation path (CreateBase -> new *BattleCard(buildInfo).Setup(
// createNullView:true)); it's private, so reflect it rather than reimplement the 14-arg
// BuildInfo wiring. The public CardCreatorBase.CreateCard goes through prefab cloning.
//
// The engine's CreateCard also calls owner.SetupCardEvent(card); the raw
// CreateCardWithoutResources seam skips it, so we fold it in here. SetupCardEvent wires the
// per-card play events (BattlePlayerBase.cs:1452): for a SPELL/amulet it attaches
// OnPlay -> RemoveSpellCardFromHand and OnFinishWhenPlaySkill -> AddSpellCardToCemetery, which
// are how a non-follower leaves the hand at all (a follower's hand->field move is intrinsic to
// SetUpInplay, not event-driven). For a follower SetupCardEvent only attaches an OnEvolve hook
// that never fires on a vanilla play, so folding it in is a no-op there — making this a single
// primitive both follower and non-follower oracles can share.
public static BattleCardBase CreateHeadlessHandCard(int cardId, int index, bool isPlayer, BattleManagerBase mgr)
{
var io = mgr.CreatePlayerInnerOptionsBuilder();
var m = typeof(CardCreatorBase).GetMethod("CreateCardWithoutResources",
BindingFlags.NonPublic | BindingFlags.Static);
var card = (BattleCardBase)m.Invoke(null, new object[] { cardId, index, isPlayer, mgr, io });
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
owner.SetupCardEvent(card);
return card;
}
// Put a follower DIRECTLY onto a player's board headless (vs as a side-effect of PlayCard),
// for setting up a target board state. Create it through the shared null-view seam, then drive
// the engine's own hand->field move: HandCardToField requires the card to be in HandCardList,
// then AddInplayCards it + removes it from hand (BattlePlayerBase.cs:2568). For a vanilla
// follower the OnAddPlayCard/StopBattleHandCard/OnSummonAfter events it fires are no-ops (no
// fanfare), so the follower lands on the board at its CardCSVData base stats. M2 proved the
// hand->field placement path resolves headless.
public static BattleCardBase PutFollowerInPlay(BattleManagerBase mgr, int cardId, int index, bool isPlayer)
{
var card = CreateHeadlessHandCard(cardId, index, isPlayer, mgr);
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
owner.HandCardList.Add(card);
owner.HandCardToField(card);
return card;
}
// Push a known card onto a player's DECK headless (the M9 draw oracle's setup primitive). The
// bare `new SingleBattleMgr(...)` construction leaves DeckCardList non-null-but-empty (ctor at
// BattlePlayerBase.cs:1050), and a card's deck membership IS its `IsInDeck` (BattleCardBase.cs:970
// `=> SelfBattlePlayer.DeckCardList.Contains(this)`) — so no separate "in deck" flag is needed.
// Create the card through the same null-view seam hand/board cards use, then drive the engine's
// own AddToDeck (BattlePlayerBase.cs:3038): for a vanilla follower it is just DeckCardList.Add
// (HasDeckSelfSkill is false; the XorShiftRandom/IsMulliganEnd reshuffle bookkeeping short-
// circuits on the null/inactive headless RNG). The drawn card is then the engine's own deck
// object, so the oracle can assert deck->hand identity by reference, not just by id.
public static BattleCardBase SeedDeck(BattleManagerBase mgr, int cardId, int index, bool isPlayer)
{
var card = CreateHeadlessHandCard(cardId, index, isPlayer, mgr);
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
owner.AddToDeck(card);
return card;
}
// Build a headless battle wired for AUTHORITATIVE RNG: real rolls under IsForecast (via the
// injected source on HeadlessBattleMgr) AND IsRandomDraw=true (the second gate — without it the
// random-select filters bypass the roll and pick index 0; BattleManagerBase.cs:415,
// SkillRandomSelectFilter.cs:42). Mirrors the opponent/turn/leader-life wiring every oracle does.
// Returns the constructed HeadlessBattleMgr; the caller seeds hands/decks/boards and plays.
public static HeadlessBattleMgr NewAuthoritativeBattle(IRandomSource rng)
{
EnsureProcessGlobals(); // sets IsForecast = true among other globals
BattleManagerBase.IsRandomDraw = true; // the second RNG gate (F-RNG-2)
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng);
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
SetField(player, "_opponentBattlePlayer", enemy);
SetField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
InitLeaderLife(mgr); // a 0-life leader reads as game-over and blocks plays
InitCardTemplates(mgr); // the draw VFX touches the drawn card's view layer
return mgr;
}
// M13 emit-path read. Builds a HeadlessNetworkBattleMgr (the emitting twin of the
// HeadlessBattleMgr NewAuthoritativeBattle returns) and stands up the OnEmit capture seam: the
// engine's own RealTimeNetworkAgent.OnEmit event (RealTimeNetworkAgent.cs:1270) fires the played
// URI before both emit guards, so capturing it needs no Engine/shim edit — just an injected agent.
// Returns (mgr, emitted-URI list). The caller seeds the hand and drives mgr.OperateMgr.PlayCard.
public static (HeadlessNetworkBattleMgr mgr, System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI> emitted)
NewNetworkEmitBattle(IRandomSource rng = null)
{
EnsureProcessGlobals(); // sets IsForecast = true among other globals
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator(), rng);
// NOTE: IsRecovery is left FALSE here (unlike the solo NewAuthoritativeBattle). The network
// emit path is gated on !IsRecovery in BOTH places: NetworkStandardBattleMgr.SendPlayCard
// (NetworkStandardBattleMgr.cs:155) and the OnSetCardComplete->SendPlayCard subscription in
// SetUpNetworkOperateEvent (NetworkBattleManagerBase.cs:927, which early-returns under
// IsRecovery). With IsRecovery=true the play would resolve state but never emit. (The solo
// NewAuthoritativeBattle uses IsRecovery=true only to collapse VFX wait delays; here the no-op
// view shims absorb the real view layer instead — see the IsForecast=false block below.)
// IsForecast MUST be false on the network emit path. BattleManagerBase.IsVirtualBattle is
// `=> IsForecast` (BattleManagerBase.cs:657), and NetworkStandardBattleMgr.SendPlayCard is gated
// on `!IsVirtualBattle` (NetworkStandardBattleMgr.cs:155) — under IsForecast=true the play
// resolves state but the emit is suppressed. EnsureInitialized leaves IsForecast=true (correct
// for the direct-ActionProcessor solo oracles, where it suppresses VFX); clear it here so the
// genuine emit fires. The cost is that VFX registration is no longer short-circuited, so the
// play exercises the real view layer — those view touches are satisfied by the no-op view shims
// (InitCardTemplates, the HandView/DetailPanel fills below). M3's damage is literal, immune to
// any play-count bump the OperateMgr path adds vs the direct path.
BattleManagerBase.IsForecast = false;
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
SetField(player, "_opponentBattlePlayer", enemy);
SetField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
InitLeaderLife(mgr); // a 0-life leader reads as game-over and blocks plays
InitCardTemplates(mgr); // play/draw VFX touches the card view layer
// The OperateMgr emit path runs SetupActionProcessorEvent (skipped by the direct-ActionProcessor
// solo oracles), which subscribes BattleMgr.DetailMgr.DetailPanelControl.UpdateCardDescriptionOnEvent
// to OnPlayComplete (BattlePlayerBase.cs:1431). DetailMgr is created in CreateManager but its
// DetailPanelControl (a UI control) is null headless. Seed the engine's own NullDetailPanelControl
// no-op so the play-complete event resolves without touching the UI.
mgr.DetailMgr.DetailPanelControl = new NullDetailPanelControl();
// Inject a headless RealTimeNetworkAgent so NetworkBattleSender's ToolboxGame.RealTimeNetworkAgent
// .* calls resolve, and subscribe OnEmit. GetUninitializedObject skips the MonoBehaviour Awake.
var agent = (RealTimeNetworkAgent)System.Runtime.Serialization.FormatterServices
.GetUninitializedObject(typeof(RealTimeNetworkAgent));
// CurrentMatchingStatus has a protected setter; seed it non-Disconnected so EmitMsgPack does not
// early-return at RealTimeNetworkAgent.cs:1272 (needed only for the best-effort payload read, Task 4;
// OnEmit fires regardless). The default on the uninitialized object is OffLine (0), which clears the
// SetCurrentMatchingStatus guards; the only side effect is a static-StringBuilder trace log, so the
// public setter runs cleanly headless. Prepared (50) is the real enum member (RealTimeNetworkAgent.cs:35).
agent.SetCurrentMatchingStatus(RealTimeNetworkAgent.MatchingStatus.Prepared);
// EmitMsgPack -> AddActionSequence (RealTimeNetworkAgent.cs:1773, fired for the PlayActions URI)
// does `_gungnir._actionSequenceNum++` and `NetworkLogger.LogInfo(...)`. On the
// GetUninitializedObject agent both are null (the real ctor builds them at :289/:301). Seed an
// uninitialized Gungnir (its ctor news a ConnectionReporter + Ticks — unneeded; AddActionSequence
// only touches the int counter) and the engine's own NetworkNullLogger no-op so the action-seq
// bookkeeping runs without crashing. Neither drives game state.
SetField(agent, "_gungnir",
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Gungnir)));
SetProperty(agent, "NetworkLogger", new NetworkNullLogger());
// Suppress the actual socket transmission. After OnEmit fires (RealTimeNetworkAgent.cs:1270, the
// O1 liveness signal), EmitMsgPack -> EmitMsgUriPack reaches the stockEmitMessageMgr / _manager.Socket
// network I/O (RealTimeNetworkAgent.cs:1444+/1487) — none of which exists headless. The engine's
// OWN _notEmit flag (set in recovery/replay) short-circuits EmitMsgUriPack at :1438 BEFORE any of
// that, so the emit stays genuine (OnEmit already fired through the real send path) while the
// byte-push is skipped. This is the only honest way to terminate the path headless: we are NOT
// faking OnEmit, only declining to open a socket we cannot open.
SetField(agent, "_notEmit", true);
var emitted = new System.Collections.Generic.List<NetworkBattleDefine.NetworkBattleURI>();
agent.OnEmit += uri => emitted.Add(uri);
Wizard.ToolboxGame.SetRealTimeNetworkBattle(agent);
return (mgr, emitted);
}
// M13 Task 4 best-effort: read the emit payload back out of the agent's stock sequencer. With
// _notEmit=true (NewNetworkEmitBattle terminates the path that way), EmitMsgUriPack short-circuits
// BEFORE stockEmitMessageMgr.StockData (RealTimeNetworkAgent.cs:1438 vs :1461), so the stock is
// expected to be null/empty — return null on any null/throw so the test degrades to Inconclusive
// rather than failing. Field `stockEmitMessageMgr` (:103) + `GetSequenceAllData()`
// (StockEmitMgr.cs:81, returns List<Dictionary<string,object>>) verified against the copied engine.
// Precondition: this is expected-null ONLY while NewNetworkEmitBattle sets _notEmit=true and leaves
// stockEmitMessageMgr unconstructed. If that harness setup changes, revisit — a non-null stock should
// then make the test ASSERT on the payload rather than defer to Inconclusive.
public static System.Collections.IList TryReadStockedEmitData(RealTimeNetworkAgent agent)
{
try
{
var f = typeof(RealTimeNetworkAgent).GetField("stockEmitMessageMgr",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var stock = f?.GetValue(agent);
if (stock == null) return null;
var m = stock.GetType().GetMethod("GetSequenceAllData");
return m?.Invoke(stock, null) as System.Collections.IList;
}
catch { return null; }
}
private static void SetField(object obj, string name, object value)
{
var f = obj.GetType().GetField(name,
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Public);
if (f == null) throw new System.InvalidOperationException(
$"{obj.GetType().Name} has no field '{name}'");
f.SetValue(obj, value);
}
// Set a property whose setter is non-public (e.g. RealTimeNetworkAgent.NetworkLogger has a
// protected setter). Walks the type hierarchy because the declaring type may be a base class.
private static void SetProperty(object obj, string name, object value)
{
var t = obj.GetType();
System.Reflection.PropertyInfo p = null;
while (t != null && p == null)
{
p = t.GetProperty(name,
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Public);
t = t.BaseType;
}
if (p == null) throw new System.InvalidOperationException(
$"{obj.GetType().Name} has no property '{name}'");
p.SetValue(obj, value);
}
}
// Test-side replica of the engine's own StandardBattleMgrContentsCreator (the practice/solo
// init path: GameMgr.cs:244 `new SingleBattleMgr(new StandardBattleMgrContentsCreator(null, null))`).
// Authored here (not copied) so we control the seed deterministically; uses the real engine
// managers verbatim. The real StandardBattleMgrContentsCreator + SingleBattlePhaseCreator were
// cut from the M1 copy set (entry-point constructors), so we reproduce them minimally.
public sealed class HeadlessContentsCreator : IBattleMgrContentsCreator
{
public int RandomSeed => 12345; // fixed; vanilla follower has no RNG so value is irrelevant
// No-op managers (vs the practice path's file-backed SingleBattleRecoveryRecordManager):
// the ctor's FirstRecoverySetting/FirstReplaySetting dereference these, and recovery/replay
// recording is irrelevant to the M2 oracle, so use the engine's own null implementations.
public IRecoveryManager RecoveryManager { get; } = new NullRecoveryManager();
public IRecoveryRecordManager RecoveryRecordManager { get; } = new NullRecoveryRecordManager();
public IReplayRecordManager ReplayRecordManager { get; } = new NullReplayRecordManager();
public IBattleResourceMgr CreateResourceMgr() => new BattleResourceMgr();
public VfxMgr CreateVfxMgr() => new VfxMgr();
public IPhaseCreator CreatePhaseCreator(BattleManagerBase battleMgr) =>
new HeadlessPhaseCreator(battleMgr);
}
// Equivalent of the engine's SingleBattlePhaseCreator: inherits PhaseCreatorBase wholesale.
public sealed class HeadlessPhaseCreator : PhaseCreatorBase
{
public HeadlessPhaseCreator(BattleManagerBase battleMgr) : base(battleMgr) { }
}
}

View File

@@ -0,0 +1,64 @@
using NUnit.Framework;
using Wizard.Battle.View;
using Wizard.Battle.View.Vfx;
namespace SVSim.BattleEngine.Tests
{
// Regression for the Heal-triggered Skill_heal NRE diagnosed 2026-06-07 (bid 799755786270).
//
// A follower with a `when_spell_play` Heal trigger fires on a spell play and routes through
// Skill_heal.Start → ClassBattleCardBase.ApplyHealing → CreatePullHandInVfx
// → HandViewBase.HandUnfocus (HandViewBase.cs:124-131)
// The base implementation does `_handControl.SetHandState(HandControl.HandState.Unfocus)`.
// HeadlessHandViewStub.CreateHandControl returns null in headless, so `_handControl` is null
// and the base method NREs unconditionally — even when the heal amount is 0.
//
// The fix overrides HandUnfocus/HandFocus/FocusRearrangeHandHand on the stub to return
// NullVfx without touching `_handControl`. These are PURE PRESENTATION methods (visual
// ease-in/ease-out of the hand cards) — no game-state implications — so no-op'ing them
// headless is safe; the surrounding state mutations in ApplyHealing (HealLife, skill triggers)
// still run.
//
// Pattern parity with the metamorphose-NRE shim fix in ViewUiTouchStubs.cs (BattleCardView.GameObject
// lazy non-null): production Unity touches that the headless engine must no-op rather than throw.
[TestFixture]
public class HeadlessHandViewStubTests
{
[Test]
public void HandUnfocus_does_not_throw_and_returns_non_null_vfx()
{
var stub = HeadlessHandViewStub.Instance;
VfxBase vfx = null;
Assert.DoesNotThrow(() => vfx = stub.HandUnfocus(),
"HandUnfocus must no-op headlessly — the live regression (bid 799755786270) crashed " +
"Skill_heal.Start when a when_spell_play Heal trigger fired with heal:0 because the " +
"base HandUnfocus dereferences a null _handControl.");
Assert.That(vfx, Is.Not.Null, "must return a non-null Vfx (caller registers it on a sequential player).");
}
[Test]
public void HandFocus_does_not_throw_and_returns_non_null_vfx()
{
var stub = HeadlessHandViewStub.Instance;
VfxBase vfx = null;
Assert.DoesNotThrow(() => vfx = stub.HandFocus(),
"HandFocus is the sister cosmetic touch (called from CreatePullHandOutVfx on the " +
"OWNER's turn). Same null _handControl, same headless no-op required.");
Assert.That(vfx, Is.Not.Null);
}
[Test]
public void FocusRearrangeHandHand_does_not_throw_and_returns_non_null_vfx()
{
var stub = HeadlessHandViewStub.Instance;
VfxBase vfx = null;
Assert.DoesNotThrow(() => vfx = stub.FocusRearrangeHandHand(),
"FocusRearrangeHandHand reads _handControl.IsHandStateFocus() before dispatching to " +
"HandFocus or HandUnfocus; the base implementation would NRE on the read.");
Assert.That(vfx, Is.Not.Null);
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using Wizard;
namespace SVSim.BattleEngine.Tests
{
// Builds the minimal Data.Master reference context a headless battle reads. In the client this
// comes from the /load/index master section; here we author just enough for the resolution path
// (currently: ClassCharacterList, so the leader/class card can resolve player/enemy class_id).
// Entries are constructed without their CSV ctor (private setters set via reflection).
public static class HeadlessMasterData
{
public const int PlayerCharaId = 1;
public const int EnemyCharaId = 2;
public const int PlayerClassId = 1; // ClanType -> class card clan
public const int EnemyClassId = 2;
public static void Install()
{
var master = (Master)FormatterServices.GetUninitializedObject(typeof(Master));
// The resolution path reads many Master.* collections (e.g. WhenPlayEffectKeywordMaster)
// and calls LINQ on them unguarded. Default every collection member to an empty instance
// so those touches no-op instead of NRE; then override the ones we need with content.
EnsureEmptyCollections(master);
var list = new List<ClassCharacterMasterData>
{
NewChara(PlayerCharaId, PlayerClassId),
NewChara(EnemyCharaId, EnemyClassId),
};
SetMember(master, "ClassCharacterList", list);
Data.Master = master;
}
// Initialize every List<>/array/Dictionary<> field/auto-property on the object to an empty
// non-null instance (only if currently null).
private static void EnsureEmptyCollections(object obj)
{
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
foreach (var f in obj.GetType().GetFields(bf))
{
if (f.GetValue(obj) != null) continue;
var empty = EmptyOf(f.FieldType);
if (empty != null) f.SetValue(obj, empty);
}
}
private static object EmptyOf(Type t)
{
if (t.IsArray) return Array.CreateInstance(t.GetElementType(), 0);
if (t.IsGenericType)
{
var def = t.GetGenericTypeDefinition();
if (def == typeof(List<>) || def == typeof(Dictionary<,>) ||
def == typeof(HashSet<>) || def == typeof(IList<>) ||
def == typeof(IDictionary<,>) || def == typeof(ICollection<>) ||
def == typeof(IEnumerable<>))
{
var concrete = def == typeof(List<>) || def == typeof(IList<>) ||
def == typeof(ICollection<>) || def == typeof(IEnumerable<>)
? typeof(List<>).MakeGenericType(t.GetGenericArguments())
: def == typeof(HashSet<>)
? typeof(HashSet<>).MakeGenericType(t.GetGenericArguments())
: typeof(Dictionary<,>).MakeGenericType(t.GetGenericArguments());
return Activator.CreateInstance(concrete);
}
}
return null;
}
private static ClassCharacterMasterData NewChara(int charaId, int classId)
{
var c = (ClassCharacterMasterData)FormatterServices.GetUninitializedObject(typeof(ClassCharacterMasterData));
SetMember(c, "chara_id", charaId);
SetMember(c, "class_id", classId);
SetMember(c, "skin_id", charaId);
SetMember(c, "is_usable", true);
return c;
}
// Set a member (auto-property backing field or field) by name, tolerating private setters.
private static void SetMember(object obj, string name, object value)
{
var t = obj.GetType();
const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var p = t.GetProperty(name, bf);
if (p != null && p.SetMethod != null) { p.SetValue(obj, value); return; }
var f = t.GetField(name, bf)
?? t.GetField($"<{name}>k__BackingField", bf);
if (f != null) { f.SetValue(obj, value); return; }
throw new InvalidOperationException($"{t.Name} has no settable member '{name}'");
}
}
}

View File

@@ -0,0 +1,134 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M8 (death VIA COMBAT MATH): a when_play TARGETED-DAMAGE spell whose amount is >= the target
// follower's life resolves to correct authoritative state HEADLESS via the same IsForecast/
// IsRecovery + ActionProcessor + selectedCards path M6/M7 proved. M3 proved `damage` to the LEADER
// (life-delta, no death). M7 proved board-removal via UNCONDITIONAL `destroy`. M8 closes the gap
// between them: the follower dies as a CONSEQUENCE of damage -> life<=0 -> the dead-check + the same
// RemoveInplayCard/cemetery path M7 lit up — the dominant real-card removal mechanic (most "deal N
// damage" cards), reached through combat math rather than a `destroy` skill.
//
// The spell is select_count=1 (proven in M6 — it hits ONLY the selected target), so the oracle is:
// with two followers on the enemy board STRADDLING the 5 damage and the LETHAL one passed as
// `selectedCards`, the selected follower (life 2 <= 5) DIES from combat math (enemy board -1, gone,
// in CemeteryList — the M7 removal assertions, but reached via damage not `destroy`), while the
// un-selected control (life 7 > 5) is UNTOUCHED (life unchanged, still on board — the M6 routing
// assertion). The STRADDLE is what makes death-via-combat-math falsifiable: the load-bearing probe
// (swap the selection to the 6/7) makes that follower SURVIVE at 2 (7-5) and NOBODY die — proving
// the removal is gated on the SELECTED follower's life reaching <= 0 (combat math), not on
// "selected gets removed" (which would be M7's unconditional `destroy`) or a blanket wipe.
[TestFixture]
public class LethalDamageSpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Lethal_damage_spell_kills_the_selected_follower_and_chips_the_survivor()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M7 oracles): opponent refs + active turn flag. The
// spell's target resolver walks player -> opponent -> opponent's in-play followers.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Put TWO vanilla followers on the ENEMY board STRADDLING the 5 damage: the SELECTED target
// has life 2 (<= 5) so it dies; the un-selected control has life 7 (> 5) and, being a
// select_count=1 spell's non-target, is untouched. (The straddle powers the load-bearing
// probe: selecting the 6/7 instead makes it survive at 2 and nobody die.)
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.LethalTargetFollowerId, 0, isPlayer: false);
var survivor = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.SurvivorTargetFollowerId, 1, isPlayer: false);
// Sanity: the chosen ids actually straddle the damage (one lethal, one not) at setup.
Assert.That(selected.Life, Is.LessThanOrEqualTo(HeadlessEngineEnv.LethalDamage),
"selected follower's life is not <= the spell damage (it would not die)");
Assert.That(survivor.Life, Is.GreaterThan(HeadlessEngineEnv.LethalDamage),
"survivor follower's life is not > the spell damage (it would not survive)");
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.LethalDamageSpellId);
// Place the lethal-damage spell in the active player's hand with PP to spare.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.LethalDamageSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyCemeteryBefore = enemy.CemeteryList.Count;
int survivorLifeBefore = survivor.Life;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine, passing the chosen (lethal) target via selectedCards.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
"ActionProcessor.PlayCard threw on a lethal targeted-damage spell");
Assert.Multiple(() =>
{
// PRIMARY M8 — death via combat math: the SELECTED follower (life <= damage) is removed
// from the enemy board and lands in the cemetery (the M7 removal dimension, reached
// through damage rather than `destroy`).
Assert.That(enemy.ClassAndInPlayCardList, Does.Not.Contain(selected),
"lethal-damaged follower still on the enemy board (death-via-damage did not remove it)");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore - 1),
"enemy board count not -1 (lethal damage did not commit a removal, or hit the wrong count)");
Assert.That(enemy.CemeteryList, Contains.Item(selected),
"lethal-damaged follower not in the enemy CemeteryList");
Assert.That(enemy.CemeteryList.Count, Is.EqualTo(enemyCemeteryBefore + 1),
"enemy cemetery count not +1");
// PRIMARY M8 — routing: the UN-SELECTED control (life > damage) is UNTOUCHED and stays on
// the board (the M6 routing assertion; select_count=1 hits only the selected target, so
// this proves the lethal removal was routed to the selection and is not a blanket wipe).
Assert.That(enemy.ClassAndInPlayCardList, Contains.Item(survivor),
"un-selected follower was removed (effect not routed, or a blanket wipe)");
Assert.That(survivor.Life, Is.EqualTo(survivorLifeBefore),
"un-selected follower took damage (effect not routed to the selection)");
// Leader untouched (the spell targets a follower, not the face).
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
"opponent leader life changed (damage hit the leader, not the selected follower)");
// Cost paid; spell leaves hand and (being a spell) does NOT occupy the board.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
});
}
}
}

View File

@@ -0,0 +1,99 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using SVSim.BattleEngine.Ambient;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests;
/// <summary>The forcing-function tests for the multi-instancing migration (Task 8). Each engine
/// instance carries its OWN <see cref="BattleAmbientContext"/> internally (SessionBattleEngine
/// constructs a per-session ctx in its field initializer and enters it on every Setup/Receive/
/// read), so two engines on two tasks must resolve independently — no shared "current mgr",
/// "current GameMgr", or "current viewer id" state. The stress test pins
/// parallel-equals-sequential to catch any residual contamination (which would manifest as a
/// life/PP/hand-count mismatch between the parallel and sequential runs).</summary>
[TestFixture, Parallelizable(ParallelScope.All)]
public class MultiInstanceEngineTests
{
[OneTimeSetUp]
public void OneTimeSetUp() => HeadlessEngineEnv.EnsureProcessGlobals();
[Test]
public async Task TwoBattles_ResolveIndependently_OnDifferentTasks()
{
var engineA = new SessionBattleEngine();
var engineB = new SessionBattleEngine();
engineA.Setup(masterSeed: 111, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck(),
seatAClass: 1, seatBClass: 2);
engineB.Setup(masterSeed: 222, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck(),
seatAClass: 5, seatBClass: 7);
var taskA = Task.Run(() => DriveBasicTurns(engineA));
var taskB = Task.Run(() => DriveBasicTurns(engineB));
await Task.WhenAll(taskA, taskB);
// Pin the engines' post-Setup state to concrete starting values: LeaderLife=20 (InitLeaderLife's
// DefaultLeaderLife, applied by SessionBattleEngine.Setup), Pp=0 (pre-first-turn, no PP refill
// has run), HandCount=0 (Setup builds the deck/leader graph but doesn't deal an opening hand —
// mulligan/draw happens once a turn-start phase runs, which DriveBasicTurns doesn't trigger).
// Both engines must report the SAME starting state regardless of distinct masterSeeds, which is
// the cross-contamination property under test: ambient isolation means neither engine's reads
// can leak into the other's seat lookups.
Assert.That(engineA.LeaderLife(true), Is.EqualTo(20));
Assert.That(engineB.LeaderLife(true), Is.EqualTo(20));
Assert.That(engineA.Pp(true), Is.EqualTo(0));
Assert.That(engineB.Pp(true), Is.EqualTo(0));
Assert.That(engineA.HandCount(true), Is.EqualTo(0));
Assert.That(engineB.HandCount(true), Is.EqualTo(0));
}
[Test]
public async Task StressN_BaselineMatches([Values(4, 8, 16)] int n)
{
var inputs = new (int seed, long[] deckA, long[] deckB)[n];
for (int i = 0; i < n; i++)
inputs[i] = (1000 + i, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck());
// Setup AND Drive both parallelize: the residual decomp-origin static accumulators
// (Wizard.LocalLog._lastTraceLogStringBuilder etc.) and the Unity Resources shim
// cache are now thread-safe (static lock / ConcurrentDictionary), so two engines
// constructing in parallel no longer corrupts shared scratch state. The full
// construct-then-read pipeline runs concurrently per task and the result still
// pins to the sequential baseline — that is the cross-contamination property
// under test (ambient isolation + safe shared statics).
var parallel = await Task.WhenAll(inputs.Select(input => Task.Run(() =>
{
var e = new SessionBattleEngine();
e.Setup(input.seed, input.deckA, input.deckB);
DriveBasicTurns(e);
return e.LeaderLife(true);
})));
var sequential = new int[n];
for (int i = 0; i < n; i++)
{
var e = new SessionBattleEngine();
e.Setup(inputs[i].seed, inputs[i].deckA, inputs[i].deckB);
DriveBasicTurns(e);
sequential[i] = e.LeaderLife(true);
}
Assert.That(parallel, Is.EqualTo(sequential));
}
[Test]
public void GameMgr_GetIns_WithoutScope_Throws()
{
Assert.That(BattleAmbient.Current, Is.Null);
Assert.Throws<System.InvalidOperationException>(() => GameMgr.GetIns());
}
private static void DriveBasicTurns(SessionBattleEngine e)
{
_ = e.LeaderLife(true);
_ = e.Pp(true);
_ = e.HandCount(true);
}
}

View File

@@ -0,0 +1,19 @@
namespace SVSim.BattleEngine.Tests
{
// Shared base for every network-emit test fixture (M13 EmitPathReadOracleTests, the
// construction-probe's OnEmit seam test, and any M14+ network fixture to come).
//
// POST-TASK-8 (multi-instancing migration): now empty. The historical hygiene gap this class
// closed (HeadlessEngineEnv.NewNetworkEmitBattle leaving IsForecast=false + a stray injected
// agent visible to a later solo fixture) was a PROCESS-GLOBAL leak via the now-deleted
// BattleManagerBase._isForecastFallback + ToolboxGame._realTimeNetworkAgentFallback statics.
// Both fields are gone: IsForecast/RealTimeNetworkAgent live on the per-test ambient context
// (TestBattleScope's BattleAmbientContext), so scope Dispose drops them. A later fixture's
// new TestBattleScope starts a fresh ctx with IsForecast=true and a null NetworkAgent by
// default — exactly the EnsureInitialized invariant the old TearDown manually restored.
//
// Kept as a marker base class so derived fixtures don't churn; can be deleted in Task 9.
public abstract class NetworkEmitFixtureBase
{
}
}

View File

@@ -0,0 +1,41 @@
using NUnit.Framework;
using SVSim.BattleEngine.Rng;
using Wizard.BattleMgr;
namespace SVSim.BattleEngine.Tests
{
// M13 step 1 (the M2 ConstructionProbe pattern): can a NetworkBattleManagerBase-derived mgr be
// built headless at all? NetworkBattleManagerSetup constructs NetworkTouchControl(this,
// _battleCamera, _backGround) + RegisterActionManager + OperateReceive — the largest new shim
// surface since M5's prefab path. Isolate "ctor runs" before any play is driven.
[TestFixture]
public class NetworkMgrConstructionProbeTests : NetworkEmitFixtureBase
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
[Test]
public void HeadlessNetworkBattleMgr_constructs_headless()
{
Assert.DoesNotThrow(() =>
{
var mgr = new HeadlessNetworkBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
Assert.That(mgr, Is.Not.Null);
});
}
[Test]
public void OnEmit_capture_seam_is_wired_via_injected_agent()
{
var (mgr, emitted) = HeadlessEngineEnv.NewNetworkEmitBattle();
_scope.Ctx.Mgr = mgr;
Assert.That(mgr, Is.Not.Null);
Assert.That(Wizard.ToolboxGame.RealTimeNetworkAgent, Is.Not.Null,
"agent must be injected so NetworkBattleSender's ToolboxGame.RealTimeNetworkAgent.* calls resolve");
Assert.That(emitted, Is.Empty, "no emit yet — only the seam is wired");
}
}
}

View File

@@ -0,0 +1,84 @@
using System.Linq;
using NUnit.Framework;
using SVSim.BattleEngine.Rng;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M12: the first card whose outcome is a GENUINE RNG roll. The M9 draw spell over a 3-card deck with
// IsRandomDraw=true selects via SkillRandomSelectFilter -> GetIns().StableRandom(poolCount), which
// HeadlessBattleMgr routes to the injected ScriptedRandomSource. The oracle asserts the engine drew
// EXACTLY the card the scripted roll selects, and (load-bearing) that the pick TRACKS the script:
// a different scripted unit draws a different card. This is the multi-outcome roll M9's one-card pool
// deliberately neutralized — it requires the F2 decoupling (real rolls under IsForecast) AND the
// IsRandomDraw=true second gate, both delivered by NewAuthoritativeBattle.
[TestFixture]
public class RandomDrawOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown]
public void ResetRandomDrawGate()
{
// NewAuthoritativeBattle sets the process-global BattleManagerBase.IsRandomDraw = true; reset it
// so this fixture doesn't leak that state into later-running fixtures (which expect the default
// false / top-of-deck draw behavior). Prevents order-dependent flakes as more RNG oracles land.
// (Now an ambient write inside the scope; harmless either way.)
BattleManagerBase.IsRandomDraw = false;
_scope?.Dispose();
_scope = null;
}
// Draw with a single scripted unit; return (drawnCardId, deckCountAfter). The deck is seeded with
// three distinguishable cards at indices 2,3,4 -> Index-order positions 0,1,2 map to
// RngDeckCardA/B/C. The draw makes one StableRandom(3) call -> index = floor(3*unit).
private (int drawnId, int deckAfter) DrawWith(double unit)
{
var mgr = HeadlessEngineEnv.NewAuthoritativeBattle(new ScriptedRandomSource(new[] { unit }));
_scope.Ctx.Mgr = mgr;
var player = mgr.BattlePlayer;
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardA, index: 2, isPlayer: true);
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardB, index: 3, isPlayer: true);
HeadlessEngineEnv.SeedDeck(mgr, HeadlessEngineEnv.RngDeckCardC, index: 4, isPlayer: true);
var spell = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.RngDrawSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(spell);
player.Pp = 10;
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(spell, selectedCards: null), "PlayCard threw on the random draw");
// The drawn card is the new hand entry that is not the spell.
var drawn = player.HandCardList.Single(c => c.CardId != HeadlessEngineEnv.RngDrawSpellId);
return (drawn.CardId, player.DeckCardList.Count);
}
[Test]
public void Random_draw_picks_the_scripted_card()
{
// unit 0.5 -> floor(3*0.5)=1 -> Index-order position 1 -> RngDeckCardB.
var (drawnId, deckAfter) = DrawWith(0.5);
Assert.Multiple(() =>
{
Assert.That(drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardB),
"scripted roll 0.5 should draw the middle (Index-order position 1) deck card");
Assert.That(deckAfter, Is.EqualTo(2), "deck should be 3 -> 2 after drawing one");
});
}
[Test]
public void Random_draw_pick_tracks_the_scripted_roll()
{
// Load-bearing: varying the scripted unit must move the pick across all three positions.
// floor(3*0.0)=0 -> A ; floor(3*0.5)=1 -> B ; floor(3*0.9)=2 -> C.
Assert.That(DrawWith(0.0).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardA), "0.0 -> position 0");
Assert.That(DrawWith(0.5).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardB), "0.5 -> position 1");
Assert.That(DrawWith(0.9).drawnId, Is.EqualTo(HeadlessEngineEnv.RngDeckCardC), "0.9 -> position 2");
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using NUnit.Framework;
using SVSim.BattleEngine.Rng;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
[TestFixture]
public class RngSeamTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
// RandomSourceBridge.Range must mirror the engine's exact roll arithmetic:
// BattleManagerBase.StableRandom does `(int)Math.Floor((double)val * unit)`.
[Test]
public void Bridge_Range_mirrors_engine_floor_arithmetic()
{
Assert.That(RandomSourceBridge.Range(7, 0.0), Is.EqualTo(0)); // floor(7*0) = 0
Assert.That(RandomSourceBridge.Range(7, 0.999), Is.EqualTo(6)); // floor(6.993) = 6 (never == val)
Assert.That(RandomSourceBridge.Range(3, 0.5), Is.EqualTo(1)); // floor(1.5) = 1 (middle of 3)
Assert.That(RandomSourceBridge.Range(1, 0.5), Is.EqualTo(0)); // floor(0.5) = 0
}
// SeededRandomSource(seed) must reproduce the engine's own generators EXACTLY: BattleManagerBase
// seeds both _stableRandom and _stableRandomOnlySelf as `new System.Random(RandomSeed)`
// (BattleManagerBase.cs:721-722). NextUnit() == synced.NextDouble(); NextSelf(max) == self.Next(max).
[Test]
public void SeededSource_reproduces_two_System_Random_streams()
{
const int seed = 12345;
var src = new SeededRandomSource(seed);
var refSynced = new System.Random(seed); // mirrors _stableRandom
var refSelf = new System.Random(seed); // mirrors _stableRandomOnlySelf (separate stream)
for (int i = 0; i < 8; i++)
Assert.That(src.NextUnit(), Is.EqualTo(refSynced.NextDouble()), $"NextUnit drift at {i}");
for (int i = 0; i < 8; i++)
Assert.That(src.NextSelf(100), Is.EqualTo(refSelf.Next(100)), $"NextSelf drift at {i}");
}
// ScriptedRandomSource feeds a known sequence (the oracle's control + the Phase-3 replay seam).
// It MUST throw on overrun, not wrap: an unexpected extra roll should fail loudly so a test
// surfaces a miscount of engine RNG calls rather than silently reusing a value.
[Test]
public void ScriptedSource_returns_sequence_then_throws_on_overrun()
{
var src = new ScriptedRandomSource(new[] { 0.1, 0.5 }, new[] { 3 });
Assert.That(src.NextUnit(), Is.EqualTo(0.1));
Assert.That(src.NextUnit(), Is.EqualTo(0.5));
Assert.That(() => src.NextUnit(), Throws.InvalidOperationException, "should throw on unit overrun");
Assert.That(src.NextSelf(99), Is.EqualTo(3));
Assert.That(() => src.NextSelf(99), Throws.InvalidOperationException, "should throw on self overrun");
}
// The decoupling (F2): the override must roll REAL values even though IsForecast == true (which
// forces the un-overridden engine methods to return 0). A ScriptedRandomSource proves the value
// came from the injected source, not the engine's zeroing.
[Test]
public void Override_rolls_real_values_under_IsForecast()
{
BattleManagerBase.IsForecast = true; // would zero the un-overridden engine RNG
// 3 units; with RandomSourceBridge.Range(val, unit) = floor(val*unit):
// StableRandom(7) with 0.5 -> floor(3.5) = 3
// StableRandomDouble() -> 0.25
// StableRandomOnlySelf(10) -> scripted self pick 4
var src = new ScriptedRandomSource(new[] { 0.5, 0.25 }, new[] { 4 });
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), src);
_scope.Ctx.Mgr = mgr;
Assert.That(mgr.StableRandom(7), Is.EqualTo(3), "StableRandom did not use the injected source");
Assert.That(mgr.randomResult, Is.EqualTo(0.5), "StableRandom must set randomResult to the rolled unit");
Assert.That(mgr.StableRandomDouble(), Is.EqualTo(0.25), "StableRandomDouble did not use the injected source");
Assert.That(mgr.randomResult, Is.EqualTo(0.25), "StableRandomDouble must set randomResult");
Assert.That(mgr.StableRandomOnlySelf(10), Is.EqualTo(4), "StableRandomOnlySelf did not use the injected source");
}
// Parity: with the DEFAULT (seeded) source, HeadlessBattleMgr.StableRandom must equal what the
// verbatim engine would compute — floor(val * new System.Random(seed).NextDouble()) — pinning the
// re-authored RandomSourceBridge arithmetic to the engine's own formula+generator. (The default
// source seeds from HeadlessContentsCreator.RandomSeed == 12345.)
[Test]
public void Default_source_matches_engine_generator_and_formula()
{
BattleManagerBase.IsForecast = true;
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator()); // default SeededRandomSource(12345)
_scope.Ctx.Mgr = mgr;
var reference = new System.Random(12345);
for (int i = 0; i < 10; i++)
{
int expected = (int)System.Math.Floor(7 * reference.NextDouble());
Assert.That(mgr.StableRandom(7), Is.EqualTo(expected), $"parity drift at roll {i}");
}
}
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- Match the engine: decompiled types are not nullable-clean and use explicit usings. -->
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Pinned to 12.0 (net8.0 default) to match SVSim.BattleEngine; see the rationale there
(the vendored decompiled engine breaks under C# 14's 'field' contextual keyword). -->
<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SVSim.BattleEngine\SVSim.BattleEngine.csproj" />
<ProjectReference Include="..\SVSim.BattleNode\SVSim.BattleNode.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Captured PvP battle (both clients) replayed through the engine in the N1 shadow test. -->
<None Include="Fixtures\**\*.ndjson" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- The loader's card-master dump (serialized CardCSVData objects). The headless fixture
reflects these into CardMaster so the resolution path can look up real card stats. -->
<Content Include="..\SVSim.Bootstrap\Data\cards.json" Link="Data\cards.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using SVSim.BattleNode.Protocol;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
internal sealed record CapturedFrame(DateTime Ts, string Direction, string Uri, MsgEnvelope Env, string RawBody);
/// <summary>Parses a battle_test ndjson capture into MsgEnvelopes the engine can ingest.
///
/// Capture quirk (verified against data_dumps/captures/battle_test): the authoritative URI lives at
/// the TOP LEVEL for SEND frames (the body omits uri/viewerId/uuid and carries only the play
/// payload) and in the BODY for RECEIVE frames (top-level uri is null). We resolve uri as
/// top ?? body, then normalize the body into a full envelope (injecting the fields a send-frame body
/// lacks) so MsgEnvelope.FromJson — which requires uri/viewerId/uuid — succeeds for both.</summary>
internal static class CaptureReplay
{
public static IReadOnlyList<CapturedFrame> Load(string fixtureFileName)
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", fixtureFileName);
var frames = new List<CapturedFrame>();
foreach (var line in File.ReadLines(path))
{
if (string.IsNullOrWhiteSpace(line)) continue;
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
var direction = root.TryGetProperty("direction", out var dEl) ? dEl.GetString() ?? "" : "";
var ts = root.TryGetProperty("ts", out var tsEl) && tsEl.ValueKind == JsonValueKind.String
? DateTime.Parse(tsEl.GetString()!, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)
: default;
if (!root.TryGetProperty("body", out var bodyEl) || bodyEl.ValueKind != JsonValueKind.Object)
continue;
string uri =
root.TryGetProperty("uri", out var tu) && tu.ValueKind == JsonValueKind.String
? tu.GetString()!
: bodyEl.TryGetProperty("uri", out var bu) && bu.ValueKind == JsonValueKind.String
? bu.GetString()!
: "None";
// Normalize: send-frame bodies are bare payloads (no envelope fields). Inject the keys
// FromJson requires; set the resolved uri.
var obj = JsonNode.Parse(bodyEl.GetRawText())!.AsObject();
obj["uri"] = uri;
if (!obj.ContainsKey("viewerId")) obj["viewerId"] = 0L;
if (!obj.ContainsKey("uuid")) obj["uuid"] = "";
var normalized = obj.ToJsonString();
MsgEnvelope env;
try { env = MsgEnvelope.FromJson(normalized); }
catch { continue; } // out-of-model / unparseable line
frames.Add(new CapturedFrame(ts, direction, uri, env, normalized));
}
return frames;
}
/// <summary>Both clients' SENT frames interleaved in capture (ts) order, each tagged with its
/// seat: cl1 == seat A == player (true), cl2 == seat B == opponent (false). This is the node's
/// both-clients-sends ingest order — the same ts ordering the N1 shadow-replay test uses, here
/// extended to merge both sides' sends rather than replaying one client's full receive stream.</summary>
public static IEnumerable<(MsgEnvelope Env, bool Seat)> InterleavedSends(
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
{
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
.OrderBy(x => x.f.Ts)
.Select(x => (x.f.Env, x.Seat));
}
/// <summary>The selfDeck idx-&gt;cardId order from the Matched frame (the order the node also
/// computed and handed the client). This is the deck the engine seats for that side.</summary>
public static IReadOnlyList<long> SelfDeckFrom(IEnumerable<CapturedFrame> frames)
{
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
if (matched is null) return Array.Empty<long>();
using var doc = JsonDocument.Parse(matched.RawBody);
if (!doc.RootElement.TryGetProperty("selfDeck", out var deck)) return Array.Empty<long>();
return deck.EnumerateArray()
.OrderBy(e => e.GetProperty("idx").GetInt32())
.Select(e => e.GetProperty("cardId").GetInt64())
.ToList();
}
/// <summary>The per-battle master seed the capture carries (Matched.selfInfo.seed) — the seed the
/// node generated and both clients used (F-N-5). Falls back to 0 if absent.</summary>
public static int SeedFrom(IEnumerable<CapturedFrame> frames)
{
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
if (matched is null) return 0;
using var doc = JsonDocument.Parse(matched.RawBody);
if (doc.RootElement.TryGetProperty("selfInfo", out var si)
&& si.TryGetProperty("seed", out var seed)
&& seed.TryGetInt32(out var v))
return v;
return 0;
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
/// <summary>
/// PHASE 4 — DECISIVE VERIFICATION (TEST-ONLY, no production fix, no Engine/*.cs edits).
///
/// QUESTION: does feeding the headless shadow engine the FULL client inputs (server-authored
/// Deal/Swap/Ready setup frames for BOTH seats + the real per-seat <c>idxChangeSeed</c>) make its
/// recovery-mode draw recompute faithful, so the "Target card was not found in hand cards"
/// divergences vanish?
///
/// This builds the explicit 2x2 {setup-frames ingested: yes/no} x {real seed: yes/no} divergence
/// table over the SAME fresh battle (907324319325, battle_test_fresh_cl1/cl2.ndjson), and — at the
/// FIRST remaining divergence — dumps the engine's hand indices/ids vs the wire's <c>playIdx</c>.
///
/// SEEDING MECHANISM (clean, both seats): the seat-B <c>Ready</c> ingest throws an NRE headless (the
/// recovery deal path isn't headless-clean for the opponent seat), so the wire <c>Ready</c> cannot be
/// relied on to seat seat B's XorShift. To inject the real seed FAITHFULLY for BOTH seats without
/// depending on the throwing Ready, we call the test seam <see cref="SessionBattleEngine"/>.
/// <c>DebugSeedIdxChange(self, oppo)</c> (-> <c>BattleManagerBase.CreateXorShift</c>) BEFORE the
/// mulligan-end frame, with the real per-seat seeds (seat A = cl1's Ready idxChangeSeed = 1430655717,
/// seat B = cl2's = 661650374). We ASSERT both <c>SelfXorShiftActive</c> and <c>OppoXorShiftActive</c>
/// are true after.
///
/// SETUP-FRAME INGEST: identical mechanism to <see cref="CaptureReplayReshuffleRootCauseTests"/> — a
/// single <c>Deal</c> (cl1's receive Deal seats BOTH hands), each seat's <c>Swap</c> (its mulligan),
/// each seat's <c>Ready</c> (mulligan-end). The {no-setup-frames} row SKIPS Deal/Swap/Ready entirely:
/// the engine's autonomous Setup hand stands, and we replay only the plays.
/// </summary>
[TestFixture]
[NonParallelizable]
public class CaptureReplayFullInputDivergenceExperimentTests
{
// Real per-seat idxChangeSeed carried by each client's Ready frame (given in the experiment brief;
// re-confirmed below against the captures).
private const int SeatASeed = 1430655717; // cl1 / seat A / player
private const int SeatBSeed = 661650374; // cl2 / seat B / opponent
private static readonly HashSet<string> SkipUris = new()
{
nameof(NetworkBattleUri.Echo),
nameof(NetworkBattleUri.ChatStamp),
nameof(NetworkBattleUri.Gungnir),
};
private static readonly HashSet<string> MulliganUris = new()
{
nameof(NetworkBattleUri.Deal),
nameof(NetworkBattleUri.Swap),
nameof(NetworkBattleUri.Ready),
};
private sealed record HandDump(string Seat, int PlayIdx, string Uri, string Reason,
IReadOnlyList<(int Index, int CardId)> SelfHand,
IReadOnlyList<(int Index, int CardId)> OppoHand,
bool PlayIdxInSelfHand, bool PlayIdxInOppoHand);
private sealed record Cell(
bool SetupFrames, bool RealSeed,
int Divergences, bool SelfXorActive, bool OppoXorActive,
HandDump? FirstNotFoundDump);
private static int ReadPlayIdx(string rawBody)
{
using var doc = JsonDocument.Parse(rawBody);
return doc.RootElement.TryGetProperty("playIdx", out var p) && p.TryGetInt32(out var v) ? v : -1;
}
// Snapshot a seat's hand as (engine Index, CardId) pairs. Reads through the SessionBattleEngine
// oracle accessors (HandCount/HandCardIndex/HandCardId).
private static List<(int, int)> HandSnapshot(SessionBattleEngine engine, bool seat)
{
var list = new List<(int, int)>();
int n = engine.HandCount(seat);
for (int i = 0; i < n; i++)
list.Add((engine.HandCardIndex(seat, i), engine.HandCardId(seat, i)));
return list;
}
private static Cell Run(bool setupFrames, bool realSeed)
{
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
Assert.That(deckA, Is.Not.Empty);
Assert.That(deckB, Is.Not.Empty);
var engine = new SessionBattleEngine();
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
Assert.That(engine.IsReady, Is.True);
// Inject the real per-seat seed BEFORE mulligan-end (Ready). Clean both-seat activation via the
// CreateXorShift seam, sidestepping the seat-B Ready NRE.
if (realSeed)
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
int divergences = 0;
HandDump? firstNotFound = null;
void Ingest(MsgEnvelope env, bool seat, string uri, string rawBody)
{
var r = engine.Receive(env, isPlayerSeat: seat);
if (!r.Diverged) return;
divergences++;
if (firstNotFound is null && (r.RejectReason ?? "").Contains("not found in hand"))
{
int playIdx = ReadPlayIdx(rawBody);
var self = HandSnapshot(engine, seat);
var oppo = HandSnapshot(engine, !seat);
firstNotFound = new HandDump(
seat ? "A" : "B", playIdx, uri, Trim(r.RejectReason),
self, oppo,
self.Any(h => h.Item1 == playIdx), oppo.Any(h => h.Item1 == playIdx));
}
}
CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
frames.First(f => f.Direction == "receive" && f.Uri == uri);
// --- Phase 1: setup frames (optional) ---------------------------------------------------------
if (setupFrames)
{
var deal = Receive(cl1, nameof(NetworkBattleUri.Deal));
Ingest(deal.Env, seat: true, nameof(NetworkBattleUri.Deal), deal.RawBody);
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
{
var swap = Receive(frames, nameof(NetworkBattleUri.Swap));
Ingest(swap.Env, seat, nameof(NetworkBattleUri.Swap), swap.RawBody);
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
Ingest(ready.Env, seat, nameof(NetworkBattleUri.Ready), ready.RawBody);
}
}
bool selfActive = engine.SelfXorShiftActive;
bool oppoActive = engine.OppoXorShiftActive;
// Snapshot the engine's post-setup hands (after Deal/Swap/Ready) for the full-inputs cell, so the
// report can compare the engine's mulligan-resolved hand against the wire's Swap/Ready move list.
if (setupFrames && realSeed)
{
TestContext.WriteLine(" [post-setup] engine SELF (seat A) hand: " +
string.Join(" ", HandSnapshot(engine, true).Select(h => $"(idx={h.Item1},cid={h.Item2})")));
TestContext.WriteLine(" [post-setup] engine OPPO (seat B) hand: " +
string.Join(" ", HandSnapshot(engine, false).Select(h => $"(idx={h.Item1},cid={h.Item2})")));
}
// --- Phase 2: replay both clients' interleaved SENDS (the plays) ------------------------------
var sends = SendsWithRawBody(cl1, cl2)
.Where(x => !SkipUris.Contains(x.Frame.Uri))
.ToList();
foreach (var x in sends)
Ingest(x.Frame.Env, x.Seat, x.Frame.Uri, x.Frame.RawBody);
return new Cell(setupFrames, realSeed, divergences, selfActive, oppoActive, firstNotFound);
}
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsWithRawBody(
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
{
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
.OrderBy(x => x.f.Ts)
.Select(x => (x.f, x.Seat));
}
private static string Trim(string? s) => (s ?? "").Split(" @ ")[0];
[Test]
public void Full_input_2x2_divergence_table_and_first_remaining_divergence_dump()
{
// Confirm the brief's per-seat seeds match the captures' Ready frames before relying on them.
ConfirmReadySeeds();
var cells = new[]
{
Run(setupFrames: false, realSeed: false), // baseline-ish: autonomous Setup hand, seed -1
Run(setupFrames: false, realSeed: true),
Run(setupFrames: true, realSeed: false),
Run(setupFrames: true, realSeed: true), // FULL INPUTS
};
TestContext.WriteLine("=== 2x2 DIVERGENCE TABLE (setup-frames x real-seed) ===");
TestContext.WriteLine("setupFrames | realSeed | divergences | selfXor | oppoXor");
foreach (var c in cells)
TestContext.WriteLine(
$" {(c.SetupFrames ? "YES" : "no ")} | {(c.RealSeed ? "YES" : "no ")} | {c.Divergences,2} | {c.SelfXorActive,-5} | {c.OppoXorActive,-5}");
var full = cells.Single(c => c.SetupFrames && c.RealSeed);
TestContext.WriteLine("");
TestContext.WriteLine($"FULL-INPUTS cell: setupFrames=YES realSeed=YES -> divergences={full.Divergences} " +
$"selfXorActive={full.SelfXorActive} oppoXorActive={full.OppoXorActive}");
if (full.FirstNotFoundDump is { } d)
{
TestContext.WriteLine("");
TestContext.WriteLine("=== FIRST 'not found in hand' DIVERGENCE (full-inputs cell) ===");
TestContext.WriteLine($" seat={d.Seat} uri={d.Uri} wire playIdx={d.PlayIdx} reason={d.Reason}");
TestContext.WriteLine($" playIdx in self hand? {d.PlayIdxInSelfHand} in oppo hand? {d.PlayIdxInOppoHand}");
TestContext.WriteLine($" engine SELF (seat {d.Seat}) hand [{d.SelfHand.Count}]: " +
string.Join(" ", d.SelfHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
TestContext.WriteLine($" engine OPPO hand [{d.OppoHand.Count}]: " +
string.Join(" ", d.OppoHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
}
else
{
TestContext.WriteLine("");
TestContext.WriteLine("FULL-INPUTS cell produced NO 'not found in hand' divergence.");
}
// EVIDENCE ASSERTIONS (pin the experiment's reproducibility, not a desired fix outcome):
Assert.Multiple(() =>
{
// The seed seam activates BOTH seats' XorShift in every realSeed cell.
foreach (var c in cells.Where(c => c.RealSeed))
{
Assert.That(c.SelfXorActive, Is.True,
$"realSeed cell (setup={c.SetupFrames}) must activate self XorShift");
Assert.That(c.OppoXorActive, Is.True,
$"realSeed cell (setup={c.SetupFrames}) must activate oppo XorShift");
}
// With NO seed seam AND NO setup frames (the live shadow's effective state — never
// ingests the seed-bearing Ready), BOTH seats' XorShift stay inactive.
var bare = cells.Single(c => !c.RealSeed && !c.SetupFrames);
Assert.That(bare.SelfXorActive, Is.False, "no-seed/no-setup leaves self XorShift inactive");
Assert.That(bare.OppoXorActive, Is.False, "no-seed/no-setup leaves oppo XorShift inactive");
// With setup frames but no seam, the seat-A Ready frame's own idxChangeSeed activates the
// SELF XorShift (seat B's Ready NREs before it can seat oppo) — so self is active, oppo isn't.
var setupNoSeam = cells.Single(c => !c.RealSeed && c.SetupFrames);
Assert.That(setupNoSeam.SelfXorActive, Is.True,
"setup-frames cell: seat-A Ready idxChangeSeed activates self XorShift");
Assert.That(setupNoSeam.OppoXorActive, Is.False,
"setup-frames cell: seat-B Ready NREs before seating oppo XorShift");
// THE DECISIVE FINDING: full inputs (setup frames + real seed, both seats' XorShift active)
// do NOT eliminate the divergences — they stay at the 14 baseline.
var full2 = cells.Single(c => c.SetupFrames && c.RealSeed);
Assert.That(full2.SelfXorActive && full2.OppoXorActive, Is.True,
"full-inputs cell has both seats' XorShift active");
Assert.That(full2.Divergences, Is.GreaterThan(0),
"REFUTED: full inputs do NOT make the recovery recompute faithful — divergences remain");
});
}
// Re-confirm the brief's per-seat seeds against the captured Ready frames (fail loudly if the
// fixtures ever drift from the assumed seeds).
private static void ConfirmReadySeeds()
{
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
int a = ReadReadySeed(cl1);
int b = ReadReadySeed(cl2);
TestContext.WriteLine($"Confirmed Ready idxChangeSeed: cl1(seatA)={a} cl2(seatB)={b}");
Assert.That(a, Is.EqualTo(SeatASeed), "cl1 Ready idxChangeSeed must equal the brief's seat-A seed");
Assert.That(b, Is.EqualTo(SeatBSeed), "cl2 Ready idxChangeSeed must equal the brief's seat-B seed");
}
private static int ReadReadySeed(IReadOnlyList<CapturedFrame> frames)
{
var ready = frames.First(f => f.Direction == "receive" && f.Uri == nameof(NetworkBattleUri.Ready));
var obj = JsonNode.Parse(ready.RawBody)!.AsObject();
return (int)obj["idxChangeSeed"]!;
}
}
}

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
/// <summary>
/// PHASE 4 — DRAW-RECOMPUTE ROOT-CAUSE VALIDATION (TEST-ONLY; no production fix; no Engine/*.cs edits).
///
/// HYPOTHESIS (from the experiment brief): the shadow diverges ("Target card was not found in hand
/// cards", post-mulligan) because the per-turn network DRAW is a SEEDED-RANDOM pick from the deck via
/// <c>mgr.StableRandom(...)</c> (SkillRandomSelectFilter.Filtering:49/58), gated by the process-global
/// <c>BattleManagerBase.IsRandomDraw</c> — which the real match-load sets true via
/// <c>StartOpening → SetupInitialGameState(areCardsRandomlyDrawn:true)</c> (BattleManagerBase.cs:1098/1110).
/// The headless <see cref="SessionBattleEngine"/>.Setup never runs SetupInitialGameState, so IsRandomDraw
/// stays FALSE and the shadow draws TOP-OF-DECK while the clients draw seeded-random → mismatch.
/// AND the shared <c>_stableRandom</c> stream must be advanced by the wire <c>spin</c> pre-roll the Ready
/// frame carries (spin=243), which <c>OperateReceive.StartOperate:80-83</c> applies but the shadow never
/// ingests — so without it the stream is offset.
///
/// ISOLATION MATRIX (this is the report's headline): setup frames + real seed are held CONSTANT (the
/// faithful baseline the prior FullInput experiment pinned at 14); the two NEW variables are toggled:
/// • {IsRandomDraw=false, no spin} = baseline (top-of-deck draws; the live shadow's effective state)
/// • {IsRandomDraw=true, no spin} = random-draw active but stream MIS-aligned (expect WORSE)
/// • {IsRandomDraw=true, +spin} = random-draw active AND stream aligned (the hypothesised fix)
///
/// SPIN APPLICATION: spin=243 appears on the Ready frame in BOTH captures (each client applies its own
/// once). Our shadow shares ONE <c>_stableRandom</c> across both seats (seated as both players), and a
/// single client's stream sits 243 draws in after ITS Ready — so we apply spin=243 ONCE, after the
/// Deal/Swap/Ready setup frames and before the plays, exactly where the real client's StartOperate would.
/// (A scan of both fixtures confirms Ready is the ONLY frame carrying a non-zero spin.)
/// </summary>
[TestFixture]
[NonParallelizable]
public class CaptureReplayRandomDrawSpinRootCauseTests
{
private const int SeatASeed = 1430655717; // cl1 / seat A / player (Ready idxChangeSeed)
private const int SeatBSeed = 661650374; // cl2 / seat B / opponent
private const int WireSpin = 243; // both captures' Ready frame spin
private static readonly HashSet<string> SkipUris = new()
{
nameof(NetworkBattleUri.Echo),
nameof(NetworkBattleUri.ChatStamp),
nameof(NetworkBattleUri.Gungnir),
};
private sealed record HandDump(string Seat, int PlayIdx, string Uri, string Reason,
int StableRandomCount,
IReadOnlyList<(int Index, int CardId)> SelfHand,
IReadOnlyList<(int Index, int CardId)> OppoHand,
bool PlayIdxInSelfHand, bool PlayIdxInOppoHand);
private sealed record Cell(bool RandomDraw, bool Spin, int Divergences, HandDump? FirstNotFound);
private static int ReadPlayIdx(string rawBody)
{
using var doc = JsonDocument.Parse(rawBody);
return doc.RootElement.TryGetProperty("playIdx", out var p) && p.TryGetInt32(out var v) ? v : -1;
}
private static List<(int, int)> HandSnapshot(SessionBattleEngine engine, bool seat)
{
var list = new List<(int, int)>();
int n = engine.HandCount(seat);
for (int i = 0; i < n; i++)
list.Add((engine.HandCardIndex(seat, i), engine.HandCardId(seat, i)));
return list;
}
private static CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
frames.First(f => f.Direction == "receive" && f.Uri == uri);
private static Cell Run(bool randomDraw, bool spin)
{
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
Assert.That(deckA, Is.Not.Empty);
Assert.That(deckB, Is.Not.Empty);
var engine = new SessionBattleEngine();
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
Assert.That(engine.IsReady, Is.True);
// CONSTANT across all cells: faithful seed seam (both seats' XorShift active), sidestepping the
// seat-B Ready NRE — identical to the FullInput experiment's full-inputs cell.
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
// NEW VARIABLE 1: the IsRandomDraw gate. Set BEFORE any draw (deal is the first draw).
engine.DebugSetRandomDraw(randomDraw);
int divergences = 0;
HandDump? firstNotFound = null;
void Ingest(MsgEnvelope env, bool seat, string uri, string rawBody)
{
var r = engine.Receive(env, isPlayerSeat: seat);
if (!r.Diverged) return;
divergences++;
if (firstNotFound is null && (r.RejectReason ?? "").Contains("not found in hand"))
{
var self = HandSnapshot(engine, seat);
var oppo = HandSnapshot(engine, !seat);
int playIdx = ReadPlayIdx(rawBody);
firstNotFound = new HandDump(
seat ? "A" : "B", playIdx, uri, Trim(r.RejectReason),
engine.DebugStableRandomCount, self, oppo,
self.Any(h => h.Item1 == playIdx), oppo.Any(h => h.Item1 == playIdx));
}
}
// --- Phase 1: setup frames (CONSTANT: Deal once + each seat's Swap + Ready) -------------------
var deal = Receive(cl1, nameof(NetworkBattleUri.Deal));
Ingest(deal.Env, seat: true, nameof(NetworkBattleUri.Deal), deal.RawBody);
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
{
var swap = Receive(frames, nameof(NetworkBattleUri.Swap));
Ingest(swap.Env, seat, nameof(NetworkBattleUri.Swap), swap.RawBody);
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
Ingest(ready.Env, seat, nameof(NetworkBattleUri.Ready), ready.RawBody);
}
// NEW VARIABLE 2: the spin pre-roll, applied at mulligan-end (after Ready, before the first
// turn-start draw) — where OperateReceive.StartOperate applies the Ready's spin in production.
// ONE application of 243 (shared stream, one client's worth of advance).
if (spin)
engine.DebugSpinPreroll(WireSpin);
// --- Phase 2: replay both clients' interleaved SENDS (the plays) ------------------------------
var sends = SendsWithRawBody(cl1, cl2)
.Where(x => !SkipUris.Contains(x.Frame.Uri))
.ToList();
foreach (var x in sends)
Ingest(x.Frame.Env, x.Seat, x.Frame.Uri, x.Frame.RawBody);
return new Cell(randomDraw, spin, divergences, firstNotFound);
}
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsWithRawBody(
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2)
{
return cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
.OrderBy(x => x.f.Ts)
.Select(x => (x.f, x.Seat));
}
private static string Trim(string? s) => (s ?? "").Split(" @ ")[0];
[Test]
public void IsRandomDraw_plus_spin_preroll_isolation_matrix()
{
try
{
ConfirmSpin();
var baseline = Run(randomDraw: false, spin: false);
var rdOnly = Run(randomDraw: true, spin: false);
var rdSpin = Run(randomDraw: true, spin: true);
TestContext.WriteLine("=== ISOLATION MATRIX (setup-frames + real-seed held CONSTANT) ===");
TestContext.WriteLine("IsRandomDraw | spin | divergences");
TestContext.WriteLine($" false | no | {baseline.Divergences}");
TestContext.WriteLine($" true | no | {rdOnly.Divergences}");
TestContext.WriteLine($" true | +243 | {rdSpin.Divergences}");
DumpFirst("baseline {false,no}", baseline);
DumpFirst("rd-only {true,no}", rdOnly);
DumpFirst("rd+spin {true,+243}", rdSpin);
Assert.Pass(
$"MATRIX baseline={baseline.Divergences} rdOnly={rdOnly.Divergences} rdSpin={rdSpin.Divergences}");
}
catch (SuccessException) { throw; }
catch (Exception ex)
{
TestContext.WriteLine("EXPERIMENT THREW: " + ex);
throw;
}
}
private static void DumpFirst(string label, Cell c)
{
if (c.FirstNotFound is not { } d)
{
TestContext.WriteLine($"[{label}] no 'not found in hand' divergence.");
return;
}
TestContext.WriteLine($"[{label}] FIRST 'not found in hand': seat={d.Seat} uri={d.Uri} " +
$"wire playIdx={d.PlayIdx} stableRandomCount={d.StableRandomCount} reason={d.Reason}");
TestContext.WriteLine($" playIdx in self hand? {d.PlayIdxInSelfHand} in oppo hand? {d.PlayIdxInOppoHand}");
TestContext.WriteLine($" SELF (seat {d.Seat}) hand [{d.SelfHand.Count}]: " +
string.Join(" ", d.SelfHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
TestContext.WriteLine($" OPPO hand [{d.OppoHand.Count}]: " +
string.Join(" ", d.OppoHand.Select(h => $"(idx={h.Index},cid={h.CardId})")));
}
/// <summary>STEP 4 (payoff check): with the hypothesised fix applied {IsRandomDraw=true, +spin},
/// does the engine reach and RESOLVE cl1's spellboost play so PlayedCardSpellboost/PlayedCardCost
/// return real (non-zero) values? cl1's deck carries the spellboost-scaling follower 101314020 at
/// deck idx 10/21/25. We replay the {true,+243} cell and, after each accepted seat-A PlayActions,
/// probe whether any in-play/cemetery card has that id with a resolved cost/spellboost. We report
/// whether the spellboost play was ever reached at all.</summary>
[Test]
public void Spellboost_play_resolution_under_random_draw_plus_spin()
{
const int SpellboostCardId = 101314020;
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
var engine = new SessionBattleEngine();
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
engine.DebugSetRandomDraw(true);
// setup frames
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, isPlayerSeat: true);
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
{
engine.Receive(Receive(frames, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: seat);
engine.Receive(Receive(frames, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: seat);
}
engine.DebugSpinPreroll(WireSpin);
int acceptedSeatAPlays = 0, divergedBeforeFirstPlay = 0;
bool spellboostResolved = false;
int sbCost = -999, sbCharge = -999;
var sends = SendsWithRawBody(cl1, cl2).Where(x => !SkipUris.Contains(x.Frame.Uri)).ToList();
bool sawFirstPlay = false;
foreach (var x in sends)
{
bool isPlay = x.Frame.Uri == nameof(NetworkBattleUri.PlayActions);
var r = engine.Receive(x.Frame.Env, isPlayerSeat: x.Seat);
if (isPlay && !sawFirstPlay) { sawFirstPlay = true; if (r.Diverged) divergedBeforeFirstPlay++; }
if (isPlay && x.Seat && !r.Diverged)
{
acceptedSeatAPlays++;
int playIdx = ReadPlayIdx(x.Frame.RawBody);
long id = engine.PlayedCardId(true, playIdx, 0);
if (id == SpellboostCardId)
{
spellboostResolved = true;
sbCost = engine.PlayedCardCost(true, playIdx, -1);
sbCharge = engine.PlayedCardSpellboost(true, playIdx, -1);
break;
}
}
}
TestContext.WriteLine($"[spellboost payoff] acceptedSeatAPlays={acceptedSeatAPlays} " +
$"divergedAtFirstPlay={divergedBeforeFirstPlay} spellboostResolved={spellboostResolved} " +
$"cost={sbCost} charge={sbCharge}");
// The replay diverges at the FIRST seat-A play (matrix shows playIdx=8 not in hand), so the
// engine never advances to the later spellboost play — the visible spellboost symptom is NOT
// fixed by {IsRandomDraw+spin} because the prerequisite (aligned draws) is not met.
Assert.That(divergedBeforeFirstPlay, Is.EqualTo(1),
"the FIRST seat-A play already diverges under {IsRandomDraw=true,+spin}");
Assert.That(spellboostResolved, Is.False,
"the spellboost play is never reached because the replay diverges at the first play");
}
private static void ConfirmSpin()
{
foreach (var fn in new[] { "battle_test_fresh_cl1.ndjson", "battle_test_fresh_cl2.ndjson" })
{
var frames = CaptureReplay.Load(fn);
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
var obj = JsonNode.Parse(ready.RawBody)!.AsObject();
int spin = obj.TryGetPropertyValue("spin", out var s) ? (int)s! : 0;
TestContext.WriteLine($"Confirmed {fn} Ready spin={spin}");
Assert.That(spin, Is.EqualTo(WireSpin), $"{fn} Ready spin must equal {WireSpin}");
}
}
}
}

View File

@@ -0,0 +1,182 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
/// <summary>
/// PHASE 4 STEP 1 — Tier 2 capture-replay root-cause VERIFICATION (NOT a fix).
///
/// Replays the FRESH smoke captures (battle 907324319325) — battle_test_fresh_cl1/cl2.ndjson —
/// through a <see cref="SessionBattleEngine"/>, then measures whether the per-seat <c>idxChangeSeed</c>
/// the real Ready frame carries is what controls the "Target card was not found in hand cards"
/// divergence symptom.
///
/// FAITHFUL SETUP (the live ShadowIngest only feeds client SENDS, which contain NO Deal/Ready, so a
/// bare send-only replay can't even seat a hand — that conflates "missing Deal" with "missing
/// reshuffle"). To ISOLATE the reshuffle/seed effect we seat each seat's hand from its OWN client's
/// RECEIVE Deal + Swap + Ready (the frames that establish the hand and reach mulligan-end), then replay
/// both clients' interleaved SENDS (the plays). The Ready frame natively carries the real per-seat
/// idxChangeSeed (cl1=1430655717, cl2=661650374), and the engine's recovery receiver calls
/// <c>CreateXorShift</c> from it (NetworkBattleReceiver.cs:1125-1126). The A/B is then:
/// • WITH-SEED: ingest the Ready frame verbatim (idxChangeSeed present) -> XorShift active;
/// • SEED-STRIPPED: ingest the SAME Ready frame with idxChangeSeed forced to -1 -> XorShift inactive
/// (this is exactly the live shadow's effective state, since it never ingests the seed-bearing Ready).
/// The ONLY difference between the two runs is whether the seed reaches CreateXorShift.
///
/// DECK SETUP MECHANISM (feasibility crux, RESOLVED): each side's deck is reconstructed from the
/// capture's <c>Matched.selfDeck</c> (idx-&gt;cardId, the exact shuffled order the node also handed the
/// client) via <see cref="CaptureReplay.SelfDeckFrom"/>; the master seed from <c>Matched.selfInfo.seed</c>.
/// The deck IS in the socket capture — no external fixture needed.
/// </summary>
[TestFixture]
[NonParallelizable]
public class CaptureReplayReshuffleRootCauseTests
{
private static readonly HashSet<string> SkipUris = new()
{
nameof(NetworkBattleUri.Echo),
nameof(NetworkBattleUri.ChatStamp),
nameof(NetworkBattleUri.Gungnir),
};
private static readonly HashSet<string> MulliganUris = new()
{
nameof(NetworkBattleUri.Deal),
nameof(NetworkBattleUri.Swap),
nameof(NetworkBattleUri.Ready),
};
private sealed record ReplayOutcome(
int FrameCount, List<string> Divergences, bool AllDivergencesPostMulligan, bool SelfXorShiftActive);
// Re-parse a captured frame, overriding the Ready body's idxChangeSeed (and oppoIdxChangeSeed if
// present). Used to STRIP the seed (-1) to model the live shadow's seed-less state.
private static MsgEnvelope OverrideReadySeed(CapturedFrame f, int newSeed)
{
var obj = JsonNode.Parse(f.RawBody)!.AsObject();
obj["idxChangeSeed"] = newSeed;
if (obj.ContainsKey("oppoIdxChangeSeed")) obj["oppoIdxChangeSeed"] = newSeed;
return MsgEnvelope.FromJson(obj.ToJsonString());
}
/// <summary>Seat both hands from each client's receive Deal+Swap+Ready, then replay both clients'
/// interleaved SENDS. <paramref name="stripSeed"/> forces the Ready idxChangeSeed to -1 (the live
/// shadow's effective state). Returns divergences + the post-setup self XorShift state.</summary>
private static ReplayOutcome Replay(bool stripSeed)
{
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
Assert.That(deckA, Is.Not.Empty, "cl1 Matched.selfDeck must reconstruct seat A's deck");
Assert.That(deckB, Is.Not.Empty, "cl2 Matched.selfDeck must reconstruct seat B's deck");
var engine = new SessionBattleEngine();
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
Assert.That(engine.IsReady, Is.True, "engine must seat from the captured decks + seed");
var divergences = new List<string>();
bool sawMulliganEnd = false;
bool anyDivergencePreMulligan = false;
void Ingest(MsgEnvelope env, bool seat, string uri)
{
if (MulliganUris.Contains(uri)) sawMulliganEnd = true;
var r = engine.Receive(env, isPlayerSeat: seat);
if (r.Diverged)
{
divergences.Add($"seat={(seat ? "A" : "B")} {uri}: {Trim(r.RejectReason)}");
if (!sawMulliganEnd) anyDivergencePreMulligan = true;
}
}
// --- Phase 1: seat both hands from the receive setup frames ----------------------------------
// A single Deal seats BOTH opening hands (cl1's receive Deal carries self=A + oppo=B), so we
// ingest Deal ONCE (as seat A) — ingesting both clients' Deals would double-deal (NRE / "Sequence
// contains more than one"). Each seat's Swap then applies that seat's mulligan, and each seat's
// Ready carries THAT seat's idxChangeSeed (cl1's for A, cl2's for B; the recovery receiver consumes
// only the SELF seed per ingest, NetworkBattleReceiver.cs:1126), reaching mulligan-end per seat.
CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
frames.First(f => f.Direction == "receive" && f.Uri == uri);
// Deal once (seat A's receive Deal seats both hands).
Ingest(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, seat: true, nameof(NetworkBattleUri.Deal));
// Each seat's mulligan swap, then each seat's Ready (its own seed).
foreach (var (frames, seat) in new[] { (cl1, true), (cl2, false) })
{
Ingest(Receive(frames, nameof(NetworkBattleUri.Swap)).Env, seat, nameof(NetworkBattleUri.Swap));
var ready = Receive(frames, nameof(NetworkBattleUri.Ready));
var readyEnv = stripSeed ? OverrideReadySeed(ready, -1) : ready.Env;
Ingest(readyEnv, seat, nameof(NetworkBattleUri.Ready));
}
bool selfActive = engine.SelfXorShiftActive;
// --- Phase 2: replay both clients' interleaved SENDS (the plays / turn ops) -------------------
var sends = CaptureReplay.InterleavedSends(cl1, cl2)
.Where(x => !SkipUris.Contains(x.Env.Uri.ToString()))
.ToList();
foreach (var (env, seat) in sends)
Ingest(env, seat, env.Uri.ToString());
return new ReplayOutcome(
FrameCount: sends.Count, divergences, !anyDivergencePreMulligan, selfActive);
}
private static string Trim(string? s) =>
(s ?? "").Split(" @ ")[0];
[Test]
public void Capture_replay_reproduces_post_mulligan_divergence_and_pins_what_the_seed_does_not_fix()
{
var withSeed = Replay(stripSeed: false);
var stripped = Replay(stripSeed: true);
TestContext.WriteLine($"WITH-SEED (Ready idxChangeSeed present): selfXorShiftActive={withSeed.SelfXorShiftActive} " +
$"playFrames={withSeed.FrameCount} divergences={withSeed.Divergences.Count}");
foreach (var d in withSeed.Divergences) TestContext.WriteLine(" DIVERGE " + d);
TestContext.WriteLine($"SEED-STRIPPED (idxChangeSeed=-1, the live shadow state): selfXorShiftActive={stripped.SelfXorShiftActive} " +
$"playFrames={stripped.FrameCount} divergences={stripped.Divergences.Count}");
foreach (var d in stripped.Divergences) TestContext.WriteLine(" DIVERGE " + d);
Assert.Multiple(() =>
{
// (1) The reported symptom reproduces DETERMINISTICALLY from the captures: the replay diverges,
// including the verbatim "Target card was not found in hand cards" exception.
Assert.That(withSeed.Divergences, Is.Not.Empty,
"the capture replay must reproduce the divergence symptom");
Assert.That(withSeed.Divergences.Any(d => d.Contains("not found in hand")), Is.True,
"the reported 'Target card was not found in hand cards' symptom must reproduce");
// (2) All divergences occur AFTER the mulligan barrier — consistent with a post-mulligan cause.
Assert.That(withSeed.AllDivergencesPostMulligan, Is.True, "with-seed divergences are post-mulligan");
Assert.That(stripped.AllDivergencesPostMulligan, Is.True, "stripped divergences are post-mulligan");
// (3) The wire seed DOES drive the engine's XorShift gate (NetworkBattleReceiver.cs:1126):
// present -> active, stripped (the live shadow's state) -> inactive.
Assert.That(withSeed.SelfXorShiftActive, Is.True,
"ingesting the real Ready (idxChangeSeed present) activates the engine's XorShift");
Assert.That(stripped.SelfXorShiftActive, Is.False,
"stripping idxChangeSeed (the live shadow's state) leaves the XorShift inactive");
// (4) THE KEY VERIFICATION FINDING — activating the XorShift via the wire seed does NOT, on its
// own, change the divergence count. The engine's recovery/watch RECEIVE path never performs
// the post-mulligan full-deck reshuffle the live client does: the XorShift's GetChangeInt is
// consumed ONLY by AddToDeckCardIndexChange (BattlePlayerBase.cs:3079) for cards added to the
// deck AFTER mulligan-end, and the per-turn draw is engine-computed off the (un-reshuffled)
// deck order, not driven by the wire's `move idx`. So "feed the seed" alone does NOT fix the
// desync headless — the eventual fix must also make the engine reshuffle the deck post-
// mulligan to match the client (or drive the draw from the wire idx). We PIN this here.
Assert.That(stripped.Divergences.Count, Is.EqualTo(withSeed.Divergences.Count),
"VERIFIED: activating the XorShift via the wire seed alone does NOT change the divergence " +
"count — the engine's receive path does not reshuffle the deck, so the seed is necessary " +
"but NOT sufficient (the fix needs the reshuffle too, not just the seed)");
});
}
}
}

View File

@@ -0,0 +1,215 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
/// <summary>
/// PHASE 4 — OPTION-A VIABILITY PROBE (TEST-ONLY; no production fix; no Engine/*.cs edits).
///
/// QUESTION: can a per-seat RNG router in the headless engine reliably attribute each StableRandom roll
/// to the correct seat — so two seats can draw from two independent same-seeded sub-streams (mirroring
/// two real clients, each with its OWN _stableRandom)?
///
/// METHOD: replay battle_test_fresh_cl1/cl2 through a <see cref="SessionBattleEngine"/> whose mgr RNG is
/// a logging source. On EVERY roll it records (a) the seat signals the mgr can read from its own state
/// (GetBattlePlayer(true/false).IsSelfTurn — the richest seat signal a mgr-level StableRandom override
/// sees; there is NO "current operating seat" field on the mgr), and (b) the live call stack (where the
/// acting seat is actually visible: MulliganCtrl._battlePlayer / BattlePlayerBase.LotteryRandomDrawCard /
/// OperateReceive.StartOperate spin pre-roll). We dump the rolls for the mulligan lotteries, the first
/// turn draws, and the spin pre-roll, and classify each — reporting whether the seat is UNAMBIGUOUS from
/// mgr STATE vs only from the STACK.
/// </summary>
[TestFixture]
[NonParallelizable]
public class CaptureReplayRngSeatAttributionProbeTests
{
private const int SeatASeed = 1430655717; // cl1 Ready idxChangeSeed
private const int SeatBSeed = 661650374; // cl2 Ready idxChangeSeed
private const int WireSpin = 243;
private static readonly HashSet<string> SkipUris = new()
{
nameof(NetworkBattleUri.Echo),
nameof(NetworkBattleUri.ChatStamp),
nameof(NetworkBattleUri.Gungnir),
};
private static CapturedFrame Receive(IReadOnlyList<CapturedFrame> frames, string uri) =>
frames.First(f => f.Direction == "receive" && f.Uri == uri);
private static IEnumerable<(CapturedFrame Frame, bool Seat)> SendsInTsOrder(
IReadOnlyList<CapturedFrame> cl1, IReadOnlyList<CapturedFrame> cl2) =>
cl1.Where(f => f.Direction == "send").Select(f => (f, Seat: true))
.Concat(cl2.Where(f => f.Direction == "send").Select(f => (f, Seat: false)))
.OrderBy(x => x.f.Ts)
.Select(x => (x.f, x.Seat));
[Test]
public void Roll_log_reveals_whether_acting_seat_is_attributable_from_state_or_only_stack()
{
var cl1 = CaptureReplay.Load("battle_test_fresh_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_fresh_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
// (5) seeds
int seedA = CaptureReplay.SeedFrom(cl1);
int seedB = CaptureReplay.SeedFrom(cl2);
TestContext.WriteLine($"=== SEEDS (Matched.selfInfo.seed) ===");
TestContext.WriteLine($" cl1 seed = {seedA}");
TestContext.WriteLine($" cl2 seed = {seedB}");
TestContext.WriteLine($" SAME? {seedA == seedB} (Ready idxChangeSeed cl1={SeatASeed} cl2={SeatBSeed} — DIFFERENT)");
TestContext.WriteLine("");
var engine = new SessionBattleEngine();
var log = engine.DebugSetupWithRollLog(masterSeed: seedA, seatADeck: deckA, seatBDeck: deckB);
Assert.That(engine.IsReady, Is.True);
engine.DebugSeedIdxChange(SeatASeed, SeatBSeed);
engine.DebugSetRandomDraw(true); // the gate that makes draws actually ROLL
// mark roll-log boundaries so we can bucket the rolls by phase
int Mark() => log.Count;
int beforeDeal = Mark();
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Deal)).Env, isPlayerSeat: true);
int afterDeal = Mark();
// seat A mulligan (Swap+Ready) then seat B mulligan
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: true);
int afterSwapA = Mark();
engine.Receive(Receive(cl1, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: true);
int afterReadyA = Mark();
engine.Receive(Receive(cl2, nameof(NetworkBattleUri.Swap)).Env, isPlayerSeat: false);
int afterSwapB = Mark();
engine.Receive(Receive(cl2, nameof(NetworkBattleUri.Ready)).Env, isPlayerSeat: false);
int afterReadyB = Mark();
// spin pre-roll (one client's 243 advance, applied once on the shared stream)
engine.DebugSpinPreroll(WireSpin);
int afterSpin = Mark();
// replay both clients' interleaved sends (the plays + turn ops -> turn-start draws fire here)
var sends = SendsInTsOrder(cl1, cl2).Where(x => !SkipUris.Contains(x.Frame.Uri)).ToList();
foreach (var x in sends)
engine.Receive(x.Frame.Env, isPlayerSeat: x.Seat);
int afterSends = Mark();
TestContext.WriteLine("=== ROLL-COUNT BY PHASE (IsRandomDraw=true) ===");
TestContext.WriteLine($" Deal : {afterDeal - beforeDeal}");
TestContext.WriteLine($" Swap A : {afterSwapA - afterDeal}");
TestContext.WriteLine($" Ready A (mulligan): {afterReadyA - afterSwapA}");
TestContext.WriteLine($" Swap B : {afterSwapB - afterReadyA}");
TestContext.WriteLine($" Ready B (mulligan): {afterReadyB - afterSwapB}");
TestContext.WriteLine($" spin pre-roll : {afterSpin - afterReadyB} (expected {WireSpin})");
TestContext.WriteLine($" all sends/plays : {afterSends - afterSpin}");
TestContext.WriteLine($" TOTAL : {log.Count}");
TestContext.WriteLine("");
DumpRange("DEAL", log, beforeDeal, afterDeal);
DumpRange("SWAP A (mulligan lottery, seat A)", log, afterDeal, afterSwapA);
DumpRange("READY A (mulligan, seat A)", log, afterSwapA, afterReadyA);
DumpRange("SWAP B (mulligan lottery, seat B)", log, afterReadyA, afterSwapB);
DumpRange("READY B (mulligan, seat B)", log, afterSwapB, afterReadyB);
DumpSpinSummary("SPIN PRE-ROLL", log, afterReadyB, afterSpin);
// first ~12 of the play phase covers the early turn-start draws for both seats
DumpRange("FIRST PLAY-PHASE ROLLS (turn draws + effects)", log, afterSpin,
System.Math.Min(afterSpin + 12, afterSends));
// === STATE-vs-STACK attribution analysis ===
AnalyzeAttribution(log, afterSpin);
Assert.Pass($"probe complete: {log.Count} rolls logged; see TestContext output for attribution analysis");
}
private static void DumpRange(string label, IReadOnlyList<SessionBattleEngine.RollEntry> log, int from, int to)
{
TestContext.WriteLine($"--- {label} [rolls {from}..{to - 1}] ({to - from} rolls) ---");
for (int i = from; i < to; i++)
{
var e = log[i];
TestContext.WriteLine($" #{e.Index} {e.Api}(arg={e.Arg}) | mgrState: self.IsSelfTurn={e.SelfIsSelfTurn} oppo.IsSelfTurn={e.OppoIsSelfTurn} | classify={Classify(e)}");
TestContext.WriteLine($" stack: {e.Stack}");
}
if (to - from == 0) TestContext.WriteLine(" (none)");
TestContext.WriteLine("");
}
private static void DumpSpinSummary(string label, IReadOnlyList<SessionBattleEngine.RollEntry> log, int from, int to)
{
TestContext.WriteLine($"--- {label} [rolls {from}..{to - 1}] ({to - from} rolls) ---");
if (to - from > 0)
{
var first = log[from];
TestContext.WriteLine($" first spin roll #{first.Index}: self.IsSelfTurn={first.SelfIsSelfTurn} oppo.IsSelfTurn={first.OppoIsSelfTurn}");
TestContext.WriteLine($" stack: {first.Stack}");
bool allStateIdentical = log.Skip(from).Take(to - from)
.All(e => e.SelfIsSelfTurn == first.SelfIsSelfTurn && e.OppoIsSelfTurn == first.OppoIsSelfTurn);
bool allViaStartOperate = log.Skip(from).Take(to - from).All(e => e.Stack.Contains("StartOperate"));
TestContext.WriteLine($" all {to - from} spin rolls have identical mgr seat-state? {allStateIdentical}");
TestContext.WriteLine($" all {to - from} spin rolls routed via OperateReceive.StartOperate? {allViaStartOperate}");
}
TestContext.WriteLine("");
}
// Best-effort classification from the STACK (the ground truth of who is rolling).
private static string Classify(SessionBattleEngine.RollEntry e)
{
string s = e.Stack;
if (s.Contains("StartOperate")) return "SPIN-PREROLL";
if (s.Contains("_LotMulliganCardIndex") || s.Contains("MulliganCtrl")) return "MULLIGAN-LOTTERY";
if (s.Contains("LotteryRandomDrawCard") || s.Contains("RandomCardDraw")) return "TURN/EFFECT-DRAW";
if (s.Contains("SkillRandomSelectFilter")) return "SKILL-FILTER-DRAW";
return "OTHER-EFFECT";
}
private static void AnalyzeAttribution(IReadOnlyList<SessionBattleEngine.RollEntry> log, int playPhaseStart)
{
TestContext.WriteLine("=== STATE-vs-STACK ATTRIBUTION ANALYSIS ===");
// 1) Does mgr-state (IsSelfTurn flags) ever change across the whole replay? If both flags are
// pinned at setup values (self=true/oppo=false) the entire time, mgr-state CANNOT distinguish
// seats — every roll looks identical from mgr state.
var distinctStates = log
.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn))
.Distinct()
.ToList();
TestContext.WriteLine($" distinct mgr seat-states observed across ALL {log.Count} rolls: {distinctStates.Count}");
foreach (var st in distinctStates)
TestContext.WriteLine($" (self.IsSelfTurn={st.Item1}, oppo.IsSelfTurn={st.Item2})");
// 2) For the mulligan lotteries: seat A's 6 rolls then seat B's 6 rolls happen back-to-back. Are
// their mgr-states distinguishable? (They should NOT be — IsSelfTurn isn't toggled during
// mulligan; both lotteries run with the same setup-time flags.)
var mulliganRolls = log.Where(e => Classify(e) == "MULLIGAN-LOTTERY").ToList();
var mulliganStates = mulliganRolls.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn)).Distinct().Count();
TestContext.WriteLine($" mulligan-lottery rolls: {mulliganRolls.Count}; distinct mgr seat-states among them: {mulliganStates}");
TestContext.WriteLine($" -> seat attributable from mgr STATE alone? {(mulliganStates >= 2 ? "MAYBE" : "NO (state identical for both seats' lotteries)")}");
bool mulliganSeatInStack = mulliganRolls.All(e => e.Stack.Contains("Mulligan") || e.Stack.Contains("_LotMulligan"));
TestContext.WriteLine($" -> mulligan rolls carry a MulliganCtrl frame on the stack? {mulliganSeatInStack}");
// 3) For the play-phase draws: are turn-start draws present at all, and do their mgr-states track
// the acting seat (i.e. does IsSelfTurn flip to identify whose turn/draw it is)?
var drawRolls = log.Skip(playPhaseStart)
.Where(e => Classify(e) is "TURN/EFFECT-DRAW" or "SKILL-FILTER-DRAW")
.ToList();
TestContext.WriteLine($" play-phase draw/filter rolls: {drawRolls.Count}");
if (drawRolls.Count > 0)
{
var drawStates = drawRolls.Select(e => (e.SelfIsSelfTurn, e.OppoIsSelfTurn)).Distinct().Count();
TestContext.WriteLine($" distinct mgr seat-states among draw rolls: {drawStates}");
}
TestContext.WriteLine("");
TestContext.WriteLine(" INTERPRETATION:");
TestContext.WriteLine(" * If distinct mgr seat-states == 1 for a phase, the StableRandom override CANNOT");
TestContext.WriteLine(" attribute that phase's rolls to a seat from mgr state — only the call STACK");
TestContext.WriteLine(" (MulliganCtrl._battlePlayer / BattlePlayerBase 'this' / OperateReceive._isPlayer)");
TestContext.WriteLine(" names the acting seat.");
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Linq;
using NUnit.Framework;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
[TestFixture]
public class CaptureReplayTests
{
[Test]
public void Load_parses_frames_and_extracts_self_deck()
{
var frames = CaptureReplay.Load("battle_test_cl1.ndjson");
Assert.That(frames, Is.Not.Empty);
var deck = CaptureReplay.SelfDeckFrom(frames);
Assert.That(deck, Is.Not.Empty, "Matched.selfDeck should parse");
Assert.That(deck.Count, Is.EqualTo(40), "a standard deck is 40 cards");
// Send PlayActions carry their URI at the top level (body.uri == None); the helper must
// resolve it correctly, not drop it to None.
Assert.That(frames.Any(f => f.Direction == "send" && f.Uri == "PlayActions"),
Is.True, "send PlayActions URI resolved from top level");
Assert.That(CaptureReplay.SeedFrom(frames), Is.GreaterThan(0), "Matched.selfInfo.seed parsed");
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Linq;
using NUnit.Framework;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
[TestFixture]
public class SessionEngineConstructionTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
[Test]
public void SessionBattleEngine_instantiates_and_is_not_ready_before_setup()
{
var engine = new SessionBattleEngine();
Assert.That(engine.IsReady, Is.False);
}
[Test]
public void Setup_builds_two_seat_network_battle_headless()
{
// Load every card id the two test decks reference so CardMaster can resolve them.
var deckA = Enumerable.Repeat(100011010L, 40).ToList(); // vanilla 1/2 follower x40
var deckB = Enumerable.Repeat(100011010L, 40).ToList();
HeadlessCardMaster.Load(100011010);
var engine = new SessionBattleEngine();
Assert.DoesNotThrow(() => engine.Setup(masterSeed: 12345, seatADeck: deckA, seatBDeck: deckB));
Assert.That(engine.IsReady, Is.True);
}
[Test]
public void Receive_one_playactions_resolves_headless()
{
// SUPERSEDED by the node-native oracle (SVSim.UnitTests HeadlessConductorTests). This test
// predates the M-HC-0b view-untangle: before it, the receive conductor resolved NOTHING
// headless (every InstantVfx the conductor fused the mutation into was no-op'd by the shared
// VfxMgr, and OperateReceive.OnReceiveDeal was never wired), so a play "ingested" without
// touching state and trivially did not reject. Now the conductor RESOLVES (HeadlessConductor
// VfxMgr runs the InstantVfx; the deal seats the hand). This test feeds the first captured
// `send PlayActions` WITHOUT first replaying the capture's Deal/mulligan, so the played card
// is not in the seated hand and the now-live resolution correctly rejects
// (RemoveSpellCardFromHand: not found). Replaying the capture's Deal first does NOT fix it:
// the seated deck order can't reproduce the capture's post-mulligan idx references (the
// documented capture-replay draw-misalignment artifact — see memory
// project_battle_headless_conductor: "validate via node-native battles"). The valid headless
// play oracle is now HeadlessConductorTests.Vanilla_play_resolves_on_engine_state_headless.
Assert.Ignore("Superseded by node-native HeadlessConductorTests (M-HC-0b). Capture-replay " +
"draw-misalignment makes a captured play unresolvable against a node-seated deck; the " +
"node-native harness is the post-M-HC-0b oracle. Revive if capture-replay alignment lands.");
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
var deck = CaptureReplay.SelfDeckFrom(cl1);
// Load ALL deck ids in ONE call: HeadlessCardMaster.Load replaces the static CardMaster each
// call, so a per-id loop would leave only the last card resolvable.
HeadlessCardMaster.Load(deck.Select(x => (int)x).Distinct().ToArray());
var engine = new SessionBattleEngine();
engine.Setup(CaptureReplay.SeedFrom(cl1), seatADeck: deck, seatBDeck: deck);
var firstPlay = cl1.First(f => f.Direction == "send" && f.Uri == "PlayActions");
var result = engine.Receive(firstPlay.Env, isPlayerSeat: true);
Assert.That(result.RejectReason, Is.Null, $"ingest threw/rejected: {result.RejectReason}");
Assert.That(result.Accepted, Is.True);
}
}
}

View File

@@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Engine;
namespace SVSim.BattleEngine.Tests.SessionEngine
{
[TestFixture]
public class SessionEngineShadowReplayTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
// Frames that are transport/keepalive, not game actions — not ingested.
private static readonly HashSet<string> SkipUris = new()
{
nameof(NetworkBattleUri.Echo),
nameof(NetworkBattleUri.ChatStamp),
nameof(NetworkBattleUri.Gungnir),
};
[Test]
public void Shadow_replay_of_captured_battle_tracks_state_without_desync()
{
// SUPERSEDED by the node-native oracle (SVSim.UnitTests HeadlessConductorTests). This test's
// "0 rejects" used to pass VACUOUSLY: before the M-HC-0b view-untangle the receive conductor
// resolved NOTHING headless (InstantVfx mutations no-op'd; OnReceiveDeal unwired), so no
// captured frame could diverge because none was applied. The retracted "shadow tracks the
// capture" claim is documented in memory project_battle_node_engine_shadow / _headless_conductor.
// Now that the conductor RESOLVES, replaying a captured stream against a node-seated deck hits
// the documented capture-replay draw-misalignment: the seated deck order can't reproduce the
// capture's post-mulligan idx references, so played cards aren't in the seated hand
// (HandCardToField/RemoveSpellCardFromHand: not found). The decision (memory
// project_battle_headless_conductor) is to validate headless resolution via NODE-NATIVE
// battles, not capture replay. The node-native oracle now covers Deal+Play.
Assert.Ignore("Superseded by node-native HeadlessConductorTests (M-HC-0b). Capture-replay " +
"against a node-seated deck hits the documented draw-misalignment artifact once the " +
"receive path actually resolves. Revive if a capture-replay alignment path lands.");
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
// One Load call with every id — Load replaces the static master each call.
HeadlessCardMaster.Load(deckA.Concat(deckB).Select(x => (int)x).Distinct().ToArray());
var engine = new SessionBattleEngine();
engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB);
// Single-client full-stream replay (cl1 as the player seat): cl1's SENT frames are its own
// actions (seat=true); its RECEIVED frames are the opponent/server actions (seat=false),
// incl. the Deal that establishes both hands. This is exactly the stream cl1's receiver
// processed, in capture (ts) order. (The node-side both-clients-sends model is exercised
// live in Task 7; here we validate engine tracking against ground truth.)
var stream = cl1.Where(f => !SkipUris.Contains(f.Uri))
.OrderBy(f => f.Ts)
.ToList();
var rejects = new List<string>();
var violations = new List<string>();
foreach (var f in stream)
{
bool seat = f.Direction == "send";
var r = engine.Receive(f.Env, isPlayerSeat: seat);
if (r.RejectReason is not null)
rejects.Add($"{f.Direction} {f.Uri}: {r.RejectReason}");
if (f.Uri == nameof(NetworkBattleUri.TurnEnd))
CheckInvariants(engine, violations, atUri: f.Uri);
}
foreach (var line in rejects) TestContext.WriteLine("REJECT " + line);
foreach (var line in violations) TestContext.WriteLine("VIOLATION " + line);
TestContext.WriteLine($"frames={stream.Count} rejects={rejects.Count} violations={violations.Count}");
Assert.Multiple(() =>
{
Assert.That(rejects, Is.Empty, "engine diverged / rejected a captured frame");
Assert.That(violations, Is.Empty, "engine state left a structural invariant");
});
}
private static void CheckInvariants(SessionBattleEngine engine, List<string> violations, string atUri)
{
foreach (var seat in new[] { true, false })
{
int life = engine.LeaderLife(seat), pp = engine.Pp(seat);
int board = engine.BoardCount(seat), hand = engine.HandCount(seat);
if (life is < 0 or > 20) violations.Add($"{atUri} seat={seat} life={life}");
if (pp is < 0 or > 10) violations.Add($"{atUri} seat={seat} pp={pp}");
if (board is < 0 or > 7) violations.Add($"{atUri} seat={seat} board={board}");
if (hand is < 0 or > 9) violations.Add($"{atUri} seat={seat} hand={hand}");
}
}
}
}

View File

@@ -0,0 +1,33 @@
using NUnit.Framework;
using SVSim.BattleNode.Sessions.Engine;
using System.Linq;
using SVSim.BattleEngine.Tests;
namespace SVSim.BattleEngine.Tests.SessionEngine;
[TestFixture]
public class SessionEngineSpellboostTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
[Test]
public void EngineGlobalInit_makes_a_fresh_engine_ready()
{
EngineGlobalInit.EnsureInitialized();
var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson");
var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson");
var deckA = CaptureReplay.SelfDeckFrom(cl1);
var deckB = CaptureReplay.SelfDeckFrom(cl2);
// Belt-and-suspenders (matches the sibling tests): load the decks into the harness master so
// this test never depends on global card-master contents. EnsureInitialized() above still
// proves EngineGlobalInit's own path works.
foreach (var id in deckA.Concat(deckB).Distinct()) HeadlessCardMaster.Load((int)id);
var engine = new SessionBattleEngine();
Assert.DoesNotThrow(() => engine.Setup(masterSeed: 12345, seatADeck: deckA, seatBDeck: deckB));
Assert.That(engine.IsReady, Is.True, "engine must be ready after EngineGlobalInit (carried-risk fix)");
}
}

View File

@@ -0,0 +1,111 @@
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M5 (next-hardest deterministic card): a when_play SUMMON_TOKEN spell resolves to correct
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the
// M2 vanilla follower / M3 fixed-damage spell / M4 self-buff follower proved (design §5 / DP4 +
// M3+ resume recipe). The new oracle dimension over M2-M4 is a BOARD-COUNT DELTA from a
// SKILL-CREATED card: the spell's `summon_token=100011020` must place exactly one NEW follower
// token (id 100011020, a neutral 2/2) onto the caster's board — a card that was never in the
// hand or deck. This is the first headless run of the PUBLIC prefab card-creation path
// (CardCreatorBase.CreateCard, createNullView:false), so it stresses the view shim in a way the
// earlier null-view-seam milestones did not.
[TestFixture]
public class SummonTokenOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Summon_token_spell_places_a_new_token_on_the_board()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M4 oracles): opponent refs + active turn flag.
// The summon resolves onto the active player's own board (`summon_side` defaults to self).
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life: this spell deals no damage, but the play-legality gate still rejects a
// play when a leader reads as a 0-life game-over state (M3 learning).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Seed the card-template prefabs the engine's internal (createNullView:false) summon
// creation path clones — the bare construction path leaves SBattleLoad null.
HeadlessEngineEnv.InitCardTemplates(mgr);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.TokenSpellId);
// Place the summon-token spell in the active player's hand with PP to spare; empty board.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.TokenSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot. ClassAndInPlayCardList holds the leader (index 0) on an empty board.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyHandBefore = enemy.HandCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a summon-token spell");
// Oracle: the board-count delta + summoned token identity is the new M5 dimension; the rest
// are the §5 spell-shaped invariants proven by M3.
Assert.Multiple(() =>
{
// Primary M5 assertion: exactly one NEW card is on the player's board (the summoned
// token), and it is the token id with its CardCSVData base stats — proving the skill
// CREATED a card, not just moved the played one.
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore + 1),
"player board count not +1 (the summoned token did not land)");
var token = player.ClassAndInPlayCardList
.SingleOrDefault(c => c.CardId == HeadlessEngineEnv.SummonedTokenId);
Assert.That(token, Is.Not.Null, "summoned token (id 100011020) not found on the board");
Assert.That(token.Atk, Is.EqualTo(HeadlessEngineEnv.SummonedTokenAtk), "token atk != base");
Assert.That(token.Life, Is.EqualTo(HeadlessEngineEnv.SummonedTokenLife), "token life != base");
// The summoned token is NOT the played card.
Assert.That(token, Is.Not.SameAs(card), "summoned token is the played spell itself");
// Cost paid.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// The spell leaves hand and (being a spell) does NOT itself occupy the board.
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
// Opponent unchanged (the summon targets the caster's own board).
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
});
}
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M6 (the first TARGET-SELECTION card): a when_play TARGETED-DAMAGE spell resolves to correct
// authoritative state HEADLESS via the same IsForecast/IsRecovery + ActionProcessor path the
// M2-M5 cards proved — but for the FIRST time exercising the `selectedCards` path of
// ActionProcessor.PlayCard (Engine/Wizard.Battle/ActionProcessor.cs:401, dormant until now;
// M2-M5 all passed selectedCards: null). The new oracle dimension is SELECTION ROUTING: with
// TWO followers on the enemy board and ONE passed as `selectedCards`, the spell's `damage=5`
// must hit the SELECTED follower and leave the un-selected one untouched. A plain "a follower
// took damage" assertion would false-pass; reading the differential (selected -5, un-selected 0)
// is what proves the selectedCards path routes the effect to the chosen target. Load-bearing is
// confirmed by swapping which follower is selected and watching the damage follow the selection.
[TestFixture]
public class TargetedDamageSpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Targeted_damage_spell_hits_only_the_selected_enemy_follower()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M5 oracles): opponent refs + active turn flag. The
// spell's target resolver walks player -> opponent -> opponent's in-play followers.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Put TWO vanilla followers on the ENEMY board (the new M6 setup). Both survive the 5
// damage, so the oracle reads a differential life-delta rather than depending on death.
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.SelectTargetFollowerId, 0, isPlayer: false);
var unselected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.UnselectTargetFollowerId, 1, isPlayer: false);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.TargetSpellId);
// Place the targeted-damage spell in the active player's hand with PP to spare.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.TargetSpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int selectedLifeBefore = selected.Life;
int unselectedLifeBefore = unselected.Life;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine, passing the chosen target via selectedCards
// (the M6 first — every prior milestone passed null).
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
"ActionProcessor.PlayCard threw on a targeted-damage spell");
// Oracle: selection routing is the new M6 dimension; the rest are the §5 spell-shaped invariants.
Assert.Multiple(() =>
{
// PRIMARY M6 assertions: the SELECTED follower takes exactly the spell's damage...
Assert.That(selected.Life, Is.EqualTo(selectedLifeBefore - HeadlessEngineEnv.TargetSpellDamage),
"selected follower did not take the spell's damage");
// ...and the UN-SELECTED follower is untouched (proves routing, not a blanket hit).
Assert.That(unselected.Life, Is.EqualTo(unselectedLifeBefore),
"un-selected follower was damaged (effect not routed to the selection)");
// Both followers survive => still on the enemy board; leader unchanged.
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore),
"enemy board count changed (a target unexpectedly left the board)");
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
"opponent leader life changed (damage hit the leader, not the selected follower)");
// Cost paid.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// Spell leaves hand and (being a spell) does NOT occupy the board.
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
});
}
}
}

View File

@@ -0,0 +1,115 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M7 (the first card to prove follower DEATH / board-removal): a when_play TARGETED-DESTROY spell
// resolves to correct authoritative state HEADLESS via the same IsForecast/IsRecovery +
// ActionProcessor + selectedCards path M6 proved — but for the FIRST time exercising a mechanic
// that REMOVES a card from the board. M2-M6 only ever ADDED to / mutated stats of cards already in
// play; none proved the engine commits board REMOVAL inside the authoritative part of PlayCard
// (rather than the cosmetic post-Process tail the prior docs flag). The new oracle dimension is
// BOARD REMOVAL: with TWO followers on the enemy board and ONE passed as `selectedCards`, the
// `destroy` must remove exactly the SELECTED follower (enemy board count -1, selected gone, landed
// in the enemy CemeteryList) while leaving the un-selected follower on the board. The un-selected-
// survives assertion is load-bearing the same way M4's delta-vs-base and M6's differential were:
// it distinguishes "the destroy was routed to the selection" from "a blanket board wipe" — and is
// confirmed by the routing already proven in M6 (the effect follows the selectedCards entry).
[TestFixture]
public class TargetedDestroySpellOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var t = obj.GetType();
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Targeted_destroy_spell_removes_only_the_selected_enemy_follower()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Minimal opponent/turn wiring (see M2-M6 oracles): opponent refs + active turn flag. The
// destroy's target resolver walks player -> opponent -> opponent's in-play followers.
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
// Seed leader life so neither leader reads as a 0-life game-over state (blocks plays, M3).
HeadlessEngineEnv.InitLeaderLife(mgr);
// Put TWO vanilla followers on the ENEMY board (the M6 setup). destroy is unconditional, so
// their stats are irrelevant — distinct ids only so the selected vs un-selected can't be
// confused. The selected one is destroyed; the un-selected one must survive.
var selected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DestroyTargetFollowerId, 0, isPlayer: false);
var unselected = HeadlessEngineEnv.PutFollowerInPlay(mgr, HeadlessEngineEnv.DestroyOtherFollowerId, 1, isPlayer: false);
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.DestroySpellId);
// Place the targeted-destroy spell in the active player's hand with PP to spare.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.DestroySpellId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int playerInplayBefore = player.ClassAndInPlayCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyCemeteryBefore = enemy.CemeteryList.Count;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine, passing the chosen target via selectedCards.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: new List<BattleCardBase> { selected }),
"ActionProcessor.PlayCard threw on a targeted-destroy spell");
// Oracle: board removal is the new M7 dimension; the rest are the §5 spell-shaped invariants.
Assert.Multiple(() =>
{
// PRIMARY M7 assertions: the SELECTED follower is removed from the enemy board...
Assert.That(enemy.ClassAndInPlayCardList, Does.Not.Contain(selected),
"selected follower still on the enemy board (destroy did not remove it)");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore - 1),
"enemy board count not -1 (a destroy did not commit, or hit the wrong number of cards)");
// ...and it landed in the enemy's CemeteryList (the engine's destroy/death path).
Assert.That(enemy.CemeteryList, Contains.Item(selected),
"destroyed follower not in the enemy CemeteryList");
Assert.That(enemy.CemeteryList.Count, Is.EqualTo(enemyCemeteryBefore + 1),
"enemy cemetery count not +1");
// ...while the UN-SELECTED follower stays on the board (proves routing, not a board wipe).
Assert.That(enemy.ClassAndInPlayCardList, Contains.Item(unselected),
"un-selected follower was destroyed (effect not routed to the selection)");
// Leader untouched (destroy targets a follower, not the face).
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore),
"opponent leader life changed (destroy hit the leader, not the selected follower)");
// Cost paid.
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
// Spell leaves hand and (being a spell) does NOT occupy the board.
Assert.That(player.HandCardList, Does.Not.Contain(card), "spell still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Does.Not.Contain(card), "spell wrongly placed on the board");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(playerInplayBefore), "player board count changed");
});
}
}
}

View File

@@ -0,0 +1,50 @@
#nullable enable
using System;
using System.Runtime.Serialization;
using SVSim.BattleEngine.Ambient;
namespace SVSim.BattleEngine.Tests;
/// <summary>Per-test ambient scope. Each test that touches engine statics wraps its body
/// in `using var scope = new TestBattleScope();` (or with an explicit Mgr/ViewerId).
///
/// The constructor enters a fresh <see cref="BattleAmbientContext"/> (carrying a brand-new
/// <see cref="GameMgr"/> so per-test mgr/DataMgr writes never bleed across tests), then
/// runs the per-ambient seeders that <see cref="HeadlessEngineEnv.EnsureProcessGlobals"/>
/// no longer does (chara ids on DataMgr, NetworkUserInfoData). Process-globals
/// (card master, LoadDetail, Crossover, Certification.udid) come from
/// <see cref="HeadlessEngineEnv.EnsureProcessGlobals"/> which runs once per process.
///
/// Public surface (vs. internal) so SVSim.UnitTests can reuse it via the same project
/// reference in Task 7.</summary>
public sealed class TestBattleScope : IDisposable
{
private readonly BattleAmbient.Scope _scope;
public BattleAmbientContext Ctx { get; }
public TestBattleScope(BattleManagerBase? mgr = null, int viewerId = 1001)
{
// Make sure process-globals are seeded before we enter; idempotent + cheap after first call.
HeadlessEngineEnv.EnsureProcessGlobals();
Ctx = new BattleAmbientContext
{
Mgr = mgr,
GameMgr = new GameMgr(),
ViewerId = viewerId,
IsForecast = true,
IsRandomDraw = true,
RecoveryInfo = (Wizard.BattleRecoveryInfo)FormatterServices
.GetUninitializedObject(typeof(Wizard.BattleRecoveryInfo)),
};
_scope = BattleAmbient.Enter(Ctx);
// Per-ambient seeders MUST run AFTER scope entry so GameMgr.GetIns() resolves to this
// scope's GameMgr (not a stray one). EnsureProcessGlobals used to do these writes against
// the global GameMgr; now they're scoped.
HeadlessEngineEnv.SeedCharaIdsOnCurrentAmbient();
HeadlessEngineEnv.SeedNetUserOnCurrentAmbient();
}
public void Dispose() => _scope.Dispose();
}

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using Wizard;
using Wizard.Battle;
namespace SVSim.BattleEngine.Tests
{
// M2 first-green (go/no-go step 2): a single zero-skill vanilla follower resolves to correct
// authoritative state HEADLESS via the proven IsForecast/IsRecovery + ActionProcessor path
// (design §5 / DP4). No Unity runtime, no VFX clock.
[TestFixture]
public class VanillaFollowerOracleTests
{
private TestBattleScope _scope;
[SetUp] public void SetUpScope() { _scope = new TestBattleScope(); }
[TearDown] public void TearDownScope() { _scope?.Dispose(); _scope = null; }
private static void SetPrivateField(object obj, string name, object value)
{
var f = obj.GetType().GetField(name,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Walk up the hierarchy if declared on a base type.
var t = obj.GetType();
while (f == null && t.BaseType != null) { t = t.BaseType; f = t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); }
Assert.That(f, Is.Not.Null, $"field {name} not found on {obj.GetType().Name}");
f.SetValue(obj, value);
}
[Test]
public void Vanilla_follower_resolves_to_correct_state()
{
BattleManagerBase.IsForecast = true; // suppress VFX registration (F1)
var mgr = new SingleBattleMgr(new HeadlessContentsCreator());
_scope.Ctx.Mgr = mgr;
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
var player = mgr.BattlePlayer;
var enemy = mgr.BattleEnemy;
// Wire the opponent links + active turn. The full BattlePlayerBase.Setup(opponent) does
// this but cascades into UI/manager init irrelevant to the resolution path, so set the
// minimal state directly: each player's opponent ref, and the active player's turn flag
// (the on-enter-play skill sweep reads opponent.IsSelfTurn / IsGameFirst).
SetPrivateField(player, "_opponentBattlePlayer", enemy);
SetPrivateField(enemy, "_opponentBattlePlayer", player);
player.IsSelfTurn = true;
enemy.IsSelfTurn = false;
var cardParam = CardMaster.GetInstanceForBattle().GetCardParameterFromId(HeadlessEngineEnv.FollowerId);
// Place the follower in the active player's hand with PP to spare; empty board otherwise.
var card = HeadlessEngineEnv.CreateHeadlessHandCard(HeadlessEngineEnv.FollowerId, 1, isPlayer: true, mgr);
player.HandCardList.Add(card);
player.Pp = 10;
// Pre-state snapshot.
int ppBefore = player.Pp;
int handBefore = player.HandCardList.Count;
int inplayBefore = player.ClassAndInPlayCardList.Count;
int enemyHandBefore = enemy.HandCardList.Count;
int enemyInplayBefore = enemy.ClassAndInPlayCardList.Count;
int enemyLeaderLifeBefore = enemy.ClassAndInPlayCardList[0].Life;
// Resolve the play through the real engine.
var pair = mgr.GetBattlePlayerPair(isPlayer: true);
var ap = new ActionProcessor(pair);
Assert.DoesNotThrow(() => ap.PlayCard(card, selectedCards: null),
"ActionProcessor.PlayCard threw on a vanilla follower");
// Oracle (§5 invariants).
Assert.Multiple(() =>
{
Assert.That(player.Pp, Is.EqualTo(ppBefore - cardParam.Cost), "PP not reduced by exactly cost");
Assert.That(player.HandCardList, Does.Not.Contain(card), "card still in hand");
Assert.That(player.HandCardList.Count, Is.EqualTo(handBefore - 1), "hand count not -1");
Assert.That(player.ClassAndInPlayCardList, Contains.Item(card), "card not in play");
Assert.That(player.ClassAndInPlayCardList.Count, Is.EqualTo(inplayBefore + 1), "in-play count not +1");
Assert.That(card.Atk, Is.EqualTo(cardParam.Atk), "follower atk != CardCSVData base");
Assert.That(card.Life, Is.EqualTo(cardParam.Life), "follower life != CardCSVData base");
// Opponent unchanged.
Assert.That(enemy.HandCardList.Count, Is.EqualTo(enemyHandBefore), "opponent hand changed");
Assert.That(enemy.ClassAndInPlayCardList.Count, Is.EqualTo(enemyInplayBefore), "opponent board changed");
Assert.That(enemy.ClassAndInPlayCardList[0].Life, Is.EqualTo(enemyLeaderLifeBefore), "opponent leader life changed");
// §5 "zero VFX registered with VfxMgr": structural here — the shim VfxMgr is a pure
// no-op (RegisterImmediate/SequentialVfx do nothing) and IsForecast suppresses
// registration in the real engine, so no VFX is ever played headless. Covered by the
// DoesNotThrow above; there is no meaningful count to assert against the no-op shim.
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
# Verbatim engine copies: never normalize line endings (keeps sha256 manifest valid).
*.cs -text

View File

View File

@@ -0,0 +1,6 @@
public class AISendIntervalTrigger : SendIntervalTrigger
{
public override void SendDataCheck(NetworkBattleManagerBase networkBattleManager, NetworkBattleDefine.NetworkBattleURI sendUri)
{
}
}

View File

@@ -0,0 +1,58 @@
using System;
using Cute;
using Wizard;
public class AITurnControl
{
private DateTime _startTime;
private bool _isStartTimer;
public AITurnControl()
{
_isStartTimer = false;
}
public void StartTurnTimer()
{
_isStartTimer = true;
_startTime = TimeUtil.GetAbsoluteTime();
}
public void SetAndStartTurnTimer(DateTime time)
{
_isStartTimer = true;
_startTime = time;
}
public void Update(IEnemyAI ai)
{
if (ToolboxGame.RealTimeNetworkAgent != null && ToolboxGame.RealTimeNetworkAgent.PlayerNetworkStatus.IsAlive && !ai.IsConnectNetwork)
{
ai.Reconnect();
}
if (!_isStartTimer || !ToolboxGame.RealTimeNetworkAgent.PlayerNetworkStatus.IsAlive)
{
if (_isStartTimer && ai.IsConnectNetwork)
{
ai.Disconnect();
}
}
else if ((float)NetworkUtility.GetTimeSpanSecond(_startTime.Ticks) >= 90f)
{
if (ai.IsStackAction)
{
ai.CleanupStackedAction();
}
if (!ai.IsStackAction)
{
ai.TurnEnd();
}
}
}
public void StopTurnTimer()
{
_isStartTimer = false;
}
}

View File

@@ -0,0 +1,166 @@
using System.Collections.Generic;
using LitJson;
using Wizard;
using Wizard.Lottery;
public class AchievedInfo
{
private const string ACHIEVEMENT = "achieved_achievement_list";
private const string MISSION = "achieved_mission_list";
private const string REWARD = "achieved_mission_reward_list";
private const string VICTORY_REWARD = "win_reward_list";
private const string GRAND_MASTER_REWARD = "grand_master_reward_list";
private const string MISSION_START = "mission_start_data";
private const string BEGINNER_MISSION_REWARD = "achieved_beginner_mission_reward_list";
private const string BEGINNER_MISSION_REWARD_MESSAGE = "achieved_beginner_mission_list";
private const string BATTLE_PASS_REWARD_LIST = "battle_pass_reward_list";
private const string BATTLE_PASS_MESSAGE_LIST = "battle_pass_message_list";
private const long DONT_NOTIFY_IF_SMALLER_THAN_SECONDS = 10L;
public List<UserMission> _missions;
public List<UserAchievement> _achievements;
public List<ReceivedReward> _rewards;
public List<ReceivedReward> _victoryRewards;
public LotteryApplyData _lotteryData = LotteryApplyData.EmptyData();
public AchievedInfo()
{
_missions = new List<UserMission>();
_achievements = new List<UserAchievement>();
_rewards = new List<ReceivedReward>();
_victoryRewards = new List<ReceivedReward>();
}
public AchievedInfo(JsonData data)
: this()
{
Read(data);
}
public void Read(JsonData data)
{
if (data.Count == 0)
{
return;
}
if (data.Keys.Contains("achieved_mission_list"))
{
JsonData jsonData = data["achieved_mission_list"];
if (jsonData != null)
{
for (int i = 0; i < jsonData.Count; i++)
{
_missions.Add(UserMission.CreateAchievedMission(jsonData[i]));
}
}
}
if (data.Keys.Contains("achieved_achievement_list"))
{
JsonData jsonData2 = data["achieved_achievement_list"];
if (jsonData2 != null)
{
for (int j = 0; j < jsonData2.Count; j++)
{
UserAchievement userAchievement = UserAchievement.CreateCompletedAchievement(jsonData2[j]);
if (!string.IsNullOrEmpty(userAchievement.OsId))
{
AchievementImpl.instance.ReleaseAchievement(userAchievement.OsId);
}
_achievements.Add(userAchievement);
}
}
}
if (data.Keys.Contains("grand_master_reward_list"))
{
JsonData jsonData3 = data["grand_master_reward_list"];
if (jsonData3 != null)
{
for (int k = 0; k < jsonData3.Count; k++)
{
_rewards.Add(ReceivedReward.CreateFromBattleResultGrandMaster(jsonData3[k]));
}
}
}
if (data.Keys.Contains("achieved_mission_reward_list"))
{
JsonData jsonData4 = data["achieved_mission_reward_list"];
if (jsonData4 != null)
{
for (int l = 0; l < jsonData4.Count; l++)
{
_rewards.Add(ReceivedReward.CreateFromBattleResult(jsonData4[l]));
}
}
}
if (data.Keys.Contains("win_reward_list"))
{
JsonData jsonData5 = data["win_reward_list"];
if (jsonData5 != null)
{
for (int m = 0; m < jsonData5.Count; m++)
{
_victoryRewards.Add(ReceivedReward.CreateVictoryReward(jsonData5[m]));
}
}
}
if (data.Keys.Contains("achieved_beginner_mission_reward_list"))
{
JsonData jsonData6 = data["achieved_beginner_mission_reward_list"];
if (jsonData6 != null)
{
for (int n = 0; n < jsonData6.Count; n++)
{
_rewards.Add(ReceivedReward.CreateFromBeginnerMissionReward(jsonData6[n]));
}
}
}
if (data.Keys.Contains("achieved_beginner_mission_list"))
{
JsonData jsonData7 = data["achieved_beginner_mission_list"];
if (jsonData7 != null)
{
for (int num = 0; num < jsonData7.Count; num++)
{
_missions.Add(UserMission.CreateAchievedMission(jsonData7[num]));
}
}
}
if (data.Keys.Contains("battle_pass_reward_list"))
{
JsonData jsonData8 = data["battle_pass_reward_list"];
if (jsonData8 != null)
{
for (int num2 = 0; num2 < jsonData8.Count; num2++)
{
_rewards.Add(ReceivedReward.CreateFromBattlePassReward(jsonData8[num2]));
}
}
}
if (data.Keys.Contains("battle_pass_message_list"))
{
JsonData jsonData9 = data["battle_pass_message_list"];
if (jsonData9 != null)
{
for (int num3 = 0; num3 < jsonData9.Count; num3++)
{
_missions.Add(UserMission.CreateAchievedMission(jsonData9[num3]));
}
}
}
_lotteryData = LotteryApplyData.Parse(data);
}
}

View File

@@ -0,0 +1,4 @@
public class AchievementInfo : HeaderData
{
public AchievementInfoDetail data;
}

View File

@@ -0,0 +1,6 @@
using System.Collections.Generic;
public class AchievementInfoDetail
{
public List<UserAchievement> user_achievement_list;
}

View File

@@ -0,0 +1,777 @@
using System;
using System.Collections.Generic;
using Cute;
using UnityEngine;
using Wizard;
using Wizard.Bingo;
using Wizard.Lottery;
using Wizard.Scripts.Network.Data.TableData;
public class AchievementWindowBase : MonoBehaviour
{
public enum AchievementType
{
None,
Reward,
Nonattainment,
AlreadyReceived,
PointRunning,
PointClear,
PointReceived
}
public GameObject goButtonReward;
public UITexture achievementIconTexture;
public UILabel labelAchievementTitle;
public UILabel labelAchievementData;
public UILabel labelAchievementCount;
public UILabel _missionWaitLabel;
public UILabel _labelMissionPeriod;
public UILabel _labelMissionNotice;
public UISprite _titleLine;
public UILabel alreadyReceived;
[NonSerialized]
public int type;
[NonSerialized]
public int iType = -1;
[NonSerialized]
public int level;
[NonSerialized]
public string strAchievementData = "3種類のスリーブを使う。";
[SerializeField]
private UILabel LabelDetailBtn;
[SerializeField]
private UITable StarTable;
[SerializeField]
private UISprite StarOriginal;
[SerializeField]
private GameObject MailReceive;
[SerializeField]
private UIGauge GaugeUI;
[SerializeField]
private UILabel GaugeLabel;
[SerializeField]
private UISprite _Separator;
[SerializeField]
private UIWidget _StarsWidget;
[SerializeField]
private UILabel _labelTopRight;
[SerializeField]
private UILabel _missionStartTime;
[SerializeField]
private UILabel _missionTimeOver;
[SerializeField]
private UILabel _applyFinish;
private const int ACHIEVEMENT_STARS_MAX = 5;
private ResourceHandler _resourceHandler;
private const string SPRITE_PREFIX_BUTTON_BLUE = "btn_common_02_s_";
private int _viewMailId;
private QuestRewardInfo _questRewardInfo;
private Action _onReceivceAchievementSuccess;
private CrossoverRewardInfo _crossoverRewardInfo;
private const int BINGO_MISSION_SPRITE_WIDTH = 752;
private void Awake()
{
LabelDetailBtn.text = Data.SystemText.Get("Common_0022");
}
public void SetType(AchievementType typeBase)
{
labelAchievementCount.gameObject.SetActive(typeBase != AchievementType.AlreadyReceived);
alreadyReceived.gameObject.SetActive(typeBase == AchievementType.AlreadyReceived || typeBase == AchievementType.PointReceived);
goButtonReward.SetActive(typeBase != AchievementType.AlreadyReceived && typeBase != AchievementType.PointReceived);
UIManager.SetObjectToGrey(goButtonReward, typeBase == AchievementType.Nonattainment || typeBase == AchievementType.PointRunning);
}
public void SetActiveGaugeUI(bool isActive)
{
GaugeUI.gameObject.SetActive(isActive);
}
public void OnRewardClick()
{
AchievementReceiveRewardTask achievementReceiveRewardTask = new AchievementReceiveRewardTask();
achievementReceiveRewardTask.SetParameter(type, level);
StartCoroutine(Toolbox.NetworkManager.Connect(achievementReceiveRewardTask, OnRequestRewardAchievement, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
}
public void OnDetail()
{
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetTitleLabel(Data.SystemText.Get("Mission_0007"));
dialogBase.SetText(strAchievementData);
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.CloseBtn);
}
private void OnRequestRewardAchievement(NetworkTask.ResultCode error)
{
OnRequestReward(error, Data.MissionInfo.data.total_reward_list);
_onReceivceAchievementSuccess.Call();
}
private void OnRequestReward(NetworkTask.ResultCode error, List<ReceivedReward> rewards)
{
base.transform.parent.gameObject.AddMissingComponent<ReceiveReward>().ShowReadDialog(rewards, MailReceive, base.gameObject, _resourceHandler);
MyPageMenu.Instance.UpdateMissionCount();
}
private void SetAchievementCommon(UserAchievement achi)
{
bool num = achi.reward_type == 4;
strAchievementData = achi.achievement_name;
SystemText systemText = Data.SystemText;
labelAchievementTitle.text = systemText.Get("Mission_0023") + strAchievementData;
labelAchievementTitle.rightAnchor.target = _StarsWidget.transform;
labelAchievementTitle.rightAnchor.relative = 0f;
if (num)
{
ReceiveReward.SetTicket(achi.RewardUserGoodsId, achi.reward_number, achievementIconTexture, labelAchievementData, _resourceHandler);
}
else
{
ReceiveReward.SetTexture((UserGoods.Type)achi.reward_type, achievementIconTexture, _resourceHandler);
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)achi.reward_type, achi.RewardUserGoodsId, achi.reward_number);
}
GaugeUI.gameObject.SetActive(value: true);
int num2 = ((achi.total_count > achi.require_number) ? achi.require_number : achi.total_count);
int require_number = achi.require_number;
labelAchievementCount.gameObject.SetActive(value: false);
GaugeLabel.text = num2 + "/" + require_number;
if (num2 != 0 && require_number != 0)
{
float value = (float)num2 / (float)require_number;
GaugeUI.Value = value;
}
else
{
GaugeUI.Value = 0f;
}
goButtonReward.GetComponent<UIButton>().GetComponentInChildren<UILabel>().text = systemText.Get("Mail_0023");
}
private void SetRunning(UserAchievement achi)
{
SetType(AchievementType.Nonattainment);
SetAchievementCommon(achi);
SetAchievementStars(achi, cleared: false);
}
private void SetAchievementStars(UserAchievement achi, bool cleared)
{
int num = achi._maxLevel;
int num2 = achi.level;
if (achi._maxLevel > 5)
{
num = 5;
num2 = ((achi.level == achi._maxLevel) ? 5 : ((achi.level <= 0 || achi.level % 5 != 0) ? (achi.level % 5) : 5));
}
for (int i = 0; i < num; i++)
{
UISprite uISprite = UnityEngine.Object.Instantiate(StarOriginal, StarOriginal.transform.localPosition, StarOriginal.transform.localRotation);
uISprite.transform.parent = StarTable.transform;
uISprite.transform.localPosition = Vector3.zero;
uISprite.transform.localScale = Vector3.one;
if ((num2 == i + 1 && cleared) || num2 > i + 1)
{
uISprite.spriteName = "achievement_star_02";
}
else
{
uISprite.spriteName = "achievement_star_01";
}
uISprite.gameObject.SetActive(value: true);
}
StarTable.repositionNow = true;
}
private void SetCanReceive(UserAchievement achi)
{
SetType(AchievementType.Reward);
SetAchievementCommon(achi);
SetAchievementStars(achi, cleared: false);
UIButton component = goButtonReward.GetComponent<UIButton>();
component.onClick.Clear();
component.onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
OnRewardClick();
}));
}
private void SetAlreadyReceived(UserAchievement achi)
{
SetType(AchievementType.AlreadyReceived);
SetAchievementCommon(achi);
SetAchievementStars(achi, cleared: true);
}
public void SetAchievement(UserAchievement achi, ResourceHandler resourceHandler, Action onReceivceAchievementSuccess)
{
_resourceHandler = resourceHandler;
_onReceivceAchievementSuccess = onReceivceAchievementSuccess;
switch (achi.achievement_status)
{
case 0:
SetRunning(achi);
break;
case 1:
SetCanReceive(achi);
break;
case 2:
SetAlreadyReceived(achi);
break;
default:
UnityEngine.Debug.LogError("unkown achievement status");
break;
}
}
public void SetCrossoverReward(CrossoverRewardInfo reward, AchievementType type, ResourceHandler resourceHandler, Action onReceiveReward)
{
_crossoverRewardInfo = reward;
_resourceHandler = resourceHandler;
SetType(type);
string texName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.RewardType, reward.RewardDetailId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
if (reward.RewardDetailId == _crossoverRewardInfo.RewardDetailId)
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
}
});
RankInfo rankInfo = Data.Load.data.GetRankInfo(Format.Crossover, reward.Rank);
labelAchievementTitle.text = Data.SystemText.Get("Profile_0042", Data.SystemText.Get(rankInfo.rank_name));
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.RewardType, reward.RewardDetailId, reward.RewardCount);
GaugeUI.gameObject.SetActive(value: false);
UIButton component = goButtonReward.GetComponent<UIButton>();
component.GetComponentInChildren<UILabel>().text = Data.SystemText.Get("Mail_0023");
goButtonReward.gameObject.SetActive(type != AchievementType.PointReceived);
UIManager.SetObjectToGrey(goButtonReward, type != AchievementType.PointClear);
component.onClick.Clear();
component.onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
ReceiveCrossoverReward(reward.RewardId, onReceiveReward);
}));
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
}
private void ReceiveCrossoverReward(int rewardId, Action onReceiveReward)
{
CrossoverReceiveRankRewardTask task = new CrossoverReceiveRankRewardTask();
task.SetParameter(rewardId);
StartCoroutine(Toolbox.NetworkManager.Connect(task, delegate
{
onReceiveReward.Call();
DialogCreator.CreateRewardReceiveDialog(task.ReceivedRewardList);
}));
}
public void SetQuestPoint(QuestRewardInfo reward, AchievementType type, ResourceHandler resourceHandler, Action onRequestRewardPointCallBack)
{
_questRewardInfo = reward;
strAchievementData = reward.Point.ToString();
labelAchievementTitle.text = Data.SystemText.Get("Quest_0019", strAchievementData);
_resourceHandler = resourceHandler;
SetType(type);
string texName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.RewardType, reward.RewardDetailId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
if (reward.RewardDetailId == _questRewardInfo.RewardDetailId)
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(texName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
}
});
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.RewardType, reward.RewardDetailId, reward.RewardCount);
GaugeUI.gameObject.SetActive(value: false);
UIButton component = goButtonReward.GetComponent<UIButton>();
component.GetComponentInChildren<UILabel>().text = Data.SystemText.Get("Mail_0023");
goButtonReward.gameObject.SetActive(type != AchievementType.PointReceived);
UIManager.SetObjectToGrey(goButtonReward, type != AchievementType.PointClear);
component.onClick.Clear();
component.onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
OnQuestPointReceive(reward.Id, onRequestRewardPointCallBack);
}));
GetComponent<UISprite>().spriteName = string.Empty;
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
}
public void SetMission(UserMission mission, ResourceHandler resourceHandler, bool canChangeMissions, bool enableSeparator, bool displayChange, Action onChangeMissionSuccess = null)
{
_resourceHandler = resourceHandler;
_Separator.gameObject.SetActive(enableSeparator);
if (mission.mission_status == 0 && SetMissionWait(mission))
{
return;
}
SystemText systemText = Data.SystemText;
if (mission.reward_type == 4)
{
ReceiveReward.SetTicket(mission.RewardUserGoodsId, mission.reward_number, achievementIconTexture, labelAchievementData, _resourceHandler);
}
else
{
ReceiveReward.SetTexture((UserGoods.Type)mission.reward_type, mission.RewardUserGoodsId, achievementIconTexture, _resourceHandler);
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)mission.reward_type, mission.RewardUserGoodsId, mission.reward_number);
}
labelAchievementTitle.text = mission.mission_name;
int require_number = mission.require_number;
bool flag = require_number > 0;
GaugeUI.gameObject.SetActive(flag);
if (flag)
{
int num = ((mission.total_count > mission.require_number) ? mission.require_number : mission.total_count);
GaugeLabel.text = num + "/" + require_number;
if (num != 0)
{
float value = (float)num / (float)require_number;
GaugeUI.Value = value;
}
else
{
GaugeUI.Value = 0f;
}
}
UIButton component = goButtonReward.GetComponent<UIButton>();
component.normalSprite = "btn_common_02_s_off";
component.pressedSprite = "btn_common_02_s_on";
component.GetComponentInChildren<UILabel>().text = systemText.Get("Mission_0029");
component.onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
ChangeMission(mission.id, mission.mission_name, onChangeMissionSuccess);
}));
goButtonReward.SetActive(displayChange && !mission.default_flag);
UIManager.SetObjectToGrey(goButtonReward, !canChangeMissions);
labelAchievementCount.gameObject.SetActive(value: false);
SetMissionPeriodLabel(mission);
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
}
public void SetBttlePassMonthlyMission(BattlePassMonthlyMission.MissionDetail mission, ResourceHandler resourceHandler)
{
_Separator.gameObject.SetActive(value: false);
_labelMissionPeriod.gameObject.SetActive(value: false);
labelAchievementCount.gameObject.SetActive(value: false);
goButtonReward.gameObject.SetActive(value: false);
_resourceHandler = resourceHandler;
labelAchievementTitle.text = mission.Name;
alreadyReceived.gameObject.SetActive(mission.IsCleared);
BattlePassMonthlyMission.MissionDetail.RewardInfo reward = mission.Reward;
if (reward == null)
{
_resourceHandler.Add(Toolbox.ResourcesManager.GetAssetTypePath("thumbnail_battle_pass_point", ResourcesManager.AssetLoadPathType.BattlePass), delegate
{
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath("thumbnail_battle_pass_point", ResourcesManager.AssetLoadPathType.BattlePass, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath);
});
labelAchievementData.text = string.Empty;
}
else if (reward.UserGoods.GoodsType == UserGoods.Type.Item)
{
ReceiveReward.SetTicket(reward.UserGoods.Id, reward.Number, achievementIconTexture, labelAchievementData, _resourceHandler);
}
else
{
ReceiveReward.SetTexture(reward.UserGoods.GoodsType, achievementIconTexture, _resourceHandler);
labelAchievementData.text = ReceiveReward.getTitle(reward.UserGoods.GoodsType, reward.UserGoods.Id, reward.Number);
}
SetViewBattlePassPointText(mission.BattlePassPoint);
int requireNumber = mission.RequireNumber;
bool flag = requireNumber > 0;
GaugeUI.gameObject.SetActive(flag);
if (flag)
{
int num = ((mission.DoneNumber > mission.RequireNumber) ? mission.RequireNumber : mission.DoneNumber);
GaugeLabel.text = num + "/" + requireNumber;
if (num != 0)
{
float value = (float)num / (float)requireNumber;
GaugeUI.Value = value;
}
else
{
GaugeUI.Value = 0f;
}
}
}
private void SetViewBattlePassPointText(int point)
{
string text = " ";
if (labelAchievementData.text == string.Empty)
{
labelAchievementData.text = Data.SystemText.Get("BattlePass_0010", point.ToString());
}
else
{
UILabel uILabel = labelAchievementData;
uILabel.text = uILabel.text + text + Data.SystemText.Get("BattlePass_0010", point.ToString());
}
}
private bool SetMissionWait(UserMission mission)
{
MissionInfoTask missionInfoTask = GameMgr.GetIns().GetMissionInfoTask();
long num = (long)Time.realtimeSinceStartup - missionInfoTask.RequestTime;
long num2 = missionInfoTask.ServerTime + num;
TimeSpan timeSpan = TimeSpan.FromSeconds(mission.start_time - num2).Add(new TimeSpan(0, 1, 0));
int num3 = timeSpan.Hours;
int num4 = timeSpan.Minutes;
if (timeSpan.TotalHours >= 24.0)
{
num3 = 24;
num4 = 0;
}
else if (num3 <= 0 && num4 <= 0)
{
return false;
}
if (mission.IsGemMission())
{
_missionWaitLabel.text = Data.SystemText.Get("Mission_0073", num3.ToString("00"), num4.ToString("00"));
_labelMissionNotice.gameObject.SetActive(value: true);
_labelMissionNotice.text = Data.SystemText.Get("Mission_0074");
}
else
{
_missionWaitLabel.text = Data.SystemText.Get("Mission_0041", num3.ToString("00"), num4.ToString("00"));
}
labelAchievementTitle.gameObject.SetActive(value: false);
labelAchievementCount.gameObject.SetActive(value: false);
labelAchievementData.gameObject.SetActive(value: false);
labelAchievementData.gameObject.SetActive(value: false);
goButtonReward.gameObject.SetActive(value: false);
GaugeUI.gameObject.SetActive(value: false);
_titleLine.gameObject.SetActive(value: false);
_missionWaitLabel.gameObject.SetActive(value: true);
return true;
}
private void SetMissionPeriodLabel(UserMission mission)
{
if (mission.end_time <= 0 || mission.IsGemMission())
{
_labelMissionPeriod.gameObject.SetActive(value: false);
return;
}
long nowUnixTime = GameMgr.GetIns().GetMissionInfoTask().NowUnixTime();
string remainingTime = ConvertTime.GetRemainingTime(TimeSpan.FromSeconds(mission.GetMissionPeriodSec(nowUnixTime)));
goButtonReward.gameObject.SetActive(value: false);
_labelMissionPeriod.gameObject.SetActive(value: true);
_labelMissionPeriod.text = remainingTime;
}
private void ChangeMission(int id, string content, Action onChangeMissionSuccess)
{
SystemText systemText = Data.SystemText;
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetTitleLabel(systemText.Get("Mission_0033"));
dialogBase.SetText(systemText.Get("Mission_0030", content));
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.DecisionBtn);
dialogBase.onPushButton1 = delegate
{
MissionRetireTask missionRetireTask = new MissionRetireTask();
missionRetireTask.SetParameter(id);
StartCoroutine(Toolbox.NetworkManager.Connect(missionRetireTask, delegate
{
onChangeMissionSuccess.Call();
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
};
}
private void OnQuestPointReceive(int rewardId, Action onRequestRewardPointCallBack)
{
QuestRewardReceiveTask task = new QuestRewardReceiveTask();
task.SetParameter(rewardId);
StartCoroutine(Toolbox.NetworkManager.Connect(task, delegate
{
onRequestRewardPointCallBack.Call();
DialogCreator.CreateRewardReceiveDialog(task.ReceiveRewardList);
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
}
public void SetHistoryItem(ItemAcquireHistory item, bool enableSeparator, ResourceHandler resourceHandler)
{
_resourceHandler = resourceHandler;
SystemText systemText = Data.SystemText;
if (item.RewardType == 4)
{
ReceiveReward.SetTicket(item.RewardUserGoodsId, item.RewardCount, achievementIconTexture, labelAchievementTitle, _resourceHandler);
}
else
{
ReceiveReward.SetTexture((UserGoods.Type)item.RewardType, achievementIconTexture, _resourceHandler);
labelAchievementTitle.text = ReceiveReward.getTitle((UserGoods.Type)item.RewardType, item.RewardUserGoodsId, item.RewardCount);
}
labelAchievementData.text = item.Message;
labelAchievementCount.text = systemText.Get("Mail_0043", ConvertTime.ToLocal(item.AcquireTime));
GaugeUI.gameObject.SetActive(value: false);
goButtonReward.SetActive(value: false);
_Separator.gameObject.SetActive(enableSeparator);
}
public void SetMail(MailData mail, Action<int, int> OnReadMail, ResourceHandler handler)
{
_resourceHandler = handler;
_viewMailId = mail.mail_id;
SetCommonMail(mail);
TimeLeftUpdate timeLeftUpdate = base.gameObject.AddMissingComponent<TimeLeftUpdate>();
timeLeftUpdate.mailData = mail;
_labelTopRight.gameObject.SetActive(value: true);
timeLeftUpdate.timeLeft = _labelTopRight;
timeLeftUpdate.UpdateTime();
SystemText systemText = Data.SystemText;
labelAchievementCount.text = systemText.Get("Mail_0043", mail.create_time);
goButtonReward.SetActive(value: true);
goButtonReward.transform.Find("RewardLabel").GetComponent<UILabel>().text = systemText.Get("Mail_0023");
UIButton component = goButtonReward.GetComponent<UIButton>();
component.normalSprite = "btn_common_02_s_off";
component.hoverSprite = "btn_common_02_s_off";
component.pressedSprite = "btn_common_02_s_on";
UIEventListener.Get(goButtonReward).onClick = delegate
{
OnReadMail(mail.mail_id, mail.mail_id);
};
}
public void SetHistoryMail(MailData mail, ResourceHandler handler)
{
_resourceHandler = handler;
_viewMailId = mail.mail_id;
SetCommonMail(mail);
TimeLeftUpdate component = base.gameObject.GetComponent<TimeLeftUpdate>();
if ((bool)component)
{
component.mailData = null;
}
_labelTopRight.gameObject.SetActive(value: false);
labelAchievementCount.text = Data.SystemText.Get("Mail_0044", mail.create_time);
goButtonReward.SetActive(value: false);
}
private void SetCommonMail(MailData mailData)
{
GaugeUI.gameObject.SetActive(value: false);
labelAchievementData.text = mailData.message;
labelAchievementTitle.text = ReceiveReward.getTitle(mailData);
string textureName = ReceiveReward.GetThumbnailName((UserGoods.Type)mailData.reward_type, mailData.RewardUserGoodsId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
if (mailData.mail_id == _viewMailId)
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
}
});
}
private void CopyAnchor(UIRect.AnchorPoint original, UIRect.AnchorPoint destination)
{
destination.target = original.target;
destination.relative = original.relative;
destination.absolute = original.absolute;
}
public void SetGetButtonToGreyOut()
{
UIManager.SetObjectToGrey(goButtonReward, b: true);
}
public void SetLottery(LotteryMissionData lotteryData, bool needCeparator)
{
string userGoodsImageName = UserGoods.GetUserGoodsImageName(lotteryData.UserGoodsType, lotteryData.ItemId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(userGoodsImageName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
base.gameObject.GetComponent<UISprite>().width = 800;
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath);
_Separator.gameObject.SetActive(needCeparator);
labelAchievementTitle.text = lotteryData.MissionTitle;
if (lotteryData.UserGoodsType == UserGoods.Type.Item)
{
labelAchievementData.text = ReceiveReward.SetTicketTitle(lotteryData.ItemId, lotteryData.ItemCount);
}
else
{
labelAchievementData.text = ReceiveReward.getTitle(lotteryData.UserGoodsType, lotteryData.ItemId, lotteryData.ItemCount);
}
goButtonReward.SetActive(value: false);
if (lotteryData.StartTime.Second > 0)
{
_missionStartTime.text = Data.SystemText.Get("Mission_0077", lotteryData.StartTime.LocalTime);
_missionStartTime.gameObject.SetActive(value: true);
}
else
{
_labelMissionPeriod.text = lotteryData.EndTime.GetShowText("Mission_0062", "Mission_0060", "Mission_0061");
_labelMissionPeriod.gameObject.SetActive(value: true);
}
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
_applyFinish.gameObject.SetActive(lotteryData.IsCleared);
_labelMissionPeriod.gameObject.SetActive(!lotteryData.IsCleared && !lotteryData.IsTimeOver);
_missionTimeOver.gameObject.SetActive(lotteryData.IsTimeOver);
GaugeLabel.text = lotteryData.MissionCurrent + "/" + lotteryData.MissionMax;
GaugeUI.Value = lotteryData.MissionRatio;
bool active = true;
if (lotteryData.IsCleared || lotteryData.MissionMax == 0)
{
active = false;
}
GaugeUI.gameObject.SetActive(active);
}
public void SetBingoMission(BingoInfoTask.BingoMissionData missionData, bool needCeparator, ResourceHandler handler)
{
_resourceHandler = handler;
base.gameObject.GetComponent<UISprite>().width = 752;
base.gameObject.GetComponent<UISprite>().enabled = false;
alreadyReceived.gameObject.SetActive(missionData.IsCleared);
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)missionData.Reward.reward_type, missionData.Reward.rewardUserGoodsId, missionData.Reward.reward_count);
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)missionData.Reward.reward_type, missionData.Reward.rewardUserGoodsId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
});
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
_titleLine.SetAnchor((GameObject)null);
_titleLine.spriteName = "quest_line_05";
_titleLine.SetDimensions(610, 2);
_Separator.spriteName = "quest_line_02";
_Separator.gameObject.SetActive(needCeparator);
goButtonReward.SetActive(value: false);
GaugeLabel.text = missionData.MissionCurrent + "/" + missionData.MissionMax;
GaugeUI.Value = missionData.MissionRatio;
labelAchievementTitle.text = missionData.MissionTitle;
}
public void SetBingoRewardDetails(ReceivedReward reward, bool needCeparator, ResourceHandler handler)
{
_resourceHandler = handler;
base.gameObject.GetComponent<UISprite>().width = 752;
base.gameObject.GetComponent<UISprite>().enabled = false;
goButtonReward.SetActive(value: false);
_titleLine.SetAnchor((GameObject)null);
_titleLine.spriteName = "quest_line_05";
_titleLine.SetDimensions(610, 2);
_Separator.spriteName = "quest_line_02";
_Separator.gameObject.SetActive(needCeparator);
labelAchievementTitle.text = string.Format(Data.SystemText.Get("Bingo_0004", reward.lineNum.ToString()));
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId, reward.reward_count);
GaugeUI.gameObject.SetActive(value: false);
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
});
}
public void SetBingoSideBarRewards(string lineNum, ReceivedReward reward, bool isCleared, bool needCeparator, ResourceHandler handler)
{
_resourceHandler = handler;
_Separator.gameObject.SetActive(needCeparator);
goButtonReward.SetActive(value: false);
labelAchievementTitle.text = string.Format(Data.SystemText.Get("Bingo_0004", lineNum));
labelAchievementData.text = ReceiveReward.getTitle((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId, reward.reward_count);
alreadyReceived.gameObject.SetActive(isCleared);
string textureName = UserGoods.GetUserGoodsImageName((UserGoods.Type)reward.reward_type, reward.rewardUserGoodsId);
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item);
_resourceHandler.Add(assetTypePath, delegate
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(textureName, ResourcesManager.AssetLoadPathType.Item, isfetch: true);
achievementIconTexture.mainTexture = Toolbox.ResourcesManager.LoadObject<Texture>(assetTypePath2);
});
}
public void SetPracticePuzzleMission(PracticePuzzleMissionData mission, ResourceHandler resourceHandler, bool canChangeMissions, bool enableSeparator, bool displayChange, Action onChangeMissionSuccess = null)
{
_resourceHandler = resourceHandler;
_Separator.gameObject.SetActive(enableSeparator);
goButtonReward.SetActive(value: false);
_ = Data.SystemText;
if (mission.UserGoodsType == UserGoods.Type.Item)
{
ReceiveReward.SetTicket(mission.ItemId, mission.ItemCount, achievementIconTexture, labelAchievementData, _resourceHandler);
}
else
{
ReceiveReward.SetTexture(mission.UserGoodsType, mission.ItemId, achievementIconTexture, _resourceHandler);
labelAchievementData.text = ReceiveReward.getTitle(mission.UserGoodsType, mission.ItemId, mission.ItemCount);
}
labelAchievementTitle.text = mission.Name;
int totalMissionCount = mission.TotalMissionCount;
bool flag = totalMissionCount > 0;
GaugeUI.gameObject.SetActive(flag);
if (flag)
{
int num = ((mission.TotalMissionCount > mission.CurrentClearCount) ? mission.CurrentClearCount : mission.TotalMissionCount);
GaugeLabel.text = num + "/" + totalMissionCount;
if (num != 0)
{
float value = (float)num / (float)totalMissionCount;
GaugeUI.Value = value;
}
else
{
GaugeUI.Value = 0f;
}
}
alreadyReceived.gameObject.SetActive(mission.IsCleared);
labelAchievementCount.gameObject.SetActive(value: false);
CopyAnchor(_labelTopRight.rightAnchor, labelAchievementTitle.rightAnchor);
}
public void SetRedEtherMission(RedEtherCampaignRewardData rewardData, ResourceHandler resourceHandler)
{
_resourceHandler = resourceHandler;
goButtonReward.SetActive(value: false);
ReceiveReward.SetTexture(rewardData.UserGoodsType, 0L, achievementIconTexture, _resourceHandler);
labelAchievementData.text = ReceiveReward.getTitle(rewardData.UserGoodsType, 0L, rewardData.ItemCount);
labelAchievementTitle.text = rewardData.MissionText;
alreadyReceived.gameObject.SetActive(rewardData.IsCleared);
GaugeUI.gameObject.SetActive(value: false);
}
}

View File

@@ -0,0 +1,356 @@
using System.Collections.Generic;
using AnimationOrTween;
using UnityEngine;
[AddComponentMenu("NGUI/Internal/Active Animation")]
public class ActiveAnimation : MonoBehaviour
{
public static ActiveAnimation current;
public List<EventDelegate> onFinished = new List<EventDelegate>();
[HideInInspector]
public GameObject eventReceiver;
[HideInInspector]
public string callWhenFinished;
private Animation mAnim;
private Direction mLastDirection;
private Direction mDisableDirection;
private bool mNotify;
private Animator mAnimator;
private string mClip = "";
private float playbackTime => Mathf.Clamp01(mAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime);
public bool isPlaying
{
get
{
if (mAnim == null)
{
if (mAnimator != null)
{
if (mLastDirection == Direction.Reverse)
{
if (playbackTime == 0f)
{
return false;
}
}
else if (playbackTime == 1f)
{
return false;
}
return true;
}
return false;
}
foreach (AnimationState item in mAnim)
{
if (!mAnim.IsPlaying(item.name))
{
continue;
}
if (mLastDirection == Direction.Forward)
{
if (item.time < item.length)
{
return true;
}
continue;
}
if (mLastDirection == Direction.Reverse)
{
if (item.time > 0f)
{
return true;
}
continue;
}
return true;
}
return false;
}
}
public void Finish()
{
if (mAnim != null)
{
foreach (AnimationState item in mAnim)
{
if (mLastDirection == Direction.Forward)
{
item.time = item.length;
}
else if (mLastDirection == Direction.Reverse)
{
item.time = 0f;
}
}
mAnim.Sample();
}
else if (mAnimator != null)
{
mAnimator.Play(mClip, 0, (mLastDirection == Direction.Forward) ? 1f : 0f);
}
}
public void Reset()
{
if (mAnim != null)
{
foreach (AnimationState item in mAnim)
{
if (mLastDirection == Direction.Reverse)
{
item.time = item.length;
}
else if (mLastDirection == Direction.Forward)
{
item.time = 0f;
}
}
return;
}
if (mAnimator != null)
{
mAnimator.Play(mClip, 0, (mLastDirection == Direction.Reverse) ? 1f : 0f);
}
}
private void Start()
{
if (eventReceiver != null && EventDelegate.IsValid(onFinished))
{
eventReceiver = null;
callWhenFinished = null;
}
}
private void Update()
{
float deltaTime = RealTime.deltaTime;
if (deltaTime == 0f)
{
return;
}
if (mAnimator != null)
{
mAnimator.Update((mLastDirection == Direction.Reverse) ? (0f - deltaTime) : deltaTime);
if (isPlaying)
{
return;
}
mAnimator.enabled = false;
base.enabled = false;
}
else
{
if (!(mAnim != null))
{
base.enabled = false;
return;
}
bool flag = false;
foreach (AnimationState item in mAnim)
{
if (!mAnim.IsPlaying(item.name))
{
continue;
}
float num = item.speed * deltaTime;
item.time += num;
if (num < 0f)
{
if (item.time > 0f)
{
flag = true;
}
else
{
item.time = 0f;
}
}
else if (item.time < item.length)
{
flag = true;
}
else
{
item.time = item.length;
}
}
mAnim.Sample();
if (flag)
{
return;
}
base.enabled = false;
}
if (!mNotify)
{
return;
}
mNotify = false;
if (current == null)
{
current = this;
EventDelegate.Execute(onFinished);
if (eventReceiver != null && !string.IsNullOrEmpty(callWhenFinished))
{
eventReceiver.SendMessage(callWhenFinished, SendMessageOptions.DontRequireReceiver);
}
current = null;
}
if (mDisableDirection != Direction.Toggle && mLastDirection == mDisableDirection)
{
NGUITools.SetActive(base.gameObject, state: false);
}
}
private void Play(string clipName, Direction playDirection)
{
if (playDirection == Direction.Toggle)
{
playDirection = ((mLastDirection != Direction.Forward) ? Direction.Forward : Direction.Reverse);
}
if (mAnim != null)
{
base.enabled = true;
mAnim.enabled = false;
if (string.IsNullOrEmpty(clipName))
{
if (!mAnim.isPlaying)
{
mAnim.Play();
}
}
else if (!mAnim.IsPlaying(clipName))
{
mAnim.Play(clipName);
}
foreach (AnimationState item in mAnim)
{
if (string.IsNullOrEmpty(clipName) || item.name == clipName)
{
float num = Mathf.Abs(item.speed);
item.speed = num * (float)playDirection;
if (playDirection == Direction.Reverse && item.time == 0f)
{
item.time = item.length;
}
else if (playDirection == Direction.Forward && item.time == item.length)
{
item.time = 0f;
}
}
}
mLastDirection = playDirection;
mNotify = true;
mAnim.Sample();
}
else if (mAnimator != null)
{
if (base.enabled && isPlaying && mClip == clipName)
{
mLastDirection = playDirection;
return;
}
base.enabled = true;
mNotify = true;
mLastDirection = playDirection;
mClip = clipName;
mAnimator.Play(mClip, 0, (playDirection == Direction.Forward) ? 0f : 1f);
}
}
public static ActiveAnimation Play(Animation anim, string clipName, Direction playDirection, EnableCondition enableBeforePlay, DisableCondition disableCondition)
{
if (!NGUITools.GetActive(anim.gameObject))
{
if (enableBeforePlay != EnableCondition.EnableThenPlay)
{
return null;
}
NGUITools.SetActive(anim.gameObject, state: true);
UIPanel[] componentsInChildren = anim.gameObject.GetComponentsInChildren<UIPanel>();
int i = 0;
for (int num = componentsInChildren.Length; i < num; i++)
{
componentsInChildren[i].Refresh();
}
}
ActiveAnimation activeAnimation = anim.GetComponent<ActiveAnimation>();
if (activeAnimation == null)
{
activeAnimation = anim.gameObject.AddComponent<ActiveAnimation>();
}
activeAnimation.mAnim = anim;
activeAnimation.mDisableDirection = (Direction)disableCondition;
activeAnimation.onFinished.Clear();
activeAnimation.Play(clipName, playDirection);
if (activeAnimation.mAnim != null)
{
activeAnimation.mAnim.Sample();
}
else if (activeAnimation.mAnimator != null)
{
activeAnimation.mAnimator.Update(0f);
}
return activeAnimation;
}
public static ActiveAnimation Play(Animation anim, string clipName, Direction playDirection)
{
return Play(anim, clipName, playDirection, EnableCondition.DoNothing, DisableCondition.DoNotDisable);
}
public static ActiveAnimation Play(Animation anim, Direction playDirection)
{
return Play(anim, null, playDirection, EnableCondition.DoNothing, DisableCondition.DoNotDisable);
}
public static ActiveAnimation Play(Animator anim, string clipName, Direction playDirection, EnableCondition enableBeforePlay, DisableCondition disableCondition)
{
if (enableBeforePlay != EnableCondition.IgnoreDisabledState && !NGUITools.GetActive(anim.gameObject))
{
if (enableBeforePlay != EnableCondition.EnableThenPlay)
{
return null;
}
NGUITools.SetActive(anim.gameObject, state: true);
UIPanel[] componentsInChildren = anim.gameObject.GetComponentsInChildren<UIPanel>();
int i = 0;
for (int num = componentsInChildren.Length; i < num; i++)
{
componentsInChildren[i].Refresh();
}
}
ActiveAnimation activeAnimation = anim.GetComponent<ActiveAnimation>();
if (activeAnimation == null)
{
activeAnimation = anim.gameObject.AddComponent<ActiveAnimation>();
}
activeAnimation.mAnimator = anim;
activeAnimation.mDisableDirection = (Direction)disableCondition;
activeAnimation.onFinished.Clear();
activeAnimation.Play(clipName, playDirection);
if (activeAnimation.mAnim != null)
{
activeAnimation.mAnim.Sample();
}
else if (activeAnimation.mAnimator != null)
{
activeAnimation.mAnimator.Update(0f);
}
return activeAnimation;
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
public class AddDamageInfo : DamageModifier
{
public int AddDamage { get; protected set; }
public AddDamageInfo(int addDamage, string damageType, CardBasePrm.ClanType damageClan, bool isUseClass, int order)
{
AddDamage = addDamage;
base.DamageType = new List<string>();
base.DamageType.AddRange(damageType.Split(new string[1] { "_and_" }, StringSplitOptions.None));
base.DamageClan = new List<CardBasePrm.ClanType> { damageClan };
base.IsUseClass = isUseClass;
base.OrderCount = order;
}
public override int Calc(int damage)
{
return damage + AddDamage;
}
}

View File

@@ -0,0 +1,20 @@
public class AddHealModifierInfo : HealModifier
{
public int AddHealAmount { get; private set; }
public AddHealModifierInfo(int addHealAmount, int order, BattleCardBase owner)
{
AddHealAmount = addHealAmount;
base.OrderCount = order;
_owner = owner;
}
public override int Calc(int healAmount, BattleCardBase healOwner, BattleCardBase target)
{
if (healOwner.IsPlayer != _owner.IsPlayer)
{
return healAmount;
}
return healAmount + AddHealAmount;
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Wizard;
public class AddTargetInfo
{
private BattleCardBase _ownerCard;
private ConditionSkillFilterCollection _conditionFilter;
private ApplySkillTargetFilterCollection _targetFilter;
private Func<SkillBase, bool> typeCheck;
private string _conditionFilterText;
private string _targetFilterText;
private string _skillTypeText;
private string _ownerCardtype;
private SkillCreator _skillCreator;
public AddTargetInfo(BattleCardBase ownerCard, string conditionFilterText, string targetFilterText, string skillTypeText, string ownerCardType, SkillBase skill)
{
_ownerCard = ownerCard;
_conditionFilterText = conditionFilterText;
_targetFilterText = targetFilterText;
_skillTypeText = skillTypeText;
_ownerCardtype = ownerCardType;
_conditionFilter = new ConditionSkillFilterCollection();
_targetFilter = new ApplySkillTargetFilterCollection();
typeCheck = SetTypeCheck(_skillTypeText, _ownerCardtype);
_skillCreator = _ownerCard.CreateSkillCreator(_ownerCard.SelfBattlePlayer, _ownerCard.OpponentBattlePlayer, _ownerCard.ResourceMgr);
string[] array = _conditionFilterText.Split('&');
List<SkillFilterCreator.ContentInfo> list = new List<SkillFilterCreator.ContentInfo>();
for (int i = 0; i < array.Length; i++)
{
SkillFilterCreator.ParseContentInfo(array[i], out var retParsedInfo);
list.Add(retParsedInfo);
}
SkillCreator.SetupSkillConditionOld(_conditionFilter, list, _ownerCard, skill);
string[] array2 = _targetFilterText.Split('&');
List<SkillFilterCreator.ContentInfo> list2 = new List<SkillFilterCreator.ContentInfo>();
for (int j = 0; j < array2.Length; j++)
{
SkillFilterCreator.ParseContentInfo(array2[j], out var retParsedInfo2);
list2.Add(retParsedInfo2);
}
_skillCreator.SetupSkillTargetOld(_targetFilter, _ownerCard, list2, skill);
}
public List<BattleCardBase> GetAddTargetCard(SkillBase skill, BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
{
if (typeCheck(skill) && FilterComparison(skill.ApplyFilterCollection))
{
return _targetFilter.Filtering(pair, checkerOption, optionValue).Cast<BattleCardBase>().ToList();
}
return null;
}
private Func<SkillBase, bool> SetTypeCheck(string skillType, string ownerCardType)
{
if (skillType != null && skillType == "damage")
{
return (SkillBase skill) => CardTypeCheck(skill.SkillPrm.ownerCard, ownerCardType) && skill is Skill_damage;
}
return (SkillBase skill) => CardTypeCheck(skill.SkillPrm.ownerCard, ownerCardType) && skill is Skill_none;
}
private bool CardTypeCheck(BattleCardBase card, string ownerCardType)
{
return ownerCardType switch
{
"all" => true,
"unit" => card.IsUnit,
"spell" => card.IsSpell,
"field" => card.IsField,
"chant_field" => card.IsChantField,
_ => false,
};
}
private bool FilterComparison(ApplySkillTargetFilterCollection ownerSkillFilter)
{
if (_conditionFilter.BattlePlayerFilter.GetType() == ownerSkillFilter.BattlePlayerFilter.GetType() && _conditionFilter.TargetFilter.GetType() == ownerSkillFilter.TargetFilter.GetType())
{
foreach (ISkillCardFilter cardType in _conditionFilter.CardFilterList)
{
if (!ownerSkillFilter.CardFilterList.Any((ISkillCardFilter s) => s.GetType() == cardType.GetType()))
{
return false;
}
}
return true;
}
return false;
}
public AddTargetInfo Clone(BattleCardBase ownerCard)
{
return new AddTargetInfo(ownerCard, _conditionFilterText, _targetFilterText, _skillTypeText, _ownerCardtype, null);
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AlleyField : BackGroundBase
{
public override int FieldId => 22;
public override int FieldEffectId => 22;
public AlleyField(string bgmId = "NONE")
: base(bgmId)
{
}
protected override void BattleFieldBuild()
{
BattleCoroutine.GetInstance().StartCoroutine(BackGroundBase.ObjectChecker(0.5f, _str3DFieldPath, delegate
{
base.Field = GameObject.Find(_str3DFieldPath);
base.Field.transform.parent = GameMgr.GetIns().m_GameManagerObj.transform;
GimicAudioList = base.Field.GetComponent<AudioList>().GimicAudioList;
_fieldModel = base.Field.transform.Find("md_bf_aley_root").gameObject;
_fieldParticles = _fieldModel.transform.Find("Particles22").gameObject;
List<string> list = new List<string>(_fieldObjDictionary.Keys);
List<GameObject> list2 = new List<GameObject>();
for (int i = 0; i < _fieldObjDictionary.Count; i++)
{
list2.Add(_fieldObjDictionary[list[i]]);
}
GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list2, delegate
{
base.SetShaderGlobalColorBG = base.Field.transform.Find("SetMaterialColorBGManager").GetComponent<SetShaderGlobalColorBG>();
base.IsLoadDone = true;
}, isBattle: true, isField: true);
}));
}
public override void StartFieldSetEffect(Vector3 pos)
{
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_SET_22, pos);
}
public override void StartFieldTapEffect(int areaId, Vector3 pos)
{
base.StartFieldTapEffect(areaId, pos);
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_TAP_22_1, pos);
}
protected override IEnumerator RunFieldOpening()
{
GameMgr.GetIns().GetSoundMgr().PlaySeByStr($"se_field_{_str3DFieldNo}_appear_1", "se_field_" + _str3DFieldNo, 0f, 0L);
_battleCamera.Camera.transform.localPosition = new Vector3(2750f, -510f, -10f);
_battleCamera.Camera.transform.localRotation = Quaternion.Euler(new Vector3(-10f, -53f, 84f));
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", new Vector3(300f, -30f, -150f), "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", new Vector3(-11f, -100f, 92f), "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
yield return new WaitForSeconds(2f);
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", _battleCamera.BattleCameraPos, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", _battleCamera.BattleCameraRot, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
yield return new WaitForSeconds(0f);
}
protected override IEnumerator RunFieldGimic(GameObject obj)
{
string tag = obj.tag;
if (tag != null && tag == "FieldGimic1")
{
_ = _gimicCntDictionary[obj.tag];
}
yield return new WaitForSeconds(0f);
}
protected override IEnumerator RunFieldShake()
{
yield return new WaitForSeconds(0f);
}
}

View File

@@ -0,0 +1,8 @@
namespace AnimationOrTween;
public enum Direction
{
Reverse = -1,
Toggle,
Forward
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;
using Wizard;
using Wizard.Battle;
public class ApplySkillTargetFilterCollection : SkillFilterCollectionBase
{
public List<ISkillCustomSelectFilter> ApplyCustomSelectFilterList { get; set; }
public List<ISkillExclutionFilter> ApplyExclutionFilterList { get; private set; }
public ISkillSelectFilter ApplySelectFilter { get; set; }
public List<ApplySkillTargetFilterCollection> ApplyAndFilter { get; set; }
public ApplySkillTargetFilterCollection()
{
ApplyCustomSelectFilterList = new List<ISkillCustomSelectFilter>();
ApplyExclutionFilterList = new List<ISkillExclutionFilter>();
ApplyAndFilter = new List<ApplySkillTargetFilterCollection>();
}
public List<IReadOnlyBattleCardInfo> Filtering(BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
{
List<IReadOnlyBattleCardInfo> list = new List<IReadOnlyBattleCardInfo>();
List<IReadOnlyBattleCardInfo> AndFilterTargets = new List<IReadOnlyBattleCardInfo>();
IEnumerable<IBattlePlayerReadOnlyInfo> battlePlayerInfos = null;
if (ApplyAndFilter.Count <= 0)
{
if (base.BattlePlayerFilter != null)
{
battlePlayerInfos = base.BattlePlayerFilter.Filtering(pair);
}
if (base.TargetFilter != null)
{
list = base.TargetFilter.Filtering(battlePlayerInfos, checkerOption).ToList();
if (BattleManagerBase.GetIns().XorShiftRandom(isSelf: true) != null && BattleManagerBase.GetIns().XorShiftRandom(isSelf: false) == null && !pair.ReadOnlySelf.IsPlayer && (base.TargetFilter is SkillTargetInHandCardFilter || base.TargetFilter is SkillTargetReturnCardFilter || base.TargetFilter is SkillTargetTokenDrawCardFilter))
{
return list;
}
}
foreach (ISkillCardFilter cardFilter in base.CardFilterList)
{
list = cardFilter.Filtering(list, optionValue).ToList();
}
int i = 0;
for (int count = ApplyCustomSelectFilterList.Count; i < count; i++)
{
list = ApplyCustomSelectFilterList[i].Filtering(list, battlePlayerInfos, checkerOption).ToList();
}
for (int j = 0; j < ApplyExclutionFilterList.Count; j++)
{
list = ApplyExclutionFilterList[j].Filtering(list, battlePlayerInfos, checkerOption, optionValue).ToList();
}
}
else
{
for (int k = 0; k < ApplyAndFilter.Count; k++)
{
List<BattleCardBase> cards = ApplyAndFilter[k].Filtering(pair, checkerOption, optionValue).Cast<BattleCardBase>().ToList();
List<IReadOnlyBattleCardInfo> collection = (from IReadOnlyBattleCardInfo x in ApplyAndFilter[k].SelectFilter.Filtering(cards, optionValue, checkerOption)
where !AndFilterTargets.Contains(x)
select x).ToList();
AndFilterTargets.AddRange(collection);
}
}
List<IReadOnlyBattleCardInfo> list2 = list.ToList();
list2.AddRange(AndFilterTargets);
return list2;
}
public bool SimpleFiltering(IReadOnlyBattleCardInfo targetCard, BattlePlayerReadOnlyInfoPair pair, SkillConditionCheckerOption checkerOption, SkillOptionValue optionValue)
{
List<IReadOnlyBattleCardInfo> list = new List<IReadOnlyBattleCardInfo> { targetCard };
IEnumerable<IBattlePlayerReadOnlyInfo> battlePlayerInfos = base.BattlePlayerFilter.Filtering(pair);
for (int i = 0; i < base.CardFilterList.Count; i++)
{
list = base.CardFilterList[i].Filtering(list, optionValue).ToList();
}
for (int j = 0; j < ApplyCustomSelectFilterList.Count; j++)
{
list = ApplyCustomSelectFilterList[j].Filtering(list, battlePlayerInfos, checkerOption).ToList();
}
for (int k = 0; k < ApplyExclutionFilterList.Count; k++)
{
list = ApplyExclutionFilterList[k].Filtering(list, battlePlayerInfos, checkerOption, optionValue).ToList();
}
return list.Count() > 0;
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using UnityEngine;
public class AreaBGInfo : MonoBehaviour
{
private readonly List<ChapterExtraData> _chapterExtraDatas = new List<ChapterExtraData>
{
new ChapterExtraData
{
SectionId = 2,
ExtraTextureChapter = 10,
BGExtraEffectPath = "scn_map_change_1",
SeType = Se.TYPE.SE_MAP_TREE_EFFECT
},
new ChapterExtraData
{
SectionId = 2,
ExtraTextureChapter = 11,
ExtraTextureIndex = { 1, 2 },
BGSuffix = "b"
},
new ChapterExtraData
{
SectionId = 2,
ExtraTextureChapter = 12,
ExtraTextureIndex = { 1, 2 },
BGSuffix = "b"
},
new ChapterExtraData
{
SectionId = 1,
ExtraTextureChapter = 4,
BGExtraEffectPath = "scn_map_change_1",
SeType = Se.TYPE.SE_MAP_TREE_EFFECT,
ClanType = CardBasePrm.ClanType.NEMESIS
},
new ChapterExtraData
{
SectionId = 1,
ExtraTextureChapter = 5,
ExtraTextureIndex = { 1, 2 },
BGSuffix = "b",
ClanType = CardBasePrm.ClanType.NEMESIS
},
new ChapterExtraData
{
SectionId = 1,
ExtraTextureChapter = 6,
ExtraTextureIndex = { 1, 2 },
BGSuffix = "b",
ClanType = CardBasePrm.ClanType.NEMESIS
},
new ChapterExtraData
{
SectionId = 9,
ExtraTextureChapter = 1,
BGSectionId = 4,
BGExtraEffectPath = "scn_map_change_4",
BGFirstClearEffectPath = "scn_map_change_2",
FirstClearSeType = Se.TYPE.SE_MAP_SECTION9_CHAPTER1,
SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER,
ChapterMoveTime = 0f,
FirstClearEffectDelayTime = 1.2f,
FirstClearMoveOutDelayTime = 0.5f
},
new ChapterExtraData
{
SectionId = 9,
ExtraTextureChapter = 2,
BGSectionId = 7,
BGExtraEffectPath = "scn_map_change_4",
BGFirstClearEffectPath = "scn_map_change_3",
FirstClearSeType = Se.TYPE.SE_MAP_SECTION9_CHAPTER2,
SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER,
FirstClearEffectDelayTime = 1.2f,
FirstClearMoveDelayTime = 1f
},
new ChapterExtraData
{
SectionId = 9003,
ExtraTextureIndex = { 1, 2 },
BGSuffix = "c"
}
};
public List<ChapterExtraData> GetExtraChapters(int sectionId, int? selectStoryClassId)
{
List<ChapterExtraData> list = _chapterExtraDatas.FindAll((ChapterExtraData item) => item.SectionId == sectionId);
if (sectionId == 20)
{
list.AddRange(AreaBGInfoSection20.GetChapterExtraDatas());
}
return list.FindAll((ChapterExtraData item) => (CardBasePrm.ClanType?)item.ClanType == (CardBasePrm.ClanType?)selectStoryClassId || item.ClanType == CardBasePrm.ClanType.NONE);
}
}

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using Cute;
public class AreaBGInfoSection20
{
public const int SECTION_ID = 20;
public const int WERUSA_START = 10;
private const int WERUSA_END = 17;
private const int WERUSA_BG_SECTION_ID = 12;
private const int LEVIRU_START = 18;
private const int LEVIRU_END = 25;
private const int LEVIRU_BG_SECTION_ID = 10;
private const int IZUNIA_CHANGE_TEXTURE_START = 3;
private const int IZUNIA_END = 9;
private const int IZUNIA_BG_SECTION_ID = 2;
public const int NATERA_START = 26;
private const int NATERA_END = 33;
private const int NATERA_BG_SECTION_ID = 9;
private const int LAST_BATTLE_START = 34;
private const int LAST_BATTLE_END = 40;
private const int LAST_BATTLE_BG_SECTION_ID = 20;
public static List<ChapterExtraData> GetChapterExtraDatas()
{
List<ChapterExtraData> list = new List<ChapterExtraData>();
list.AddRange(Chapter1_2());
list.AddRange(Chapter3_9());
list.AddRange(OtherWorldChapters(10, 17, 12, new List<int> { 1, 2, 6, 7 }, addTreeEffect: true));
list.AddRange(OtherWorldChapters(18, 25, 10, new List<int> { 1, 2 }, addTreeEffect: true));
list.AddRange(OtherWorldChapters(26, 33, 9, new List<int> { 2, 3, 4, 7, 8, 9 }, addTreeEffect: true));
list.AddRange(OtherWorldChapters(34, 40, 20, null, addTreeEffect: false));
return list;
}
private static List<ChapterExtraData> OtherWorldChapters(int start, int end, int section, List<int> extraTextureIndex, bool addTreeEffect)
{
List<ChapterExtraData> list = new List<ChapterExtraData>();
for (int i = start; i <= end; i++)
{
ChapterExtraData chapterExtraData = new ChapterExtraData
{
SectionId = 20,
ExtraTextureChapter = i,
BGSectionId = section
};
chapterExtraData.AddTreeEffect = addTreeEffect;
if (i == 17 || i == 25 || i == 33)
{
chapterExtraData.BGExtraEffectPath = "scn_map_change_9";
chapterExtraData.SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER;
}
if (extraTextureIndex.IsNotNullOrEmpty())
{
chapterExtraData.ExtraTextureIndex = extraTextureIndex;
chapterExtraData.BGSuffix = "b";
}
list.Add(chapterExtraData);
}
return list;
}
private static List<ChapterExtraData> Chapter1_2()
{
return new List<ChapterExtraData>
{
new ChapterExtraData
{
SectionId = 20,
ExtraTextureChapter = 1,
BGExtraEffectPath = "scn_map_change_9",
SeType = Se.TYPE.SE_MAP_SECTION20_CHANGE_CHAPTER1
},
new ChapterExtraData
{
SectionId = 20,
ExtraTextureChapter = 2,
BGExtraEffectPath = "scn_map_change_8",
AttachExtraEffectToBgRoot = true,
SeType = Se.TYPE.SE_MAP_TREE_EFFECT
}
};
}
private static List<ChapterExtraData> Chapter3_9()
{
List<ChapterExtraData> list = new List<ChapterExtraData>();
for (int i = 3; i <= 9; i++)
{
ChapterExtraData chapterExtraData = new ChapterExtraData
{
SectionId = 20,
ExtraTextureChapter = i,
ExtraTextureIndex = { 1, 2, 6 },
BGSectionId = 2,
BGSuffix = "b"
};
if (i == 9)
{
chapterExtraData.BGExtraEffectPath = "scn_map_change_9";
chapterExtraData.SeType = Se.TYPE.SE_MAP_SECTION9_CHANGE_CHAPTER;
}
list.Add(chapterExtraData);
}
return list;
}
public static bool IsSpeedUpParticleTransition(int previousChapter, int nextChapter)
{
bool flag = previousChapter == 33 && nextChapter == 32;
return previousChapter == 0 || flag;
}
}

View File

@@ -0,0 +1,530 @@
using System.Collections;
using System.Collections.Generic;
using Cute;
using UnityEngine;
using Wizard;
public class AreaSelInfo : MonoBehaviour
{
private enum eTableCategory
{
CARD,
SLEEVE,
OTHER,
MAX
}
private const float LEFTSTAGEINFO_X_IN = 0f;
private const float LEFTSTAGEINFO_X_OUT = -1120f;
private const int CLEARPRESENT_MAX = 3;
private static readonly Vector3 CLEARPRESENT_CARD_COLLISIONSIZE = new Vector3(175f, 230f, 1f);
private const int CLEARPRESENT_CARD_DEPTHOFFSET = 50;
private const int CLEARPRESENT_RESOURCELIST_CAPACITY = 2;
private static readonly string[] CLEARPRESENT_NAME = new string[10] { "", "Common_0205", "", "Common_0201", "", "", "", "", "", "Common_0115" };
private static readonly string[] CLEARPRESENT_THUMBNAIL_SPRITENAME = new string[10] { "", "thumbnail_liquid", "", "thumbnail_crystal", "", "", "thumbnail_card", "thumbnail_emblem", "thumbnail_title", "thumbnail_rupy" };
private readonly Vector3 REWARD_TABLE_DEFAULT_POSITION = new Vector3(45f, -85.3f, 0f);
private readonly Vector3 REWARD_TABLE_CARD_POSITION = new Vector3(28.5f, -85.3f, 0f);
private const float TABLE_CONTAINS_CARD_REWARD_OFFSET_X = -16f;
private const int REWARD_BG_BASIC_WIDTH = 250;
private const int REWARD_BG_OFFSET_WIDTH_PER_GOODS = 90;
private readonly Dictionary<UserGoods.Type, float> REWARD_BG_OFFSET_MAGNIFICATION = new Dictionary<UserGoods.Type, float>
{
{
UserGoods.Type.Degree,
2f
},
{
UserGoods.Type.Card,
1.2f
},
{
UserGoods.Type.Sleeve,
1.2f
},
{
UserGoods.Type.Skin,
1.2f
},
{
UserGoods.Type.RedEther,
1f
},
{
UserGoods.Type.Rupy,
1f
},
{
UserGoods.Type.Item,
1f
},
{
UserGoods.Type.Emblem,
1f
}
};
private const float LABEL_ONLY_DEGREE_OFFSET_Y = -10f;
[SerializeField]
private GameObject _clearRewardPrefab;
private List<AreaSelectClearReward> _clearRewardList = new List<AreaSelectClearReward>();
[SerializeField]
private UITable _tableRoot;
[SerializeField]
private UITable[] _tableRewardsCategory = new UITable[3];
[SerializeField]
private GameObject _cardObjEvacuationRoot;
[SerializeField]
private UISprite _spriteRewardBackground;
[SerializeField]
private UILabel _labelAcquired;
private List<UIBase_CardManager.CardObjData> _cardObjList = new List<UIBase_CardManager.CardObjData>();
[SerializeField]
private GameObject CardDetailRoot;
[SerializeField]
private CardDetailUI CardDetailPrefab;
private CardDetailUI _cardDetail;
private List<string> _loadFileList = new List<string>();
private bool _isLoadEnd = true;
public void SetClearPresent(StoryChapterData chapterData)
{
if (chapterData == null || chapterData.Rewards == null)
{
return;
}
if (chapterData.Rewards.Length != 0)
{
base.gameObject.SetActive(value: true);
bool isCleared = chapterData.IsCleared;
for (int i = 0; i < _clearRewardList.Count; i++)
{
_clearRewardList[i].gameObject.SetActive(value: false);
}
List<long> list = new List<long>();
for (int j = 0; j < chapterData.Rewards.Length; j++)
{
StoryChapterData.StoryReward storyReward = chapterData.Rewards[j];
if (storyReward == null)
{
continue;
}
if (j >= _clearRewardList.Count)
{
break;
}
if (storyReward.RewardType == 5)
{
if (list.Contains(storyReward.RewardUserGoodsId))
{
continue;
}
list.Add(storyReward.RewardUserGoodsId);
}
_clearRewardList[j].gameObject.SetActive(value: true);
_clearRewardList[j].ShowReward((UserGoods.Type)storyReward.RewardType, storyReward.RewardUserGoodsId, storyReward.RewardNumber, isCleared);
}
RepositionRewards();
SetRewardBackgroundWidth();
SetAcquiredLabel(isCleared);
}
else
{
base.gameObject.SetActive(value: false);
}
}
private void RepositionRewards()
{
for (int i = 0; i < _tableRewardsCategory.Length; i++)
{
_tableRewardsCategory[i].gameObject.SetActive(value: false);
}
for (int j = 0; j < _clearRewardList.Count; j++)
{
if (_clearRewardList[j].gameObject.activeSelf)
{
int num = _clearRewardList[j].RewardGoodsType switch
{
UserGoods.Type.Card => 0,
UserGoods.Type.Sleeve => 1,
_ => 2,
};
Transform obj = _clearRewardList[j].gameObject.transform;
obj.SetParent(_tableRewardsCategory[num].transform);
obj.SetAsLastSibling();
_tableRewardsCategory[num].gameObject.SetActive(value: true);
}
}
for (int k = 0; k < _tableRewardsCategory.Length; k++)
{
if (_tableRewardsCategory[k].gameObject.activeInHierarchy)
{
_tableRewardsCategory[k].Reposition();
}
}
_tableRoot.Reposition();
if (_tableRewardsCategory[0].gameObject.activeInHierarchy)
{
_tableRoot.transform.localPosition = REWARD_TABLE_CARD_POSITION;
for (int l = 0; l < _tableRewardsCategory.Length; l++)
{
if (l != 0)
{
Vector3 localPosition = _tableRewardsCategory[l].transform.localPosition;
localPosition.x += -16f;
_tableRewardsCategory[l].transform.localPosition = localPosition;
}
}
}
else
{
_tableRoot.transform.localPosition = REWARD_TABLE_DEFAULT_POSITION;
}
}
private void SetRewardBackgroundWidth()
{
float num = 0f;
for (int i = 0; i < _clearRewardList.Count; i++)
{
if (_clearRewardList[i].gameObject.activeInHierarchy)
{
UserGoods.Type rewardGoodsType = _clearRewardList[i].RewardGoodsType;
num += REWARD_BG_OFFSET_MAGNIFICATION[rewardGoodsType];
}
}
int width = 250 + (int)(90f * num);
_spriteRewardBackground.width = width;
}
private void SetAcquiredLabel(bool isAcquired)
{
_labelAcquired.gameObject.SetActive(isAcquired);
if (!isAcquired)
{
return;
}
float a = float.MaxValue;
float num = float.MinValue;
bool flag = true;
for (int i = 0; i < _clearRewardList.Count; i++)
{
if (_clearRewardList[i].gameObject.activeInHierarchy)
{
Transform rewardTransform = _clearRewardList[i].GetRewardTransform();
a = Mathf.Min(a, rewardTransform.position.x);
num = Mathf.Max(num, rewardTransform.position.x);
if (_clearRewardList[i].RewardGoodsType != UserGoods.Type.Degree)
{
flag = false;
}
}
}
Vector3 position = _labelAcquired.transform.position;
position.x = Mathf.Lerp(a, num, 0.5f);
_labelAcquired.transform.position = position;
Vector3 localPosition = _labelAcquired.transform.localPosition;
localPosition.y = _tableRoot.transform.localPosition.y + (flag ? (-10f) : 0f);
_labelAcquired.transform.localPosition = localPosition;
}
public void LoadClearPresent(IReadOnlyList<StoryChapterData> stageDataList)
{
if (!_isLoadEnd)
{
return;
}
_isLoadEnd = false;
ReleaseClearPresent();
if (CardDetailPrefab != null)
{
_cardDetail = Object.Instantiate(CardDetailPrefab);
_cardDetail.transform.parent = CardDetailRoot.transform;
_cardDetail.transform.localPosition = Vector3.zero;
_cardDetail.transform.localScale = Vector3.one;
_cardDetail.OnClose = OnCardDetailClose;
_cardDetail.gameObject.SetActive(value: false);
_cardDetail.Initialize(_cardDetail.gameObject.layer, CardMaster.CardMasterId.Default);
_cardDetail.IsShowFlavorTextButton = true;
_cardDetail.IsShowVoiceButton = true;
_cardDetail.IsShowEvolutionButton = true;
}
_clearRewardList.Clear();
for (int i = 0; i < 3; i++)
{
AreaSelectClearReward component = NGUITools.AddChild(_tableRoot.gameObject, _clearRewardPrefab).GetComponent<AreaSelectClearReward>();
_clearRewardList.Add(component);
}
List<int> list = new List<int>(2);
List<string> loadPath = new List<string>();
StoryChapterData.StoryReward storyReward = null;
for (int j = 0; j < stageDataList.Count; j++)
{
for (int k = 0; k < stageDataList[j].Rewards.Length; k++)
{
string text = string.Empty;
storyReward = stageDataList[j].Rewards[k];
if (storyReward == null)
{
continue;
}
if (storyReward.RewardType == 5)
{
if (!list.Contains((int)storyReward.RewardUserGoodsId))
{
list.Add((int)storyReward.RewardUserGoodsId);
}
}
else if (storyReward.RewardType == 4)
{
string userGoodsImageName = UserGoods.GetUserGoodsImageName(UserGoods.Type.Item, storyReward.RewardUserGoodsId);
text = Toolbox.ResourcesManager.GetAssetTypePath(userGoodsImageName, ResourcesManager.AssetLoadPathType.Item);
}
else if (storyReward.RewardType == 6)
{
long existingSleeveId = Toolbox.ResourcesManager.GetExistingSleeveId(storyReward.RewardUserGoodsId);
text = Toolbox.ResourcesManager.GetAssetTypePath(existingSleeveId.ToString(), ResourcesManager.AssetLoadPathType.SleeveTexture);
Sleeve sleeve = Data.Master.SleeveMgr.Get(existingSleeveId);
if (sleeve.IsPremiumSleeve)
{
UIManager.GetInstance().getUIBase_CardManager().AddPremireSleevePath(ref loadPath, sleeve);
}
}
else if (storyReward.RewardType == 8)
{
foreach (string degreeResource in DegreeHelper.GetDegreeResourceList(storyReward.RewardUserGoodsId, DegreeHelper.DegreeType.SMALL, isFetch: false))
{
if (!loadPath.Contains(degreeResource))
{
loadPath.Add(degreeResource);
}
}
}
else if (storyReward.RewardType == 7)
{
text = Toolbox.ResourcesManager.GetAssetTypePath(storyReward.RewardUserGoodsId.ToString(), ResourcesManager.AssetLoadPathType.Emblem_M);
}
else if (storyReward.RewardType == 10)
{
text = Toolbox.ResourcesManager.GetAssetTypePath(storyReward.RewardUserGoodsId.ToString(), ResourcesManager.AssetLoadPathType.ClassCharaSkinThumbnail);
}
if (text != string.Empty && !loadPath.Contains(text))
{
loadPath.Add(text);
}
}
}
if (list.Count == 0 && loadPath.Count == 0)
{
_isLoadEnd = true;
}
else
{
StartCoroutine(LoadClearPresentInner(list, loadPath));
}
}
private IEnumerator LoadClearPresentInner(List<int> cardidlist, List<string> rewardPathList)
{
UIManager uiMgr = UIManager.GetInstance();
UIBase_CardManager uiCardMgr = uiMgr.getUIBase_CardManager();
bool isLoadRewardEnd = false;
_loadFileList.Clear();
bool isLoadCard = cardidlist.Count > 0;
if (isLoadCard)
{
int layer = LayerMask.NameToLayer("FrontUI");
uiMgr.CardLoadSelect(base.gameObject, cardidlist, layer, is2D: true);
}
StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(rewardPathList, delegate
{
_loadFileList.AddRange(rewardPathList);
isLoadRewardEnd = true;
}));
while ((isLoadCard && (!uiCardMgr.getCreateEndFlag() || !uiCardMgr.isAssetAllReady)) || !isLoadRewardEnd)
{
yield return null;
}
_cardObjList = uiMgr.getCardList2DObjs();
if (_cardObjList != null)
{
for (int num = 0; num < _cardObjList.Count; num++)
{
GameObject cardObj = _cardObjList[num].CardObj;
if (!(cardObj == null))
{
cardObj.SetActive(value: false);
UITexture[] componentsInChildren = cardObj.GetComponentsInChildren<UITexture>(includeInactive: true);
for (int num2 = 0; num2 < componentsInChildren.Length; num2++)
{
componentsInChildren[num2].depth += 50;
}
UILabel[] componentsInChildren2 = cardObj.GetComponentsInChildren<UILabel>(includeInactive: true);
for (int num3 = 0; num3 < componentsInChildren2.Length; num3++)
{
componentsInChildren2[num3].depth += 50;
}
UISprite[] componentsInChildren3 = cardObj.GetComponentsInChildren<UISprite>(includeInactive: true);
for (int num4 = 0; num4 < componentsInChildren3.Length; num4++)
{
componentsInChildren3[num4].depth += 50;
}
cardObj.GetComponent<CardListTemplate>().HideNum();
cardObj.AddComponent<BoxCollider>().size = CLEARPRESENT_CARD_COLLISIONSIZE;
cardObj.AddComponent<UIEventListener>().onClick = _cardDetail.OnPushCardDetailOn;
}
}
}
for (int num5 = 0; num5 < 3; num5++)
{
_clearRewardList[num5].Init(_cardObjList, _cardObjEvacuationRoot);
}
_isLoadEnd = true;
}
public void ReleaseClearPresent()
{
if (_cardDetail != null)
{
Object.Destroy(_cardDetail.gameObject);
_cardDetail = null;
}
if (_cardObjList != null)
{
for (int i = 0; i < _cardObjList.Count; i++)
{
Object.Destroy(_cardObjList[i].CardObj.gameObject);
}
_cardObjList.Clear();
}
Toolbox.ResourcesManager.RemoveAssetGroup(_loadFileList);
_loadFileList.Clear();
}
private void OnCardDetailClose()
{
}
public bool GetLoadEnd()
{
return _isLoadEnd;
}
public void ResetInfoPosition()
{
base.gameObject.transform.localPosition = new Vector3(0f, base.gameObject.transform.localPosition.y, base.gameObject.transform.localPosition.z);
}
public void MoveToScreen(bool isIn, bool isImmediate)
{
MoveToScreenObj(base.gameObject, isIn ? 0f : (-1120f), isImmediate ? 0f : 0.5f);
}
public void MoveToScreenObj(GameObject target, float localPosX, float time)
{
if (Mathf.Approximately(time, 0f))
{
Vector3 localPosition = target.transform.localPosition;
localPosition.x = localPosX;
target.transform.localPosition = localPosition;
return;
}
iTween.MoveTo(target, iTween.Hash("islocal", true, "x", localPosX, "time", time));
}
public static string GetPresentItemName(int itemID, long userGoodsId)
{
switch ((UserGoods.Type)itemID)
{
case UserGoods.Type.RedEther:
case UserGoods.Type.Rupy:
return Data.SystemText.Get(CLEARPRESENT_NAME[itemID]);
case UserGoods.Type.Item:
{
Item item = Data.Master.ItemList.Find((Item data) => data.UserGoodsId == userGoodsId);
if (item == null)
{
return string.Empty;
}
return item.name;
}
case UserGoods.Type.Sleeve:
{
Sleeve sleeve = Data.Master.SleeveMgr.Get(userGoodsId);
if (sleeve == null)
{
return string.Empty;
}
return sleeve.sleeve_name;
}
case UserGoods.Type.Emblem:
{
Emblem emblem = Data.Master.EmblemMgr.Get(userGoodsId);
if (emblem == null)
{
return string.Empty;
}
return emblem._name;
}
case UserGoods.Type.Degree:
{
Degree degree = Data.Master.DegreeMgr.Get((int)userGoodsId);
if (degree == null)
{
return string.Empty;
}
return degree._name;
}
case UserGoods.Type.Skin:
{
ClassCharacterMasterData charaPrmByCharaId = GameMgr.GetIns().GetDataMgr().GetCharaPrmByCharaId((int)userGoodsId);
if (charaPrmByCharaId == null)
{
return string.Empty;
}
return charaPrmByCharaId.chara_name;
}
case UserGoods.Type.SpotCardPoint:
return Data.SystemText.Get("Common_0161");
case UserGoods.Type.MyPageBG:
return Data.Master.MyPageCustomBGMaster[userGoodsId.ToString()].Name;
default:
return string.Empty;
}
}
public static string GetPresentItemSpriteName(int itemID)
{
if (itemID < 0 || itemID >= CLEARPRESENT_THUMBNAIL_SPRITENAME.Length)
{
return string.Empty;
}
return CLEARPRESENT_THUMBNAIL_SPRITENAME[itemID];
}
}

View File

@@ -0,0 +1,489 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Cute;
using UnityEngine;
[Serializable]
public class AreaSelectBG
{
private const float BGTEXTURE_WIDTH = 1024f;
private const float BGTEXTURE_HEIGHT = 1024f;
private const int BGTEXTURE_NUM_X = 4;
private const int BGTEXTURE_NUM_Y = 3;
private const float BGTEXTURE_WIDTH_HALF_LEFT = 2048f;
private const float BGTEXTURE_WIDTH_HALF_RIGHT = 2560f;
private const float BGTEXTURE_HEIGHT_HALF_UP = 1536f;
private const float BGTEXTURE_HEIGHT_HALF_BOTTOM = 1536f;
private const float BGTEXTURE_DRAG_MARGIN = 5f;
private const float BGTEXTURE_DRAG_DECELERATION = 0.875f;
private const float BGTEXTURE_DRAGARROW_ANIM_SPEED = 1f;
private const float BGDRAG_SEC_MAXSPEED = 0.25f;
private const float BGDRAG_SEC_RESETUPTOTIME_MAX = 0.5f;
private AreaSelectUI _areaSelectUI;
[SerializeField]
private GameObject _BGRoot;
[SerializeField]
private UITexture[] _BGTexture;
[SerializeField]
private AreaBGInfo _areaBGInfo;
private ParticleSystem _bgEffect;
private AreaSelectEffectControlBase _bgEffectControl;
[SerializeField]
private GameObject _BGDragCollision;
private Vector2 _BGDragDelta = Vector2.zero;
private bool _isBGDragEnable;
private float _BGDragSec;
private float _BGDragSecResetUpToTime;
private List<string> _loadedResources = new List<string>();
private bool _isLoadEndBGTexture = true;
private bool _isLoadEndParticle = true;
private StorySectionData _sectionData;
private int? _sectionClassId;
private List<ChapterExtraData> _extraChapters = new List<ChapterExtraData>();
private bool _isNormalBgSet = true;
private bool _changeChapterFirstCall = true;
private float _currentAspectRatio;
private Vector3 _currentBGScale = Vector3.one;
private Vector3 _currentParentPos = Vector3.zero;
private Vector2 _minMovablePos = Vector3.one;
private Vector2 _maxMovablePos = Vector3.one;
public ChapterExtraData ChapterExtraData { get; private set; }
public ChapterExtraData TransitionChapterExtraData { get; private set; }
public int BeforeChapterId { get; private set; }
public GameObject GetBGRoot()
{
return _BGRoot;
}
public bool GetLoadEnd()
{
if (_isLoadEndBGTexture)
{
return _isLoadEndParticle;
}
return false;
}
public void SetActiveBGEffect(bool isActive)
{
_bgEffect.gameObject.SetActive(isActive);
if (_bgEffectControl != null)
{
_bgEffectControl.SetActiveBGEffect(isActive);
}
}
public bool IsBGDragEnable()
{
return _isBGDragEnable;
}
public void Init(AreaSelectUI areaSelectUI)
{
_areaSelectUI = areaSelectUI;
UIEventListener component = _BGDragCollision.GetComponent<UIEventListener>();
if (null != component)
{
component.onDrag = OnDragBG;
}
SetBGDragEnable(enable: false);
}
public void Term()
{
int i = 0;
for (int num = _BGTexture.Length; i < num; i++)
{
_BGTexture[i].mainTexture = null;
}
}
private string GetBackGroundPath(int backGroundId, int index, bool isFetch = false)
{
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + backGroundId.ToString("00") + "_" + (index + 1).ToString("00"), ResourcesManager.AssetLoadPathType.Background, isFetch);
}
private string GetExtraBackGroundPath(StorySectionData sectionData, int index, string suffix, bool isFetch = false)
{
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + sectionData.BackGroundId.ToString("00") + "_" + (index + 1).ToString("00") + suffix, ResourcesManager.AssetLoadPathType.Background, isFetch);
}
private string GetExtraBackGroundPath(int backgroundId, int index, string suffix, bool isFetch = false)
{
return Toolbox.ResourcesManager.GetAssetTypePath("bg_story_" + backgroundId.ToString("00") + "_" + (index + 1).ToString("00") + suffix, ResourcesManager.AssetLoadPathType.Background, isFetch);
}
private string GetMapEffectPath(int backGroundId, bool isFetch = false)
{
return Toolbox.ResourcesManager.GetAssetTypePath("scn_map_world_" + backGroundId, ResourcesManager.AssetLoadPathType.Effect2D, isFetch);
}
private string GetTreeEffectPath(int backGroundId, bool isFetch = false)
{
return Toolbox.ResourcesManager.GetAssetTypePath("scn_map_world_tree_" + backGroundId, ResourcesManager.AssetLoadPathType.Effect2D, isFetch);
}
public void LoadBG(StorySectionData sectionData, int? sectionClassId)
{
_sectionData = sectionData;
_sectionClassId = sectionClassId;
_extraChapters = _areaBGInfo.GetExtraChapters(_sectionData.Id, sectionClassId);
_loadedResources.Add(GetMapEffectPath(sectionData.BackGroundId));
_isLoadEndBGTexture = false;
for (int i = 0; i < _BGTexture.Length; i++)
{
_loadedResources.Add(GetBackGroundPath(sectionData.BackGroundId, i));
}
foreach (ChapterExtraData extraChapter in _extraChapters)
{
int num = sectionData.BackGroundId;
if (extraChapter.IsUseOtherSectionBG())
{
num = extraChapter.BGSectionId;
for (int j = 0; j < _BGTexture.Length; j++)
{
_loadedResources.Add(GetBackGroundPath(num, j));
}
_loadedResources.Add(GetMapEffectPath(num));
if (extraChapter.AddTreeEffect)
{
_loadedResources.Add(GetTreeEffectPath(num));
}
}
foreach (int item in extraChapter.ExtraTextureIndex)
{
_loadedResources.Add(GetExtraBackGroundPath(num, item, extraChapter.BGSuffix));
}
if (!string.IsNullOrEmpty(extraChapter.BGExtraEffectPath))
{
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGExtraEffectPath, ResourcesManager.AssetLoadPathType.Effect2D));
}
if (!string.IsNullOrEmpty(extraChapter.BGFirstClearEffectPath))
{
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGFirstClearEffectPath, ResourcesManager.AssetLoadPathType.Effect2D));
}
}
_areaSelectUI.StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(_loadedResources, _OnLoadEndBG));
_isLoadEndParticle = false;
}
public void UnLoadBG()
{
Toolbox.ResourcesManager.RemoveAssetGroup(_loadedResources);
_loadedResources.Clear();
_sectionData = null;
_sectionClassId = null;
}
private void _OnLoadEndBG()
{
List<GameObject> list = new List<GameObject>();
_bgEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetMapEffectPath(_sectionData.BackGroundId, isFetch: true)) as GameObject).GetComponent<ParticleSystem>();
Vector3 localScale = _bgEffect.transform.localScale;
_bgEffect.transform.parent = _BGRoot.transform;
_bgEffect.transform.localScale = localScale;
_bgEffect.transform.localPosition = Vector3.zero;
_bgEffectControl = _bgEffect.gameObject.GetComponent<AreaSelectEffectControlBase>();
list.Add(_bgEffect.gameObject);
if (_bgEffectControl != null)
{
_bgEffectControl._backGroundEffects.Add(_bgEffectControl.BASE_EFFECT_INDEX, _bgEffect);
_bgEffectControl._backGroundEffects.Add(_sectionData.BackGroundId, _bgEffect);
}
for (int i = 0; i < _BGTexture.Length; i++)
{
_BGTexture[i].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(_sectionData.BackGroundId, i, isFetch: true)) as Texture;
}
foreach (ChapterExtraData extraChapter in _extraChapters)
{
Dictionary<int, Texture> bGTexture = extraChapter.BGTexture;
int num = _sectionData.BackGroundId;
if (extraChapter.IsUseOtherSectionBG())
{
num = extraChapter.BGSectionId;
for (int j = 0; j < _BGTexture.Length; j++)
{
bGTexture.Add(j, Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(num, j, isFetch: true)) as Texture);
}
if (num != _sectionData.BackGroundId && !_bgEffectControl._backGroundEffects.ContainsKey(num))
{
ParticleSystem particleSystem = InitParticle(num, extraChapter.AddTreeEffect);
_bgEffectControl._backGroundEffects.Add(num, particleSystem);
list.Add(particleSystem.gameObject);
}
}
foreach (int item in extraChapter.ExtraTextureIndex)
{
if (!bGTexture.ContainsKey(item))
{
bGTexture.Add(item, Toolbox.ResourcesManager.LoadObject(GetExtraBackGroundPath(num, item, extraChapter.BGSuffix, isFetch: true)) as Texture);
}
}
if (!string.IsNullOrEmpty(extraChapter.BGExtraEffectPath))
{
string assetTypePath = Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGExtraEffectPath, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true);
extraChapter.ExtraEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(assetTypePath) as GameObject);
if (extraChapter.AttachExtraEffectToBgRoot)
{
extraChapter.ExtraEffect.transform.parent = _BGRoot.transform;
extraChapter.ExtraEffect.transform.localPosition = Vector3.zero;
}
else
{
extraChapter.ExtraEffect.transform.parent = _areaSelectUI.transform;
}
extraChapter.ExtraEffect.SetActive(value: false);
List<string> collection = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(extraChapter.ExtraEffect, null);
_loadedResources.AddRange(collection);
}
if (!string.IsNullOrEmpty(extraChapter.BGFirstClearEffectPath))
{
string assetTypePath2 = Toolbox.ResourcesManager.GetAssetTypePath(extraChapter.BGFirstClearEffectPath, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true);
extraChapter.FirstClearEffect = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(assetTypePath2) as GameObject);
extraChapter.FirstClearEffect.transform.parent = _areaSelectUI.transform;
extraChapter.FirstClearEffect.SetActive(value: false);
List<string> collection2 = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(extraChapter.FirstClearEffect, null);
_loadedResources.AddRange(collection2);
}
}
List<string> collection3 = GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list, delegate
{
_isLoadEndParticle = true;
});
_loadedResources.AddRange(collection3);
_isLoadEndBGTexture = true;
}
private ParticleSystem InitParticle(int bgId, bool addTreeEffect = false)
{
Vector3 vector = default(Vector3);
ParticleSystem component = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetMapEffectPath(bgId, isFetch: true)) as GameObject).GetComponent<ParticleSystem>();
vector = component.transform.localScale;
component.transform.parent = _BGRoot.transform;
component.transform.localScale = vector;
component.transform.localPosition = Vector3.zero;
if (addTreeEffect)
{
GameObject gameObject = UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(GetTreeEffectPath(bgId, isFetch: true)) as GameObject);
gameObject.transform.parent = component.transform;
gameObject.transform.localScale = Vector3.one;
gameObject.transform.localPosition = Vector3.zero;
}
return component;
}
public void OnChangeSelectChapter(StoryChapterData chapterData, bool isFirstClear)
{
TransitionChapterExtraData = null;
if (_extraChapters.Count > 0)
{
int intChapterId = int.Parse(chapterData.ChapterId);
ChapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == intChapterId);
}
if (_bgEffectControl != null)
{
_bgEffectControl.OnChangeSelectChapter(this, _sectionData, _sectionClassId, chapterData, isFirstClear);
}
}
public void SetExtraTexture(int chapterId)
{
if (_extraChapters.Count == 0)
{
return;
}
ChapterExtraData chapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == chapterId || n.SectionId == 9003);
if (chapterExtraData != null && chapterExtraData.IsChangeBG())
{
int num = _sectionData.BackGroundId;
if (chapterExtraData.IsUseOtherSectionBG())
{
num = chapterExtraData.BGSectionId;
for (int num2 = 0; num2 < _BGTexture.Length; num2++)
{
_BGTexture[num2].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(num, num2, isFetch: true)) as Texture;
}
}
foreach (int item in chapterExtraData.ExtraTextureIndex)
{
_BGTexture[item].mainTexture = Toolbox.ResourcesManager.LoadObject(GetExtraBackGroundPath(num, item, chapterExtraData.BGSuffix, isFetch: true)) as Texture;
}
_isNormalBgSet = false;
}
else if (!_isNormalBgSet)
{
for (int num3 = 0; num3 < _BGTexture.Length; num3++)
{
_BGTexture[num3].mainTexture = Toolbox.ResourcesManager.LoadObject(GetBackGroundPath(_sectionData.BackGroundId, num3, isFetch: true)) as Texture;
}
_isNormalBgSet = true;
}
}
public List<int> GetChaptersWithDifferentBackgroundFrom(int chapterId)
{
ChapterExtraData chapterExtra = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == chapterId);
IEnumerable<ChapterExtraData> source = ((chapterExtra == null || !chapterExtra.IsUseOtherSectionBG()) ? _extraChapters.Where((ChapterExtraData c) => c.IsUseOtherSectionBG() && c.BGSectionId != _sectionData.BackGroundId) : _extraChapters.Where((ChapterExtraData c) => c.BGSectionId != chapterExtra.BGSectionId));
return source.Select((ChapterExtraData s) => s.ExtraTextureChapter).ToList();
}
public void SetExtraEffect(int chapterId, bool isFirstClear = false)
{
if (_extraChapters.Count == 0)
{
return;
}
List<ChapterExtraData> list = new List<ChapterExtraData>();
list = ((BeforeChapterId >= chapterId) ? _extraChapters.FindAll((ChapterExtraData n) => n.ExtraTextureChapter != 0 && BeforeChapterId > n.ExtraTextureChapter && n.ExtraTextureChapter >= chapterId) : _extraChapters.FindAll((ChapterExtraData n) => n.ExtraTextureChapter != 0 && BeforeChapterId <= n.ExtraTextureChapter && n.ExtraTextureChapter < chapterId));
BeforeChapterId = chapterId;
TransitionChapterExtraData = list.FirstOrDefault((ChapterExtraData n) => n.ExtraEffect != null);
if (isFirstClear)
{
return;
}
if (list.Count() == 0 || TransitionChapterExtraData == null)
{
_changeChapterFirstCall = false;
return;
}
TransitionChapterExtraData = list.FirstOrDefault((ChapterExtraData n) => n.ExtraEffect != null);
if (!_changeChapterFirstCall && TransitionChapterExtraData != null && TransitionChapterExtraData.ExtraEffect != null)
{
TransitionChapterExtraData.ExtraEffect.SetActive(value: false);
TransitionChapterExtraData.ExtraEffect.SetActive(value: true);
GameMgr.GetIns().GetSoundMgr().PlaySe(TransitionChapterExtraData.SeType);
}
_changeChapterFirstCall = false;
}
public IEnumerator SetClearEffect()
{
ChapterExtraData clearChapterExtraData = _extraChapters.FirstOrDefault((ChapterExtraData n) => n.ExtraTextureChapter == BeforeChapterId);
if (clearChapterExtraData != null && clearChapterExtraData.FirstClearEffect != null)
{
yield return new WaitForSeconds(clearChapterExtraData.FirstClearEffectDelayTime);
clearChapterExtraData.FirstClearEffect.SetActive(value: false);
clearChapterExtraData.FirstClearEffect.SetActive(value: true);
GameMgr.GetIns().GetSoundMgr().PlaySe(clearChapterExtraData.FirstClearSeType);
}
}
public void SetupEnd()
{
if (_bgEffectControl != null)
{
_bgEffectControl.SetupEnd();
}
}
public void OnDragBG(GameObject obj, Vector2 delta)
{
_BGDragSecResetUpToTime = 0.5f;
_BGDragSec += Time.deltaTime;
_BGDragSec = Mathf.Min(_BGDragSec, 0.25f);
float t = Mathf.Clamp(_BGDragSec / 0.25f, 0f, 1f);
_BGDragDelta = Vector2.Lerp(Vector2.zero, delta, t);
}
public void UpdateBGDrag()
{
if (_BGDragSecResetUpToTime > 0f)
{
_BGDragSecResetUpToTime -= Time.deltaTime;
if (_BGDragSecResetUpToTime < 0f)
{
_BGDragSecResetUpToTime = 0f;
_BGDragSec = 0f;
}
}
if (!Mathf.Approximately(_BGDragDelta.x, 0f) || !Mathf.Approximately(_BGDragDelta.y, 0f))
{
_BGDragDelta.x *= 0.875f;
_BGDragDelta.y *= 0.875f;
Vector3 localPosition = _BGRoot.transform.localPosition;
localPosition.x += _BGDragDelta.x;
localPosition.y += _BGDragDelta.y;
UpdateMovableRange();
localPosition.x = Mathf.Clamp(localPosition.x, _minMovablePos.x, _maxMovablePos.x);
localPosition.y = Mathf.Clamp(localPosition.y, _minMovablePos.y, _maxMovablePos.y);
_BGRoot.transform.localPosition = localPosition;
}
}
public void SetBGDragEnable(bool enable)
{
_isBGDragEnable = enable;
_BGDragCollision.SetActive(enable);
_BGDragDelta = Vector2.zero;
}
private void UpdateMovableRange()
{
float num = (float)Screen.width / (float)Screen.height;
Vector3 localScale = _BGRoot.transform.localScale;
Vector3 localPosition = _BGRoot.transform.parent.transform.localPosition;
if (!(localScale == _currentBGScale) || num != _currentAspectRatio || !(localPosition == _currentParentPos))
{
float num2 = UIManager.GetInstance().UIManagerRoot.manualHeight;
if (1.7777778f > num)
{
num2 *= 1.7777778f / num;
}
float num3 = num2 * num * 0.5f;
float num4 = num2 * 0.5f;
float num5 = 2048f * localScale.x;
float num6 = 2560f * localScale.x;
float num7 = 1536f * localScale.y;
float num8 = 1536f * localScale.y;
_minMovablePos.x = 0f - num6 + num3 + 5f - localPosition.x;
_maxMovablePos.x = num5 - num3 - 5f - localPosition.x;
_minMovablePos.y = 0f - num7 + num4 + 5f - localPosition.y;
_maxMovablePos.y = num8 - num4 - 5f - localPosition.y;
_currentAspectRatio = num;
_currentBGScale = localScale;
_currentParentPos = localPosition;
}
}
}

View File

@@ -0,0 +1,136 @@
using System.Collections.Generic;
using Cute;
using UnityEngine;
public class AreaSelectChapterEffect
{
private static readonly Vector3 EFFECT_SCALE = new Vector3(320f, 320f, 320f);
private AreaSelectUI _areaSelectUI;
private List<string> _loadedResources = new List<string>();
private bool _isLoadEnd = true;
private Transform _effectParent;
private Dictionary<string, ParticleSystem> _effectList = new Dictionary<string, ParticleSystem>();
private string _playingEffect = "";
public bool GetLoadEnd()
{
return _isLoadEnd;
}
public void Init(AreaSelectUI areaselectUI, Transform effectParent)
{
_areaSelectUI = areaselectUI;
_effectParent = effectParent;
_playingEffect = "";
}
public void Term()
{
foreach (KeyValuePair<string, ParticleSystem> effect in _effectList)
{
ParticleSystem value = effect.Value;
if (!(null == value))
{
Object.Destroy(value.gameObject);
}
}
_effectList.Clear();
_playingEffect = "";
}
public void LoadEffect(List<StoryChapterData> chapterDataList)
{
_isLoadEnd = false;
string path = "";
int i = 0;
for (int count = chapterDataList.Count; i < count; i++)
{
path = chapterDataList[i].ChapterEffectPath;
if (!string.IsNullOrEmpty(path) && !_effectList.ContainsKey(path))
{
_effectList.Add(path, null);
_loadedResources.Add(Toolbox.ResourcesManager.GetAssetTypePath(path, ResourcesManager.AssetLoadPathType.Effect2D));
}
}
_areaSelectUI.StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(_loadedResources, delegate
{
List<GameObject> list = new List<GameObject>(_effectList.Count);
int j = 0;
for (int count2 = chapterDataList.Count; j < count2; j++)
{
path = chapterDataList[j].ChapterEffectPath;
if (!string.IsNullOrEmpty(path) && !(null != _effectList[path]))
{
GameObject gameObject = Toolbox.ResourcesManager.LoadObject(Toolbox.ResourcesManager.GetAssetTypePath(path, ResourcesManager.AssetLoadPathType.Effect2D, isfetch: true)) as GameObject;
if (!(null == gameObject))
{
_effectList[path] = Object.Instantiate(gameObject).GetComponent<ParticleSystem>();
_effectList[path].transform.parent = _effectParent;
_effectList[path].transform.localPosition = Vector3.zero;
_effectList[path].transform.localScale = EFFECT_SCALE;
_effectList[path].gameObject.SetActive(value: false);
list.Add(_effectList[path].gameObject);
}
}
}
_loadedResources.AddRange(GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list, delegate
{
_isLoadEnd = true;
}));
}));
}
public void UnLoadEffect()
{
Toolbox.ResourcesManager.RemoveAssetGroup(_loadedResources);
_loadedResources.Clear();
}
public void PlayEffect(string path, Vector3 pos)
{
if (!string.IsNullOrEmpty(path) && !(_playingEffect == path) && _effectList.ContainsKey(path) && !(null == _effectList[path]))
{
_effectList[path].gameObject.SetActive(value: true);
_effectList[path].Play();
_effectList[path].transform.localPosition = pos;
_playingEffect = path;
SetParticleSystemsSpeed(1f);
}
}
public void StopEffect(float? simulationSpeedAfterStop)
{
if (_effectList.ContainsKey(_playingEffect) && !(null == _effectList[_playingEffect]))
{
if (simulationSpeedAfterStop.HasValue)
{
SetParticleSystemsSpeed(simulationSpeedAfterStop.Value);
}
_effectList[_playingEffect].Stop();
_playingEffect = "";
}
}
public string GetPlayingEffect()
{
return _playingEffect;
}
private void SetParticleSystemsSpeed(float speed)
{
ParticleSystem.MainModule main = _effectList[_playingEffect].main;
main.simulationSpeed = speed;
ParticleSystem[] componentsInChildren = _effectList[_playingEffect].GetComponentsInChildren<ParticleSystem>();
for (int i = 0; i < componentsInChildren.Length; i++)
{
ParticleSystem.MainModule main2 = componentsInChildren[i].main;
main2.simulationSpeed = speed;
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public class AreaSelectEffectControlBase : MonoBehaviour
{
[NonSerialized]
public Dictionary<int, ParticleSystem> _backGroundEffects = new Dictionary<int, ParticleSystem>();
[NonSerialized]
public readonly int BASE_EFFECT_INDEX = -1;
protected bool IsSetupEnd { get; private set; }
public virtual void SetupEnd()
{
IsSetupEnd = true;
}
public virtual void OnChangeSelectChapter(AreaSelectBG areaSelectBG, StorySectionData sectionData, int? sectionClassId, StoryChapterData chapterData, bool isFirstClear)
{
}
public virtual void SetActiveBGEffect(bool effect)
{
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class AreaSelectMapIcon
{
public const int MAPICONLIST_CAPACITY_DEFAULT = 8;
private readonly string MAP_ICON_EFFECT_NAME_CIRCLE4 = "ef_circle4_add_nor_1";
private readonly string MAP_ICON_EFFECT_NAME_TWINKLE1 = "ef_twinkle1_add_nor_1";
private readonly Color32 MAP_ICON_EFFECT_COLOR_CLEARED = new Color32(byte.MaxValue, 192, 64, byte.MaxValue);
private readonly Color32 MAP_ICON_EFFECT_COLOR_ALREADY_READ_CIRCLE4 = new Color32(78, 95, 125, byte.MaxValue);
private readonly Color32 MAP_ICON_EFFECT_COLOR_ALREADY_READ_TWINKLE1 = new Color32(190, 218, 242, byte.MaxValue);
[SerializeField]
private GameObject MapIconRoot;
[SerializeField]
private UISprite MapIconOriginal;
private List<UISprite> MapIconList;
private GameObject MapIconEffect;
public void Init()
{
}
public void Term()
{
MapIconEffect = null;
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
}
public void SetupMapIcon(List<StoryChapterData> chapterDataList)
{
if (MapIconList != null)
{
int i = 0;
for (int count = MapIconList.Count; i < count; i++)
{
UnityEngine.Object.Destroy(MapIconList[i].gameObject);
}
}
MapIconList = new List<UISprite>(8);
UISprite uISprite = null;
for (int j = 0; j < chapterDataList.Count; j++)
{
if (chapterDataList[j].IsReleased)
{
uISprite = UnityEngine.Object.Instantiate(MapIconOriginal);
if (!(null == uISprite))
{
uISprite.transform.parent = MapIconRoot.transform;
uISprite.transform.localPosition = new Vector3(chapterDataList[j].MapIconPos.x, chapterDataList[j].MapIconPos.y);
uISprite.transform.localScale = Vector3.one;
uISprite.name = "mapicon_" + j;
uISprite.gameObject.SetActive(chapterDataList[j].IsDisplayMapIcon);
MapIconList.Add(uISprite);
}
}
}
MapIconOriginal.gameObject.SetActive(value: false);
}
public Vector3 GetMapIconPos(int chapterIndex, bool isLocal)
{
if (MapIconList == null)
{
return Vector3.zero;
}
if (chapterIndex < 0 || chapterIndex >= MapIconList.Count)
{
return Vector3.zero;
}
if (!isLocal)
{
return MapIconList[chapterIndex].transform.position;
}
return MapIconList[chapterIndex].transform.localPosition;
}
public void SetActiveMapIcon(int chapterIndex, bool isActive)
{
if (chapterIndex >= 0 && chapterIndex < MapIconList.Count)
{
MapIconList[chapterIndex].gameObject.SetActive(isActive);
}
}
public void StartMapIconEffect(StoryChapterData.ChapterClearStatus clearState, int chapterIndex)
{
Effect effect = null;
switch (clearState)
{
case StoryChapterData.ChapterClearStatus.Cleared:
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
effect.ChangeParticleColor(MAP_ICON_EFFECT_COLOR_CLEARED);
break;
case StoryChapterData.ChapterClearStatus.AlreadyRead:
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
MotionUtils.ChangeParticleSystemColor(effect.transform.Find(MAP_ICON_EFFECT_NAME_CIRCLE4).gameObject, MAP_ICON_EFFECT_COLOR_ALREADY_READ_CIRCLE4);
MotionUtils.ChangeParticleSystemColor(effect.transform.Find(MAP_ICON_EFFECT_NAME_TWINKLE1).gameObject, MAP_ICON_EFFECT_COLOR_ALREADY_READ_TWINKLE1);
break;
default:
effect = GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
break;
}
MapIconEffect = effect.GetGameObjIns();
UpdateMapIconEffectPos(chapterIndex);
}
public void StopMapIconEffect()
{
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_CLEARED);
GameMgr.GetIns().GetEffectMgr().Stop(EffectMgr.EffectType.CMN_MAP_MAPICON_NOTCLEARED);
}
public void UpdateMapIconEffectPos(int chapterIndex)
{
if (MapIconList != null && chapterIndex >= 0 && chapterIndex < MapIconList.Count && !(null == MapIconEffect))
{
Vector3 position = MapIconList[chapterIndex].transform.position;
MapIconEffect.transform.position = position;
}
}
public GameObject GetMapIconObject(int chapterIndex)
{
return MapIconList[chapterIndex].gameObject;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
using UnityEngine;
public class AreaSelectUtility
{
public const string BTN_IMAGE_NAME_SUFFIX_OFF = "{0}_off";
public const string BTN_IMAGE_NAME_SUFFIX_ON = "{0}_on";
public static readonly string ChapterSelectBtnPathPrefix = "btn_story_select";
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_GRADIENT_TOP = new Color32(byte.MaxValue, 245, 161, byte.MaxValue);
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_GRADIENT_BUTTOM = new Color32(byte.MaxValue, 209, 71, byte.MaxValue);
public static readonly Color32 CLEAR_LABEL_COLOR_CLEARD_EFFECT_OUTLINE8 = new Color32(94, 67, 31, byte.MaxValue);
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_TOP = new Color32(245, 249, byte.MaxValue, byte.MaxValue);
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_BUTTOM = new Color32(190, 218, 242, byte.MaxValue);
public static readonly Color32 CLEAR_LABEL_COLOR_ALREADY_READ_EFFECT_OUTLINE8 = new Color32(60, 73, 96, byte.MaxValue);
public static void SetClearLabelColor(UILabel clearLabel, StoryChapterData.ChapterClearStatus clearState)
{
switch (clearState)
{
case StoryChapterData.ChapterClearStatus.Cleared:
clearLabel.gradientTop = CLEAR_LABEL_COLOR_CLEARD_GRADIENT_TOP;
clearLabel.gradientBottom = CLEAR_LABEL_COLOR_CLEARD_GRADIENT_BUTTOM;
clearLabel.effectColor = CLEAR_LABEL_COLOR_CLEARD_EFFECT_OUTLINE8;
break;
case StoryChapterData.ChapterClearStatus.AlreadyRead:
clearLabel.gradientTop = CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_TOP;
clearLabel.gradientBottom = CLEAR_LABEL_COLOR_ALREADY_READ_GRADIENT_BUTTOM;
clearLabel.effectColor = CLEAR_LABEL_COLOR_ALREADY_READ_EFFECT_OUTLINE8;
break;
}
}
}

View File

@@ -0,0 +1,346 @@
using System.Collections.Generic;
using UnityEngine;
using Wizard;
using Wizard.Scripts.Network.Data.TableData.Arena.TwoPick;
using Wizard.Scripts.Network.Data.TaskData.Arena.TwoPick;
public class ArenaColosseum : ArenaEntryDataBase
{
public enum eRound
{
FinalNotAdvance = -1,
Round1 = 1,
Round2B = 2,
Round2A = 3,
FinalB = 4,
FinalA = 5,
FinalMin = 4,
RoundMax = 5,
Undecided = 6,
Lose = 7
}
public enum eStageNo
{
Stage1 = 1,
Stage2,
FinalStage,
Max
}
public enum eEntryStatus
{
TwoPickClassSelect = 1,
TwoPickCardSelect,
SetUpComplete
}
public enum eRule
{
NONE = 0,
RotationBo1 = 1,
UnlimitedBo1 = 2,
TwoPick = 3,
TwoPickChaos = 4,
Crossover = 5,
MyRotation = 6,
HOF = 31,
WindFall = 33,
Avatar = 39
}
public enum eDeckIndex
{
Main = 0,
First = 0,
Second = 1,
Third = 2
}
public struct Detail
{
public string RoundTimeText { get; set; }
public string RoundTimeStartText { get; set; }
public string RoundTimeEndText { get; set; }
public string GroupName { get; set; }
public int MaxBattleNum { get; set; }
public int BreakThroughNum { get; set; }
public int MaxEntryNum { get; set; }
}
public class TwoPick
{
public CandidateClass CandidateClass { get; set; }
public CandidateCardInfo CandidateCard { get; set; }
public Deck DeckData { get; set; }
public CandidateChaos CandidateChaos { get; set; }
}
public enum eResultEffect
{
None,
GroupA,
Final,
Clear
}
private bool _isRankMatching;
public bool CanUseNonPossessionCard;
public int DeckEntryId { get; set; }
public bool IsColosseumPeriod { get; set; }
public bool IsRoundPeriod { get; set; }
public eEntryStatus EntryStatus { get; set; }
public Format DeckFormat { get; set; }
public eRule Rule { get; set; }
public bool IsNormalTwoPick { get; set; }
public int ChaosNum { get; set; }
public bool IsTwoPickRule
{
get
{
if (Rule != eRule.TwoPick)
{
return Rule == eRule.TwoPickChaos;
}
return true;
}
}
public bool NeedsFirstTips { get; set; }
public int ColosseumId { get; set; }
public int ChaoseTipsId { get; set; }
public bool IsSpecialDeckSelectRule
{
get
{
if (Rule != eRule.HOF)
{
return Rule == eRule.WindFall;
}
return true;
}
}
public bool IsDeckMaxNumberChange => Rule == eRule.WindFall;
public int DeckMaxNumber
{
get
{
if (Rule == eRule.WindFall)
{
return 35;
}
return 40;
}
}
public bool IsRankMatching
{
get
{
return _isRankMatching;
}
set
{
if (_isRankMatching != value)
{
_isRankMatching = value;
if (RealTimeNetworkAgent.FinishTaskBase != null)
{
RealTimeNetworkAgent.FinishTaskBase = new ColosseumBattleFinishTask();
}
}
}
}
public List<DeckData> DeckList { get; set; }
public eRound Round { get; set; }
public int ServerRoundId { get; set; }
public eStageNo StageNo { get; set; }
public string Name { get; set; }
public List<ReceivedReward> RewardList { get; set; }
public eResultEffect ResultEffect { get; set; }
public string ColorCodeId { get; set; }
public string CardPool { get; set; }
public int RetryRemainingNum { get; set; }
public int BattleMax { get; set; }
public int ClearWinNum { get; set; }
public bool IsDeckEntry { get; set; }
public bool IsFreeEntry
{
get
{
return Data.MyPageNotifications.data.IsColosseumFreeEntry;
}
set
{
Data.MyPageNotifications.data.IsColosseumFreeEntry = value;
}
}
public bool IsRetry { get; set; }
public bool IsLastDay { get; set; }
public bool IsClear { get; set; }
public bool IsRetire { get; set; }
public bool IsFinish { get; set; }
public bool IsDeckDeleted { get; set; }
public bool IsFinalRoundTry { get; set; }
public eRound NextRound { get; set; }
public double RemainingUnixTime { get; set; }
public float RemainingSinceTime { get; set; }
public double RemainingServerUnixTime { get; set; }
public string NextRoundStartTimeText { get; set; }
public string NowRoundTimeText { get; set; }
public string ColosseumTimeText { get; set; }
public string AnnounceNo { get; set; }
public Detail[] DetailData { get; set; }
public eStageNo FocusStageNo { get; set; }
public int WinBattleNum { get; set; }
public List<bool> BattleResultList { get; set; }
public List<int> BoxGradeList { get; set; }
public int FinalRoundEliminateCount { get; set; }
public TwoPick TwoPickData { get; set; }
public ArenaColosseum()
{
base.LootBoxType = PlayerStaticData.LootBoxType.COLOSSEUM;
DeckList = new List<DeckData>();
RewardList = new List<ReceivedReward>();
BattleResultList = new List<bool>();
BoxGradeList = new List<int>();
DetailData = new Detail[5];
Rule = eRule.TwoPick;
TwoPickData = new TwoPick();
TwoPickData.CandidateClass = new CandidateClass();
TwoPickData.CandidateCard = new CandidateCardInfo();
TwoPickData.CandidateChaos = new CandidateChaos();
}
public int GetRoundNumber(eRound inRound)
{
switch (inRound)
{
case eRound.Round1:
return 1;
case eRound.Round2B:
case eRound.Round2A:
return 2;
default:
return 0;
}
}
public string GetGroupText(eRound inRound)
{
switch (inRound)
{
case eRound.Round2A:
case eRound.FinalA:
return Data.SystemText.Get("Colosseum_0020");
case eRound.Round2B:
case eRound.FinalB:
return Data.SystemText.Get("Colosseum_0021");
default:
return "";
}
}
public eStageNo GetStageNoFromRoundId(eRound inRoundId)
{
switch (inRoundId)
{
case eRound.Round1:
return eStageNo.Stage1;
case eRound.Round2B:
case eRound.Round2A:
return eStageNo.Stage2;
case eRound.FinalB:
case eRound.FinalA:
return eStageNo.FinalStage;
default:
return eStageNo.Stage1;
}
}
public bool IsFinalRound()
{
if (Round == eRound.FinalA || Round == eRound.FinalB)
{
return true;
}
return false;
}
public DialogBase CreateDetailDialog(GameObject defaultDetailPrefab)
{
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
GameObject gameObject = Object.Instantiate(defaultDetailPrefab);
dialogBase.SetObj(gameObject);
gameObject.GetComponent<ColosseumDetail>().Init(dialogBase);
return dialogBase;
}
public void ApiRuleParseAndSet(int apiRule)
{
ArenaColosseum colosseumData = Data.ArenaData.ColosseumData;
colosseumData.Rule = (eRule)apiRule;
colosseumData.DeckFormat = ArenaData.ApiDeckFormatParse(colosseumData.Rule);
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using LitJson;
using UnityEngine;
using Wizard;
using Wizard.Scripts.Network.Data.TaskData.Arena;
public class ArenaCompetition : ArenaEntryDataBase
{
public enum EntryStatusType
{
NotEntry,
NotChallenge,
NotRegistDeck,
InBattle
}
public enum FreebieStatusType
{
InFreeBattle,
CanPermanentEntry,
PermanentEntryDone
}
public enum EntryCostType
{
EntryWithFree,
EntryWithCost
}
private bool _isRankMatching;
public bool IsCompetitionPeriod { get; private set; }
public bool IsEntry { get; set; }
public bool IsInFreeBattleRegistDeck { get; set; }
public bool NeedsFirstTips { get; private set; }
public int CompetitionId { get; private set; }
public FreebieStatusType FreebieStatus { get; set; }
public Format DeckFormat { get; private set; }
public ArenaColosseum.eRule Rule { get; private set; }
public bool IsSpecialMode { get; private set; }
public string NowRoundTimeText { get; private set; }
public string EntryEndTimeText { get; private set; }
public string EndTimeText { get; private set; }
public double EntryRemainingUnixTime { get; set; }
public double RemainingUnixTime { get; set; }
public float RemainingSinceTime { get; set; }
public double RemainingServerUnixTime { get; set; }
public string EntryTimeText { get; private set; }
public List<DeckData> DeckList { get; set; }
public List<Wizard.Scripts.Network.Data.TaskData.Arena.Reward> EntryRewardList { get; set; }
public bool IsRewardReceived { get; private set; }
public string AnnounceId { get; private set; }
public string CompetitionName { get; private set; }
public int MaxEntryCount { get; private set; }
public int MaxChallengeCount { get; private set; }
public int MaxWinCount { get; private set; }
public int BestWinCount { get; private set; }
public int MaxLoseCount { get; private set; }
public int RestChallangeCount { get; private set; }
public int RestEntryCount { get; private set; }
public int CurrentWinCount { get; private set; }
public int FreebieChallengeCount { get; private set; }
public bool IsChampion { get; private set; }
public bool IsEntryTimeEnd { get; private set; }
public int MaxBattleCount { get; private set; }
public int IsCompletedTwoPickDeck { get; private set; }
public int MaxFreebieChallengeCount { get; private set; }
public EntryStatusType EntryStatus { get; private set; }
public EntryCostType CostType { get; private set; }
public bool IsRankMatching
{
get
{
return _isRankMatching;
}
set
{
if (_isRankMatching != value)
{
_isRankMatching = value;
if (RealTimeNetworkAgent.FinishTaskBase != null)
{
RealTimeNetworkAgent.FinishTaskBase = new CompetitionBattleFinishTask();
}
}
}
}
public ArenaCompetition()
{
}
public ArenaCompetition(JsonData responseData)
{
JsonData jsonData = responseData["data"]["competition_info"];
IsCompetitionPeriod = jsonData["is_competition_period"].ToBoolean();
if (IsCompetitionPeriod)
{
Rule = (ArenaColosseum.eRule)jsonData["deck_format"].ToInt();
DeckFormat = ArenaData.ApiDeckFormatParse(Rule);
IsEntry = jsonData["is_entry"].ToBoolean();
IsInFreeBattleRegistDeck = jsonData["is_in_battle"].ToBoolean();
IsSpecialMode = jsonData["is_special_mode"].ToInt() == 1;
string text = ConvertTime.ToLocal(DateTime.Parse(jsonData["entry_start_time"].ToString()));
EntryRemainingUnixTime = ConvertTime.DateTimeToUnixTime(DateTime.Parse(jsonData["entry_end_time"].ToString()));
string text2 = ConvertTime.ToLocal(DateTime.Parse(jsonData["entry_end_time"].ToString()));
EntryTimeText = Data.SystemText.Get("Colosseum_0033", text, text2);
EntryEndTimeText = text2;
string text3 = ConvertTime.ToLocal(DateTime.Parse(jsonData["start_time"].ToString()));
RemainingUnixTime = ConvertTime.DateTimeToUnixTime(DateTime.Parse(jsonData["end_time"].ToString()));
string text4 = ConvertTime.ToLocal(DateTime.Parse(jsonData["end_time"].ToString()));
NowRoundTimeText = Data.SystemText.Get("Colosseum_0033", text3, text4);
EndTimeText = text4;
RemainingSinceTime = Time.realtimeSinceStartup;
RemainingServerUnixTime = responseData["data_headers"]["servertime"].ToDouble();
NeedsFirstTips = jsonData.GetValueOrDefault("is_display_tips", 0) == 1;
CompetitionId = jsonData.GetValueOrDefault("competition_id", 0);
FreebieStatus = (FreebieStatusType)jsonData["freebie_status"].ToInt();
DeckList = new List<DeckData>();
EntryRewardList = new List<Wizard.Scripts.Network.Data.TaskData.Arena.Reward>();
JsonData jsonData2 = jsonData["featured_entry_reward_list"];
for (int i = 0; i < jsonData2.Count; i++)
{
Wizard.Scripts.Network.Data.TaskData.Arena.Reward item = new Wizard.Scripts.Network.Data.TaskData.Arena.Reward(jsonData2[i]);
EntryRewardList.Add(item);
}
IsRewardReceived = jsonData["is_received_featured_entry_reward"].ToBoolean();
if (jsonData["announce_id"] != null)
{
AnnounceId = jsonData["announce_id"].ToString();
}
MaxEntryCount = jsonData.GetValueOrDefault("max_entry_count", 0);
MaxChallengeCount = jsonData.GetValueOrDefault("max_challenge_count", 0);
MaxWinCount = jsonData.GetValueOrDefault("max_win_count", 0);
MaxLoseCount = jsonData.GetValueOrDefault("max_lose_count", 0);
MaxBattleCount = jsonData.GetValueOrDefault("max_battle_count", 0);
MaxFreebieChallengeCount = jsonData["max_freebie_challenge_count"].ToInt();
crystalCost = jsonData.GetValueOrDefault("crystal_cost", 0);
rupyCost = jsonData.GetValueOrDefault("rupy_cost", 0);
BestWinCount = jsonData["max_win_count_in_entry"].ToInt();
RestChallangeCount = jsonData["rest_challenge_num"].ToInt();
RestEntryCount = jsonData["rest_entry_num"].ToInt();
CurrentWinCount = jsonData["current_win_count"].ToInt();
FreebieChallengeCount = jsonData["freebie_challenge_count"].ToInt();
EntryStatus = (EntryStatusType)jsonData["entry_status"].ToInt();
CostType = (EntryCostType)jsonData["entry_type"].ToInt();
IsChampion = jsonData.GetValueOrDefault("is_champion", 0) == 1;
CompetitionName = jsonData.GetValueOrDefault("competition_name", string.Empty).Replace("\\n", "\n");
double num = RemainingServerUnixTime + (double)Time.realtimeSinceStartup - (double)RemainingSinceTime;
IsEntryTimeEnd = EntryRemainingUnixTime - num < 0.0;
bool flag = CompetitionId <= PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.COMPETITION_JOIN_BUTTON_LATEST_ID);
Data.MyPageNotifications.data.IsCompetitionBadge = !IsRewardReceived && EntryStatus == EntryStatusType.NotEntry && !IsEntryTimeEnd && !flag;
base.ExpirtyInfo = new ShopExpirtyInfo(jsonData["sales_period_info"]);
if (DeckFormat == Format.TwoPick)
{
IsCompletedTwoPickDeck = jsonData["is_completed_two_pick_deck"].ToInt();
}
}
base.LootBoxType = PlayerStaticData.LootBoxType.COMPETITION;
}
public void SetRestChallangeCountByEntry(JsonData responseData)
{
RestChallangeCount = responseData["rest_challenge_count"].ToInt();
IsEntry = true;
}
}

View File

@@ -0,0 +1,80 @@
using LitJson;
using Wizard;
public class ArenaData : HeaderData
{
public enum eARENA_PAY
{
None = 0,
Crystal = 1,
Ticket = 3,
Rupy = 4,
Free = 5
}
public ArenaTwoPickData TwoPickData { get; set; }
public SealedData SealedData { get; private set; }
public SealedMyPageResponseData SealedMyPageResponseData { get; private set; }
public ArenaColosseum ColosseumData { get; set; }
public ArenaCompetition CompetitionData { get; set; }
public ArenaData()
{
SealedData = new SealedData();
ColosseumData = new ArenaColosseum();
CompetitionData = new ArenaCompetition();
}
public ArenaData(JsonData data)
: this()
{
if (data != null)
{
JsonData data2 = data[0];
TwoPickData = new ArenaTwoPickData(data2);
}
}
public void ClearSealedData()
{
SealedData = new SealedData();
}
public void SetSealedMyPageResponseData(JsonData rootData)
{
if (rootData.Keys.Contains("sealed_info"))
{
SealedMyPageResponseData = new SealedMyPageResponseData(rootData["sealed_info"]);
}
}
public static Format ApiDeckFormatParse(ArenaColosseum.eRule rule)
{
Format format = Format.Rotation;
switch (rule)
{
case ArenaColosseum.eRule.RotationBo1:
return Format.Rotation;
case ArenaColosseum.eRule.UnlimitedBo1:
return Format.Unlimited;
case ArenaColosseum.eRule.TwoPick:
case ArenaColosseum.eRule.TwoPickChaos:
return Format.TwoPick;
case ArenaColosseum.eRule.HOF:
case ArenaColosseum.eRule.WindFall:
return Format.Max;
case ArenaColosseum.eRule.Crossover:
return Format.Crossover;
case ArenaColosseum.eRule.MyRotation:
return Format.MyRotation;
case ArenaColosseum.eRule.Avatar:
return Format.Avatar;
default:
return Format.Max;
}
}
}

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections;
using UnityEngine;
using Wizard;
public abstract class ArenaEntryBase : MonoBehaviour
{
[SerializeField]
protected GameObject ArenaEntryDialog;
[SerializeField]
protected GameObject CompetitionEntryDialog;
[SerializeField]
protected UIButton ButtonEntry;
[SerializeField]
protected UIButton ButtonResume;
[SerializeField]
protected GameObject HeadLineObject;
private UIWidget[] _headlineWidgetArray;
private Color[] _headlineWidgetDefaultColorArray;
private Coroutine _initCoroutine;
protected bool _isFreeEntry;
protected bool _isCompetition;
protected string[] _labelsText;
protected string _entryDialogTitleText;
protected Action _initFunc;
protected Action _resumeFunc;
protected Func<bool> _isJoinFunc;
protected Action _freeEntryFunc;
protected Action _freeBattleFunc;
protected Func<bool> _isFreeBattleCompetition;
protected Action _entryFunc;
private const float GLAY_SCALE = 0.33f;
protected abstract void EntryDialogCreate(GameObject inDialog);
protected virtual void EntryBaseInit(GameObject costRootObject)
{
_headlineWidgetArray = costRootObject.transform.GetComponentsInChildren<UIWidget>();
_headlineWidgetDefaultColorArray = new Color[_headlineWidgetArray.Length];
for (int i = 0; i < _headlineWidgetArray.Length; i++)
{
_headlineWidgetDefaultColorArray[i] = _headlineWidgetArray[i].color;
}
}
private void OnEnable()
{
UpdateMenu();
}
private void OnDestroy()
{
if (_initCoroutine != null)
{
StopCoroutine(_initCoroutine);
}
}
public void UpdateMenu()
{
_initCoroutine = UIManager.GetInstance().StartCoroutine(_InitCoroutine());
}
protected virtual IEnumerator _InitCoroutine()
{
while (!MyPageMenu.IsMyPageRequestEnd)
{
yield return null;
}
if (_initFunc != null)
{
_initFunc();
}
ButtonEntry.onClick.Clear();
ButtonEntry.onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
if (_entryFunc != null)
{
_entryFunc();
}
else if (_isFreeEntry)
{
_freeEntryFunc();
}
else
{
DialogBase.Size size = ((Data.ArenaData.CompetitionData.CostType != ArenaCompetition.EntryCostType.EntryWithCost) ? DialogBase.Size.M : DialogBase.Size.XL);
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetSize(size);
dialogBase.SetTitleLabel(_entryDialogTitleText);
GameObject gameObject = UnityEngine.Object.Instantiate(_isCompetition ? CompetitionEntryDialog : ArenaEntryDialog);
EntryDialogCreate(gameObject);
if (_isCompetition && Data.ArenaData.CompetitionData.CostType == ArenaCompetition.EntryCostType.EntryWithCost)
{
PlayerPrefsWrapper.SetValue(PlayerPrefsWrapper.COMPETITION_JOIN_BUTTON_LATEST_ID, Data.ArenaData.CompetitionData.CompetitionId);
Data.MyPageNotifications.data.IsCompetitionBadge = false;
UIManager.GetInstance()._Footer.UpdateArenaBadgeIcon();
UpdateCompetitionEntryBadge();
}
gameObject.GetComponent<ArenaEntryDialogBase>().ParentDialog = dialogBase;
dialogBase.SetObj(gameObject.gameObject);
DialogBase.ButtonLayout buttonLayout = DialogBase.ButtonLayout.CloseBtn;
dialogBase.SetButtonLayout(buttonLayout);
}
}));
ButtonResume.onClick.Clear();
ButtonResume.onClick.Add(new EventDelegate(delegate
{
_resumeFunc();
}));
UpdateEntryResumeButton();
}
protected virtual void UpdateCompetitionEntryBadge()
{
}
protected virtual void UpdateEntryResumeButton()
{
bool flag = _isJoinFunc();
ButtonEntry.gameObject.SetActive(!flag);
ButtonResume.gameObject.SetActive(flag);
if (flag)
{
for (int i = 0; i < _headlineWidgetArray.Length; i++)
{
_headlineWidgetArray[i].color = new Color(_headlineWidgetDefaultColorArray[i].r * 0.33f, _headlineWidgetDefaultColorArray[i].g * 0.33f, _headlineWidgetDefaultColorArray[i].b * 0.33f, 255f);
}
}
else
{
for (int j = 0; j < _headlineWidgetArray.Length; j++)
{
_headlineWidgetArray[j].color = _headlineWidgetDefaultColorArray[j];
}
}
}
}

View File

@@ -0,0 +1,16 @@
using Wizard;
public abstract class ArenaEntryDataBase
{
public bool isJoin;
public int crystalCost;
public int rupyCost;
public int ticketCost;
public ShopExpirtyInfo ExpirtyInfo { get; set; }
public PlayerStaticData.LootBoxType LootBoxType { get; set; }
}

View File

@@ -0,0 +1,175 @@
using UnityEngine;
using Wizard;
public abstract class ArenaEntryDialogBase : MonoBehaviour
{
protected Se.TYPE _entryButtonSe = Se.TYPE.SYS_BTN_DECIDE_TRANS;
private const int CHECK_DIALOG_DEPTH = 10;
private ArenaEntryDataBase _entryData;
private ArenaEntryDialogData _dialogData;
protected ArenaData.eARENA_PAY _payType;
public bool IsCompetition;
protected string _mainTextId;
protected string _ticketSpriteName;
protected string _arenaNameTextId;
public DialogBase ParentDialog { get; set; }
protected abstract void Init();
protected abstract int GetTicketNum();
protected abstract ArenaEntryDataBase GetEntryData();
private void Start()
{
Init();
_entryData = GetEntryData();
_dialogData = GetComponent<ArenaEntryDialogData>();
SystemText systemText = Data.SystemText;
if (_dialogData._mainText != null)
{
_dialogData._mainText.text = systemText.Get(_mainTextId);
}
if (_dialogData._ticketHaveTitle != null)
{
_dialogData._ticketHaveTitle.text = systemText.Get("Arena_0037");
_dialogData._ticketHaveNum.text = GetTicketNum().ToString();
_dialogData._ticketHaveUnit.text = systemText.Get("Common_0117");
_dialogData._ticketButtonTitle.text = systemText.Get("Arena_0004");
_dialogData._ticketButtonSubTitle.text = systemText.Get("Arena_0038");
_dialogData._ticketButtonUseNum.text = _entryData.ticketCost.ToString();
_dialogData._ticketSprite.spriteName = _ticketSpriteName;
_dialogData._ticketButton.onClick.Add(new EventDelegate(delegate
{
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
{
OnClickTicketEntryButton();
}
}));
}
if (_dialogData._rupyHaveTitle != null)
{
_dialogData._rupyHaveTitle.text = systemText.Get("Shop_0065");
_dialogData._rupyHaveNum.text = PlayerStaticData.UserRupyCount.ToString();
_dialogData._rupyHaveUnit.text = systemText.Get("Common_0120");
_dialogData._rupyButtonTitle.text = (IsCompetition ? systemText.Get("Competition_0067") : systemText.Get("Arena_0036"));
_dialogData._rupyButtonSubTitle.text = systemText.Get("Shop_0062");
_dialogData._rupyButtonUseNum.text = _entryData.rupyCost.ToString();
_dialogData._rupyButton.onClick.Add(new EventDelegate(delegate
{
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
{
OnClickRupyEntryButton();
}
}));
}
_dialogData._crystalHaveTitle.text = systemText.Get("Shop_0064");
_dialogData._crystalHaveNum.text = PlayerStaticData.UserCrystalCount.ToString();
_dialogData._crystalHaveUnit.text = systemText.Get("Common_0116");
_dialogData._crystalButtonTitle.text = (IsCompetition ? systemText.Get("Competition_0046") : systemText.Get("Arena_0023"));
_dialogData._crystalButtonSubTitle.text = systemText.Get("Shop_0061");
_dialogData._crystalButtonUseNum.text = _entryData.crystalCost.ToString();
if (_entryData.ticketCost > GetTicketNum())
{
SetButtonDisable(_dialogData._ticketButton, _dialogData._ticketButtonTitle);
}
if (_entryData.rupyCost > PlayerStaticData.UserRupyCount)
{
SetButtonDisable(_dialogData._rupyButton, _dialogData._rupyButtonTitle);
}
_dialogData._crystalButton.onClick.Add(new EventDelegate(delegate
{
if (ParentDialog.GetNowScene() == DialogBase.DialogScene.WAIT)
{
OnClickCrystalEntryButton();
}
}));
}
private void SetButtonDisable(UIButton in_Button, UILabel in_Label)
{
in_Button.GetComponent<UIButton>().isEnabled = false;
in_Label.color = LabelDefine.TEXT_COLOR_BUTTON_DISABLE;
in_Button.GetComponent<TweenColor>().duration = 0f;
}
private void OnClickTicketEntryButton()
{
int ticketNum = GetTicketNum();
SystemText systemText = Data.SystemText;
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
dialogBase.SetPanelDepth(10);
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
dialogBase.SetObj(component.gameObject);
component.SetTicketConfirmDialog(_entryData.ticketCost, ticketNum, _arenaNameTextId, _ticketSpriteName);
dialogBase.onPushButton1 = Entry;
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
dialogBase.SetButtonText(systemText.Get("Dia_Arena_003_Button"));
dialogBase.ClickSe_Btn1 = _entryButtonSe;
_payType = ArenaData.eARENA_PAY.Ticket;
}
private void OnClickCrystalEntryButton()
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
if (PlayerStaticData.IsLootBoxRegulation(_entryData.LootBoxType))
{
LootBoxDialogUtility.CreateLootBoxRegulationDialog(_entryData.LootBoxType);
return;
}
if (_entryData.crystalCost > PlayerStaticData.UserCrystalCount)
{
ShopCommonUtility.CreateCrystalShortagePopup();
return;
}
int userCrystalCount = PlayerStaticData.UserCrystalCount;
SystemText systemText = Data.SystemText;
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
dialogBase.SetPanelDepth(10);
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
dialogBase.SetObj(component.gameObject);
component.SetClystalConfirmDialog(_entryData.crystalCost, userCrystalCount, _arenaNameTextId, _entryData.ExpirtyInfo);
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
string text_btn = (IsCompetition ? systemText.Get("Competition_0036") : systemText.Get("Dia_Arena_004_Button"));
dialogBase.SetButtonText(text_btn);
dialogBase.ClickSe_Btn1 = _entryButtonSe;
dialogBase.onPushButton1 = Entry;
_payType = ArenaData.eARENA_PAY.Crystal;
}
private void OnClickRupyEntryButton()
{
int userRupyCount = PlayerStaticData.UserRupyCount;
SystemText systemText = Data.SystemText;
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_DECIDE);
DialogBase dialogBase = UIManager.GetInstance().CreateDialogClose();
dialogBase.SetTitleLabel(systemText.Get("Arena_0005"));
dialogBase.SetPanelDepth(10);
ArenaBuyDialog component = Object.Instantiate(_dialogData.BuyDialogObject).GetComponent<ArenaBuyDialog>();
dialogBase.SetObj(component.gameObject);
component.SetRupyConfirmDialog(_entryData.rupyCost, userRupyCount, _arenaNameTextId);
dialogBase.onPushButton1 = Entry;
dialogBase.SetButtonLayout(DialogBase.ButtonLayout.BlueBtn_CancelBtn);
string text_btn = (IsCompetition ? systemText.Get("Competition_0036") : systemText.Get("Dia_Arena_005_Button"));
dialogBase.SetButtonText(text_btn);
dialogBase.ClickSe_Btn1 = _entryButtonSe;
_payType = ArenaData.eARENA_PAY.Rupy;
}
protected virtual void Entry()
{
ParentDialog.CloseWithoutSelect();
}
}

View File

@@ -0,0 +1,52 @@
using UnityEngine;
public class ArenaEntryDialogData : MonoBehaviour
{
public GameObject BuyDialogObject;
public UILabel _mainText;
public UISprite _ticketSprite;
public UIButton _ticketButton;
public UIButton _rupyButton;
public UIButton _crystalButton;
public UILabel _ticketHaveTitle;
public UILabel _ticketHaveNum;
public UILabel _ticketHaveUnit;
public UILabel _ticketButtonTitle;
public UILabel _ticketButtonSubTitle;
public UILabel _ticketButtonUseNum;
public UILabel _rupyHaveTitle;
public UILabel _rupyHaveNum;
public UILabel _rupyHaveUnit;
public UILabel _rupyButtonTitle;
public UILabel _rupyButtonSubTitle;
public UILabel _rupyButtonUseNum;
public UILabel _crystalHaveTitle;
public UILabel _crystalHaveNum;
public UILabel _crystalHaveUnit;
public UILabel _crystalButtonTitle;
public UILabel _crystalButtonSubTitle;
public UILabel _crystalButtonUseNum;
}

View File

@@ -0,0 +1,80 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArenaField : BackGroundBase
{
public override int FieldId => 9;
public ArenaField(string bgmId = "NONE")
: base(bgmId)
{
}
protected override void BattleFieldBuild()
{
BattleCoroutine.GetInstance().StartCoroutine(BackGroundBase.ObjectChecker(0.5f, _str3DFieldPath, delegate
{
base.Field = GameObject.Find(_str3DFieldPath);
base.Field.transform.parent = GameMgr.GetIns().m_GameManagerObj.transform;
GimicAudioList = base.Field.GetComponent<AudioList>().GimicAudioList;
_fieldModel = base.Field.transform.Find("md_bf_arna_root").gameObject;
_fieldParticles = _fieldModel.transform.Find("Particles09").gameObject;
_fieldObjDictionary.Add(_fieldParticles.name, _fieldParticles);
_fieldParticleSystemDictionary.Add("opening", _fieldParticles.transform.Find("opening").GetComponent<ParticleSystem>());
List<string> list = new List<string>(_fieldObjDictionary.Keys);
List<GameObject> list2 = new List<GameObject>();
for (int i = 0; i < _fieldObjDictionary.Count; i++)
{
list2.Add(_fieldObjDictionary[list[i]]);
}
GameMgr.GetIns().GetEffectMgr().SetUIParticleShader(list2, delegate
{
base.SetShaderGlobalColorBG = base.Field.transform.Find("SetMaterialColorBGManager").GetComponent<SetShaderGlobalColorBG>();
base.IsLoadDone = true;
}, isBattle: true, isField: true);
}));
}
public override void StartFieldSetEffect(Vector3 pos)
{
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_SET_9, pos);
}
public override void StartFieldTapEffect(int areaId, Vector3 pos)
{
base.StartFieldTapEffect(areaId, pos);
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_FIELD_TAP_9_1, pos);
}
protected override IEnumerator RunFieldOpening()
{
GameMgr.GetIns().GetSoundMgr().PlaySeByStr($"se_field_{_str3DFieldNo}_appear_1", "se_field_" + _str3DFieldNo, 0f, 0L);
_fieldParticleSystemDictionary["opening"].Play();
_battleCamera.Camera.transform.localPosition = new Vector3(2700f, -880f, 300f);
_battleCamera.Camera.transform.localRotation = Quaternion.Euler(new Vector3(-19f, -90f, 90f));
Vector3[] bezierCubic = MotionUtils.GetBezierCubic(new Vector3(2700f, -880f, 300f), new Vector3(1700f, -550f, -220f), new Vector3(700f, -200f, 20f), new Vector3(-240f, -190f, -70f), 10);
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("path", bezierCubic, "movetopath", false, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", new Vector3(-37f, -117f, 107f), "time", 1f, "delay", 1f, "islocal", true, "easetype", iTween.EaseType.easeInOutQuad));
yield return new WaitForSeconds(2f);
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_CAMERA_ZOOM_OUT);
iTween.MoveTo(_battleCamera.Camera.gameObject, iTween.Hash("position", _battleCamera.BattleCameraPos, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
iTween.RotateTo(_battleCamera.Camera.gameObject, iTween.Hash("rotation", _battleCamera.BattleCameraRot, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeInOutExpo));
yield return new WaitForSeconds(0f);
}
protected override IEnumerator RunFieldGimic(GameObject obj)
{
string tag = obj.tag;
if (tag != null && tag == "FieldGimic1")
{
_ = _gimicCntDictionary[obj.tag];
}
yield return new WaitForSeconds(0f);
}
protected override IEnumerator RunFieldShake()
{
yield return new WaitForSeconds(0f);
}
}

View File

@@ -0,0 +1,101 @@
using Cute;
using UnityEngine;
using Wizard;
public class ArenaNextSceneSelector : INextSceneSelector
{
private BattleResultUIController m_battleResultNewControl;
private bool _movingToMyPage;
public ArenaNextSceneSelector(BattleResultUIController battleResultControl)
{
m_battleResultNewControl = battleResultControl;
_movingToMyPage = false;
}
public void Setup(bool isWin, GameObject gameObject)
{
if (m_battleResultNewControl.ResultMsgReportBtnFlag)
{
m_battleResultNewControl.ReportBtnObj.labels[0].text = Data.SystemText.Get("Con_Management_001_Button");
m_battleResultNewControl.ReportBtnObj.gameObject.SetActive(value: true);
m_battleResultNewControl.ReportBtnObj.buttons[0].onClick.Clear();
m_battleResultNewControl.ReportBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
{
ConsistencyReportButtonAction.CreateReportConfirmWindow();
}));
}
m_battleResultNewControl.MissionBtnObj.labels[0].text = Data.SystemText.Get("Battle_0200");
m_battleResultNewControl.MissionBtnObj.buttons[0].onClick.Clear();
m_battleResultNewControl.MissionBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
UIManager.GetInstance().createInSceneCenterLoading();
MissionInfoTask missionInfoTask = GameMgr.GetIns().GetMissionInfoTask();
missionInfoTask.SetParameter();
m_battleResultNewControl.StartCoroutine(Toolbox.NetworkManager.Connect(missionInfoTask, delegate
{
m_battleResultNewControl.CreateMissionList();
}, BaseTask.OnRequestFailed, BaseTask.OnFailedErrorCode));
}));
m_battleResultNewControl.HomeBtnObj.labels[0].text = Data.SystemText.Get("Battle_0202");
m_battleResultNewControl.HomeBtnObj.buttons[0].onClick.Clear();
m_battleResultNewControl.HomeBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
{
MoveToMyPage();
}));
if (GameMgr.GetIns().GetDataMgr().IsColosseumBattleType())
{
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Battle_0489");
}
else if (GameMgr.GetIns().GetDataMgr().IsCompetitionBattleType())
{
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Competition_0021");
}
else if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.Sealed)
{
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Sealed_BattleResult_0001");
}
else
{
m_battleResultNewControl.RetryBtnObj.labels[0].text = Data.SystemText.Get("Battle_0203");
}
m_battleResultNewControl.RetryBtnObj.buttons[0].onClick.Clear();
m_battleResultNewControl.RetryBtnObj.buttons[0].onClick.Add(new EventDelegate(delegate
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_COMMON_BUTTON);
if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.TwoPick)
{
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.TwoPick);
}
else if (GameMgr.GetIns().GetDataMgr().m_BattleType == DataMgr.BattleType.Sealed)
{
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.Sealed);
}
else if (GameMgr.GetIns().GetDataMgr().IsCompetitionBattleType())
{
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.CompetitionLobby);
}
else
{
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.Colosseum);
}
}));
}
public void Show()
{
iTween.MoveTo(m_battleResultNewControl.ButtonGrid.gameObject, iTween.Hash("position", m_battleResultNewControl.DefaultPosDict["ButtonGrid"], "time", 0.5f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
}
private void MoveToMyPage()
{
if (!_movingToMyPage)
{
_movingToMyPage = true;
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_BTN_CANCEL_TRANS);
GameMgr.GetIns().GetBattleCtrl().BattleEnd(UIManager.ViewScene.MyPage);
}
}
}

View File

@@ -0,0 +1,204 @@
using System.Collections;
using UnityEngine;
using Wizard;
public class ArenaResultAnimationAgent : ResultAnimationAgent
{
public override IEnumerator RunUI(BattleResultUIController battleResultControl, INextSceneSelector nextSceneSelector, bool isWin)
{
m_BattleCamera.m_CutInCamera.gameObject.SetActive(value: false);
if (battleResultControl.IsDraw)
{
battleResultControl.TitleWin.gameObject.SetActive(value: false);
battleResultControl.TitleLose.gameObject.SetActive(value: false);
battleResultControl.TitleDraw.gameObject.SetActive(value: true);
battleResultControl.TitleDraw.transform.localScale = Vector3.one * 10f;
battleResultControl.TitleDraw.alpha = 0f;
battleResultControl.Bg.color = new Color32(0, 48, 16, 0);
battleResultControl.ResultTitle.spriteName = "result_top_lose";
}
else if (isWin)
{
battleResultControl.TitleWin.gameObject.SetActive(value: true);
battleResultControl.TitleLose.gameObject.SetActive(value: false);
battleResultControl.TitleDraw.gameObject.SetActive(value: false);
battleResultControl.TitleWin.transform.localScale = Vector3.one * 10f;
battleResultControl.TitleWin.alpha = 0f;
battleResultControl.Bg.color = new Color32(32, 24, 0, 0);
battleResultControl.ResultTitle.spriteName = "result_top_win";
}
else
{
battleResultControl.TitleWin.gameObject.SetActive(value: false);
battleResultControl.TitleLose.gameObject.SetActive(value: true);
battleResultControl.TitleDraw.gameObject.SetActive(value: false);
battleResultControl.TitleLose.transform.localScale = Vector3.one * 10f;
battleResultControl.TitleLose.alpha = 0f;
battleResultControl.Bg.color = new Color32(0, 24, 48, 0);
battleResultControl.ResultTitle.spriteName = "result_top_lose";
}
battleResultControl.MainPanel.alpha = 1f;
yield return new WaitForSeconds(0.1f);
if (battleResultControl.IsDraw)
{
TweenAlpha.Begin(battleResultControl.TitleDraw.gameObject, 0.2f, 1f);
iTween.ScaleTo(battleResultControl.TitleDraw.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOULOSE);
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_LOSE);
}
else if (isWin)
{
TweenAlpha.Begin(battleResultControl.TitleWin.gameObject, 0.2f, 1f);
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOUWIN);
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_WIN);
}
else
{
TweenAlpha.Begin(battleResultControl.TitleLose.gameObject, 0.2f, 1f);
iTween.ScaleTo(battleResultControl.TitleLose.gameObject, iTween.Hash("scale", Vector3.one, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_YOULOSE);
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_JINGLE_LOSE);
}
TweenAlpha.Begin(battleResultControl.Bg.gameObject, 0.5f, 0.75f);
yield return new WaitForSeconds(0.2f);
TweenAlpha.Begin(battleResultControl.ArcaneIn.gameObject, 0.5f, 1f);
TweenAlpha.Begin(battleResultControl.ArcaneOut.gameObject, 0.5f, 1f);
iTween.ScaleTo(battleResultControl.ArcaneIn.gameObject, iTween.Hash("scale", Vector3.one, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
iTween.ScaleTo(battleResultControl.ArcaneOut.gameObject, iTween.Hash("scale", Vector3.one, "time", 2f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
if (battleResultControl.IsDraw)
{
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_3, Vector3.zero);
battleResultControl.TitleDraw.transform.localScale = Vector3.one;
iTween.ScaleTo(battleResultControl.TitleDraw.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
}
else if (isWin)
{
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_1, Vector3.zero);
battleResultControl.TitleWin.transform.localScale = Vector3.one;
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
}
else
{
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_TITLE_2, Vector3.zero);
battleResultControl.TitleLose.transform.localScale = Vector3.one;
iTween.ScaleTo(battleResultControl.TitleLose.gameObject, iTween.Hash("scale", Vector3.one * 1.1f, "time", 2f, "islocal", true, "easetype", iTween.EaseType.linear));
}
HideEmotionMessage();
if (battleResultControl.ResultMsgWindowFlag)
{
StartCoroutine(battleResultControl.ShowSpecialResultInfo());
}
yield return new WaitForSeconds(2f);
RankWinnerReward winnerReward = GameMgr.GetIns()._rankWinnerReward;
if (winnerReward == null)
{
int value = PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.BATTLE_WINNER_REWARD_GRADE);
string value2 = PlayerPrefsWrapper.GetValue(PlayerPrefsWrapper.BATTLE_WINNER_REWARD_STRING);
if (value != 0 && value2 != "")
{
winnerReward = UIManager.GetInstance().createRankWinnerReward();
GameMgr.GetIns()._rankWinnerReward = winnerReward;
winnerReward.SetInfomation(value, value2);
winnerReward.gameObject.SetActive(value: false);
}
}
if (winnerReward != null && isWin)
{
float seconds = 3f;
StartCoroutine(winnerReward.ResultWinnerReward());
yield return new WaitForSeconds(seconds);
StartCoroutine(winnerReward.HideRewardObject());
}
if (!battleResultControl.IsDraw && ShowRewardDialog(battleResultControl))
{
while (battleResultControl.IsRewardWait)
{
yield return null;
}
}
if (Data.ArenaBattleFinish.data != null)
{
TreasureBoxCpResultInfo treasureBoxCpResultInfo = Data.ArenaBattleFinish.data.TreasureBoxCpResultInfo;
if (treasureBoxCpResultInfo.IsPlayGradeUpAnimation())
{
yield return TreasureBoxCpOpenBoxAnimation(battleResultControl, treasureBoxCpResultInfo.AfterGrade);
}
if (treasureBoxCpResultInfo.IsBoxOpened())
{
yield return CreateTreasureBoxCpRewardDialog(treasureBoxCpResultInfo);
}
}
if (battleResultControl.IsDraw)
{
TweenAlpha.Begin(battleResultControl.TitleDraw.gameObject, 0.2f, 0f);
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_3, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
}
else if (isWin)
{
TweenAlpha.Begin(battleResultControl.TitleWin.gameObject, 0.2f, 0f);
iTween.ScaleTo(battleResultControl.TitleWin.gameObject, iTween.Hash("scale", Vector3.one * 3f, "time", 0.2f, "islocal", true, "easetype", iTween.EaseType.easeInQuad));
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_1, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
}
else
{
TweenAlpha.Begin(battleResultControl.TitleLose.gameObject, 0.2f, 0f);
GameMgr.GetIns().GetEffectMgr().Start(EffectMgr.EffectType.CMN_RESULT_BACK_2, battleResultControl.AnchorBottom.transform.position, battleResultControl.AnchorBottom.gameObject);
}
yield return new WaitForSeconds(0.2f);
if (isWin)
{
GameMgr.GetIns().GetSoundMgr().PlayBGM(Bgm.BGM_TYPE.SYS_WIN_LOOP);
}
else
{
GameMgr.GetIns().GetSoundMgr().PlayBGM(Bgm.BGM_TYPE.SYS_LOSE_LOOP);
}
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_WINDOW_APPER);
iTween.MoveTo(battleResultControl.ClassCharObj.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ClassCharObj"], "time", 0.5f, "delay", 0.1f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
iTween.MoveTo(battleResultControl.ResultTitle.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ResultTitle"], "time", 0.5f, "delay", 0f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
iTween.MoveTo(battleResultControl.ClassInfo.gameObject, iTween.Hash("position", battleResultControl.DefaultPosDict["ClassInfo"], "time", 0.5f, "delay", 0.3f, "islocal", true, "easetype", iTween.EaseType.easeOutExpo));
yield return new WaitForSeconds(1f);
if (isWin)
{
PlayWinVoice();
}
if (battleResultControl.AddClassExp > 0)
{
battleResultControl.SettingAddClassExpTextAnimation();
yield return new WaitForSeconds(0.5f);
for (int i = 0; i < 10; i++)
{
GameMgr.GetIns().GetSoundMgr().PlaySe(Se.TYPE.SYS_RESULT_GAUGEUP);
yield return new WaitForSeconds(0.05f);
}
yield return new WaitForSeconds(0.5f);
}
bool _isFinishBattlePass = false;
battleResultControl.SetBattlePassGauge(delegate
{
_isFinishBattlePass = true;
});
while (!_isFinishBattlePass)
{
yield return null;
}
if (Data.RedEtherCampaignResultData != null)
{
bool isFinishRedEther = false;
RedEtherCampaignPanel.Create(battleResultControl.gameObject, Data.RedEtherCampaignResultData, battleResultControl, delegate
{
isFinishRedEther = true;
});
while (!isFinishRedEther)
{
yield return null;
}
yield return ShowRewardDialog(Data.RedEtherCampaignResultData.RewardList);
}
battleResultControl.GreySpriteBGVisible = false;
nextSceneSelector.Show();
battleResultControl.PrepareAchievementLog();
battleResultControl.FinishResult();
}
}

View File

@@ -0,0 +1,22 @@
using UnityEngine;
public class ArenaResultAnimationHandler : IResultAnimationHandler
{
private readonly GameObject m_resultAnimationAgentObj;
private readonly ArenaResultAnimationAgent m_resultAnimationAgentIns;
public ResultAnimationAgent m_resultAnimationAgent => m_resultAnimationAgentIns;
public ArenaResultAnimationHandler(BattleCamera battleCamera)
{
m_resultAnimationAgentObj = new GameObject();
m_resultAnimationAgentIns = m_resultAnimationAgentObj.AddComponent<ArenaResultAnimationAgent>();
m_resultAnimationAgentIns.GetComponent<ArenaResultAnimationAgent>().SetBattleCamera(battleCamera);
}
public void Destroy()
{
Object.Destroy(m_resultAnimationAgentObj);
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using LitJson;
using Wizard;
using Wizard.Lottery;
public class ArenaResultReporter : IBattleResultReporter
{
public bool IsEnd => Data.ArenaBattleFinish.data != null;
public int ClassExp => GetClassExp();
public List<UserAchievement> UserAchievement => GetUserAchievementList();
public List<UserMission> UserMission => GetUserMissionList();
public List<ReceivedReward> MissionRewards => Data.ArenaBattleFinish.data._missionRewards;
public List<ReceivedReward> VictoryRewards => Data.ArenaBattleFinish.data._victoryRewards;
public LotteryApplyData LotteryData => LotteryApplyData.EmptyData();
public bool IsDataExist
{
get
{
if (Data.ArenaBattleFinish.data != null)
{
return Data.ArenaBattleFinish.data.IsProcessed;
}
return false;
}
}
public MyPageHomeDialogData HomeDialogData => null;
public void Report(bool isWin)
{
}
public void Destroy()
{
}
public JsonData GetFinishResponseData()
{
return Data.ArenaBattleFinish.data._responseData;
}
public List<UserAchievement> GetUserAchievementList()
{
return Data.ArenaBattleFinish.data.achieved_achievement_list;
}
public List<UserMission> GetUserMissionList()
{
return Data.ArenaBattleFinish.data.achieved_mission_list;
}
public int GetClassExp()
{
return Data.ArenaBattleFinish.data.get_class_chara_experience;
}
}

View File

@@ -0,0 +1,24 @@
using LitJson;
using Wizard;
public class ArenaTwoPickData : ArenaEntryDataBase
{
public ChallengeData ChallengeData { get; private set; }
public ArenaTwoPickData(JsonData data)
{
isJoin = data["is_join"].ToBoolean();
crystalCost = data["cost"].ToInt();
rupyCost = data["rupy_cost"].ToInt();
ticketCost = data["ticket_cost"].ToInt();
base.LootBoxType = PlayerStaticData.LootBoxType.TWOPICK;
if (data.Keys.Contains("sales_period_info"))
{
base.ExpirtyInfo = new ShopExpirtyInfo(data["sales_period_info"]);
}
if (data.Keys.Contains("format_info"))
{
ChallengeData = new ChallengeData(data["format_info"]);
}
}
}

View File

@@ -0,0 +1,117 @@
using System.Collections.Generic;
using UnityEngine;
public class ArrowControl : MonoBehaviour
{
[SerializeField]
private GameObject ArrowHead;
[SerializeField]
private GameObject ArrowEfc;
[SerializeField]
private int DivideCnt = 10;
[SerializeField]
private bool isEvo;
private IList<GameObject> ArrowEfcList;
private GameObject FromObj;
private GameObject ToObj;
private bool isOn;
private bool _isTargettingEnemy;
private float ChangeTime;
private IList<int> ArrowTarList;
private void Start()
{
ArrowEfcList = new List<GameObject>();
ArrowEfcList.Add(ArrowEfc);
for (int i = 1; i < DivideCnt; i++)
{
GameObject gameObject = Object.Instantiate(ArrowEfc);
if (!(null == gameObject))
{
gameObject.transform.parent = base.transform;
ArrowEfcList.Add(gameObject);
}
}
ArrowTarList = new List<int>();
for (int j = 0; j < DivideCnt; j++)
{
ArrowTarList.Add(j);
}
HideArrow();
}
private void Update()
{
if (isOn)
{
SetArrowLine();
}
}
public void ShowArrow(GameObject fromObj, GameObject toObj, bool isTargettingEnemy)
{
FromObj = fromObj;
ToObj = toObj;
_isTargettingEnemy = isTargettingEnemy;
isOn = true;
base.gameObject.SetActive(value: true);
}
public void HideArrow()
{
isOn = false;
for (int i = 0; i < DivideCnt; i++)
{
ArrowEfcList[i].SetActive(value: false);
}
base.gameObject.SetActive(value: false);
}
private void SetArrowLine()
{
if (isEvo)
{
ChangeTime -= Time.deltaTime * 5f;
}
else
{
ChangeTime -= Time.deltaTime;
}
if (ChangeTime <= 0f)
{
ChangeTime = 1f;
ArrowTarList.Add(ArrowTarList[0]);
ArrowTarList.RemoveAt(0);
}
ArrowHead.transform.position = ToObj.transform.position;
Vector3 position = FromObj.transform.position;
Vector3 position2 = ToObj.transform.position;
Vector3 p = (_isTargettingEnemy ? position : position2) + Vector3.back * Vector3.Distance(position, position2) + Vector3.down * Vector3.Distance(position, position2) * -0.5f;
Vector3[] array = new Vector3[DivideCnt];
array = MotionUtils.GetBezierQuad(position, p, position2, DivideCnt);
for (int i = 0; i < array.Length; i++)
{
float num = 1f - ChangeTime;
if (ArrowTarList[i] != 0)
{
ArrowEfcList[i].SetActive(value: true);
ArrowEfcList[i].transform.position = (array[ArrowTarList[i]] - array[ArrowTarList[i] - 1]) * num + array[ArrowTarList[i] - 1];
}
else
{
ArrowEfcList[i].SetActive(value: false);
ArrowEfcList[i].transform.position = array[0];
}
}
}
}

View File

@@ -0,0 +1,76 @@
using UnityEngine;
[ExecuteInEditMode]
public class AspectCamera : MonoBehaviour
{
public Vector2 aspect = new Vector2(4f, 3f);
public Color32 backgroundColor = Color.black;
private float aspectRate;
private Camera _camera;
private static Camera _backgroundCamera;
private int sizeVal = 1;
public const float LOWER_LIMIT_ASPECT_RATIO = 0.5625f;
public const float UPPER_LIMIT_ASPECT_RATIO = 0.4618f;
public const float LOWER_LIMIT_ASPECT_RATIO_RECIPROCAL = 1.7777778f;
private const float SAFE_AREA_RATE = 0.892f;
private const float SAFE_AREA_NONE_RATE = 1f;
public static float SafeAreaRate;
private void Start()
{
aspectRate = aspect.x / aspect.y;
_camera = GetComponent<Camera>();
SafeAreaRate = 1f;
float num = (float)Screen.height / (float)Screen.width;
if (num < 0.5625f)
{
num = Mathf.Max(num, 0.4618f);
float t = (0.5625f - num) / 0.10069999f;
SafeAreaRate = Mathf.Lerp(1f, 0.892f, t);
}
}
private void UpdateScreenRate()
{
float num = aspect.y / aspect.x;
float num2 = (float)Screen.height / (float)Screen.width;
if (num2 < 0.5625f)
{
num2 = 0.5625f;
}
if (num > num2)
{
float num3 = num2 / num;
_camera.rect = new Rect(0f, 0f, 1f, 1f);
_camera.orthographicSize = (float)sizeVal * num3;
}
else
{
float num4 = num / num2;
_camera.rect = new Rect(0f, 0f, 1f, 1f);
_camera.orthographicSize = (float)sizeVal / num4;
}
}
private bool IsChangeAspect()
{
return _camera.aspect == aspectRate;
}
private void Update()
{
UpdateScreenRate();
_camera.ResetAspect();
}
}

View File

@@ -0,0 +1,44 @@
using UnityEngine;
public class AspectCameraPerspective : MonoBehaviour
{
private Camera m_camera;
private bool m_isSetFOV;
public void UpdateFov()
{
m_isSetFOV = false;
}
private void Start()
{
m_camera = GetComponent<Camera>();
}
private void Update()
{
if (!m_isSetFOV && GameMgr.GetIns() != null && m_camera != null)
{
float num = 0f;
float num2 = 0f;
if (Screen.width > Screen.height)
{
num = Screen.width;
num2 = Screen.height;
}
else
{
num = Screen.height;
num2 = Screen.width;
}
float num3 = num / num2;
if (num3 > 1.7777778f)
{
num3 = 1.7777778f;
}
m_camera.fieldOfView = Mathf.Atan2(1f, num3) * 57.29578f * 2f;
m_isSetFOV = true;
}
}
}

View File

@@ -0,0 +1,54 @@
public class AssetBundleEditorTag
{
public enum BUNDLE_CATEGORY
{
BG,
CARD,
EFFECT,
MASTER,
STORY,
UI,
UIDOWNLOAD,
TUTORIAL,
PACKBOX,
UILANG,
STORYLANG,
FONT,
SLEEVE,
MAX
}
public enum CardStatType
{
CARD_STAT_NORMAL,
CARD_STAT_FOIL,
CARD_STAT_PROMOTION
}
public struct categoryProps
{
public string name;
public categoryProps(string in_name)
{
name = in_name;
}
}
public static categoryProps[] categoryNameList = new categoryProps[13]
{
new categoryProps("bg"),
new categoryProps("card"),
new categoryProps("effect"),
new categoryProps("master"),
new categoryProps("story"),
new categoryProps("ui"),
new categoryProps("uidownload"),
new categoryProps("tutorial"),
new categoryProps("packbox"),
new categoryProps("uilang"),
new categoryProps("storylang"),
new categoryProps("font"),
new categoryProps("sleeve")
};
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Generic;
using Wizard.Battle;
public class AttachedSkillInformation
{
public SkillCollectionBase AttachedSkills { get; protected set; }
public List<string> OwnerCardNameList { get; protected set; }
public List<int> OwnerCardIdList { get; protected set; }
public List<long> DuplicateBanNum { get; protected set; }
public List<SkillBase> CreatorSkillList { get; protected set; }
public List<int> CreatorSkillIndexList { get; protected set; }
public AttachedSkillInformation(BattleCardBase card)
{
AttachedSkills = new SkillCollectionBase(card);
OwnerCardNameList = new List<string>();
OwnerCardIdList = new List<int>();
DuplicateBanNum = new List<long>();
CreatorSkillList = new List<SkillBase>();
CreatorSkillIndexList = new List<int>();
}
public AttachedSkillInformation(BattleCardBase card, SkillCollectionBase skills, List<string> nameList, List<int> idList, List<long> duplicateBanNum, List<SkillBase> createrList, List<int> creatorSkillIndexList)
{
AttachedSkills = skills.Clone(card);
OwnerCardNameList = new List<string>(nameList);
OwnerCardIdList = new List<int>(idList);
DuplicateBanNum = new List<long>(duplicateBanNum);
CreatorSkillList = new List<SkillBase>(createrList);
CreatorSkillIndexList = new List<int>(creatorSkillIndexList);
}
public void Add(SkillBase skill, string ownerCardName, int ownerCardID, long duplicateBanNum, SkillBase creatorSkill, int index)
{
AttachedSkills.Add(skill);
OwnerCardNameList.Add(ownerCardName);
OwnerCardIdList.Add(ownerCardID);
DuplicateBanNum.Add(duplicateBanNum);
CreatorSkillList.Add(creatorSkill);
CreatorSkillIndexList.Add(index);
}
public void Remove(SkillBase skill, BattleCardBase owner, long duplicateBanNum, SkillBase creatorSkill, int index)
{
string name = owner.GetName();
int cardId = owner.CardId;
Remove(skill, name, cardId, duplicateBanNum, creatorSkill, index);
}
public void Remove(SkillBase skill, string ownerCardName, int ownerCardID, long duplicateBanNum, SkillBase creatorSkill, int index)
{
AttachedSkills.Remove(skill);
OwnerCardNameList.Remove(ownerCardName);
OwnerCardIdList.Remove(ownerCardID);
DuplicateBanNum.Remove(duplicateBanNum);
CreatorSkillList.Remove(creatorSkill);
CreatorSkillIndexList.Remove(index);
}
public void Clear()
{
AttachedSkills.Clear();
OwnerCardNameList.Clear();
OwnerCardIdList.Clear();
DuplicateBanNum.Clear();
CreatorSkillList.Clear();
CreatorSkillIndexList.Clear();
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Wizard.Battle;
public class AttachingAbilityInfo
{
public SkillBase Skill { get; private set; }
public List<IReadOnlyBattleCardInfo> TargetCards { get; private set; }
public AttachingAbilityInfo(SkillBase skill, List<IReadOnlyBattleCardInfo> targetCards)
{
Skill = skill;
TargetCards = targetCards;
}
}

View File

@@ -0,0 +1,528 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Wizard;
using Wizard.Battle.Touch;
using Wizard.Battle.View;
using Wizard.Battle.View.Vfx;
public class AttackSelectControl
{
public class AttackPair
{
public class AttackPairCard
{
public IBattleCardView _battleCardView;
public bool _isReady;
public bool _hasStartedMoving;
public AttackPairCard(IBattleCardView battleCardBase)
{
_battleCardView = battleCardBase;
}
public AttackPairCard(AttackPairCard attackPairCard)
{
_battleCardView = attackPairCard._battleCardView;
_isReady = attackPairCard._isReady;
_hasStartedMoving = attackPairCard._hasStartedMoving;
}
public void Clear()
{
_battleCardView = null;
_isReady = false;
_hasStartedMoving = false;
}
}
public AttackPairCard _attackInitiator;
public AttackPairCard _attackTarget;
public bool IsAttackPairReady
{
get
{
if (_attackInitiator._isReady)
{
return _attackTarget._isReady;
}
return false;
}
}
public AttackPair(IBattleCardView attackInitiator, IBattleCardView attackTarget)
{
_attackInitiator = new AttackPairCard(attackInitiator);
_attackTarget = new AttackPairCard(attackTarget);
}
public AttackPair(AttackPair attackPair)
{
_attackInitiator = new AttackPairCard(attackPair._attackInitiator);
_attackTarget = new AttackPairCard(attackPair._attackTarget);
}
public bool Compare(IBattleCardView attackInitiatorView, IBattleCardView attackTargetView)
{
if (_attackInitiator._battleCardView == attackInitiatorView)
{
return _attackTarget._battleCardView == attackTargetView;
}
return false;
}
public void Clear()
{
_attackInitiator.Clear();
_attackTarget.Clear();
}
}
public class WaitUntilAttackPairIsReadyVfx : VfxBase
{
private AttackPair _attackPair;
public WaitUntilAttackPairIsReadyVfx(AttackPair attackPair)
{
_attackPair = attackPair;
}
public override void Play()
{
BattleCoroutine.GetInstance().StartCoroutine(Wait());
}
private IEnumerator Wait()
{
while (!_attackPair.IsAttackPairReady)
{
yield return null;
}
IsEnd = true;
}
}
private BattleCardBase currentAttackInitiatorBattleCard;
private bool areAttackPairsBeingUpdated;
private readonly AttackPair currentAttackPair = new AttackPair(null, null);
private readonly List<AttackPair> successfulAttackPairs = new List<AttackPair>();
public const float Z_FLOAT_AMOUNT = -100f;
private const float EPSILON = 0.1f;
private const float SMOOTHING_AMOUNT = 0.01f;
private const float DECAY_MULTIPLIER = 10f;
private const float IDLING_POSITION = 0.025390625f;
private IBattleCardView currentAttackInitiator
{
get
{
return currentAttackPair._attackInitiator._battleCardView;
}
set
{
currentAttackPair._attackInitiator._battleCardView = value;
}
}
private IBattleCardView currentAttackTarget
{
get
{
return currentAttackPair._attackTarget._battleCardView;
}
set
{
currentAttackPair._attackTarget._battleCardView = value;
}
}
public void Update()
{
float t = MotionUtils.CalculateFrameRateIndependantDampingConstant(0.01f, 10f);
if (currentAttackInitiator != null && !currentAttackInitiator._attackTargetSelectInfo.IsCardInvolvedInAttack)
{
MoveCardUpwards(currentAttackPair._attackInitiator, t);
}
if (currentAttackTarget != null && !currentAttackTarget._attackTargetSelectInfo.IsCardInvolvedInAttack)
{
MoveCardUpwards(currentAttackPair._attackTarget, t);
}
}
public void RegisterAttackInitiator(BattleCardBase attackInitiatorCard, BattlePlayerBase opponentBattlePlayer)
{
currentAttackInitiatorBattleCard = attackInitiatorCard;
currentAttackInitiator = attackInitiatorCard.BattleCardView;
ToggleAttackableCardFrameEffects(isEnabled: true, opponentBattlePlayer);
attackInitiatorCard.BattleCardView._attackTargetSelectInfo._isBeingSelectedInAttack = true;
if (!attackInitiatorCard.BattleCardView._attackTargetSelectInfo.IsCardInvolvedInAttack)
{
ResetCardOrientationAndStopMovement(attackInitiatorCard.BattleCardView);
}
}
public void RegisterAttackTarget(IBattleCardView attackTargetCard)
{
if (currentAttackTarget == attackTargetCard)
{
return;
}
if (attackTargetCard != null)
{
attackTargetCard._attackTargetSelectInfo._isBeingSelectedInAttack = true;
if (!attackTargetCard._attackTargetSelectInfo.IsCardInvolvedInAttack)
{
ResetCardOrientationAndStopMovement(attackTargetCard);
}
}
currentAttackPair._attackTarget._isReady = !IsCardTranslatable(attackTargetCard);
currentAttackPair._attackTarget._hasStartedMoving = !IsCardTranslatable(attackTargetCard);
ResetCardPosition(currentAttackTarget);
if (currentAttackTarget != null)
{
currentAttackTarget._attackTargetSelectInfo._isBeingSelectedInAttack = false;
}
currentAttackTarget = attackTargetCard;
}
public virtual void RegisterAttackPair(AttackPair attackPair)
{
IBattleCardView battleCardView = attackPair._attackInitiator._battleCardView;
IBattleCardView battleCardView2 = attackPair._attackTarget._battleCardView;
if (attackPair == null || battleCardView == null || battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn == null)
{
ResetCardPosition(currentAttackInitiator);
ResetCardPosition(currentAttackTarget);
return;
}
successfulAttackPairs.Add(attackPair);
battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Enqueue(attackPair);
battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Enqueue(attackPair);
if (!areAttackPairsBeingUpdated)
{
BattleCoroutine.GetInstance().StartCoroutine(UpdateAttackPairs());
}
}
public void CancelAttackSelect(bool wasAttackSuccessful, BattlePlayerBase opponentBattlePlayer)
{
if (wasAttackSuccessful)
{
AttackPair attackPair = new AttackPair(currentAttackPair);
RegisterAttackPair(attackPair);
}
else
{
ResetCardPosition(currentAttackInitiator);
ResetCardPosition(currentAttackTarget);
}
if (currentAttackInitiatorBattleCard != null)
{
ToggleAttackableCardFrameEffects(isEnabled: false, opponentBattlePlayer);
}
if (currentAttackInitiator != null)
{
currentAttackInitiator._attackTargetSelectInfo._isBeingSelectedInAttack = false;
}
if (currentAttackTarget != null)
{
currentAttackTarget._attackTargetSelectInfo._isBeingSelectedInAttack = false;
}
currentAttackInitiatorBattleCard = null;
currentAttackPair.Clear();
}
public void ResetCardOrientationAndStopMovement(IBattleCardView targetCard)
{
if (!targetCard._attackTargetSelectInfo.IsUneffectedByAttackTargetting)
{
iTween.Stop(targetCard.CardWrapObject);
targetCard.CardWrapObject.transform.rotation = Quaternion.identity;
}
}
public virtual VfxBase ResetCardAfterAttackOnReplay()
{
return InstantVfx.Create(delegate
{
for (int i = 0; i < successfulAttackPairs.Count(); i++)
{
IBattleCardView battleCardView = successfulAttackPairs[i]._attackInitiator._battleCardView;
if (battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
{
battleCardView._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
}
ResetCardPosition(battleCardView);
IBattleCardView battleCardView2 = successfulAttackPairs[i]._attackTarget._battleCardView;
if (battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
{
battleCardView2._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
}
ResetCardPosition(battleCardView2);
}
});
}
public virtual VfxBase ResetCardAfterAttack(IBattleCardView cardToReset)
{
return InstantVfx.Create(delegate
{
if (cardToReset._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Count > 0)
{
cardToReset._attackTargetSelectInfo._attackPairsCardIsInvolvedIn.Dequeue();
}
if (cardToReset._attackTargetSelectInfo.IsCardInvolvedInAttack)
{
cardToReset._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn._attackTarget._isReady = true;
}
ResetCardPosition(cardToReset);
});
}
private void ResetCardPosition(IBattleCardView targetCard)
{
if (!BattleManagerBase.GetIns().IsRecovery && IsCardTranslatable(targetCard) && !targetCard._attackTargetSelectInfo.IsCardInvolvedInAttack && !targetCard._attackTargetSelectInfo.IsUneffectedByAttackTargetting)
{
ImmediateVfxMgr.GetInstance().Register(SequentialVfxPlayer.Create(InstantVfx.Create(delegate
{
iTween.Stop(targetCard.CardWrapObject);
}), new DelaySetupVfx(() => (targetCard._attackTargetSelectInfo._isBeingSelectedInAttack || targetCard._attackTargetSelectInfo.IsCardInvolvedInAttack) ? ((VfxBase)NullVfx.GetInstance()) : ((VfxBase)new FallToGroundVfx(targetCard.CardWrapObject)))));
}
}
public virtual void StartCardIdling(IBattleCardView battleCardView)
{
iTween.Stop(battleCardView.CardWrapObject);
iTween.MoveAdd(battleCardView.CardWrapObject, iTween.Hash("z", 0.025390625f, "time", Random.Range(0.5f, 0.6f), "looptype", iTween.LoopType.pingPong, "easetype", iTween.EaseType.easeInOutQuad));
}
public virtual VfxBase RemoveAttackPairVfx(IBattleCardView attackInitiator, IBattleCardView attackTarget)
{
AttackPair attackPairToRemove = null;
for (int i = 0; i < successfulAttackPairs.Count; i++)
{
if (successfulAttackPairs[i].Compare(attackInitiator, attackTarget))
{
attackPairToRemove = successfulAttackPairs[i];
break;
}
}
if (attackPairToRemove != null)
{
VfxBase vfxBase = CreateWaitUntilAttackPairIsReadyVfx(attackPairToRemove);
VfxBase vfxBase2 = InstantVfx.Create(delegate
{
successfulAttackPairs.Remove(attackPairToRemove);
});
return SequentialVfxPlayer.Create(vfxBase, vfxBase2);
}
return NullVfx.GetInstance();
}
private void ToggleAttackableCardFrameEffects(bool isEnabled, BattlePlayerBase opponentBattlePlayer)
{
List<BattleCardBase> classAndInPlayCardList = opponentBattlePlayer.ClassAndInPlayCardList;
for (int i = 0; i < classAndInPlayCardList.Count; i++)
{
if (CanCardAttackTarget(currentAttackInitiatorBattleCard, classAndInPlayCardList[i], opponentBattlePlayer.InPlayCards) && classAndInPlayCardList[i].AreCanBeAttackedConditionsFulfilled)
{
classAndInPlayCardList[i].BattleCardView._inPlayFrameEffect.ToggleTargetSelectEffect(isEnabled);
}
}
currentAttackInitiator._inPlayFrameEffect.ToggleTargetSelectEffect(isEnabled, isAttackTargetSelectInitiator: true);
}
private VfxBase CreateWaitUntilAttackPairIsReadyVfx(AttackPair attackPair)
{
return new WaitUntilAttackPairIsReadyVfx(attackPair);
}
private IEnumerator UpdateAttackPairs()
{
areAttackPairsBeingUpdated = true;
while (successfulAttackPairs.Count > 0)
{
float t = MotionUtils.CalculateFrameRateIndependantDampingConstant(0.01f, 10f);
for (int i = 0; i < successfulAttackPairs.Count; i++)
{
AttackPair attackPair = successfulAttackPairs[i];
if (!attackPair.IsAttackPairReady)
{
AttackPair.AttackPairCard attackInitiator = attackPair._attackInitiator;
AttackPair.AttackPairCard attackTarget = attackPair._attackTarget;
if (attackInitiator._battleCardView._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn == attackPair)
{
MoveCardUpwards(attackInitiator, t);
}
if (attackTarget._battleCardView._attackTargetSelectInfo.CurrentAttackPairCardIsInvolvedIn == attackPair)
{
MoveCardUpwards(attackTarget, t);
}
}
}
yield return null;
}
areAttackPairsBeingUpdated = false;
}
private void MoveCardUpwards(AttackPair.AttackPairCard attackPairCard, float t)
{
if (BattleManagerBase.GetIns().IsRecovery)
{
attackPairCard._isReady = true;
}
else
{
if (attackPairCard == null || attackPairCard._battleCardView == null)
{
return;
}
IBattleCardView battleCardView = attackPairCard._battleCardView;
if (IsCardTranslatable(battleCardView) && !battleCardView._attackTargetSelectInfo.IsUneffectedByAttackTargetting && !attackPairCard._isReady)
{
if (!attackPairCard._hasStartedMoving)
{
attackPairCard._hasStartedMoving = true;
ResetCardOrientationAndStopMovement(battleCardView);
}
Transform transform = battleCardView.CardWrapObject.transform;
if (!IsCardFullyTranslated(battleCardView))
{
Vector3 b = CalculateFinalFloatingPosition(battleCardView);
transform.localPosition = Vector3.Lerp(transform.transform.localPosition, b, t);
}
else
{
transform.localPosition = CalculateFinalFloatingPosition(battleCardView);
attackPairCard._isReady = true;
StartCardIdling(battleCardView);
}
}
}
}
private Vector3 CalculateFinalFloatingPosition(IBattleCardView battleCardView)
{
Vector3 localPosition = battleCardView.CardWrapObject.transform.transform.localPosition;
localPosition.z = -100f;
return localPosition;
}
public bool IsCardTranslatable(IBattleCardView cardToTranslate)
{
if (cardToTranslate != null)
{
return !cardToTranslate.CardInfo.IsClass;
}
return false;
}
private bool IsCardFullyTranslated(IBattleCardView cardBeingTranslated)
{
return Mathf.Abs(cardBeingTranslated.CardWrapObject.transform.localPosition.z - -100f) < 0.1f;
}
public static bool CanCardAttackTarget(BattleCardBase Attacker, BattleCardBase Target, IEnumerable<BattleCardBase> TargetInPlayCards)
{
bool flag = false;
bool isClass = Target.IsClass;
if (TargetInPlayCards.Any((BattleCardBase c) => c.SkillApplyInformation.IsGuard && !c.CantBeFocusedAttack(Attacker)))
{
flag = true;
}
if (Attacker.SkillApplyInformation.IsIgnoreGuard)
{
flag = false;
}
if (Attacker.AttackableCount <= 0)
{
return false;
}
if ((!Attacker.SkillApplyInformation.IsQuick || !Attacker.SkillApplyInformation.IsRush) && !Attacker.Attackable)
{
return false;
}
if (isClass)
{
if (!Attacker.SkillApplyInformation.IsQuick)
{
if (Attacker.IsFirstTurn)
{
return false;
}
if (!Attacker.Attackable)
{
return false;
}
}
if (Attacker.IsCantAttackClass)
{
return false;
}
if (Attacker.SkillApplyInformation.IsForceAttackUnit && Attacker.OpponentBattlePlayer.InPlayCards.Any((BattleCardBase c) => !c.CantBeFocusedAttack(Attacker) && c.IsUnit && !AttackTargetSelectTouchProcessor.CheckAttackToUnitNotHasGuardError(Attacker, c)))
{
return false;
}
}
if (!Target.IsInplay)
{
return false;
}
if (Target.IsField || Target.CantBeFocusedAttack(Attacker))
{
return false;
}
if (flag && (isClass || !Target.SkillApplyInformation.IsGuard))
{
return false;
}
if (isClass && Attacker.IsCantAttackClass)
{
return false;
}
if (Target.IsUnit && Attacker.SkillApplyInformation.IsSkillCantAtkUnit)
{
return false;
}
if (Target.IsUnit && Attacker.SkillApplyInformation.IsSkillCantAtkUnitBaseCardId && Attacker.SkillApplyInformation.CantAtkUnitBaseCardIdList.Contains(Target.BaseParameter.BaseCardId))
{
return false;
}
if (!isClass && Attacker.SkillApplyInformation.IsSkillCantAtkUnitNotHasGuard && !Target.SkillApplyInformation.IsGuard)
{
return false;
}
return true;
}
public static bool IsAttackPossible(BattleCardBase attacker, BattleCardBase target, IEnumerable<BattleCardBase> opponentInPlayCards)
{
if (attacker.Attackable)
{
return CanCardAttackTarget(attacker, target, opponentInPlayCards);
}
return false;
}
public static bool IsAttackPossible(AIVirtualCard attacker, AIVirtualCard target, BattlePlayerBase opponent)
{
if (attacker.BaseCard.Attackable)
{
return CanCardAttackTarget(attacker.BaseCard, target.BaseCard, opponent.InPlayCards);
}
return false;
}
}

View File

@@ -0,0 +1,7 @@
using UnityEngine;
public class AudioList : MonoBehaviour
{
[SerializeField]
public string[] GimicAudioList;
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class BMFont
{
[HideInInspector]
[SerializeField]
private int mSize = 16;
[HideInInspector]
[SerializeField]
private int mBase;
[HideInInspector]
[SerializeField]
private int mWidth;
[HideInInspector]
[SerializeField]
private int mHeight;
[HideInInspector]
[SerializeField]
private string mSpriteName;
[HideInInspector]
[SerializeField]
private List<BMGlyph> mSaved = new List<BMGlyph>();
private Dictionary<int, BMGlyph> mDict = new Dictionary<int, BMGlyph>();
public bool isValid => mSaved.Count > 0;
public int charSize
{
get
{
return mSize;
}
set
{
mSize = value;
}
}
public int baseOffset
{
get
{
return mBase;
}
set
{
mBase = value;
}
}
public int texWidth
{
get
{
return mWidth;
}
set
{
mWidth = value;
}
}
public int texHeight
{
get
{
return mHeight;
}
set
{
mHeight = value;
}
}
public int glyphCount
{
get
{
if (!isValid)
{
return 0;
}
return mSaved.Count;
}
}
public string spriteName
{
get
{
return mSpriteName;
}
set
{
mSpriteName = value;
}
}
public List<BMGlyph> glyphs => mSaved;
public BMGlyph GetGlyph(int index, bool createIfMissing)
{
BMGlyph value = null;
if (mDict.Count == 0)
{
int i = 0;
for (int count = mSaved.Count; i < count; i++)
{
BMGlyph bMGlyph = mSaved[i];
mDict.Add(bMGlyph.index, bMGlyph);
}
}
if (!mDict.TryGetValue(index, out value) && createIfMissing)
{
value = new BMGlyph();
value.index = index;
mSaved.Add(value);
mDict.Add(index, value);
}
return value;
}
public BMGlyph GetGlyph(int index)
{
return GetGlyph(index, createIfMissing: false);
}
public void Clear()
{
mDict.Clear();
mSaved.Clear();
}
public void Trim(int xMin, int yMin, int xMax, int yMax)
{
if (isValid)
{
int i = 0;
for (int count = mSaved.Count; i < count; i++)
{
mSaved[i]?.Trim(xMin, yMin, xMax, yMax);
}
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
[Serializable]
public class BMGlyph
{
public int index;
public int x;
public int y;
public int width;
public int height;
public int offsetX;
public int offsetY;
public int advance;
public int channel;
public List<int> kerning;
public int GetKerning(int previousChar)
{
if (kerning != null && previousChar != 0)
{
int i = 0;
for (int count = kerning.Count; i < count; i += 2)
{
if (kerning[i] == previousChar)
{
return kerning[i + 1];
}
}
}
return 0;
}
public void SetKerning(int previousChar, int amount)
{
if (kerning == null)
{
kerning = new List<int>();
}
for (int i = 0; i < kerning.Count; i += 2)
{
if (kerning[i] == previousChar)
{
kerning[i + 1] = amount;
return;
}
}
kerning.Add(previousChar);
kerning.Add(amount);
}
public void Trim(int xMin, int yMin, int xMax, int yMax)
{
int num = x + width;
int num2 = y + height;
if (x < xMin)
{
int num3 = xMin - x;
x += num3;
width -= num3;
offsetX += num3;
}
if (y < yMin)
{
int num4 = yMin - y;
y += num4;
height -= num4;
offsetY += num4;
}
if (num > xMax)
{
width -= num - xMax;
}
if (num2 > yMax)
{
height -= num2 - yMax;
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using UnityEngine;
[Serializable]
public class BMSymbol
{
public string sequence;
public string spriteName;
private UISpriteData mSprite;
private bool mIsValid;
private int mLength;
private int mOffsetX;
private int mOffsetY;
private int mWidth;
private int mHeight;
private int mAdvance;
private Rect mUV;
public int length
{
get
{
if (mLength == 0)
{
mLength = sequence.Length;
}
return mLength;
}
}
public int offsetX => mOffsetX;
public int offsetY => mOffsetY;
public int width => mWidth;
public int height => mHeight;
public int advance => mAdvance;
public Rect uvRect => mUV;
public void MarkAsChanged()
{
mIsValid = false;
}
public bool Validate(UIAtlas atlas)
{
if (atlas == null)
{
return false;
}
if (!mIsValid)
{
if (string.IsNullOrEmpty(spriteName))
{
return false;
}
mSprite = ((atlas != null) ? atlas.GetSprite(spriteName) : null);
if (mSprite != null)
{
Texture texture = atlas.texture;
if (texture == null)
{
mSprite = null;
}
else
{
mUV = new Rect(mSprite.x, mSprite.y, mSprite.width, mSprite.height);
mUV = NGUIMath.ConvertToTexCoords(mUV, texture.width, texture.height);
mOffsetX = mSprite.paddingLeft;
mOffsetY = mSprite.paddingTop;
mWidth = mSprite.width;
mHeight = mSprite.height;
mAdvance = mSprite.width + (mSprite.paddingLeft + mSprite.paddingRight);
mIsValid = true;
}
}
}
return mSprite != null;
}
}

View File

@@ -0,0 +1,272 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Cute;
using UnityEngine;
using Wizard.Battle.View.Vfx;
public class BackGroundBase
{
protected string _bgmId;
protected BattleCamera _battleCamera;
protected GameObject _fieldModel;
protected GameObject _fieldParticles;
protected IDictionary<string, Animation> m_FieldAnimationDictionary;
protected IDictionary<string, GameObject> _fieldObjDictionary;
protected IDictionary<string, Animator> m_FieldAnimatorDictionary;
protected IDictionary<string, ParticleSystem> _fieldParticleSystemDictionary;
protected IDictionary<string, int> _gimicCntDictionary;
public string[] GimicAudioList;
protected string _str3DFieldNo;
protected string _str3DFieldPath;
protected BattleManagerBase m_BtlMgrIns;
protected string m_FieldAssetPath;
protected List<string> m_SoundAssetPathList;
protected float m_RandomActionTime;
protected bool IsFieldRandom;
private Coroutine battleLoadCoroutine;
public virtual int FieldId => 1;
public virtual int FieldEffectId => FieldId;
public GameObject Field { get; protected set; }
public GameObject m_Battle3DContainer { get; protected set; }
public GameObject m_BattleCutInContainer { get; protected set; }
public SetShaderGlobalColorBG SetShaderGlobalColorBG { get; protected set; }
public bool IsLoadDone { get; protected set; }
public BackGroundBase(string bgmId = "NONE")
{
_battleCamera = null;
m_Battle3DContainer = null;
m_BattleCutInContainer = null;
m_BtlMgrIns = BattleManagerBase.GetIns();
IsLoadDone = false;
_str3DFieldNo = "";
_str3DFieldPath = "";
m_FieldAssetPath = "";
Field = null;
_fieldModel = null;
_fieldParticles = null;
_bgmId = bgmId;
m_RandomActionTime = 0f;
IsFieldRandom = false;
m_SoundAssetPathList = new List<string>();
_fieldObjDictionary = new Dictionary<string, GameObject>();
m_FieldAnimationDictionary = new Dictionary<string, Animation>();
m_FieldAnimatorDictionary = new Dictionary<string, Animator>();
_fieldParticleSystemDictionary = new Dictionary<string, ParticleSystem>();
_gimicCntDictionary = new Dictionary<string, int>();
SetShaderGlobalColorBG = null;
Physics.gravity = new Vector3(0f, 0f, 9.8f);
_str3DFieldNo = GetFieldIdString(FieldId);
_gimicCntDictionary.Add("FieldGimic1", 0);
_gimicCntDictionary.Add("FieldGimic2", 0);
_gimicCntDictionary.Add("FieldGimic3", 0);
}
public void Dispose()
{
UnityEngine.Object.DestroyImmediate(Field);
Field = null;
_fieldModel = null;
_fieldParticles = null;
_fieldObjDictionary.Clear();
m_FieldAnimationDictionary.Clear();
m_FieldAnimatorDictionary.Clear();
_fieldParticleSystemDictionary.Clear();
m_SoundAssetPathList.Clear();
_gimicCntDictionary.Clear();
SetShaderGlobalColorBG = null;
BattleCoroutine.GetInstance().StopCoroutine(battleLoadCoroutine);
}
public void CreateField(BattleCamera battleCamera, GameObject battle3DContainer, GameObject cutInContainer)
{
_battleCamera = battleCamera;
m_Battle3DContainer = battle3DContainer;
m_BattleCutInContainer = cutInContainer;
Camera componentInChildren = m_Battle3DContainer.GetComponentInChildren<Camera>();
Camera component = componentInChildren.transform.Find("Camera 3DGround").GetComponent<Camera>();
_battleCamera.SetUp(componentInChildren, m_BattleCutInContainer.transform.Find("Camera").GetComponent<UICamera>(), component);
LoadField();
}
protected void LoadField()
{
IsLoadDone = false;
m_BtlMgrIns = BattleManagerBase.GetIns();
_str3DFieldNo = GetFieldIdString(FieldEffectId);
_str3DFieldPath = "3DField" + GetFieldIdString(FieldId);
m_SoundAssetPathList.Add($"s/se_field_{_str3DFieldNo}.acb");
m_SoundAssetPathList.Add(string.Format("b/bgm_field_{0}.acb", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo));
m_SoundAssetPathList.Add(string.Format("b/bgm_field_{0}.awb", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo));
m_FieldAssetPath = Toolbox.ResourcesManager.GetAssetTypePath(_str3DFieldPath, ResourcesManager.AssetLoadPathType.Field3D);
List<string> additionalAssetList = CollectAdditionalAssets();
GameMgr.GetIns().GetEffectMgr().InitCommonEffect(string.Format("Json/FIeld" + _str3DFieldNo + "EffectData", _str3DFieldNo), isBattle: true);
battleLoadCoroutine = BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(m_SoundAssetPathList, delegate
{
BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetAsync(m_FieldAssetPath, delegate
{
Toolbox.ResourcesManager.BattleListAssetPathList.AddRange(m_SoundAssetPathList);
Toolbox.ResourcesManager.BattleListAssetPathList.Add(m_FieldAssetPath);
(UnityEngine.Object.Instantiate(Toolbox.ResourcesManager.LoadObject(Toolbox.ResourcesManager.GetAssetTypePath(_str3DFieldPath, ResourcesManager.AssetLoadPathType.Field3D, isfetch: true))) as GameObject).name = _str3DFieldPath;
if (additionalAssetList.IsNotNullOrEmpty())
{
BattleCoroutine.GetInstance().StartCoroutine(Toolbox.ResourcesManager.LoadAssetGroupAsync(additionalAssetList, delegate
{
Toolbox.ResourcesManager.BattleListAssetPathList.AddRange(additionalAssetList);
BattleFieldBuild();
}));
}
else
{
BattleFieldBuild();
}
}));
}));
}
private string GetFieldIdString(int fieldId)
{
return fieldId.ToString((fieldId < 100) ? "00" : "0000");
}
private string GetFieldIdString(string fileldId)
{
if (int.TryParse(fileldId, out var result))
{
return result.ToString((result < 100) ? "00" : "0000");
}
return fileldId;
}
protected virtual void BattleFieldBuild()
{
}
protected virtual List<string> CollectAdditionalAssets()
{
return null;
}
public virtual void StartFieldSetEffect(Vector3 pos)
{
}
public virtual void StartFieldTapEffect(int areaId, Vector3 pos)
{
BattleManagerBase.GetIns().BattlePlayer.PlayerBattleView.IsTouchable();
}
public void StartFieldOpening()
{
PlayBgm();
OpeningVfx.OpenningLogStep = "StartFieldOpening";
IsFieldRandom = true;
BattleCoroutine.GetInstance().StartCoroutine(RunFieldOpening());
}
public void PlayBgm()
{
GameMgr.GetIns().GetSoundMgr().PlayBGM(string.Format("bgm_field_{0}", (_bgmId != "NONE") ? GetFieldIdString(_bgmId) : _str3DFieldNo), 0f, 0L);
}
protected virtual IEnumerator RunFieldOpening()
{
yield return new WaitForSeconds(0f);
}
public static IEnumerator ObjectChecker(float fWaitSecs, string strObjectFind, Action callback)
{
while (GameObject.Find(strObjectFind) == null || !GameMgr.GetIns().GetEffectMgr().IsFieldEffectReady || !GameMgr.GetIns().GetEffectMgr().IsBattleUIEffectReady)
{
yield return null;
}
callback();
}
public void StartFieldGimic(GameObject obj)
{
if (!GameMgr.GetIns().IsReplayBattle && BattleManagerBase.GetIns().BattlePlayer.PlayerBattleView.IsTouchable())
{
BattleCoroutine.GetInstance().StartCoroutine(RunFieldGimic(obj));
}
}
protected virtual IEnumerator RunFieldGimic(GameObject obj)
{
yield return new WaitForSeconds(0f);
}
public void StartFieldShake()
{
BattleCoroutine.GetInstance().StartCoroutine(RunFieldShake());
}
protected virtual IEnumerator RunFieldShake()
{
yield return new WaitForSeconds(0f);
}
public virtual void UpdateFieldRandom()
{
}
public void AddParticleToFieldObjDictionary(string targetPath)
{
string[] array = targetPath.Split('/');
List<Transform> list = new List<Transform>();
list.Add(_fieldParticles.transform);
List<Transform> list2 = new List<Transform>();
for (int i = 0; i < array.Length; i++)
{
list2 = new List<Transform>();
for (int j = 0; j < list.Count; j++)
{
list2.AddRange(FindAllChildByName(list[j], array[i]));
}
list = new List<Transform>(list2);
}
for (int k = 0; k < list2.Count; k++)
{
_fieldObjDictionary.Add(targetPath + "_" + k, list2[k].gameObject);
}
}
public List<Transform> FindAllChildByName(Transform parent, string name)
{
List<Transform> list = new List<Transform>();
for (int i = 0; i < parent.childCount; i++)
{
Transform child = parent.GetChild(i);
if (child.name == name)
{
list.Add(child);
}
}
return list;
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using Wizard.Battle;
internal class BaseCardIDComp : EqualityComparer<IReadOnlyBattleCardInfo>
{
public override bool Equals(IReadOnlyBattleCardInfo x, IReadOnlyBattleCardInfo y)
{
if (x == y)
{
return true;
}
if (x.BaseParameter.BaseCardId == y.BaseParameter.BaseCardId)
{
return true;
}
return false;
}
public override int GetHashCode(IReadOnlyBattleCardInfo obj)
{
return obj.BaseParameter.BaseCardId;
}
}

View File

@@ -0,0 +1,79 @@
using UnityEngine;
using Wizard.Battle.View.Vfx;
public class BattleCamera
{
public UICamera m_CutInCamera;
public Camera Camera;
public Camera _backgroundCamera;
public Vector3 BattleCameraPos { get; private set; }
public Vector3 BattleCameraRot { get; private set; }
public BattleCamera()
{
Camera = null;
}
public void SetUp(Camera camera, UICamera cutInCamera, Camera backgroundCamera)
{
Camera = camera;
m_CutInCamera = cutInCamera;
_backgroundCamera = backgroundCamera;
BattleCameraPos = Camera.transform.localPosition;
BattleCameraRot = Camera.transform.eulerAngles;
}
public VfxBase ShakeCamera(Vector3 amount, float time, float delay)
{
ParallelVfxPlayer parallelVfxPlayer = ParallelVfxPlayer.Create();
parallelVfxPlayer.Register(InstantVfx.Create(delegate
{
iTween.ShakePosition(Camera.gameObject, iTween.Hash("amount", amount, "time", time, "delay", delay));
}));
return parallelVfxPlayer;
}
public static VfxBase ShakeCameraGameObject(GameObject obj, Vector3 amount, float time, float delay)
{
ParallelVfxPlayer parallelVfxPlayer = ParallelVfxPlayer.Create();
parallelVfxPlayer.Register(InstantVfx.Create(delegate
{
iTween.ShakePosition(obj, iTween.Hash("amount", amount, "time", time, "delay", delay));
}));
return parallelVfxPlayer;
}
public VfxBase ShakeComplete()
{
return InstantVfx.Create(delegate
{
Camera.transform.localPosition = BattleCameraPos;
Camera.transform.eulerAngles = BattleCameraRot;
iTween.Stop(Camera.gameObject);
});
}
public static VfxBase ShakeCompleteGameObject(GameObject obj, Vector3 position, Vector3 euler)
{
return InstantVfx.Create(delegate
{
obj.transform.localPosition = position;
obj.transform.eulerAngles = euler;
iTween.Stop(obj);
});
}
public Camera Get3DCamera()
{
return Camera;
}
public void Dispose()
{
Camera = null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using Wizard.Battle;
using Wizard.Battle.View;
public static class BattleCardBaseExtensions
{
public static List<IBattleCardView> ConvertToViewList(this IList<BattleCardBase> battleCardBaseList)
{
return battleCardBaseList?.Select((BattleCardBase c) => c.BattleCardView).ToList();
}
public static BattleCardBase FindFromCardId(this IList<BattleCardBase> battleCardBaseList, IBattleCardUniqueID cardId)
{
if (battleCardBaseList == null)
{
return null;
}
for (int i = 0; i < battleCardBaseList.Count; i++)
{
BattleCardBase battleCardBase = battleCardBaseList[i];
if (battleCardBase.EquelsID(cardId))
{
return battleCardBase;
}
}
return null;
}
}

View File

@@ -0,0 +1,487 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cute;
using UnityEngine;
using Wizard.Battle.View.Vfx;
public class BattleCardIconAnimations : MonoBehaviour
{
public class SkillIcon
{
public string _key;
public string _iconSpriteName;
public int LabelNumber;
public SkillIcon(string key, string iconSpriteName, int labelNumber)
{
_key = key;
_iconSpriteName = iconSpriteName;
LabelNumber = labelNumber;
}
}
private List<SkillIcon> skillIconList = new List<SkillIcon>();
private List<SkillIcon> skillIconListWithoutDuplicates = new List<SkillIcon>();
private CardTemplate cardTemplate;
private BattleCardBase _card;
private SkillCollectionBase collection;
private bool skillIconAlphaFlg;
private int skillCount;
private int _inductionLabelNumber = -1;
private const float ALPHA_BLEND_RATE = 0.6f;
private const string INDUCTION_ICON_SPRITE_NAME = "battle_notice_status_04";
private const string WHITE_RITUAL_STACK_SPRITE_NAME = "battle_notice_status_11";
public VfxBase Initialize(BattleCardBase card, SkillCollectionBase collection, bool isStackWhiteRitual = false)
{
_card = card;
this.collection = collection;
ISkillApplyInformation skillApplyInformation = card.SkillApplyInformation;
bool isEarthRiteField = (IsEarthRiteField() ? true : false);
bool hasInductionSkill = HasInductionSkill();
bool hasInductionNumberSkill = HasInductionNumberSkill();
bool hasKiller = (skillApplyInformation.IsKiller ? true : false);
bool hasDrain = (skillApplyInformation.IsDrain ? true : false);
bool hasWhenDestroySkill = (HasWhenDestroySkill() ? true : false);
bool hasGetonSkill = HasGetonSkill();
bool isGetOnAfter = _card.GetOnCards.Any();
bool hasWhiteRirualStackSkill = HasStackWhiteRitualSkill();
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
return InstantVfx.Create(delegate
{
InitializeIcon(isEarthRiteField && !hasWhiteRirualStackSkill, isEarthRiteField && hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill, hasInductionNumberSkill, hasKiller, hasDrain, hasWhenDestroySkill, hasGetonSkill, isGetOnAfter, isReplay: false, isStackWhiteRitual);
});
}
public VfxBase InitializeOnlyStack(BattleCardBase card, SkillCollectionBase collection)
{
_card = card;
this.collection = collection;
bool isEarthRiteField = IsEarthRiteField();
bool hasWhiteRirualStackSkill = HasStackWhiteRitualSkill();
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
return InstantVfx.Create(delegate
{
InitializeIcon(isEarthRiteField && !hasWhiteRirualStackSkill, isEarthRiteField && hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill: false, hasInductionNumberSkill: false, hasKiller: false, hasDrain: false, hasWhenDestroySkill: false, hasGetonSkill: false);
});
}
private void InitializeIcon(bool hasWhiteRirualSkill, bool hasWhiteRirualStackSkill, int whiteRitualCount, bool hasInductionSkill, bool hasInductionNumberSkill, bool hasKiller, bool hasDrain, bool hasWhenDestroySkill, bool hasGetonSkill, bool isGetOnAfter = false, bool isReplay = false, bool isStackWhiteRitual = false)
{
if (!(_card.BattleCardView.GameObject == null))
{
ClearAllSkillIcons();
cardTemplate = _card.BattleCardView.GameObject.GetComponent<CardTemplate>();
cardTemplate.SkillIconTemp.gameObject.transform.localPosition = new Vector3(0f, -30f, -0.1f);
cardTemplate.SkillIconTemp.gameObject.transform.localScale = new Vector3(0.2f, 0.2f, 1f);
cardTemplate.SkillIconTemp.atlas = UIManager.GetInstance().GetAtlasList().FirstOrDefault((UIAtlas s) => s.name == "Battle");
AddToIconList("white_ritual", "battle_notice_status_08", hasWhiteRirualSkill);
AddToIconList("stack_white_ritual", "battle_notice_status_11", hasWhiteRirualStackSkill, whiteRitualCount);
AddToIconList("induction", "battle_notice_status_04", hasInductionSkill);
AddToIconList("induction_number", "battle_notice_status_04", hasInductionNumberSkill, GetInductionLabelNumber());
AddToIconList("killer", "battle_notice_status_01", hasKiller);
AddToIconList("drain", "battle_notice_status_07", hasDrain);
AddToIconList("destroy", "battle_notice_status_06", hasWhenDestroySkill);
if (isReplay)
{
AddToIconList("geton", "battle_notice_status_09", hasGetonSkill);
AddToIconList("geton_after", "battle_notice_status_10", isGetOnAfter);
}
else if (isGetOnAfter)
{
AddToIconList("geton_after", "battle_notice_status_10", hasGetonSkill);
}
else
{
AddToIconList("geton", "battle_notice_status_09", hasGetonSkill);
}
PopulateSkillIconListWithoutDuplicates();
string spriteName = (skillIconListWithoutDuplicates.Any() ? skillIconListWithoutDuplicates[0]._iconSpriteName : string.Empty);
cardTemplate.SkillIconTemp.spriteName = spriteName;
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates.Any() ? skillIconListWithoutDuplicates[0].LabelNumber : (-1));
UpdateSkillIconLabelColor();
skillCount = 0;
cardTemplate.SkillIconTemp.gameObject.SetActive(value: true);
if (isStackWhiteRitual)
{
cardTemplate.SkillIconTemp.alpha = 1.5f;
skillIconAlphaFlg = false;
}
}
}
public VfxBase UpdateLabelNumber()
{
return InstantVfx.Create(delegate
{
SkillIcon skillIcon = skillIconListWithoutDuplicates.FirstOrDefault((SkillIcon i) => i._key == "induction_number");
if (skillIcon != null)
{
skillIcon.LabelNumber = GetInductionLabelNumber();
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_04")
{
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIcon.LabelNumber);
}
}
});
}
public VfxBase UpdateWhiteRitualCountLabel()
{
if (!HasStackWhiteRitualSkill() || !IsEarthRiteField())
{
return NullVfx.GetInstance();
}
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
return InstantVfx.Create(delegate
{
SkillIcon skillIcon = skillIconListWithoutDuplicates.FirstOrDefault((SkillIcon i) => i._key == "stack_white_ritual");
if (skillIcon != null)
{
skillIcon.LabelNumber = whiteRitualCount;
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_11")
{
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIcon.LabelNumber);
}
}
});
}
private void AddToIconList(string key, string spriteName, bool addCondition, int labelNumber = -1)
{
if (addCondition)
{
AddSkillIcon(key, spriteName, labelNumber);
}
}
private void PopulateSkillIconListWithoutDuplicates()
{
AddToIconListWithoutDuplicates("white_ritual");
AddToIconListWithoutDuplicates("stack_white_ritual");
AddToIconListWithoutDuplicates("induction");
AddToIconListWithoutDuplicates("induction_number");
AddToIconListWithoutDuplicates("destroy");
AddToIconListWithoutDuplicates("killer");
AddToIconListWithoutDuplicates("drain");
AddToIconListWithoutDuplicates("geton");
}
private void AddToIconListWithoutDuplicates(string key)
{
if (skillIconList.Any((SkillIcon c) => c._key == key) && !skillIconListWithoutDuplicates.Any((SkillIcon c) => c._key == key))
{
SkillIcon skillIcon = skillIconList.SingleOrDefault((SkillIcon c) => c._key == key && c._iconSpriteName != null);
skillIconListWithoutDuplicates.Add(new SkillIcon(key, skillIcon._iconSpriteName, skillIcon.LabelNumber));
}
}
public VfxBase ShowIcon()
{
return InstantVfx.Create(delegate
{
cardTemplate.SkillIconTemp.gameObject.SetActive(value: true);
});
}
public VfxBase HideIcon()
{
return InstantVfx.Create(delegate
{
cardTemplate.SkillIconTemp.gameObject.SetActive(value: false);
});
}
private void Update()
{
if (cardTemplate != null)
{
if (cardTemplate.SkillIconTemp.gameObject.activeSelf)
{
SkillIconAlphaBlend();
}
else
{
cardTemplate.SkillIconTemp.alpha = 0f;
}
}
}
public void AddSkillIcon(string key, string fileName, int labelNumber = -1)
{
string iconSpriteName = ((!skillIconList.Any((SkillIcon v) => v._key == key)) ? fileName : null);
skillIconList.Add(new SkillIcon(key, iconSpriteName, labelNumber));
skillIconListWithoutDuplicates = skillIconList.Where((SkillIcon v) => v._iconSpriteName != null).ToList();
}
public void DeleteSkillIcon(string key)
{
if (skillIconList.Any((SkillIcon v) => v._key == key))
{
skillIconList.Remove(skillIconList.Where((SkillIcon v) => v._key == key).Last());
}
skillIconListWithoutDuplicates = skillIconList.Where((SkillIcon v) => v._iconSpriteName != null).ToList();
if (skillIconListWithoutDuplicates.Count == 0)
{
cardTemplate.SkillIconTemp.spriteName = string.Empty;
cardTemplate.SkillIconLabelTemp.text = string.Empty;
}
ChangeTexture();
}
public void DeleteUnneededSkillIcons()
{
RemoveSkillIconFromList("white_ritual", () => !IsEarthRiteField());
RemoveSkillIconFromList("induction", () => !HasInductionSkill());
RemoveSkillIconFromList("induction_number", () => !HasInductionNumberSkill());
RemoveSkillIconFromList("destroy", () => !HasWhenDestroySkill());
}
private void RemoveSkillIconFromList(string key, Func<bool> deleteCondition)
{
if (deleteCondition())
{
DeleteSkillIcon(key);
}
}
private void ChangeTexture()
{
if (skillIconListWithoutDuplicates.Count() - 1 > skillCount)
{
skillCount++;
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[skillCount]._iconSpriteName;
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[skillCount].LabelNumber);
}
else if (skillIconListWithoutDuplicates.Count() != 0)
{
skillCount = 0;
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[skillCount]._iconSpriteName;
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[skillCount].LabelNumber);
}
UpdateSkillIconLabelColor();
}
private void UpdateSkillIconLabelColor()
{
if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_11")
{
cardTemplate.SkillIconLabelTemp.color = Color.white;
cardTemplate.SkillIconLabelTemp.effectColor = Color.black;
}
else if (cardTemplate.SkillIconTemp.spriteName == "battle_notice_status_04")
{
cardTemplate.SkillIconLabelTemp.color = Color.black;
cardTemplate.SkillIconLabelTemp.effectColor = Color.white;
}
}
private void ChangeSkillIconLabel(UILabel label, int labelNumber)
{
if (labelNumber == -1)
{
label.text = string.Empty;
}
else
{
label.text = labelNumber.ToString();
}
}
public void ClearAllSkillIcons()
{
skillIconList.Clear();
skillIconListWithoutDuplicates.Clear();
}
private void SkillIconAlphaBlend()
{
bool flag = cardTemplate.SkillIconLabelTemp.text.IsNotNullOrEmpty();
if (skillIconListWithoutDuplicates.Count > 1)
{
if (skillIconAlphaFlg)
{
cardTemplate.SkillIconTemp.alpha += (flag ? (0.6f * Time.deltaTime * 2f) : (0.6f * Time.deltaTime));
}
else
{
cardTemplate.SkillIconTemp.alpha -= (flag ? (0.6f * Time.deltaTime * 2f) : (0.6f * Time.deltaTime));
}
}
else if (cardTemplate.SkillIconTemp.spriteName == string.Empty)
{
cardTemplate.SkillIconTemp.alpha = 1f;
if (skillIconListWithoutDuplicates.Count > 0)
{
if (cardTemplate.SkillIconTemp.spriteName != skillIconListWithoutDuplicates[0]._iconSpriteName)
{
cardTemplate.SkillIconTemp.spriteName = skillIconListWithoutDuplicates[0]._iconSpriteName;
}
ChangeSkillIconLabel(cardTemplate.SkillIconLabelTemp, skillIconListWithoutDuplicates[0].LabelNumber);
UpdateSkillIconLabelColor();
}
}
else
{
cardTemplate.SkillIconTemp.alpha = 1f;
}
if (skillIconAlphaFlg && cardTemplate.SkillIconTemp.alpha >= (flag ? 2f : 1f))
{
skillIconAlphaFlg = false;
}
else if (!skillIconAlphaFlg && cardTemplate.SkillIconTemp.alpha <= 0f)
{
ChangeTexture();
skillIconAlphaFlg = true;
}
}
private bool HasWhenDestroySkill()
{
return collection._skillTimingInfo.IsWhenDestroy;
}
public bool HasInductionSkill()
{
for (int i = 0; i < collection.Count(); i++)
{
SkillBase skillBase = collection.ElementAt(i);
if (skillBase.IsInductionSkill && skillBase.SkillPrm.buildInfo._icon == "induction")
{
return true;
}
}
return false;
}
public bool HasStackWhiteRitualSkill()
{
return collection.Any((SkillBase x) => x is Skill_stack_white_ritual);
}
public bool HasGetonSkill()
{
return collection.Any((SkillBase x) => x is Skill_geton);
}
public bool HasInductionNumberSkill()
{
for (int i = 0; i < collection.Count(); i++)
{
SkillBase skillBase = collection.ElementAt(i);
if (skillBase.IsInductionSkill && skillBase.SkillPrm.buildInfo._icon != "induction" && skillBase.SkillPrm.buildInfo._icon.Contains("induction"))
{
return true;
}
}
return false;
}
public int GetInductionLabelNumber()
{
if (_inductionLabelNumber != -1)
{
return _inductionLabelNumber;
}
SkillBase skillBase = collection.FirstOrDefault((SkillBase s) => s.IsInductionSkill && s.SkillPrm.buildInfo._icon != "induction" && s.SkillPrm.buildInfo._icon.Contains("induction"));
if (skillBase == null)
{
return -1;
}
SkillOptionValue skillOptionValue = new SkillOptionValue(skillBase.SkillPrm.buildInfo._icon);
skillOptionValue.SetupFilterVariable(BattleManagerBase.GetIns().GetBattlePlayerInfoPair(_card.IsPlayer), _card, isPrePlay: false, null);
return skillOptionValue.GetInt(SkillFilterCreator.ContentKeyword.induction);
}
private bool IsEarthRiteField()
{
if (_card.IsField || _card.IsChantField)
{
return _card.IsTribe(CardBasePrm.TribeType.WHITE_RITUAL);
}
return false;
}
public VfxBase UpdateSkillIconInReplay(List<NetworkBattleReceiver.InplaySkillEffect> inplaySkillEffectList, int inductionNumber, bool isInitialize, bool isStackWhiteRitual = false)
{
if (!isInitialize && _card.HasStackWhiteRitualAndOtherIconSkill() && skillIconListWithoutDuplicates.Count < 2)
{
return NullVfx.GetInstance();
}
_inductionLabelNumber = inductionNumber;
bool hasWhiteRitualSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.WhiteRitual);
bool hasWhiteRirualStackSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.StackWhiteRitual);
bool hasInductionSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Induction);
bool hasInductionNumberSkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.InductionNumber);
bool hasKiller = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Killer);
bool hasDrain = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Drain);
bool hasWhenDestroySkill = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Destroy);
bool hasGeton = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.Geton);
bool hasGetonAfter = inplaySkillEffectList.Contains(NetworkBattleReceiver.InplaySkillEffect.GetonAfter);
int whiteRitualCount = _card.SkillApplyInformation.WhiteRitualCount;
return InstantVfx.Create(delegate
{
if (skillIconList.Count == 0 || isInitialize)
{
InitializeIcon(hasWhiteRitualSkill, hasWhiteRirualStackSkill, whiteRitualCount, hasInductionSkill, hasInductionNumberSkill, hasKiller, hasDrain, hasWhenDestroySkill, hasGeton, hasGetonAfter, isReplay: true, isStackWhiteRitual);
}
else
{
UpdateSkillIcon("white_ritual", "battle_notice_status_08", hasWhiteRitualSkill);
UpdateSkillIcon("stack_white_ritual", "battle_notice_status_11", hasWhiteRirualStackSkill, whiteRitualCount);
UpdateSkillIcon("induction", "battle_notice_status_04", hasInductionSkill);
UpdateSkillIcon("induction_number", "battle_notice_status_04", hasInductionNumberSkill, GetInductionLabelNumber());
UpdateSkillIcon("killer", "battle_notice_status_01", hasKiller);
UpdateSkillIcon("drain", "battle_notice_status_07", hasDrain);
UpdateSkillIcon("destroy", "battle_notice_status_06", hasWhenDestroySkill);
UpdateSkillIcon("geton", "battle_notice_status_09", hasGeton);
UpdateSkillIcon("geton_after", "battle_notice_status_10", hasGetonAfter);
}
});
}
private void UpdateSkillIcon(string key, string spriteName, bool hasIcon, int labelNumber = -1)
{
if (hasIcon && !skillIconList.Any((SkillIcon v) => v._key == key))
{
AddToIconList(key, spriteName, hasIcon, labelNumber);
}
else if (!hasIcon && skillIconList.Any((SkillIcon v) => v._key == key))
{
DeleteSkillIcon(key);
}
}
public void DeleteSkillIcons()
{
if (!(cardTemplate == null))
{
DeleteSkillIcon("white_ritual");
DeleteSkillIcon("stack_white_ritual");
DeleteSkillIcon("induction");
DeleteSkillIcon("induction_number");
DeleteSkillIcon("destroy");
DeleteSkillIcon("killer");
DeleteSkillIcon("drain");
DeleteSkillIcon("geton");
}
}
public int GetIconListCount()
{
return skillIconListWithoutDuplicates.Count;
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Cute;
using UnityEngine;
using Wizard;
using Wizard.Battle.UI;
using Wizard.Battle.View.Vfx;
public class BattleControl : MonoBehaviour
{
private BattleManagerBase m_BtlMgrIns;
private int FirstAttack;
public void Init()
{
m_BtlMgrIns = BattleManagerBase.GetIns();
GameMgr.GetIns().GetInputMgr().SetLayerMask(512);
LocalLog.AccumulateLastTraceLog("StartBattleCoroutine ");
StartCoroutine(WaitLoadOpponentObjectToBattleStart(m_BtlMgrIns.LoadOpponentObjects()));
}
private IEnumerator WaitLoadOpponentObjectToBattleStart(VfxBase vfx)
{
if (GameMgr.GetIns().IsNetworkBattle)
{
FirstAttack = ToolboxGame.RealTimeNetworkAgent.GetIsFirstPlayer();
}
while (!vfx.IsEnd)
{
yield return null;
}
LocalLog.AccumulateLastTraceLog("DecideFirstUser End ");
m_BtlMgrIns.StartOpening(FirstAttack);
}
public void BattleEnd(UIManager.ViewScene MoveTo, Action callback = null, Action<UIManager.ChangeViewSceneParam> paramCustomize = null, object sceneParam = null)
{
ToolboxGame.UIManager.gameObject.SetActive(value: true);
UIManager.ChangeViewSceneParam changeViewSceneParam = new UIManager.ChangeViewSceneParam();
changeViewSceneParam.OnBeforeChange = delegate
{
BattleManagerBase.GetIns().DisposeBattleGameObj();
};
changeViewSceneParam.OnChange = delegate
{
GameMgr.GetIns().GetEffectMgr().DestroyBattleEffectContainer();
GameMgr.GetIns().GetDataMgr().ResetEnemyData();
GameMgr.GetIns().DestroyBattleManagements();
GameMgr.GetIns().GetGameObjMgr().GetUIContainer()
.SetActive(value: false);
if (callback != null)
{
callback();
}
};
paramCustomize.Call(changeViewSceneParam);
StartCoroutine(UnloadAllResources(MoveTo, changeViewSceneParam, null, sceneParam));
}
private IEnumerator UnloadAllResources(UIManager.ViewScene MoveTo = UIManager.ViewScene.None, UIManager.ChangeViewSceneParam param = null, Action callback = null, object sceneParam = null)
{
BattleLogManager.GetInstance().Clear();
GameMgr.GetIns().GetEffectMgr().ClearLastCacheEffect();
StopAllTweens();
yield return Resources.UnloadUnusedAssets();
GC.Collect();
callback?.Invoke();
if (MoveTo != UIManager.ViewScene.None)
{
UIManager.GetInstance().ChangeViewScene(MoveTo, param, sceneParam);
}
}
public IEnumerator BattleEnd(Action callback = null)
{
BattleRelease();
yield return Resources.UnloadUnusedAssets();
GC.Collect();
callback?.Invoke();
}
public void BattleRelease()
{
ToolboxGame.UIManager.gameObject.SetActive(value: true);
GameMgr.GetIns().GetEffectMgr().DestroyBattleEffectContainer();
GameMgr.GetIns().GetDataMgr().ResetEnemyData();
if (BattleManagerBase.GetIns() != null)
{
BattleManagerBase.GetIns().DisposeBattleGameObj();
}
GameMgr.GetIns().DestroyBattleManagements();
GameMgr.GetIns().GetGameObjMgr().GetUIContainer()
.SetActive(value: false);
BattleLogManager.GetInstance().Clear();
GameMgr.GetIns().GetEffectMgr().ClearLastCacheEffect();
StopAllTweens();
}
private void StopAllTweens()
{
HashSet<GameObject> hashSet = new HashSet<GameObject>();
for (int i = 0; i < iTween.tweens.Count; i++)
{
if (iTween.tweens[i] != null)
{
GameObject gameObject = (GameObject)iTween.tweens[i]["target"];
if (gameObject != null)
{
hashSet.Add(gameObject);
}
}
}
foreach (GameObject item in hashSet)
{
if (item != null)
{
iTween.Stop(item);
}
}
iTween.tweens.Clear();
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections;
using UnityEngine;
public class BattleCoroutine
{
private static BattleCoroutine m_instance;
private static MonoBehaviour _coroutineObject;
public static BattleCoroutine GetInstance()
{
if (m_instance == null)
{
m_instance = new BattleCoroutine();
}
if (_coroutineObject == null)
{
GameObject gameObject = Object.Instantiate(Resources.Load("Prefab/Game/_BattleCoroutine")) as GameObject;
if (null != gameObject)
{
_coroutineObject = gameObject.GetComponent<MonoBehaviour>();
}
}
return m_instance;
}
public Coroutine StartCoroutine(IEnumerator enumerator)
{
return _coroutineObject.StartCoroutine(enumerator);
}
public void StopAllCoroutines()
{
_coroutineObject.StopAllCoroutines();
}
public void StopCoroutine(IEnumerator enumerator)
{
if (enumerator != null)
{
_coroutineObject.StopCoroutine(enumerator);
}
}
public void StopCoroutine(Coroutine enumerator)
{
if (enumerator != null)
{
_coroutineObject.StopCoroutine(enumerator);
}
}
}

View File

@@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cute;
using UnityEngine;
using Wizard;
using Wizard.Battle;
using Wizard.Battle.Player.Emotion;
using Wizard.Battle.View;
using Wizard.Battle.View.Vfx;
public class BattleEnemy : BattlePlayerBase
{
private readonly Vector3 OFFSET_THINK_ICON_FROM_CLASSVIEW = new Vector3(0.62f, 0.15f, 0f);
private IEmotion _emotion;
private readonly Vector3 FIELD_CENTER_POSITION = new Vector3(0f, 0.25f, 0f);
public override bool IsGameFirst => !base.BattleMgr.IsFirst;
public override bool IsPlayer => false;
public override IBattlePlayerView BattleView => BattleEnemyView;
public override IEmotion Emotion => _emotion;
public virtual IBattlePlayerView BattleEnemyView { get; protected set; }
public bool EnableEnemyAI { get; set; }
public override int Turn
{
get
{
if (!base.BattleMgr.IsFirst)
{
return base.BattleMgr.FirstTurn;
}
return base.BattleMgr.SecondTurn;
}
set
{
if (base.BattleMgr.IsFirst)
{
base.BattleMgr.SecondTurn = value;
}
else
{
base.BattleMgr.FirstTurn = value;
}
}
}
public event Action<List<int>> OnMulliganEndForReplay;
public BattleEnemy(BattleManagerBase battleMgr, BattleCamera battleCamera, BackGroundBase backGround, IInnerOptionsBuilder innerOptionsBuilder)
: base(battleMgr, battleCamera, backGround, innerOptionsBuilder)
{
}
protected override void Initialize()
{
BattleEnemyView = new BattleEnemyView(this);
}
protected override void CreateSelfBattleCard()
{
EnemyClassBattleCard item = new EnemyClassBattleCard(new ClassBattleCardBase.ClassBuildInfo(_isPlayer: false, 20, this, base.BattleMgr.BattlePlayer, base.BattleMgr, base.BattleMgr.BattleResourceMgr));
base.ClassAndInPlayCardList.Add(item);
}
public override void Setup(BattlePlayerBase opponentBattlePlayer)
{
_emotion = _innerOptionsBuilder.CreateEnemyEmotion((IClassBattleCardView)base.Class.BattleCardView);
base.Setup(opponentBattlePlayer);
}
public override void SetupClone(BattlePlayerBase sourceBattlePlayer, BattlePlayerBase virtualOpponentBattlePlayer, CloneActualFlags cloneFlags)
{
sourceBattlePlayer.CopyToVirtualBase(this, virtualOpponentBattlePlayer, cloneFlags);
}
public override VfxBase StartTurnControl(string log = "")
{
if (GameMgr.GetIns().IsAdminWatch)
{
UpdateHandCardsPlayability();
}
Turn++;
SequentialVfxPlayer sequentialVfxPlayer = TurnEvolveControl(BattleView.EpIcon);
VfxBase vfx = TurnStart();
sequentialVfxPlayer.Register(vfx);
VfxBase vfx2 = BattleManagerBase.GetIns().JudgeBattleResult();
sequentialVfxPlayer.Register(vfx2);
sequentialVfxPlayer.Register(CreateThinkingVfx(base.BattleMgr));
return sequentialVfxPlayer;
}
public VfxBase CreateThinkingVfx(BattleManagerBase battleMgr)
{
if (GameMgr.GetIns().IsAdminWatch)
{
return NullVfx.GetInstance();
}
return new DelaySetupVfx(() => new ThinkIconShowVfx(delegate
{
Vector3 position = base.BattleCamera.Get3DCamera().WorldToScreenPoint(base.Class.BattleCardView.Transform.position + OFFSET_THINK_ICON_FROM_CLASSVIEW);
return UIManager.GetInstance().getCamera().ScreenToWorldPoint(position);
}, battleMgr.BattleResourceMgr));
}
public override VfxBase UsePp(int pp, bool isNewReplayMoveTurn = false)
{
base.UsePp(pp);
int usedPp = base.Pp;
int maxPp = base.PpTotal;
Vector3 labelPosition = default(Vector3);
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
sequentialVfxPlayer.Register(InstantVfx.Create(delegate
{
Vector3 position = base.BattleCamera.Get3DCamera().WorldToScreenPoint(StatusPanelControl.GetPPPanel().transform.Find("PPIcon/PPLabel").transform.position);
labelPosition = UIManager.GetInstance().getCamera().ScreenToWorldPoint(position);
}));
sequentialVfxPlayer.Register(new DelaySetupVfx(() => m_vfxCreator.CreateUsePp(usedPp, maxPp, labelPosition, isNewReplayMoveTurn)));
return sequentialVfxPlayer;
}
protected override VfxBase TurnStartDrawCard(SkillProcessor skillProcessor)
{
NullVfx.GetInstance();
int drawCount = ((IsGameFirst || Turn != 1) ? 1 : 2);
VfxWith<IEnumerable<BattleCardBase>> vfxWith = RandomCardDraw(drawCount, skillProcessor);
VfxBase vfxBase = CardDrawVfx(vfxWith.Value);
SequentialVfxPlayer result = SequentialVfxPlayer.Create(vfxWith.Vfx, vfxBase);
if (!base.Class.IsDead && EnableEnemyAI)
{
base.BattleMgr.EnemyAI.ExecuteEnemyAI(useWait: true);
}
_ = base.Class.IsDead;
return result;
}
public override VfxBase CardDrawVfx(IEnumerable<BattleCardBase> DrawList, bool skipShuffle = false, bool isOpenDrawSkill = false)
{
SequentialVfxPlayer sequentialVfxPlayer = SequentialVfxPlayer.Create();
if (GameMgr.GetIns().IsAdminWatch)
{
foreach (BattleCardBase card in DrawList)
{
if (card.BaseCost != card.Cost)
{
List<int> costList = card.BattleCardView.GetUseCostList(card.Cost);
sequentialVfxPlayer.Register(InstantVfx.Create(delegate
{
card.BattleCardView.UpdateCost(costList);
}));
}
}
}
sequentialVfxPlayer.Register(new OpponentDrawCardVfx(DrawList, isOpenDrawSkill));
sequentialVfxPlayer.Register(new OpponentDrawCardToHandVfx(DrawList.ToList(), 0.4f, isOpenDrawSkill, skipShuffle));
return sequentialVfxPlayer;
}
public override VfxBase TurnEnd()
{
ParallelVfxPlayer result = ParallelVfxPlayer.Create(base.TurnEnd(), new ThinkIconHideVfx(base.BattleMgr.BattleResourceMgr));
if (GameMgr.GetIns().IsAdminWatch)
{
foreach (BattleCardBase handCard in base.HandCardList)
{
handCard.BattleCardView.HideCanPlayEffect();
}
}
return result;
}
protected override void SetActive()
{
if (GameMgr.GetIns().IsAdminWatch)
{
UpdateHandCardsPlayability();
}
if (!IsGameFirst || Turn != 1)
{
base.IsChoiceBraveEffectTiming = true;
BattleEnemyView.UpdateChoiceBraveButtonPulsateEffectAndSprite();
}
}
public override BattlePlayerBase CreateVirtualPlayer()
{
return new VirtualBattleEnemy(base.BattleMgr, base.BattleCamera, base.BackGround);
}
public override void UpdateHandCardsPlayability(bool areArrowsForcedOff = false)
{
foreach (BattleCardBase handCard in _opponentBattlePlayer.HandCardList)
{
handCard.BattleCardView.areArrowsForcedOff = areArrowsForcedOff;
handCard.BattleCardView.UpdateMovability();
}
if (!GameMgr.GetIns().IsAdmin)
{
return;
}
foreach (BattleCardBase handCard2 in base.HandCardList)
{
handCard2.BattleCardView.areArrowsForcedOff = areArrowsForcedOff;
handCard2.BattleCardView.UpdateMovability();
}
if (base.IsSelfTurn)
{
BattleView.UpdateChoiceBraveButtonPulsateEffectAndSprite();
}
}
public override VfxBase MoveToHand(List<BattleCardBase> cardsToMoveToHand)
{
return SequentialVfxPlayer.Create(new OpponentDrawCardToHandVfx(cardsToMoveToHand.ToList(), 0.3f), InstantVfx.Create(delegate
{
UpdateHandCardsPlayability();
}));
}
public override EffectBattle GetSkillEffect(string skillEffectPath)
{
return GameMgr.GetIns().GetEffectMgr().GetEnemyEffectBattle(skillEffectPath);
}
public override Vector3 GetFieldCenterPosition()
{
return FIELD_CENTER_POSITION;
}
public override VfxBase TurnStartDraw(SkillProcessor skillProcessor)
{
return base.TurnStartDraw(skillProcessor);
}
public void CallRecordingMulliganEnd(List<int> cardIndexList)
{
this.OnMulliganEndForReplay.Call(cardIndexList);
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using Wizard;
public class BattleFinishParam : BaseParam
{
public int class_id;
public int total_turn;
public int evolve_count;
public int enemy_evolve_count;
public int battle_result;
public int is_retire;
public Dictionary<string, int> mission;
public string recovery_data;
public int SDTRB;
public string[] prosessing_time_data;
}

Some files were not shown because too many files have changed in this diff Show More