atomic pair - purchasing couples them, so neither moves alone.
SkillData.Level is gone. It was an [Export] on a cached Resource: one
instance per process, so the level never persisted, leaked between save
slots, and made "unowned" indistinguishable from "level 1". Level now
comes from the Playthrough and is passed to ISkill.Initialize.
SkillManager reconciles from SkillSetChanged instead of being told by
each caller. The old shape was "record it, then tell SkillManager" as two
calls at four sites; AddSkill's early return when no player is registered
left the skill owned but inert. Reconciliation also tracks the level each
component was built at, so an upgrade rebuilds it rather than being
patched in from outside.
SkillUnlockerComponent collapses to one TryBuy - unlock and upgrade were
two methods each doing affordability, debit, level mutation and manager
notification in order.
The guard three components copy-pasted around Upgrades[Level - 1] - and
which BrickArmor omitted - now lives once in SkillComponentBase.
Hud reads coins from CoinsChanged instead of rebuilding translated
strings every frame. Lives still polls; it moves with the death path.
narrowest safe step: only level identity and progression move. Coins,
lives, skills and death stay entirely on GameStateStore and behave as
before, so nothing is tracked in two places.
GameManager addresses levels by stable LevelId via the catalogue;
LevelScenes is gone, so reordering levels no longer invalidates saves.
Every level entry goes through EnterLevel, which fixes LevelStarted never
firing on the first level or after Continue - ghost recording and splits
have been dead outside mid-campaign.
GoTo announces after a frame, once the incoming scene is actually
current. ChangeSceneToPacked is deferred, so the old code handed
GhostEventHandler the scene being torn down.
FinishLevel takes LevelTime? - GetCurrentLevelTime can legitimately be 0
and inventing a value is exactly the bad data LevelTime refuses.
Continue distinguishes absent from corrupt and refuses to overwrite a
corrupt save. Statistics move to the Profile. 69 tests.
- Fix double-execution bug in LevelStateHandler (coins/skills were committed twice per level)
- Fix DamageComponent to track multiple targets via HashSet instead of single node
- Fix HealthComponent: update health immediately, decouple from PlayerController via signal wiring in PlayerController
- Remove dead loop in SkillManager.RemoveSkill; simplify O(n²) throw skill loop
- Replace GetNode<T>(path) with .Instance across managers; add Instance to StatisticsManager/SpeedRunManager
- GameManager.GetPlayer() now uses EventBus.PlayerSpawned instead of scanning all scene nodes
- UIManager.UiStack: remove [Export], use private List<Control>
- PlayerState: extract DefaultLives constant, simplify CreateDefault()