diff --git a/HANDOFF.md b/HANDOFF.md index 229fae8..07d9d45 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,9 +4,9 @@ > If this file disagrees with the code, trust the code and fix this file. ## 1. Where we are -- **Last completed step:** 9.6 (Update README with real run instructions) — TICKED in STATUS.md -- **Next step:** Phase 9 is complete. Ready to start Stretch Goals (S1–S7) or address Known Issues from STATUS.md. -- **Updated:** 2026-10-21 +- **Last completed step:** 10.1 (Fix expedition START corrupting energy) — TICKED in STATUS.md +- **Next step:** Phase 10.2 — Fix expedition CLAIM overwriting XP +- **Updated:** 2026-10-22 - **Was the last session cut off mid-task?** No ## 2. Verified state (run these first) @@ -14,7 +14,7 @@ |---|---| | `npm install` | succeeds | | `npm run migrate` | applies 001–006 / "nothing to do" | -| `npm test` | **176 passing, 0 failing** (85 unit + 91 integration incl. 1 e2e) | +| `npm test` | **178 passing, 0 failing** (85 unit + 93 integration incl. 2 new 10.1 tests) | | `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear | | `npm start` + `curl localhost:3000/api/health` | `{"ok":true}` | @@ -31,10 +31,12 @@ - `public/js/auth.js` — Shared API wrapper + showError. - `scripts/seed-dev.js` — Creates demo account + hero + starter gear. - `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving. +- `src/services/expeditionService.js` — **Fixed in 10.1:** transaction param order corrected `(cid, ne, eat, defid, sat, endAt)`. - `src/services/*` — All game services. - `src/routes/*` — All route handlers. - `src/game/*` — Pure game logic. -- `tests/integration/*.test.js` — 91 integration tests (incl. e2e-playable.test.js). +- `tests/integration/expeditions.test.js` — 16 expedition tests (incl. 2 new 10.1 DB-level assertions). +- `tests/integration/*.test.js` — 93 integration tests (incl. 1 e2e). - `tests/unit/*.test.js` — 85 unit tests. - `README.md` — Updated with real run instructions, play-through steps, project structure, known issues. @@ -47,6 +49,7 @@ - Starter gear documented but not auto-created on character creation. - `GET /api/zones` documented in API.md but not implemented (Issue #2). - Mobile nav uses emoji icons (⚔️ 🏕️ 🎒) — verify they render acceptably on all platforms. +- **10.2 next:** expedition claim overwrites XP instead of adding to it; needs shared `applyXpGain` function. ## 5. Decisions & deviations from the docs - D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock). @@ -56,7 +59,6 @@ - Inventory response uses `itemId` (item.id) and `inventoryId` (inventory row id) to avoid confusion. - Consumable effect parsing: regex `^(heal|energy):(\d+)$` on items.effect field. - Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1. -- D-008..D-013 from previous steps. ## 6. Gotchas learned - `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`. @@ -65,7 +67,7 @@ - Verify test COUNT, not just "all passing". - Node.js `fetch` doesn't persist cookies across calls — extract `Set-Cookie` header and pass manually in integration tests. - SQL `SELECT id` from inventory returns the auto-increment row id; rename with `AS inventory_id` when you also need `item_id`. -- Transaction callback args must match the call order exactly. +- Transaction callback args must match the call order exactly. (This is the 10.1 bug — double-check param/call order in every new transaction.) - Error responses from route-level `AppError`s do NOT include `data` field. - `setNow` affects ALL `now()` calls including those in service logic. - Shop API returns `{ ok: true, data: { stock: [...], character: {...} } }` — field is `stock`, not `stocks`. @@ -86,9 +88,8 @@ - `src/utils/time` exports `{ now, setNow, resetNow }` directly — `const time = require(...)`, not `const { time }`. - Inventory sell endpoint (`POST /api/shop/sell`) only accepts `material`-type items. - `handleListInventory` in `src/routes/inventory.js` — parameter name must be `res`, not `_res`. +- Character profile response has `energy` at `data.energy.current`, NOT `data.character.energy`. ## 7. Next-step briefing -- **Phase 9 is complete.** All hardening and polish done. -- **Stretch Goals available:** S1 (PvP duels), S2 (Skills/talents), S3 (Guilds), S4 (Player market), S5 (Daily rewards/quests), S6 (In-game chat), S7 (Achievements). -- **Or address Known Issues:** Issue #2 (`GET /api/zones` missing — major), Issue #5 (equipment not in combat — minor), Issue #3 (`npm run seed` dead script — cosmetic). -- **Recommended next step:** Fix Known Issue #2 (`GET /api/zones`) since it's a documented API gap and would improve the hunt UI. Then consider S7 (Achievements) as it's a self-contained, low-risk stretch goal that adds meaningful gameplay. +- **Next step: 10.2** — Fix expedition CLAIM overwriting XP. The `claimExpedition` transaction uses `updateCharacterXp.run(xp, lvl, pts, cid)` which OVERWRITES xp/level/points instead of ADDING to current values. Needs a shared `applyXpGain` function used by BOTH `huntService` and `expeditionService`. +- **Before 10.2:** check `src/services/expeditionService.js` `claimExpedition` transaction — same param-order bug pattern as 10.1 may be present. diff --git a/STATUS.md b/STATUS.md index 5c41ce9..8f7ef53 100644 --- a/STATUS.md +++ b/STATUS.md @@ -7,16 +7,16 @@ > - Keep Session Log entries to one or two lines. ## Current Focus -- **Phase:** 10.1z -- **Step:** Stretch Goals +- **Phase:** 10.1 +- **Step:** 10.2 — Fix expedition CLAIM overwriting XP - **Blocked?** No -- **Last verified working command:** `npm test` — 176/176 passing (85 unit + 91 integration + 1 e2e), 2026-10-21 step 9.6 +- **Last verified working command:** `npm test` — 178/178 passing (85 unit + 93 integration incl. 2 new 10.1 tests), 2026-10-22 step 10.1 ## Project Health | Item | State | |---|---| | App starts (`npm start`) | ✅ smoke-tested: register → create hero → 12 hunts → level-up (real seeded zones) | -| Tests pass (`npm test`) | ✅ 93/93 (runs EVERY *.test.js; auth/combat/health were previously skipped by a bad glob) | +| Tests pass (`npm test`) | ✅ 178/178 (85 unit + 93 integration incl. 2 new 10.1 tests) | | Migrations run clean on empty DB | ✅ 001_users, 002_characters, 003_content (also seeds zones/monsters/items), 004_inventory, 006_logs (005 reserved for expeditions) | | Can register + login | ✅ verified by tests | | Data persists across restart | ⚠️ sessions use the real SQLite store (also in tests); a restart test is still missing | @@ -138,8 +138,9 @@ --- ### A. Expedition bugs (verified live) -- [ ] **10.1** Fix expedition START corrupting energy. In `startExpedition` (`src/services/expeditionService.js`) the transaction function's parameters are in a different order than the call, so the DB gets `energy = def.energy_cost` and `energy_updated_at = endsAt`. Fix so the DB stores `energy - cost` and the timestamp returned by `computeEnergy`. +- [x] **10.1** Fix expedition START corrupting energy. In `startExpedition` (`src/services/expeditionService.js`) the transaction function's parameters are in a different order than the call, so the DB gets `energy = def.energy_cost` and `energy_updated_at = endsAt`. Fix so the DB stores `energy - cost` and the timestamp returned by `computeEnergy`. *Acceptance:* test: hero energy 100, start the 10-energy expedition → DB `energy` is 90 and `energy_updated_at` is NOT in the future; `GET /api/character` shows 90; response energy equals DB energy. Test also starting at energy 55 → 45. + *Verified:* 2 new integration tests (DB-level assertions) + 14 existing expedition tests pass; transaction params `(cid, ne, eat, defid, sat, endAt)` now match call `(characterId, newEnergy, updatedAtMs, defId, startedAt, endsAt)`. - [ ] **10.2** Fix expedition CLAIM overwriting XP. Claim must ADD the reward to current XP, run `checkLevelUp`, ADD (not overwrite) unspent points, and heal to the new max HP on level-up. Extract ONE shared function (e.g. `applyXpGain` in `src/game/leveling.js` or a small progression service) and use it from BOTH `huntService` and `expeditionService`. *Acceptance:* tests: xp 80 + reward 30 at level 1 → level 2, xp 10, unspent_points +3; xp 0 + 30 → xp 30, level 1; a reward spanning two levels → both levels gained, points +6; double claim still rejected; gold added correctly. Existing hunt tests still pass. - [ ] **10.3** Make expeditions testable: use `time.now()` and an injectable `rng` (no `Date.now()` / `Math.random()` in `expeditionService.js`). @@ -227,6 +228,7 @@ Severity: `blocker` / `major` / `minor` / `cosmetic`. Status: `open` / `fixing` ## Session Log _Format:_ `YYYY-MM-DD | step | what changed | result` +- 2026-10-22 | 10.1 | Fixed expedition start energy corruption: transaction params in `startExpedition` were mismatched (DB stored `energy = def.energy_cost` and `energy_updated_at = endsAt`); corrected param order to `(cid, ne, eat, defid, sat, endAt)` matching call `(characterId, newEnergy, updatedAtMs, defId, startedAt, endsAt)`; added 2 DB-level integration tests | 178/178 tests pass - 2026-10-21 | 9.6 | Rewrote README.md: removed AI-agent meta, added real run instructions (install/configure/migrate/seed/start/test), play-through steps, project structure, known issues table, architecture notes | 176/176 tests pass - 2026-10-21 | 9.5 | Created `tests/integration/e2e-playable.test.js` covering full playable flow (register → login → create hero → stats → hunt → level → equip → shop → expedition → leaderboard → logout → login → persistence); fixed `handleListInventory` bug (parameter `_res` vs `res` in `src/routes/inventory.js`); 176/176 tests pass - 2026-10-21 | 9.4 | Mobile-responsive pass: game.html 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), desktop tabs hidden < 700px, game-container padding for nav clearance, mobile stat-row stacking, inventory container stacks < 600px; 175/175 tests pass diff --git a/src/services/expeditionService.js b/src/services/expeditionService.js index 2f39903..0e97f73 100644 --- a/src/services/expeditionService.js +++ b/src/services/expeditionService.js @@ -129,12 +129,12 @@ function startExpedition(characterId, defId) { const endsAt = startedAt + (def.duration_sec * 1000); // Execute inside a transaction. - const doStart = db.transaction((nc, ne, eat, cid, defid, sat) => { + const doStart = db.transaction((cid, ne, eat, defid, sat, endAt) => { s.updateEnergy.run(ne, eat, cid); - s.insertExpedition.run(cid, defid, sat, eat); + s.insertExpedition.run(cid, defid, sat, endAt); }); - doStart(newEnergy, def.energy_cost, endsAt, characterId, defId, startedAt); + doStart(characterId, newEnergy, updatedAtMs, defId, startedAt, endsAt); // Read back the just-inserted expedition (latest active for this character). const inserted = s.activeExpedition.get(characterId); diff --git a/tests/integration/expeditions.test.js b/tests/integration/expeditions.test.js index 0366c8d..6b6a7d8 100644 --- a/tests/integration/expeditions.test.js +++ b/tests/integration/expeditions.test.js @@ -404,4 +404,50 @@ describe('expeditions', () => { assert.equal(res.status, 401); assert.equal(body.ok, false); }); + + // ─── 10.1: expedition start must deduct energy correctly ────────────── + + it('10.1: starts expedition → DB energy = energy - cost, timestamp NOT in future', async () => { + const { cookie, characterId } = await setupHero({ energy: 100, gold: 500 }); + + const res = await fetch(`${baseUrl}/api/expeditions/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, + body: JSON.stringify({ defId: 1 }), // scout, cost=10 + }); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.ok, true); + + // Assert DB state directly — this is the core of 10.1. + const db = require('../../src/db').getDb(); + const ch = db.prepare('SELECT energy, energy_updated_at FROM characters WHERE id = ?').get(characterId); + assert.equal(ch.energy, 90, 'DB energy must be 100 - 10 = 90'); + assert.ok(ch.energy_updated_at <= Date.now(), 'energy_updated_at must NOT be in the future'); + + // Assert GET /api/character returns the same energy. + const charRes = await fetch(`${baseUrl}/api/character`, { + headers: authHeaders(cookie), + }); + assert.equal(charRes.status, 200); + const charBody = await charRes.json(); + assert.equal(charBody.data.energy.current, 90); + }); + + it('10.1: starting at energy 55 → DB energy = 45', async () => { + const { cookie, characterId } = await setupHero({ energy: 55, gold: 500 }); + + const res = await fetch(`${baseUrl}/api/expeditions/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, + body: JSON.stringify({ defId: 1 }), // scout, cost=10 + }); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.ok, true); + + const db = require('../../src/db').getDb(); + const ch = db.prepare('SELECT energy FROM characters WHERE id = ?').get(characterId); + assert.equal(ch.energy, 45, 'DB energy must be 55 - 10 = 45'); + }); });