# DECISIONS.md — Architecture Decision Log Record any choice that deviates from or fills a gap in the docs. Newest at the bottom. Format: ``` ## D-NNN: Title - Date: - Context: (what problem / ambiguity) - Decision: (what you chose) - Alternatives considered: - Consequences: ``` ## D-001: SQLite + better-sqlite3 as the datastore - Date: (project start) - Context: Needs persistent accounts and game state with zero setup for a local build. - Decision: SQLite file in `data/`, synchronous driver, WAL mode. - Alternatives considered: Postgres (heavier setup), JSON files (no integrity/transactions). - Consequences: Single-process deployment; synchronous transactions give simple double-spend protection. Migrate to Postgres only if scaling beyond one server. ## D-002: Lazy time evaluation instead of tick loops - Date: (project start) - Context: Game must progress while players are offline. - Decision: Store timestamps; compute energy/timers on demand. - Consequences: No background workers, trivially testable with injected clocks. ## D-003: Vanilla frontend, no build step - Date: (project start) - Context: Reduce tooling surface area for a local-model build. - Decision: Static HTML/CSS/JS served by Express. - Consequences: Slightly more DOM boilerplate; far fewer moving parts. ## D-004: Both combatants attack every round - Date: 2026-10-20 (review) - Context: Combat treated "higher speed acts first" as "only the faster side acts", so the slower side never attacked. - Decision: Each round, both attack; faster first; hero wins speed ties; a kill ends the round. - Consequences: Round log has up to 2 entries per round (max 40). Win rates need re-tuning (see STATUS issue 1). ## D-022: Material sell prices use item-level formula - Context: BALANCE.md says materials sell at `common 2×level, uncommon 4×level, rare 8×level` — but "level" was ambiguous (hero level vs item level). The code used `defaultRate * heroLevel / itemMinLevel`, making high-level heroes overpay and low-level heroes underpay for high-level materials. - Decision: Use `materialSellPrice(rarity, itemMinLevel)` from `src/game/rewards.js` — price depends on the ITEM's level, not the hero's. BALANCE.md table corrected to match the formula (e.g., Drake Scale: 90g → 112g). - Consequences: Existing shop tests that asserted hero-level prices updated; material sell prices are now consistent regardless of hero level. ## D-005: Equipment stats are counted once in derived stats - Context: maxHp added equipment VIT twice (once in totalVit, once as a separate term). - Decision: `calcDerived` in `src/game/stats.js` is the ONLY derived-stat formula; combat and services call it. ## D-006: Real SQLite session store everywhere; unref its cleanup timer - Context: better-sqlite3-session-store ignores `expired.clear:false` (`clear || true`), so its setInterval kept tests alive; the project had used a memory store in tests, so the real store was never exercised. - Decision: Wrap store creation so its interval is unref'd. Tests now use the real store. ## D-007: `src/utils/time.js` is the only clock - Decision: Services call `time.now()`; tests use `time.setNow()`. Game functions take `nowMs`/`rng` as parameters. ## D-023: Shared grantItem for inventory item granting - Date: 2026-10-23 - Context: `huntService` and `shopService` both need to grant items to inventory. `huntService` had a local `grantItem` that correctly creates one row per unit for non-stackable items, but `shopService` duplicated the logic incorrectly (inserted full quantity for non-stackables). - Decision: Extract `grantItem(s, characterId, item, quantity, now)` to `src/game/inventory.js`. Both services import it. The service's stmtCache must include `stackRow`, `addToStack`, `insertInv` with the exact same SQL used by huntService. - Consequences: Single source of truth for item-granting logic. Any future service that grants items (expeditions, quests, etc.) can reuse it.