# Phase 3.K.1 — Completion Report **Date completed:** 2026-04-17 **Source plan:** `2026-04-17-phase3-k1-test-harness.md` **Source spec:** `../specs/2026-04-17-phase3-k1-test-harness-design.md` **Execution mode:** subagent-driven-development (fresh subagent per task, two-stage review). ## Status - **Code:** ✅ Shipped. All 11 tasks complete. - **Automated verification:** ✅ `./gradlew core:test` → 58/58 green. `./gradlew :core:build :lwjgl3:build` → BUILD SUCCESSFUL. - **Manual smoke test:** ⏳ Pending user — see checklist below. - **Git:** project is not a git repository; no commits were made. All work lives in the working tree. ## What was delivered ### New files (10) Production (1): - `core/src/main/java/ru/project/tower/GameContext.java` — composition-root holder for shared persistent-state services. `public final` fields for `PlayerStats`, `PlayerAbilities`, `GameState`, `TalentTree`, and two `Preferences` refs. Tests (9): - `core/src/test/java/ru/project/tower/HarnessSanityTest.java` - `core/src/test/java/ru/project/tower/FakePreferences.java` — in-memory `Preferences` impl, reused by every downstream test. - `core/src/test/java/ru/project/tower/FakePreferencesTest.java` - `core/src/test/java/ru/project/tower/PlayerStatsTest.java` - `core/src/test/java/ru/project/tower/PlayerAbilitiesTest.java` - `core/src/test/java/ru/project/tower/GameStateTest.java` - `core/src/test/java/ru/project/tower/TalentTreeTest.java` - `core/src/test/java/ru/project/tower/TalentDataTest.java` - `core/src/test/java/ru/project/tower/GameContextTest.java` ### Modified files (10) Production classes (singletons → DI): - `PlayerStats.java` — public `PlayerStats(Preferences)` ctor, `getInstance()` deleted, `instance` field deleted, `PREFS_NAME` deleted, `Gdx` import deleted, `preferences` field made `final`. - `PlayerAbilities.java` — public `PlayerAbilities(Preferences)` ctor, `getInstance()` deleted, `instance` field deleted, `PREFS_NAME` deleted, `preferences` field made `final`. `Gdx` import kept (used by `Gdx.app.error(...)`). - `GameState.java` — ctor made public, `getInstance()` deleted, `instance` field deleted. - `TalentTree.java` — public `TalentTree(PlayerStats)` ctor, `getInstance()` deleted, `instance` field deleted, two internal `PlayerStats.getInstance()` calls replaced with injected `stats` field, `talents`/`roots` made `final`. Composition root + screens: - `Main.java` — rewritten; builds `GameContext` from two `Preferences`, passes to `MainScreen`. - `MainScreen.java` — 2-arg ctor, 3 downstream `new X(game)` → `new X(game, ctx)`. - `FirstScreen.java` — 2-arg ctor, 3 new cached fields (`ctx`, `gameState`, `talentTree`), 4 cached services assigned at ctor top (closed an NPE window), 13 `getInstance()` call-sites replaced, internal `new MainScreen(game)` → `new MainScreen(game, ctx)`, 2 redundant local shadows removed. - `AbilityShopScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(game, ctx)`. - `AbilitySelectionScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(gameInstance, ctx)` + `new FirstScreen(gameInstance, ctx)`. - `TalentTreeScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(game, ctx)`. Build: - `core/build.gradle` — `testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'`, `testImplementation 'org.assertj:assertj-core:3.26.3'`, `test { useJUnitPlatform() }`. ### Test counts (by class) | Test class | Tests | |---|---| | HarnessSanityTest | 1 | | FakePreferencesTest | 7 | | PlayerStatsTest | 14 | | PlayerAbilitiesTest | 8 | | GameStateTest | 6 | | TalentTreeTest | 12 | | TalentDataTest | 6 | | GameContextTest | 4 | | **Total** | **58** | ### Invariants verified at completion - `grep "getInstance(" core/src/main/java/ru/project/tower -r` → 0 matches. - `grep "private static \w+ instance;" core/src/main/java/ru/project/tower -r` → 0 matches. - `./gradlew core:test` → 58 passed, 0 failed. - `./gradlew :core:build :lwjgl3:build` → BUILD SUCCESSFUL. ## Manual smoke test — ⏳ pending Run `lwjgl3/src/main/java/ru/project/tower/lwjgl3/Lwjgl3Launcher` and verify: - [ ] Main menu renders with persisted currency + max-wave. - [ ] Talent Tree: purchased levels display, new purchase deducts currency, back returns to menu. - [ ] Ability Shop: owned abilities display correctly, new purchase (e.g., Shield) persists after a restart. - [ ] Ability Selection → Start Game → wave 1 plays, firing kills enemies, death returns to menu, currency reflects loot. - [ ] Mid-run shop flow: save/restore works (upgrade survives shop round-trip). - [ ] Close and relaunch: all persisted state (currency, max wave, unlocked abilities, talent levels) survives. If any check fails, report back — most likely a missed call-site or a `ctx` threading bug; fix scope is minutes, not hours. ## Carry-over items (non-blocking) These were flagged by reviewers during K.1 execution but are either pre-existing (not regressions introduced by K.1) or are tiny cleanups better folded into the next spec. They do not block K.2. ### Pre-existing design issues surfaced by the new test harness - **`PlayerStats.getRegenTalentLevel()` sums only `regen_1`.** Other aggregate getters sum two nodes. If a `regen_2` node is added later, this getter silently reports the wrong value. Add a `regen_2` node or a comment explaining the omission. (Flagged in Task 3 review.) - **`PlayerStats.incrementTalentNode(String)` accepts any string, including typos.** Phantom nodes are created in the in-memory `talentLevels` map but not persisted (saveData iterates `TALENT_NODE_IDS`). Consider a validation guard. (Flagged in Task 3 review.) - **`PlayerStats.resetData()` Javadoc says "для тестирования"** but the test suite doesn't call it — callers live elsewhere. Clarify the comment or remove the method. (Task 3 review.) - **`PlayerAbilities.resetData()` clears everything, including `freeze` and `explosion` which default to `true` on construction.** Intentional per the existing code, but the name implies "factory defaults" which it isn't. Rename or add a comment. (Task 4 review.) - **`GameState.resetGameState()` doesn't reset `spawnedBosses` or the gameplay stat fields** (money, squareHealth, bulletSpeed, etc.). If this method is the "start new run" boundary, that's a latent bug. (Task 5 review.) - **`FirstScreen.java` field declarations near lines 429–435 are non-final** while the `ctx` field is `final`. Consistency drift inside one file. (Final review.) - **`Main.ctx` is `public` non-final.** Safe given libGDX `Game.create()` semantics, but `public final` would document intent. (Final review.) ### Test-coverage gaps (not bugs) - `TalentTreeTest.getStatBonus_*` exercises only the `damage_1` multiplier. A second multiplier (cooldown_1, health_1, etc.) would lock more of the formula. (Task 6 review.) - `TalentDataTest.enum_whenKeystone_thenEnumMatches` is a tautology (`KEYSTONE != DAMAGE`). Safe to delete. (Final review.) - `PlayerAbilitiesTest.purchaseAbility_whenUnknownType_thenNoChange` doesn't assert that the unknown key wasn't written to Preferences. (Task 4 review.) - `PlayerAbilitiesTest.construct_whenPreferencesPrePopulated_thenHonoursStored` seeds only `freeze` and `shield` — doesn't verify that absent `explosion` key falls through to the `true` default. (Task 4 review.) ### FakePreferences polish - `FakePreferences.put(Map)` bulk-put isn't guarded — values inserted through this path could be types no getter recognises (e.g., `Double`, `Short`). No test exercises this. (Task 2 review.) - `FakePreferences.getFloat`/`getLong` and the no-default single-arg getters have no tests. (Task 2 review.) ## What's next K.1 is the foundation. The remaining specs in the K-series each handle one subsystem extraction out of `FirstScreen`'s god-class. Each follows the same brainstorm → spec → plan → subagent-driven-implementation cycle. | Spec | Scope | Dependency | |---|---|---| | **K.2** | `BulletSystem` extraction — bullet tick, bullet-vs-circle collision, homing/pierce/aoe/chain resolution | K.1 | | **K.3** | `SpawnController` extraction — `spawnCircle()` cascade | K.1 | | **K.4** | `WaveController` extraction — `startNextWave` / `endWave` / scaling | K.1 | | **K.5** | `EnemyBehaviour` tests — `tick()` for `HealerCircleData`, `DasherCircleData`, `SniperCircleData` (and others) | K.1 + Phase 3 A1/A2/A3 | | **K.6** | `AbilitySystem` extraction — cooldowns, activations, effects | K.1 | | **K.7** | `UpgradeSystem` extraction — mid-run upgrade pick, money, apply-effect | K.1 | Recommended next step: brainstorm **K.2** (`BulletSystem`) — highest-value extraction because bullet logic is central to the game, has the most flags (`piercesLeft`, `homing`, `aoeRadius`, `chainJumps`), and is the first place Phase 3's `homing` keystone + C2 chain work will land. Phase 3 gameplay tasks (A–J from the parent Phase 3 plan) remain untouched and can proceed in parallel with K.2+ when desired.