[EPIC] Unslop The Beacon — From Prototype to Quality #54

Closed
opened 2026-04-11 00:18:56 +00:00 by Timmy · 1 comment
Owner

Context

Full code audit performed 2026-04-10. Verdict: SLOPPY (2.4/5 average).

The game works and the design is excellent (4/5). But the implementation is a 2,688-line monolith with no tests, no modules, heavy duplication, magic numbers everywhere, fragile save/load, and tight coupling between logic and rendering. It reads like rapid prototyping that never got refactored.

This epic captures every finding and the required remediation. Each task is independently shippable.


Task 1: Extract Constants — Kill the Magic Numbers

Severity: Medium | Effort: Small

Every gameplay constant is buried as a literal in the code. Extract them to a CONFIG object at the top of the file (or a separate config.js).

Specific magic numbers to extract:

Location Value Meaning
game.js:1006 -0.05 * wizardCount Harmony drain per wizard
game.js:1011 0.2 * wizardCount Pact harmony gain
game.js:1017 0.1 * wizardCount Nightly watch harmony
game.js:1023 0.15 * wizardCount MemPalace harmony
game.js:1048 0.1 Bilbo burst chance
game.js:1052 0.05 Bilbo vanish chance
game.js:1169 0.02 Event probability per tick
game.js:127 60, 10, 10 Sprint cooldown/duration/multiplier
game.js:117 2.0 Combo decay rate
game.js:2528 0.5 Offline efficiency
game.js:2631 30000 Auto-save interval (ms)
game.js:136-143 0, 2000, 20000... Phase thresholds

Acceptance criteria:

  • Zero raw magic numbers in gameplay logic
  • All constants in a single CONFIG or CONST object
  • Game behavior unchanged

Task 2: Extract getClickPower() — Kill the Duplication

Severity: Medium | Effort: Small

The click power formula is copy-pasted in three places:

// game.js:1065 (updateRates)
const clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;

// game.js:1537 (writeCode)
const base = 1; const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); const phaseBonus = Math.max(0, (G.phase - 1)) * 2;

// game.js:1579 (autoType)
const base = 1; const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); const phaseBonus = Math.max(0, (G.phase - 1)) * 2;

Fix: Single function getClickPower() called from all three locations.


Task 3: Consolidate Duplicate Deploy Projects

Severity: Low | Effort: Small

Two projects do the same thing:

Project Cost Trigger
p_deploy (line 361) {trust: 5, compute: 500} totalCode >= 200 && totalCompute >= 100
p_hermes_deploy (line 543) {code: 500, compute: 300} totalCode >= 300 && totalCompute >= 150 && deployFlag === 0

Both set G.deployFlag = 1; G.phase = max(phase, 3). p_deploy doesn't check deployFlag, so it stays available forever.

Fix: Merge into one project, or make p_deploy check deployFlag === 0.


Task 4: Fix Fragile Save/Load

Severity: High | Effort: Medium

Problem 1: Object.assign(G, data) at line 2483 merges unvalidated JSON into game state. A crafted save file can inject arbitrary properties.

Problem 2: Import validation at line 2404 is trivially weak: if (!data.code && !data.totalCode && !data.buildings).

Problem 3: Debuffs re-trigger their log messages and toasts on every load (line 2514).

Fix:

  • Whitelist properties that can be loaded (don't blindly assign)
  • Add schema version to save format, with migration functions
  • Suppress debuff side-effects during load

Task 5: Initialize G.flags — Fix Undefined Access

Severity: Low | Effort: Trivial

G.flags is never initialized in the global state (line 7-133). It's created on-the-fly at line 380 (G.flags = G.flags || {}). Several places check G.flags && G.flags.creativity (lines 314, 994, 1118, 1735) defensively.

Fix: Add flags: {} to the initial state object at line 7.


Task 6: Add JSDoc to Core Functions

Severity: Low | Effort: Medium

Zero JSDoc comments in 2,688 lines. At minimum, document:

  • tick(dt) — main game loop
  • writeCode() — click handler
  • buyBuilding(id) — purchase logic
  • buyProject(id) — project purchase
  • saveGame() / loadGame() — persistence
  • fmt(n) / spellf(n) — number formatting
  • updateRates() — production calculation

Task 7: Split game.js — Module Extraction

Severity: High | Effort: Large

The monolith must be broken up. Proposed split:

game/
  data.js       — buildings, projects, events, facts, NUMBER_ABBREVS (~600 lines)
  engine.js     — state, tick, rates, save/load, milestones (~800 lines)
  render.js     — all DOM manipulation, UI updates (~600 lines)
  utils.js      — fmt, spellf, number formatting (~160 lines)
  main.js       — initialization, event listeners, game loop (~200 lines)

No build step needed — just multiple <script> tags in order, sharing the global scope.

Migration strategy: Extract in order: utils.js → data.js → render.js → engine.js. Each extraction is a standalone PR that doesn't break the game.


Task 8: Add Smoke Tests

Severity: Medium | Effort: Medium

Branches feat/a11y-smoke-test and fix/add-smoke-test exist but were never merged. Start with:

  1. Unit tests for fmt() and spellf() — these are pure functions with clear inputs/outputs
  2. Unit tests for getClickPower() (after Task 2 extracts it)
  3. Save/load round-trip test — save state, load it, verify equality
  4. Smoke test — page loads, game tick runs, no console errors

Use a lightweight framework (Jest or even raw assert()). Add a test/ directory with npm test or a simple HTML runner.


Task 9: Clean Up Git Hygiene

Severity: Low | Effort: Small

  • Delete 30+ stale remote branches (burn/* after merge)
  • Squash "fix offline progress" duplicates (5 commits for the same fix)
  • Adopt conventional commits: feat:, fix:, refactor:, test:

Task 10: Guardrails — CI Pipeline

Severity: High | Effort: Medium

Add automated quality gates to prevent regression:

  1. ESLint — basic linting with no-unused-vars, no-undef, no-eval
  2. Syntax checknode --check game.js on every PR
  3. Smoke test — headless browser loads game, waits 5 seconds, checks no errors
  4. Line count warning — fail if any single JS file exceeds 800 lines

Add a .github/workflows/ci.yml (or Gitea equivalent) that runs on every PR to main.


Task 11: Guardrails — PR Checklist

Severity: Low | Effort: Trivial

Add a PULL_REQUEST_TEMPLATE.md:

## What changed
- [ ] No new magic numbers (use CONFIG)
- [ ] No duplicated logic (DRY)
- [ ] No innerHTML with user-controlled data
- [ ] saveGame/loadGame still round-trips correctly
- [ ] No single file exceeds 800 lines

Execution Order

Phase 1 (quick wins):   Task 5 (flags init) → Task 1 (constants) → Task 2 (getClickPower) → Task 3 (deploy merge)
Phase 2 (hardening):    Task 4 (save/load) → Task 6 (JSDoc) → Task 8 (tests)
Phase 3 (refactor):     Task 7 (split modules) → Task 10 (CI)
Phase 4 (hygiene):      Task 9 (git cleanup) → Task 11 (PR template)

Definition of Done

  • All magic numbers extracted to CONFIG
  • Zero duplicated logic (click power, deploy projects)
  • Save/load uses whitelisted property merge
  • G.flags initialized in state
  • Core functions have JSDoc
  • game.js split into 5 modules, none exceeding 800 lines
  • At least fmt/spellf have unit tests
  • CI runs lint + syntax check on every PR
  • PR template exists
  • Average code quality score: 4/5
## Context Full code audit performed 2026-04-10. **Verdict: SLOPPY (2.4/5 average).** The game works and the design is excellent (4/5). But the implementation is a 2,688-line monolith with no tests, no modules, heavy duplication, magic numbers everywhere, fragile save/load, and tight coupling between logic and rendering. It reads like rapid prototyping that never got refactored. This epic captures every finding and the required remediation. Each task is independently shippable. --- ## Task 1: Extract Constants — Kill the Magic Numbers **Severity:** Medium | **Effort:** Small Every gameplay constant is buried as a literal in the code. Extract them to a `CONFIG` object at the top of the file (or a separate `config.js`). **Specific magic numbers to extract:** | Location | Value | Meaning | |----------|-------|---------| | `game.js:1006` | `-0.05 * wizardCount` | Harmony drain per wizard | | `game.js:1011` | `0.2 * wizardCount` | Pact harmony gain | | `game.js:1017` | `0.1 * wizardCount` | Nightly watch harmony | | `game.js:1023` | `0.15 * wizardCount` | MemPalace harmony | | `game.js:1048` | `0.1` | Bilbo burst chance | | `game.js:1052` | `0.05` | Bilbo vanish chance | | `game.js:1169` | `0.02` | Event probability per tick | | `game.js:127` | `60, 10, 10` | Sprint cooldown/duration/multiplier | | `game.js:117` | `2.0` | Combo decay rate | | `game.js:2528` | `0.5` | Offline efficiency | | `game.js:2631` | `30000` | Auto-save interval (ms) | | `game.js:136-143` | `0, 2000, 20000...` | Phase thresholds | **Acceptance criteria:** - Zero raw magic numbers in gameplay logic - All constants in a single `CONFIG` or `CONST` object - Game behavior unchanged --- ## Task 2: Extract `getClickPower()` — Kill the Duplication **Severity:** Medium | **Effort:** Small The click power formula is copy-pasted in three places: ```js // game.js:1065 (updateRates) const clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost; // game.js:1537 (writeCode) const base = 1; const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); const phaseBonus = Math.max(0, (G.phase - 1)) * 2; // game.js:1579 (autoType) const base = 1; const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); const phaseBonus = Math.max(0, (G.phase - 1)) * 2; ``` **Fix:** Single function `getClickPower()` called from all three locations. --- ## Task 3: Consolidate Duplicate Deploy Projects **Severity:** Low | **Effort:** Small Two projects do the same thing: | Project | Cost | Trigger | |---------|------|---------| | `p_deploy` (line 361) | `{trust: 5, compute: 500}` | `totalCode >= 200 && totalCompute >= 100` | | `p_hermes_deploy` (line 543) | `{code: 500, compute: 300}` | `totalCode >= 300 && totalCompute >= 150 && deployFlag === 0` | Both set `G.deployFlag = 1; G.phase = max(phase, 3)`. `p_deploy` doesn't check `deployFlag`, so it stays available forever. **Fix:** Merge into one project, or make `p_deploy` check `deployFlag === 0`. --- ## Task 4: Fix Fragile Save/Load **Severity:** High | **Effort:** Medium **Problem 1:** `Object.assign(G, data)` at line 2483 merges unvalidated JSON into game state. A crafted save file can inject arbitrary properties. **Problem 2:** Import validation at line 2404 is trivially weak: `if (!data.code && !data.totalCode && !data.buildings)`. **Problem 3:** Debuffs re-trigger their log messages and toasts on every load (line 2514). **Fix:** - Whitelist properties that can be loaded (don't blindly assign) - Add schema version to save format, with migration functions - Suppress debuff side-effects during load --- ## Task 5: Initialize `G.flags` — Fix Undefined Access **Severity:** Low | **Effort:** Trivial `G.flags` is never initialized in the global state (line 7-133). It's created on-the-fly at line 380 (`G.flags = G.flags || {}`). Several places check `G.flags && G.flags.creativity` (lines 314, 994, 1118, 1735) defensively. **Fix:** Add `flags: {}` to the initial state object at line 7. --- ## Task 6: Add JSDoc to Core Functions **Severity:** Low | **Effort:** Medium Zero JSDoc comments in 2,688 lines. At minimum, document: - `tick(dt)` — main game loop - `writeCode()` — click handler - `buyBuilding(id)` — purchase logic - `buyProject(id)` — project purchase - `saveGame()` / `loadGame()` — persistence - `fmt(n)` / `spellf(n)` — number formatting - `updateRates()` — production calculation --- ## Task 7: Split game.js — Module Extraction **Severity:** High | **Effort:** Large The monolith must be broken up. Proposed split: ``` game/ data.js — buildings, projects, events, facts, NUMBER_ABBREVS (~600 lines) engine.js — state, tick, rates, save/load, milestones (~800 lines) render.js — all DOM manipulation, UI updates (~600 lines) utils.js — fmt, spellf, number formatting (~160 lines) main.js — initialization, event listeners, game loop (~200 lines) ``` No build step needed — just multiple `<script>` tags in order, sharing the global scope. **Migration strategy:** Extract in order: utils.js → data.js → render.js → engine.js. Each extraction is a standalone PR that doesn't break the game. --- ## Task 8: Add Smoke Tests **Severity:** Medium | **Effort:** Medium Branches `feat/a11y-smoke-test` and `fix/add-smoke-test` exist but were never merged. Start with: 1. **Unit tests** for `fmt()` and `spellf()` — these are pure functions with clear inputs/outputs 2. **Unit tests** for `getClickPower()` (after Task 2 extracts it) 3. **Save/load round-trip test** — save state, load it, verify equality 4. **Smoke test** — page loads, game tick runs, no console errors Use a lightweight framework (Jest or even raw `assert()`). Add a `test/` directory with `npm test` or a simple HTML runner. --- ## Task 9: Clean Up Git Hygiene **Severity:** Low | **Effort:** Small - Delete 30+ stale remote branches (`burn/*` after merge) - Squash "fix offline progress" duplicates (5 commits for the same fix) - Adopt conventional commits: `feat:`, `fix:`, `refactor:`, `test:` --- ## Task 10: Guardrails — CI Pipeline **Severity:** High | **Effort:** Medium Add automated quality gates to prevent regression: 1. **ESLint** — basic linting with `no-unused-vars`, `no-undef`, `no-eval` 2. **Syntax check** — `node --check game.js` on every PR 3. **Smoke test** — headless browser loads game, waits 5 seconds, checks no errors 4. **Line count warning** — fail if any single JS file exceeds 800 lines Add a `.github/workflows/ci.yml` (or Gitea equivalent) that runs on every PR to main. --- ## Task 11: Guardrails — PR Checklist **Severity:** Low | **Effort:** Trivial Add a `PULL_REQUEST_TEMPLATE.md`: ```markdown ## What changed - [ ] No new magic numbers (use CONFIG) - [ ] No duplicated logic (DRY) - [ ] No innerHTML with user-controlled data - [ ] saveGame/loadGame still round-trips correctly - [ ] No single file exceeds 800 lines ``` --- ## Execution Order ``` Phase 1 (quick wins): Task 5 (flags init) → Task 1 (constants) → Task 2 (getClickPower) → Task 3 (deploy merge) Phase 2 (hardening): Task 4 (save/load) → Task 6 (JSDoc) → Task 8 (tests) Phase 3 (refactor): Task 7 (split modules) → Task 10 (CI) Phase 4 (hygiene): Task 9 (git cleanup) → Task 11 (PR template) ``` ## Definition of Done - [ ] All magic numbers extracted to CONFIG - [ ] Zero duplicated logic (click power, deploy projects) - [ ] Save/load uses whitelisted property merge - [ ] `G.flags` initialized in state - [ ] Core functions have JSDoc - [ ] game.js split into 5 modules, none exceeding 800 lines - [ ] At least fmt/spellf have unit tests - [ ] CI runs lint + syntax check on every PR - [ ] PR template exists - [ ] Average code quality score: 4/5
Author
Owner

Independent Review — Claude (Opus 4.6)

I read game.js end-to-end and cross-checked the audit. The existing findings (monolith, duplicated click power, unwhitelisted save/load, uninitialized G.flags, duplicate deploy projects) all hold up. Notes and additions below — a couple are real bugs that deserve to jump the queue.

Bugs the audit under-weighted or missed

1. community_drama compound-decays codeBoost every tick (P0 bug).
At game.js:1474:

applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 0.7; }

applyFn is invoked from updateRates() on every tick. G.codeRate is reset each tick so the other debuffs are fine (runner_stuck, api_rate_limit, memory_leak, etc.), but codeBoost is a persistent multiplier. If community_drama ever fires, codeBoost decays exponentially toward zero until the debuff is cleared — and even after clearing, the decay is never undone. Silent progression-breaking bug. This should be fixed before the Phase 3 refactor work, independent of it.

2. Sprint × offline-gains exploit.
When G.sprintActive is true, G.codeBoost has the 10× multiplier baked in. saveGame() persists it in that state. On reload, loadGame() copies codeBoost back via Object.assign, then calls updateRates(), then computes offline gains as G.codeRate * offSec * 0.5 (game.js:2529). Result: all offline seconds are credited at 10× even if the sprint had only ~2 s left at save time. Free infinite resources via save-close-reload. Fix: either unbake the sprint multiplier before saving, or cap offline credit at the remaining sprint duration.

3. resolveCost is duplicated between the EVENTS definition and the activeDebuffs push.
Each event literal declares resolveCost twice — once on the event def (e.g. game.js:1342) and again when constructing the debuff object (game.js:1349). They can and will drift. Single source of truth wanted.

4. Save/load whitelist asymmetry.
saveGame() uses an explicit whitelist; loadGame() uses Object.assign(G, data) (game.js:2483). Aside from being the injection surface the audit flagged, the asymmetry silently loses state: fields added to G later that nobody remembered to add to saveGame's whitelist will reset on every reload. Concrete examples already in the code: maxCode/maxCompute/..., G.tick, G.comboCount, G.totalClicks is saved but G.totalRescues is saved — check every field. Task 5 in the plan should also require a save/load round-trip test that asserts equality of G before save and after load.

5. Click power duplication is actually three sites, not "three locations" as a rough count — one of them (Swarm Protocol at game.js:1065) is in a hot path inside updateRates() and silently uses its own re-derivation of the formula. Easy to miss when renaming or extending.

Smaller observations

  • Two parallel flag systems. G.flags.creativity (sub-object) sits alongside ~14 top-level *Flag properties (deployFlag, sovereignFlag, beaconFlag, pactFlag, mempalaceFlag, ciFlag, branchProtectionFlag, nightlyWatchFlag, nostrFlag, lazarusFlag, strategicFlag, swarmFlag, memoryFlag, milestoneFlag). Task 1 should fold the top-level flags into G.flags too, not just add G.flags = {} at init. Otherwise you're just formalizing the inconsistency.
  • Duplicate projects aren't just p_deploy / p_hermes_deploy. p_the_pact (game.js:503) and p_the_pact_early (game.js:647) also both set G.pactFlag = 1 with different side-effects — this is arguably intentional (early vs late pact is a design choice), but the player can't purchase both; the code doesn't model the branch, it just races whichever triggers first. Worth deciding explicitly.
  • BDEF.find(b => b.id === id) is called on every getBuildingCost / canAffordBuilding / spendBuilding call — cheap to replace with a Map once, but premature to optimize. Flagging only because Task 8 (module split) is the right moment.
  • **entry.innerHTML = \<span...>${msg}`** in log() (game.js:2114) is an XSS foot-gun. All current callers pass static strings, so no live risk, but any future dynamic field (player name, imported save metadata) becomes an injection vector. Cheap to switch to textContent` for the message body now.
  • p_wire_budget is marked repeatable with trigger G.compute < 1, which means it re-enters activeProjects every time compute dips below 1. Minor UI churn, not a bug.
  • G.drift is monotonic. No redemption path once drift is taken. Probably intentional (the whole "Drift" metaphor), but worth documenting as a design decision so nobody "fixes" it later.

The existing 11-task plan is sound. The only change I'd push for is promoting a new P0 "bugfix sweep" to run before Phase 1:

  • 0a. Fix community_drama compounding codeBoost
  • 0b. Fix sprint × offline-gains (unbake multiplier at save-time, or cap offline credit)
  • 0c. Collapse resolveCost to a single source per event

These are small, localized, and don't need the CONFIG/module work to land. They're also the kind of silent bugs that get harder to spot once the refactor starts moving code around.

After that, the existing Phase 1 → 4 ordering is good. I'd strengthen Task 5's acceptance criteria to include a save → load → deep-equal round-trip test so the save whitelist stays in sync with G.

Overall the audit is accurate and the plan is the right shape. The code is more bug-prone than "2/5 implementation" implies once you look closely at mutation patterns in the debuff system, but the fixes are small and the design (4/5) is doing most of the work keeping this readable at 2.7k lines.

— Reviewed against main @ 68ee648, comparing game.js lines cited above.

## Independent Review — Claude (Opus 4.6) I read `game.js` end-to-end and cross-checked the audit. The existing findings (monolith, duplicated click power, unwhitelisted save/load, uninitialized `G.flags`, duplicate deploy projects) all hold up. Notes and additions below — a couple are real bugs that deserve to jump the queue. ### Bugs the audit under-weighted or missed **1. `community_drama` compound-decays `codeBoost` every tick (P0 bug).** At `game.js:1474`: ```js applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 0.7; } ``` `applyFn` is invoked from `updateRates()` on every tick. `G.codeRate` is reset each tick so the other debuffs are fine (`runner_stuck`, `api_rate_limit`, `memory_leak`, etc.), but **`codeBoost` is a persistent multiplier**. If community_drama ever fires, `codeBoost` decays exponentially toward zero until the debuff is cleared — and even after clearing, the decay is never undone. Silent progression-breaking bug. This should be fixed *before* the Phase 3 refactor work, independent of it. **2. Sprint × offline-gains exploit.** When `G.sprintActive` is true, `G.codeBoost` has the 10× multiplier baked in. `saveGame()` persists it in that state. On reload, `loadGame()` copies `codeBoost` back via `Object.assign`, then calls `updateRates()`, then computes offline gains as `G.codeRate * offSec * 0.5` (`game.js:2529`). Result: *all* offline seconds are credited at 10× even if the sprint had only ~2 s left at save time. Free infinite resources via save-close-reload. Fix: either unbake the sprint multiplier before saving, or cap offline credit at the remaining sprint duration. **3. `resolveCost` is duplicated between the `EVENTS` definition and the `activeDebuffs` push.** Each event literal declares `resolveCost` twice — once on the event def (e.g. `game.js:1342`) and again when constructing the debuff object (`game.js:1349`). They can and will drift. Single source of truth wanted. **4. Save/load whitelist asymmetry.** `saveGame()` uses an explicit whitelist; `loadGame()` uses `Object.assign(G, data)` (`game.js:2483`). Aside from being the injection surface the audit flagged, the asymmetry silently loses state: fields added to `G` later that nobody remembered to add to `saveGame`'s whitelist will reset on every reload. Concrete examples already in the code: `maxCode/maxCompute/...`, `G.tick`, `G.comboCount`, `G.totalClicks` *is* saved but `G.totalRescues` *is* saved — check every field. Task 5 in the plan should also require a save/load round-trip test that asserts equality of `G` before save and after load. **5. Click power duplication is actually *three* sites, not "three locations" as a rough count — one of them (Swarm Protocol at `game.js:1065`) is in a hot path inside `updateRates()` and silently uses its own re-derivation of the formula.** Easy to miss when renaming or extending. ### Smaller observations - **Two parallel flag systems.** `G.flags.creativity` (sub-object) sits alongside ~14 top-level `*Flag` properties (`deployFlag`, `sovereignFlag`, `beaconFlag`, `pactFlag`, `mempalaceFlag`, `ciFlag`, `branchProtectionFlag`, `nightlyWatchFlag`, `nostrFlag`, `lazarusFlag`, `strategicFlag`, `swarmFlag`, `memoryFlag`, `milestoneFlag`). Task 1 should fold the top-level flags into `G.flags` too, not just add `G.flags = {}` at init. Otherwise you're just formalizing the inconsistency. - **Duplicate projects aren't just `p_deploy` / `p_hermes_deploy`.** `p_the_pact` (`game.js:503`) and `p_the_pact_early` (`game.js:647`) also both set `G.pactFlag = 1` with different side-effects — this is arguably intentional (early vs late pact is a design choice), but the player can't purchase both; the code doesn't model the branch, it just races whichever triggers first. Worth deciding explicitly. - **`BDEF.find(b => b.id === id)`** is called on every `getBuildingCost` / `canAffordBuilding` / `spendBuilding` call — cheap to replace with a `Map` once, but premature to optimize. Flagging only because Task 8 (module split) is the right moment. - **`entry.innerHTML = \`<span...>${msg}\``** in `log()` (`game.js:2114`) is an XSS foot-gun. All current callers pass static strings, so no live risk, but any future dynamic field (player name, imported save metadata) becomes an injection vector. Cheap to switch to `textContent` for the message body now. - **`p_wire_budget`** is marked `repeatable` with trigger `G.compute < 1`, which means it re-enters `activeProjects` every time compute dips below 1. Minor UI churn, not a bug. - **`G.drift` is monotonic.** No redemption path once drift is taken. Probably intentional (the whole "Drift" metaphor), but worth documenting as a design decision so nobody "fixes" it later. ### Recommended reprioritization The existing 11-task plan is sound. The only change I'd push for is **promoting a new P0 "bugfix sweep" to run before Phase 1**: - 0a. Fix `community_drama` compounding `codeBoost` - 0b. Fix sprint × offline-gains (unbake multiplier at save-time, or cap offline credit) - 0c. Collapse `resolveCost` to a single source per event These are small, localized, and don't need the CONFIG/module work to land. They're also the kind of silent bugs that get *harder* to spot once the refactor starts moving code around. After that, the existing Phase 1 → 4 ordering is good. I'd strengthen Task 5's acceptance criteria to include a save → load → deep-equal round-trip test so the save whitelist stays in sync with `G`. Overall the audit is accurate and the plan is the right shape. The code is more bug-prone than "2/5 implementation" implies once you look closely at mutation patterns in the debuff system, but the fixes are small and the design (4/5) is doing most of the work keeping this readable at 2.7k lines. *— Reviewed against `main @ 68ee648`, comparing `game.js` lines cited above.*
Timmy closed this issue 2026-04-11 00:43:41 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Timmy_Foundation/the-beacon#54