Commit Graph

193 Commits

Author SHA1 Message Date
c1c3e7b020 chore: editor artifacts - catalogue typed array, script uids, compiled translations 2026-07-28 19:46:30 +02:00
627900b2e8 feat(progression): composition root and event republisher (slice 9)
ProgressionRoot autoload builds the JSON stores from
GlobalizePath("user://"), loads the Profile, reads the authored upgrade
tables into a SkillCatalogue, and owns the ProgressionService. Every
mutating operation is wrapped so draining and republishing cannot be
forgotten - the one risk of the DrainEvents design, now confined to one
file.

ProgressionEventRepublisher turns domain records into EventBus signals
(ADR-0004), so existing subscribers and scene wiring keep working. New
RunEnded signal: GameOverScreen reads Lives directly today, which is the
last-life soft-lock.

Registered after SkillManager, since the skill catalogue is built from
AvailableSkills. GameStateStore stays until slice 10 moves the callers.

Transitional: LevelStarted is republished with a catalogue index because
GhostManager names files ghost_level_{index}.json.
2026-07-28 19:43:04 +02:00
f85c16c641 fix(levels): ScenePath must be a string, not a PackedScene
slice 7 regression. level_village_2..5.tscn each reference their own
LevelResource for the death screen label, so pointing the resource back
at the scene forms a cycle Godot cannot resolve - "Parse Error: Busy"
and the level fails to load entirely.

ScenePath was a string for exactly this reason. Reverted, and the rot it
allowed (forest_1 aimed at a nonexistent scene) is now caught by
LevelCatalogue.Validate checking every path resolves, plus a repo-level
structural check - fixed by validation rather than by typing.

project.godot: restores the C# feature flag and dialogue_manager
uses_dotnet, which `godot --headless --import` strips on every run.
2026-07-28 19:40:02 +02:00
7a69292066 feat(progression): json stores on a real filesystem (slice 8)
new progression.adapters project, no engine reference. Stores take a
directory path; Godot resolves user:// with GlobalizePath and passes it
in, which is what makes them testable at all - originally I'd written
this slice off as untestable.

version lives in a file envelope, not the snapshot, so the domain only
knows the current shape. A newer-build save reports Corrupt with a
reason instead of masquerading as "no save" and getting overwritten.

atomic write via temp + rename. One file per aggregate: saving a run
leaves the profile file byte-identical, asserted.

guard rail fixed - it was matching prose in doc comments, now anchored
to using-directives plus a project.assets.json check. 63 tests.
2026-07-28 19:35:43 +02:00
f991bb99fc feat(levels): level catalogue with stable ids (slice 7)
LevelResource gains a stable Id and swaps the string ScenePath for an
editor-tracked PackedScene. The old path field was never read and had
rotted - forest_1 pointed at level_forest_5.tscn, which doesn't exist,
and borrowed LEVEL_5_NAME so it would have displayed "VILLAGE - 5".

LevelCatalogue lists levels in campaign order and produces the domain's
Campaign; an authoring mistake fails at boot because Campaign.Of rejects
blank and duplicate ids. Not a domain port - nothing in the domain
resolves a level to a scene.

All 8 scenes now have a resource; campaign keeps the existing 6
(tutorial_1, village_1..5). village_6 and forest_1 are catalogued but
deliberately out of the campaign.

GameManager.LevelScenes stays until slice 10 rewrites GameManager.
2026-07-28 19:28:56 +02:00
330e10880c feat(progression): use cases (slice 6)
ProgressionService holds the loaded Profile, active Playthrough and
active Attempt, so "exactly one active Attempt" has a testable home.

EnterLevel always announces LevelStarted - the reason ghost recording
and splits never worked on level 1 or after a Continue. Refuses
unreachable levels. Does not touch campaign position, so a Replay
leaves it alone.

SkillCatalogue returns null for a skill with no authored upgrades,
killing the Upgrades[0].Cost overrun. PurchaseSkill reads the current
level from the ledger first, so one Attempt can unlock then upgrade.
HandlePlayerDeath now persists - death was free if you quit.

SkillLevel.First/Next driven out here by a real caller. 53 tests.
2026-07-28 19:10:06 +02:00
003ec4b010 feat(progression): snapshots, ports, in-memory adapters (slice 5)
flat snapshot record per aggregate - separate files mean saving a run
doesn't rewrite the Profile, deleting a slot is deleting a file, and a
corrupt slot doesn't cost achievements or best times.

snapshots hold primitives so Restore re-runs the value-object factories;
a serialiser building LevelId directly would bypass every guard.

LoadResult has three outcomes: Absent != Corrupt. Collapsing them is the
bug where a version bump made ContinueGame overwrite the save.

Profile.Restore writes collections directly - replaying UnlockAchievement
would re-fire popups and Steam pushes on every load. 44 tests.
2026-07-28 19:02:41 +02:00
05149bb4a9 feat(progression): Profile aggregate (slice 4)
best times only improve; achievement unlock announces on the transition
only, so popup/Steam subscribers cannot double-fire; statistics are
increase-only (IncrementStat took any int).

RelockAchievement is console-only and documented as not reaching Steam -
a Steam achievement cannot be un-earned, so the disagreement is
permanent and deliberate.

refactor: extract EventLog, composed by both aggregates now that the
duplication actually exists. 34 tests.
2026-07-28 18:56:58 +02:00
ffa2b986ae feat(progression): Playthrough aggregate (slice 3)
no LoseLife (ADR-0005). SettleDeath / SettleCompletion defer to the
Attempt's terminal outcome, so both are atomic and idempotent.
SettleCompletion branches on HasCleared itself - a Replay cannot bank
or move campaign position backwards (ADR-0003).

Campaign value object holds authored level order; ReachableLevels is
derived, replacing the stored UnlockedLevels. Coins.IsAtLeast driven
out by cross-scope affordability.

29 tests. CONTEXT.md: Campaign, Settle.
2026-07-28 18:52:20 +02:00
40bd5c1d86 feat(progression): Attempt ledger with terminal-outcome guard (slice 2)
Attempt carries its LevelId so settlement decides First Clear vs Replay
without trusting the caller. Outcome is terminal and refuses to change:
one death costs one life, one completion banks once.

Coins.Zero/Add driven out by the ledger rather than added up front.
21 tests. ADR-0005: Playthrough settles Attempts, has no LoseLife.
2026-07-28 18:40:20 +02:00
9767941c65 feat(progression): remaining slice 1 value objects
Lives (floors at zero, exhaustion observable), LevelId, SkillId,
PlaythroughId (never self-generates, keeps tests deterministic),
StatName enum, LevelTime rejects NaN/infinity.

14 tests. Arithmetic/comparison members deferred to the slices whose
behaviour needs them rather than added speculatively.
2026-07-28 18:27:26 +02:00
313a5315b1 feat(progression): Coins, LevelTime, SkillLevel core invariants (slice 1)
three bug-encoding invariants, one red/green cycle each:
- Coins.TrySubtract fails instead of clamping
- LevelTime rejects zero (continued-run zero best-times)
- SkillLevel has no zero (unowned is absence, not Level=1)

plan: drop signed NetCoins; add Attempt outcome as idempotency guard
2026-07-28 18:22:46 +02:00
c3bbf3282a chore: scaffold progression domain + test projects (slice 0)
net8.0 classlib, no engine ref, EnableDynamicLoading; xunit test project
referencing domain only; excluded from Godot's **/*.cs glob; .gdignore both
2026-07-28 18:14:24 +02:00
dd113af786 docs: progression CONTEXT, ADRs 0001-0004, phase A plan
ignore .idea/ .superpowers/ bin/ obj/; drop 3 identical csproj backups
2026-07-28 18:11:01 +02:00
6b784303b4 fix: update author name in plugin configuration 2026-04-26 05:09:18 +02:00
562be1049d fix: replace deprecated AddControlToDock with AddDock(EditorDock) 2026-04-26 05:07:13 +02:00
b2a406ca2b add cellular automata generator 2026-04-26 05:02:03 +02:00
7f0869ffa4 fix: update unique IDs and texture path in collapsing_block scene 2026-04-26 03:36:23 +02:00
7f1b4b5e0e levels update 2026-04-26 03:15:44 +02:00
8769308c85 feat: add 'down' action to remappable actions and update unique IDs in input settings 2026-04-26 02:43:50 +02:00
4b63368286 fix: update Godot.NET.Sdk version to 4.6.2 2026-04-24 11:44:03 +02:00
cbf625d2ff fix: update scene files for unique IDs and adjust JumpForce in JumpPadComponent 2026-03-19 11:32:48 +01:00
057cf7b368 feat: add UID files for InputDeviceManager, SettingsManager, DisplaySettings, GameplaySettings, and InputSettings 2026-03-19 03:53:49 +01:00
829d081eab fix: unsubscribe VisibilityChanged in _ExitTree, use IsActionReleased for ui_cancel, expose ApplyGamepadDeadzone public 2026-03-19 03:51:01 +01:00
38c86f1a67 feat: gameplay settings screen (deadzone + sensitivity) 2026-03-19 03:47:15 +01:00
e647cd7b29 feat: marketplace keyboard/gamepad navigation 2026-03-19 03:44:35 +01:00
9043cfa63c fix: InputSettings PhysicalKeycode, visibility guard, dead code, delegate save 2026-03-19 03:42:47 +01:00
dc46769c11 feat: input remapping screen with conflict detection and config persistence 2026-03-19 03:39:19 +01:00
47600be867 fix: route resolution change through SettingsManager.ApplyDisplaySettings 2026-03-19 03:36:29 +01:00
3e74959bb9 fix: DisplaySettings focus mode, remove duplicate ApplyWindowMode 2026-03-19 03:35:28 +01:00
8cce1866b1 feat: display settings screen with window mode and resolution picker 2026-03-19 03:32:40 +01:00
0105698c33 fix: guard GrabFirstFocus on pointerless, unsub VisibilityChanged in _ExitTree 2026-03-19 03:29:36 +01:00
3b18e328b2 feat: gamepad/keyboard UI navigation on all menus 2026-03-19 03:26:50 +01:00
2bc0b76050 fix: joypad drift threshold + move InputDevice enum inside class 2026-03-19 03:24:19 +01:00
d83e3b4d82 feat: add InputDeviceManager autoload for input device tracking 2026-03-19 03:22:17 +01:00
42d7df27ae fix: SettingsManager null guard, ui_ filter, double cast for sensitivity/deadzone 2026-03-19 03:21:04 +01:00
fd54e16bf4 fix: make GamepadDeadzone/Sensitivity/WindowMode/Resolution setters public 2026-03-19 03:18:46 +01:00
92a3c4f79a feat: add SettingsManager autoload for display, gamepad and input binding settings 2026-03-19 03:17:26 +01:00
654a8b7ea7 fix: add .worktrees/ to gitignore 2026-03-19 03:15:09 +01:00
53ebbbbac3 feat: add movement presets for platform and spaceship, update abilities in scenes 2026-03-19 02:55:03 +01:00
551dc09485 feat: add unique identifier for MovementPreset 2026-03-19 02:46:55 +01:00
814d9736d0 refactor: movement system - MovementPreset, decouple abilities, fix timing 2026-03-19 02:45:26 +01:00
adaeb35fdd chore: ignore .worktrees dir 2026-03-19 02:43:41 +01:00
eeefca4d4e refactor: enhance GameStateStore integration and improve skill management 2026-03-19 02:33:07 +01:00
3e36e48e97 refactor: update collision shape disabling method for improved performance 2026-03-19 02:11:28 +01:00
1e9814a9d8 refactor: replace direct manager instances with lazy accessors for improved performance 2026-03-19 02:10:36 +01:00
470b0c3a8c refactor: implement singleton pattern for key managers and improve resource access 2026-03-19 02:00:56 +01:00
cfef49fbc7 refactor: standardization round 2
- ConsoleManager: lazy GameManager/AchievementManager via Instance (fixes NullRef on console commands)
- AchievementManager, GhostManager: add static Instance property
- GhostEventHandler: use GhostManager.Instance, add _ExitTree unsubscription
- SpeedRunManager: remove unused IsVisible guard; TimeUpdated now emits when running
- SpeedRunHud: use SpeedRunManager.Instance, remove dead IsVisible binding
- SaveDataDto: moved to scripts/State/SaveDataDto.cs
- GameManager.AddCoins: XML doc clarifying permanent vs session coins
2026-03-19 01:50:20 +01:00
321905e68e refactor: fix bugs and improve architecture
- 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()
2026-03-19 01:41:14 +01:00
427bef6509 Add EventBus to project settings 2026-02-02 12:48:17 +01:00