Stage, enemy, encounter and event contract¶
Status: This is a dated stage/enemy design and source baseline. For the current approach, retain existing enemy and stage systems where wiring is sufficient; see the roadmap and arena comparison.
R0 design/source audit, 2026-10-26, batch 31. Follow the work-order plan, run/bootstrap, player capabilities and combat contract. This specifies extraction, authoring and migration constraints. It does not implement the shared scene, new event boundary, enemy refactor or encounters. Preserve the current one/two-player behavior, authored timing, random calls, difficulty values, environments and save/progression identity. Four-player participation is designed separately; no new HP/spawn/drop scaling is inferred.
Reference and coverage¶
Reference is the current working source at HEAD 026941da, including earlier stabilization and uncommitted batch-30 planning. It is not certified Steam source. Existing 608 source pins remain unchanged. The following maps preserve inspected source/data and distinguish catalog routes from runtime reachability:
| Evidence | Coverage and interpretation |
|---|---|
| Stage definitions | Six LevelData assets and 26 difficulty rows; three assets belong to Bootloader's level catalog. Stable IDs, availability, duration, boss/music, schedules and dialog references are recorded. |
| Spawn schedules | All 41 SpawnerData assets / 1,854 event rows, including stage/difficulty uses, intervals/windows/directions and serialized runtime flags. Catalog association does not prove an event executes in a selectable run. |
| Enemy authored baseline | 135 direct Enemy/Boss components in 135 prefabs, with authored stats, hit points, drops, boundary policy, state order and schedule routes. 101 prefabs have direct references from schedules used by boot-catalog stage maps; three boss prefabs are referenced directly by those stage assets. Summoned content and prototype retirement still need route closure. |
| Behavior bindings | 518 state/mechanic components, including 21 state types, exact same-object component order, transition IDs and authored values/callback fields. This is direct YAML wiring, not resolved imported variants or a semantic review of every boss method. |
| Source consumers | 69 lexical category rows across 61 scripts, including declarations, Editor and Tutorial candidates; not a compiled call graph. |
| Scene summaries and components | Three arenas plus Experimental: raw file hashes, component counts, prefab instances and external GUID references; 45 selected camera/map/run/presentation component records. Component hashes use normalized text. Custom component fields include nested authored data. Unity Tilemap cell payloads remain covered by source hashes, not interpreted as resolved terrain. |
| Event connections | 84 C# occurrences across 11 scripts: six declarations, six subscriptions, five removals and 67 publisher/invocation candidates. Includes static GameEvents and instance OnStateChanged. Lexical occurrences are not runtime delivery counts. |
| Persistent callbacks | 93 enemy/state/mechanic registration edges, with owner/target identity and method lines. Dynamic UnityAction arguments, execution and prefab modifications require import checks. A separate scan of 356 animation clips finds no serialized functionName entries; it does not resolve Animator/controller behavior generally. |
| Comparison cases | 24 source-derived timing/ordering/arithmetic scenarios and future verification requirements. None was executed in Unity. |
The serialized scan covers 1,256 .asset/.prefab/.unity files, including recovery and prototype content. Focused review covers Enemy/Boss and base state/attack/mechanic ownership, SpawnerEventManager/SpawnerData, Timer, stage initialization/selection/retry/result boundaries, difficulty HP, map/camera composition and representative spawn/chase/idle/ranged/charge/summon/mechanic behavior. State-specific data is inventoried for all mapped prefabs; complete boss/projectile/variant semantic and runtime coverage is not claimed. Validate each migrated path and its inherited/overridden content before retirement.
Authoritative owners and disposition¶
| Responsibility | Target authority / lifetime | Existing disposition |
|---|---|---|
| Stage definition | Stable stage/difficulty IDs, authored environment/config, schedule definitions, music/boss/clear and eligibility inputs. Immutable during a run. | Retain LevelData asset GUIDs and IDs. Extend/adapt its authoring boundary; do not reorder stage ordinals or collapse save identities into the shared scene name. |
| Stage runtime | Elapsed clocks, schedule cursors/admission state, active encounter instances, boss completion and owned environment/actors. One run generation. | Extract mutable flags from SpawnerData/StageMap and SpawnerEventManager. Unlocked state belongs to the profile adapter, not the shared definition. |
| Shared scene composition | Camera/HUD/menu bindings and run entry, environment creation/binding/cleanup. | Replace copied arena manager ownership incrementally. GameInitialization becomes explicit preparation; GameManager orchestration splits across existing contracts. No new universal manager. |
| Enemy assembly/lifetime | Definition/prefab, complete run/lease context, collider/render/physics, hit points and explicit entry state. | Retain prefab GUIDs and authored values initially; adapt Entity/Enemy lifecycle to prepare-before-activate. Do not create a parallel SO stat copy while prefab fields remain authoritative. |
| Enemy capabilities | Vitals/status/targeting/movement and attack execution, with active-roster and damage interfaces. | Extract Enemy's mixed simulation/feedback/search responsibilities; retain ordered formulas, contact/status guards and existing target policies. |
| State/behavior execution | One current state, explicit transition policy, per-instance timers/counters and owned work. | Retain authored state graph/behavior configurations; adapt EnemyBaseState/EnemyAttackState and subclasses. Replace implicit first-component entry with an explicit equivalent entry reference after comparison. State exit does not indiscriminately cancel valid attacks. |
| Enemy death/drop outcome | Defeat versus despawn reason, once-only score/drop/counter/completion and unregister. | Separate Enemy.Death, DeathAnimation/Rewarding and off-screen return. Preserve their different outcomes; repeated teardown must not become a second death. |
| Encounter scheduling | Clock-based eligibility/admission, direction selection, special/boss instances and phase transitions. | Extract SpawnerEventManager; keep presentation outside scheduling and move flags to per-run instances. Correctness changes require explicit scenario evidence. |
| Events and observers | Typed facts with scope, origin and ownership; application adapters own persistent/service effects. | Retain GameEvents as a temporary compatibility bridge; migrate producers/subscribers together. State/mechanic UnityEvents remain authoring connections until their consumers migrate. |
Each instantiated environment, enemy, attack and drop has one owning run/lease. Application services survive run teardown. Definitions, profile unlocks and input sessions are not reset when a pooled enemy is reused.
One gameplay scene: concrete authoring boundary¶
The scene is the shared runtime host; the stage remains a content identity. A validated request supplies stage/difficulty, roster/characters, modifiers and execution policy. The environment prefab supplies its tilemap/generator configuration, colliders, spawn markers, bounds and optional explicitly declared behavior. Resolve live transforms/components from that instance; project SOs reference assets and marker identities, not scene-object instances.
Current scenes are similar, not identical. All three arenas have the common run owners from the prior wiring map. Stage 1 also has a direct migrated MenuController missing from the Stage 2/3 map. Their MapGenerator values share scale0.03, threshold0.8, visible cells34×26, offset(1,0), octaves2, persistence0.3 and lacunarity2.5, while content differs:
| Arena | Ornament spacing | In/out ornament entries | RuleTile references |
|---|---|---|---|
| Meadow / Stage 1 | 95 | 11 / 13 | Stage-1-specific pair in component map. |
| Crystal Mine / Stage 2 | 97 | 7 / 8 | Stage-2-specific pair. |
| Snowdrift Vale / Stage 3 | 98 | 7 / 10 | Stage-3-specific pair. |
Carry each scene's actual camera, materials, lighting/volume, tile/ornament data, physics/collision, HUD/menu/prefab overrides and audio into its migration manifest. The maps are starting evidence, not a complete resolved scene-equivalence certificate. MapGenerator uses random noise offsets and ornament draws, owns generated-cell dictionaries and instantiated children, and runs from camera-cell changes. Reset must clear the old instance rather than leave its generated set attached to the next stage. Do not normalize terrain, density, seed/order or first-generation timing while consolidating scenes.
For a normal run: resolve definitions → instantiate environment inactive → bind/validate markers, camera/actors/schedules and feedback → prepare capabilities/state → activate/register → admit clocks and encounters. Failure rolls back the owned instance and cannot advance progression. Retry creates fresh runtime state for the same selected content and roster; switching to another stage unloads old environment/registries/work first. Preserve start/end gate choreography through existing adapters where required.
Maintain existing StageMap.id, stageName, stageIndex, asset GUIDs and saved keys. MainMenu start and GameMenu retry currently use sceneName; introduce a validated routing adapter while both old and migrated content coexist, then migrate each verified stage to the shared runtime. Do not point every asset at one existing arena without extracting its environment. Stage 4/5 are unavailable and outside the boot catalog; Experimental has a separate route and is not authorized as newly released content.
A development stage request enters this same runtime with isolated data/result/service adapters. Dummy assets and a development-only launcher cannot become production scene/prefab/catalog/Resources dependencies. A genuinely unusual layout supplies a named behavior or separate content composition sharing the same owners; an exception requires a concrete unmet binding/behavior requirement. Old arenas remain until their full content and callers are accounted for.
Existing encounter behavior to retain and characterize¶
Timer has three scaled clocks. currentTime is the normal special-schedule/end-condition clock; currentSpawnerTime increments deltaTime×multiplier for creep schedules; currentGameTime is the elapsed/display/stat clock. Timer.Start resets the first two and multiplier1, not every field. OpenGate starts all three. Boss entry stops currentTime/currentSpawnerTime while currentGameTime remains active. Pause via timeScale affects all scaled progression. GameOver stops all three. Keep the distinction and existing payout inputs; do not substitute one elapsed-time counter.
Spawner initialization waits for spawnerManagerInit, captures Camera.main, enters Battle and clears both schedules' isActive/isSpawning flags. Each Update evaluates creeps, specials, then suppression expiry. Windows include both endpoints. Creep uses currentSpawnerTime and special uses currentTime. Admission sets isSpawning before a scaled spawnRate wait; positions use the camera when the wait completes. The source is frame-admitted scheduling, not accumulated catch-up spawning.
SpawnEnemy checks left, right, top and bottom independently. Each true explicit direction can spawn; spawnRandom selects using a shared lastSpawnDir cursor (left0/right1/top2/bottom>=3) and its existing precedence. Preserve branch order, camera margins and random overloads. Creep events clear isSpawning at the tail; special events remain latched until another route resets them. The tail helper blocks only BossFight, not a changed window/suppression/GameOver. Test pending callbacks before deciding cancellation fixes; do not silently alter intervals or spawn density.
Miniboss admission assigns60 seconds of creep suppression; Special assigns10. Matching any killId from the special schedule clears suppression and all creep flags. It is content-ID matching, not live-instance correlation. Record overlapping specials, off-screen removal, resets and expired-run delivery. This is gameplay scheduling evidence, not a diagnosis of the deferred main-menu freeze.
A Boss-type scheduled entry enters BossFight and changes music before normal spawning. Boss admission is driven inside iteration of SpecialSpawnData; the empty-list and selected-boss/schedule-reference cases need checks. SpawnBoss stops two clocks/following, marks current enemies fleeing, waits1s, spawns entrance effect, waits configured spawnDelay, spawns/binds the boss and animates its HUD. Scaled waits and independent tweens must retain their existing policy. Boss.Death first performs base enemy death, then sets IsBossDefeated, restores stage music, announces BONUS and requests ChangeStage. Here ChangeStage resumes the battle phase/clocks/camera; it does not load another arena. Preserve boss admission/completion/reset semantics rather than inferring a multi-boss system from that name.
GameManager admits an end outcome when currentTime reaches EndGameTime, separately from boss defeat. Preserve that distinction and result choreography; the reward/progression contract owns complete payout/unlock semantics. Scheduler teardown rejects expired run work, while actor death, retained projectiles and normal phase changes follow their specified policies.
Enemy lifecycle and behavior invariants¶
Awake caches Entity dependencies and initializes all same-object state components. OnEnable resets shared status, HP from difficulty, targeting/aggro/border/invulnerability/mark flags, publishes target addition, then requests the first state and clears the mark visual. LoadState uses the first GetComponents result; StartState returns if it already equals currentState. The new explicit entry/reset boundary must characterize fresh/reused actors and listener observations before changing that sequence. Required-reference failures should be caught before activation instead of producing a half-registered actor.
Enemy.Update orders HP view, flip, off-screen timer, behavior decision, status, aggro timer and player-position refresh. Fleeing precedes null-target/death/stun guards; tagged-player searches and cached target positions have their current timing. Target selection uses active tagged objects rather than an explicit joined roster. Move it behind the roster/target interface without inventing additional-player aggro weights. HP is base×(1+difficulty bonus/100): −20/0/20/40/70 for Easy through Chaos.
EnemyBaseState sets current state and invokes onEnter before starting EnterState. Direct EnterNextState does not call the prior ExitState automatically; default ExitState invokes onExit and uses nextState[0]. AttackState has distinct current-state-only versus continuously ticking cooldown modes, IsAttacking admission, animation delay and range-dependent next states. Preserve transform-based chase, Rigidbody charge, freeze application and the particular behavior's snapshot/live-target choice. State/mechanic fields are per instance; old waits/tweens/delegates must not mutate a recycled lease. Do not blanket-cancel projectiles on actor death: their authored caster-death policy may allow them to continue.
Contact and attack wrappers preserve the combat/player damage/status contract. Enemy.TakeDamage itself does not supply every caller's invulnerability/death gate. It emits adjusted/truncated DamageDealt, changes HP, consumes feedback random draws, invokes hit listeners, retargets and resolves lethal/knockback work in source order. Poison sets resistance−10 during CCDuration. State registration/UnityAction callbacks and configured onAttack/onHit/onDeath behavior must survive their migration; a class inventory or event-name match does not establish execution.
Normal Death marks dead/collider off; publishes nonempty killId, global kill count and target removal; adds score and conditional Chaos fragments; plays sound/zeros velocity; starts DeathAnimation, which triggers animation and Rewarding before its delay; then invokes onDeathEvent. Base Death is not guarded against every external repeated call. Off-screen expiry unregisters and pools without those kill/score/drop events. These become explicit defeated/despawned reasons with once-only outcome guards, while preserving normal outputs.
DropReward rolls integer [0,dropPercentage), accepts below dropChance, then rolls count inclusively through dropMax. Chest substitution has another random draw. Coin/Chest/BossChest item instances receive exp/gold fields after pool activation. Preserve denominators, ordering, source identity and payload preparation. Fixing stale item/attack state is a scoped correctness decision, not permission to rebalance drops. Some authored summon/state paths have additional source risks (for example division by loop index0 in nonrandom EASSummonCreature); establish route/runtime evidence before correcting them.
Event contracts and compatibility¶
| Existing notification | Producers / consumers | Target boundary |
|---|---|---|
| AddTarget(Enemy) | Enemy.OnEnable → Player target registration. | Run/lease-scoped actor registration with valid hit points and complete context; late players query the current registry. |
| RemoveTarget(Enemy) | Enemy defeat/off-screen expiry → Player unregister. | Idempotent unregister with actor instance and reason, separate from reward-bearing defeat. Audit direct pool/scene unload paths too. |
| EnemyDeath(string) | Enemy.Death for nonempty killId → spawner suppression and GameDataCount. | Attributable enemy-defeat fact with content and live encounter identity. Compatibility adapter preserves legacy key/guard and ordering; it must not double-count. |
| AddDataCount(string,int) | Combat/pickups/run outcomes → GameDataCount. | Typed owner facts mapped to existing persistent counter keys/amounts at equivalent publication points. Do not delay all counters until run end by assumption. |
| AchievementTrigger(string) | GameDataCount → application AchievementManager. | Application-owned adapter with initialized profile/service context, same intended trigger timing, explicit registration lifetime and optional-service policy. |
| GameStateService.OnStateChanged | Instance state setter, including pause/reward/end/boss flags. | Retain the authoritative state query and defined setter/clock ordering; notifications do not become competing clock owners. |
Use typed C# interfaces/events initially with explicit application/run scopes. Commands such as ApplyDamage, AcceptReward and StartEncounter go to one owner and return results; facts inform views/counters/other observers. Carry run generation, actor/slot/lease identity, definition ID and the values required by the consumer. No mutable scene-object pointer or display-name/string key alone proves that a callback belongs to the current run.
Define synchronous delivery, re-entry, listener-error and subscription ownership before conversion. Existing order-sensitive chains need comparison; no async bus or assumed subscriber order supplies gameplay sequencing. A view failure must not become the hidden authority for death or stage completion. De-duplicate bridge delivery and retain old names/signatures until all source and serialized listeners move. Player/Spawner/GameDataCount currently pair enable/disable subscriptions; AchievementManager registers at application initialization. Its different count is lifetime evidence, not a reproduced listener leak.
Snapshot current state for late observers; events are not a persistence log. Dispose run subscriptions and invalidate old callbacks before returning actors/environment. Do not clear application listeners when a run ends. Validate dynamic state/mechanic UnityEvent targets and arguments; the 93 persistent edges are part of content authoring, not automatically replaced by the new notification boundary.
Authoring recipes and acceptance¶
Enemy: choose a stable prefab/content identity; wire the Enemy/Boss capability, required physics/render/animator/collider, tag/layers and hit-point receivers; assign an explicit entry state and validated transition graph; bind each state's target/attack/presentation dependencies and mechanic callbacks; define difficulty/base stats, contact/attack policy, defeat/drop and off-screen policy. Resolve child-versus-root collider routing and every projectile/summon payload. Validate graph edges, required callbacks/Shadow/HUD references and reset fields before activation. Retain working prefab data instead of moving every field to a new SO by rule.
Stage: select stable stage/difficulty IDs; bind environment prefab/generator/tile/ornament data, camera/bounds/spawn markers, schedule/boss/music/dialog/clear and reward eligibility; validate each enemy/reference/window/interval/direction and clear policy. Reuse definitions without storing live event flags. Invalid references fail preparation; legacy odd values get explicit dispositions rather than silent clamping. Create a second stage configuration without adding a stage-number switch or copying run managers.
F4 fixture: first one passive target, then one production simple enemy, then a short encounter, then representative boss/phase behavior. Exercise prepare/cancel at each wait, repeated spawn/reuse/defeat/despawn, target loss, state interruption, status, drops and once-only counters. Run two different existing stage configurations sequentially in the same gameplay scene and compare all recorded environment/camera/audio/schedule differences. Verify event delivery after retry/unload/late subscription and the absence of old-run mutations. Preserve authored definitions and normal saves; confirm development assets/results remain isolated. These are future checks, not a passed full playthrough.
Open dispositions and implementation order¶
| Follow-up | Required evidence / owner |
|---|---|
| SE-01 · Environment and imported wiring | Resolve scene/prefab overrides, generated/preauthored terrain, lighting/audio/UI/bounds and build dependency differences before each arena migrates. The raw maps do not replace Unity import verification. |
| SE-02 · State and lease reset | Characterize first-state reuse, transition/re-entry, state timers, mechanics/delegates/tweens and caster/projectile lifetimes. Choose cancellation/reset per owned operation. |
| SE-03 · Encounter admission and outcomes | Characterize pending-window/suppression/GameOver callbacks, empty-special boss admission, simultaneous specials, boss definition mismatch and duplicate death/clear. Record intentional bug dispositions without changing cadence or payout. |
| SE-04 · Content-route closure | Trace summoned/projectile/variant references and special/boss behavior methods from each migrated catalog route. Unreferenced-in-one-map is not deletion authority. |
| SE-05 · Event migration | Resolve persistent/dynamic callback targets, application/run registration, payload/order, re-entry/error and bridge removal tests. No global conversion before owning slices exist. |
| SE-06 · Progression and extra players | Complete reward/payout/unlock/result and added-player participation contracts using preserved IDs and current clocks. No guessed scaling or new stage/mode. |
R1 implements minimal run/lease/registry/event lifetime and the shared scene skeleton; R2 supplies roster/input; R3/R4 supply player/ability/combat boundaries and the passive receiver. R5a migrates one real enemy/state/attack path before other enemy/boss content. R5b migrates environment/schedules/phase ownership into the shared scene, one existing stage at a time. R6/R7 integrate rewards/profile/UI/services; R8 finishes released content/Tutorial and removes verified obsolete scenes/bridges. Keep a rollback boundary per stage and preserve a legacy entry route until its replacement's actual checks pass.
This completes this bounded core contract and its static maps, not R0 as a whole or every migration follow-up. The reward/pickup/progression core contract now records that next boundary, the owner-confirmed shared/distinct card-count policy and unresolved payout/scarcity/recovery details. The persistence/UI/service contract and first R1a lifetime slice now cover those next boundaries; the phase gate register retains SE-01–SE-06. Only the isolated lifetime core is admitted next, with scene integration at R1b. Tutorial and dedicated GAME-01 work remain parked.