diff --git a/HANDOFF.md b/HANDOFF.md index 40674a3..ff097b2 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,8 +4,8 @@ > If this file disagrees with the code, trust the code and fix this file. ## 1. Where we are -- **Last completed step:** 10.9 — Allocation UI: unspent points display with + buttons per stat on dashboard stats panel — TICKED in STATUS.md -- **Next step:** Phase 10.10 — POST /api/character/rest endpoint + dashboard button +- **Last completed step:** 10.10 — Rest endpoint: POST /api/character/rest costs level*5 gold, heals to full max HP + dashboard button — TICKED in STATUS.md +- **Next step:** Phase 10.11 — Natural HP regeneration: +1% max HP/min, lazy computation, new migration adds hp_updated_at - **Updated:** 2026-10-23 - **Was the last session cut off mid-task?** No @@ -14,29 +14,30 @@ |---|---| | `npm install` | succeeds | | `npm run migrate` | applies 001–006 / "nothing to do" | -| `npm test` | **222 passing, 0 failing** (85 unit + 137 integration incl. 8 allocation UI tests) | +| `npm test` | **227 passing, 0 failing** (85 unit + 142 integration incl. 5 rest endpoint tests) | | `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear | | `npm start` + `curl localhost:3000/` | 200 HTML landing page | -| Open `/game.html` logged in → check dashboard for allocation UI | Shows unspent points with + buttons when > 0, hidden when 0 | +| Open `/game.html` logged in → check dashboard | Shows rest button when HP < max, hidden when at full HP | ## 3. What exists now (files that matter) -- `public/game.html` — Added `#allocation-section` div (hidden by default) with unspent points display, alloc-rows container, alloc-total, alloc-apply-btn. -- `public/js/game.js` — Added 5 allocation functions: `buildAllocationRows`, `handleAllocIncrement`, `updateAllocState`, `handleAllocApply`, `initAllocationUI`. Calls `initAllocationUI` from `loadCharacter`. Apply button listener attached in init. -- `public/css/main.css` — Added allocation UI styles: `#allocation-section`, `.alloc-row`, `.alloc-btn`, `.btn-alloc-apply`. -- `tests/integration/step-10.9.test.js` — 8 tests (5 HTML/JS structure + 2 API flow + 1 full flow). -- `src/services/characterService.js` — `allocatePoints` function (from 10.8). -- `src/routes/character.js` — `handleAllocate` handler (from 10.8). +- `src/services/characterService.js` — Added `rest()` function: costs level*5 gold, heals to full max HP, errors NOTHING_TO_HEAL/INSUFFICIENT_GOLD +- `src/routes/character.js` — Added `handleRest` handler, exported `postRest` +- `src/app.js` — Mounted `POST /api/character/rest` route +- `public/game.html` — Added `#rest-section` div with `#rest-btn` button (hidden by default) +- `public/css/main.css` — Added `.btn-rest` styles +- `public/js/game.js` — Added `initRestUI()`, `handleRest()`, calls `initRestUI` from `loadCharacter`, attaches click handler +- `tests/integration/step-10.10.test.js` — 5 tests (happy heal, full HP rejection, insufficient gold, cross-user isolation, level scaling) ## 4. Half-finished / fragile things - No leaderboard UI yet (stub page exists; real UI in 10.17). -- HP never regenerates naturally; only level-up restores it. +- HP never regenerates naturally; only level-up/rest restores it. (10.11 next) - No restart-persistence test for sessions. - `npm run seed` script in package.json points to nonexistent `scripts/seed.js`. - Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md). - 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.10 next:** Rest endpoint — costs level*5 gold, heals to full max HP. +- **10.11 next:** Natural HP regeneration — +1% max HP/min, lazy computation like energy, migration adds hp_updated_at ## 5. Decisions & deviations from the docs - D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock). @@ -94,8 +95,12 @@ - **10.8:** For HTTP integration tests that need DB state setup, use direct service calls to set up state, then test the HTTP endpoint. - **10.9:** Allocation UI must use `textContent`, never `innerHTML` with dynamic data — use `replaceChildren()` instead of `innerHTML = ''` to clear containers. - **10.9:** `initAllocationUI` called from `loadCharacter` ensures UI stays in sync after any character data change (allocation, hunt, level-up). +- **10.10:** `setupHero` helper must use `.then()` chain (not `return async () => { ... }`) — the latter returns a function that doesn't execute when awaited. +- **10.10:** Warrior maxHp at level 1 = 135 (50 + 8*10 + 1*5); test setup must account for this when setting "full HP" values. ## 7. Next-step briefing -- **Next step: 10.10** — Rest endpoint: `POST /api/character/rest` costs `level * 5` gold, heals to full max HP. Errors: `NOTHING_TO_HEAL` (422) if HP is already full, `INSUFFICIENT_GOLD` (422). One transaction. Add a "Rest (N gold)" button to the dashboard. -- Read API.md §Rest and BALANCE.md §Rest for the spec. -- The endpoint is similar to allocate (auth, zod validation, transaction, returns updated character + derived). +- **Next step: 10.11** — Natural HP regeneration: +1% max HP/min, computed lazily like energy. +- New migration (next unused number, likely 007) adds `hp_updated_at INTEGER` column to characters table; backfill existing rows with `energy_updated_at`. +- Add pure function `computeHp(current, maxHp, updatedAtMs, nowMs)` in `src/game/hp.js` with unit tests (partial minute, cap at max, clock backwards, large gap). +- Apply computeHp before HP checks in: hunting, `getCharacterProfile`, potion use, and rest. +- Heals/damage must set `hp_updated_at = now`. diff --git a/STATUS.md b/STATUS.md index 90382e9..ed9cd3c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -8,9 +8,9 @@ ## Current Focus - **Phase:** 10 -- **Step:** 10.10 — Rest endpoint: POST /api/character/rest costs level*5 gold, heals to full max HP, errors on nothing-to-heal or insufficient gold +- **Step:** 10.11 — Natural HP regeneration: +1% max HP/min, lazy computation, new migration adds hp_updated_at - **Blocked?** No -- **Last verified working command:** `npm test` — 222/222 passing (85 unit + 137 integration incl. 8 allocate UI tests), 2026-10-23 step 10.9 +- **Last verified working command:** `npm test` — 227/227 passing (85 unit + 142 integration incl. 5 rest endpoint tests), 2026-10-23 step 10.10 ## Project Health | Item | State | @@ -278,5 +278,6 @@ _Format:_ `YYYY-MM-DD | step | what changed | result` - 2026-10-20 | 2.4 | Created `public/create-hero.html`, `public/game.html`, `public/js/create-hero.js`, `public/js/game.js` | app loads - 2026-10-21 | 8.2 | Created `src/services/heroProfileService.js` (getHeroProfile), `src/routes/heroProfile.js` (GET /api/heroes/:name, public), integrated into app.js; 5 integration tests covering public lookup, case-insensitivity, equipped items, 404, no-auth access | 167/167 tests pass - 2026-10-21 | 9.1 | Walked SECURITY.md checklist: fixed `innerHTML` with dynamic data in `game.js` (renderActiveExpedition) and `profile.js` (renderProfile stats) — converted to createElement/textContent; added missing general `/api` rate limiter (120/min/IP per SECURITY.md spec); verified all 9 checklist items pass | 172/172 tests pass +- 2026-10-23 | 10.10 | Created `POST /api/character/rest` endpoint (costs level*5 gold, heals to full max HP, errors NOTHING_TO_HEAL/INSUFFICIENT_GOLD); added "Rest (N gold)" button to dashboard; 5 integration tests | 227/227 tests pass - 2026-10-21 | 9.2 | Created `scripts/seed-dev.js`: runs migrations, creates demo user "demo"/"demo123", hero "Aldric" (warrior), grants starter gear (rusty sword, leather jerkin, 2 healing potions); idempotent; skips in production | 172/172 tests pass - 2026-10-21 | 9.3 | Added shared toast system (`public/js/toast.js`), 404/500 error pages (`public/404.html`, `public/500.html`), CSS loading spinner + button loading states, server-side catch-all 404 + 500 HTML serving; removed duplicate showToast from game.js/shop.js; 3 integration tests for error pages | 175/175 tests pass diff --git a/public/css/main.css b/public/css/main.css index 3eb398c..017a03d 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -1123,3 +1123,25 @@ button:hover { background: #c73650; } color: #888; cursor: not-allowed; } + +.btn-rest { + background: #2a9d8f; + color: #fff; + border: none; + border-radius: 4px; + padding: 0.35rem 1rem; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.btn-rest:hover:not(:disabled) { + background: #21867a; +} + +.btn-rest:disabled { + background: #444; + color: #888; + cursor: not-allowed; +} diff --git a/public/game.html b/public/game.html index 3f03a48..0fe0ed2 100644 --- a/public/game.html +++ b/public/game.html @@ -41,6 +41,9 @@ HP: +
Energy: diff --git a/public/js/game.js b/public/js/game.js index 5e706dc..c7f3f4a 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -170,6 +170,9 @@ async function loadCharacter() { // Initialize allocation UI initAllocationUI(character.unspent_points); + + // Initialize rest UI + initRestUI(character.hp_current, character.maxHp, character.gold, character.level); } catch (err) { // Not authenticated — redirect to login window.location.href = '/login.html'; @@ -974,6 +977,41 @@ async function handleAllocApply() { } } +/** + * Handle rest button: heal to full HP for gold cost. + */ +async function handleRest() { + const restBtn = document.getElementById('rest-btn'); + if (!restBtn || restBtn.disabled) return; + + restBtn.disabled = true; + restBtn.textContent = 'Resting...'; + + try { + const data = await api.post('/character/rest', {}); + if (!data) { + showToast('Rest failed.', 'error'); + return; + } + + showToast('Fully rested!', 'success'); + + // Reload character to get updated stats + await loadCharacter(); + } catch (err) { + const msg = err.message || 'Rest failed.'; + if (msg.includes('NOTHING_TO_HEAL') || msg.includes('full health')) { + showToast('Already at full health.', 'info'); + } else if (msg.includes('INSUFFICIENT_GOLD') || msg.includes('gold')) { + showToast('Not enough gold to rest.', 'error'); + } else { + showToast(msg, 'error'); + } + // Reload to restore state + await loadCharacter(); + } +} + /** * Initialize or refresh the allocation UI based on current character data. */ @@ -992,6 +1030,27 @@ function initAllocationUI(unspentPoints) { } } +/** + * Initialize the rest button based on character HP and gold. + */ +function initRestUI(hpCurrent, maxHp, gold, level) { + const section = document.getElementById('rest-section'); + if (!section) return; + + const cost = level * 5; + const restBtn = document.getElementById('rest-btn'); + + if (hpCurrent >= maxHp) { + section.style.display = 'none'; + } else { + section.style.display = 'block'; + if (restBtn) { + restBtn.textContent = `Rest (${cost}g)`; + restBtn.disabled = gold < cost; + } + } +} + // ===== Init ===== document.addEventListener('DOMContentLoaded', async () => { await loadCharacter(); @@ -1010,6 +1069,12 @@ document.addEventListener('DOMContentLoaded', async () => { if (applyBtn) { applyBtn.addEventListener('click', handleAllocApply); } + + // Rest button + const restBtn = document.getElementById('rest-btn'); + if (restBtn) { + restBtn.addEventListener('click', handleRest); + } }); // Logout handler diff --git a/src/app.js b/src/app.js index 10fc9fb..36fc49a 100644 --- a/src/app.js +++ b/src/app.js @@ -66,6 +66,7 @@ function createApp() { app.post('/api/character', requireAuth, characterRoutes.postCharacter); app.get('/api/character', requireAuth, characterRoutes.getCharacter); app.post('/api/character/allocate', requireAuth, characterRoutes.postAllocate); + app.post('/api/character/rest', requireAuth, characterRoutes.postRest); // Hunt routes (require session) app.post('/api/hunt', requireAuth, huntRoutes.postHunt); diff --git a/src/routes/character.js b/src/routes/character.js index 83e2d9c..9b8daa2 100644 --- a/src/routes/character.js +++ b/src/routes/character.js @@ -1,8 +1,8 @@ -// Character routes: POST /api/character (create), GET /api/character (profile), POST /api/character/allocate. +// Character routes: POST /api/character (create), GET /api/character (profile), POST /api/character/allocate, POST /api/character/rest. const zod = require('zod'); const { AppError } = require('../utils/errors'); const { requireAuth } = require('../middleware/session'); -const { createCharacter, getCharacterProfile, allocatePoints } = require('../services/characterService'); +const { createCharacter, getCharacterProfile, allocatePoints, rest } = require('../services/characterService'); const { CLASS_KEYS } = require('../game/classes'); // Validation schemas @@ -64,10 +64,18 @@ async function handleAllocate(req, res) { res.json({ ok: true, data: result }); } +async function handleRest(req, res) { + const userId = req.session.userId; + const result = rest(userId); + + res.json({ ok: true, data: result }); +} + module.exports = { routes: { postCharacter: wrap(handleCreate), getCharacter: wrap(handleGet), postAllocate: wrap(handleAllocate), + postRest: wrap(handleRest), }, }; diff --git a/src/services/characterService.js b/src/services/characterService.js index 5ff7a8c..82e3248 100644 --- a/src/services/characterService.js +++ b/src/services/characterService.js @@ -283,4 +283,81 @@ function allocatePoints(userId, allocation) { }; } -module.exports = { createCharacter, getCharacterProfile, spendEnergy, getCharacterIdForUser, allocatePoints }; +/** + * Rest: heal to full max HP for gold cost = level * 5. + * Runs in a transaction. Errors: NOTHING_TO_HEAL, INSUFFICIENT_GOLD. + * @param {number} userId - the user's ID + * @returns {object} { character, derived } updated profile + */ +function rest(userId) { + const db = getDb(); + const stmts = getStmts(); + + // Get character + const character = stmts.findByUserId.get(userId); + if (!character) { + throw new AppError('NO_CHARACTER', 'No character found.', 404); + } + + // Compute derived stats to know max HP + const derived = calcDerived( + { str: character.str, agi: character.agi, int: character.int, vit: character.vit }, + character.level, + character.class + ); + + const cost = character.level * 5; + + // Check if already at full HP + if (character.hp_current >= derived.maxHp) { + throw new AppError('NOTHING_TO_HEAL', 'Already at full health.', 422); + } + + // Check if enough gold + if (character.gold < cost) { + throw new AppError('INSUFFICIENT_GOLD', + `Not enough gold. Need ${cost}, have ${character.gold}.`, 422); + } + + // Heal to full and deduct gold in a transaction + const update = db.transaction((hp, gold, cid) => { + db.prepare( + 'UPDATE characters SET hp_current = ?, gold = ? WHERE id = ?' + ).run(hp, gold, cid); + }); + + update(derived.maxHp, character.gold - cost, character.id); + + // Refresh character data + const updated = db.prepare('SELECT * FROM characters WHERE id = ?').get(character.id); + + return { + character: { + id: updated.id, + name: updated.name, + class: updated.class, + level: updated.level, + xp: updated.xp, + gold: updated.gold, + str: updated.str, + agi: updated.agi, + int: updated.int, + vit: updated.vit, + unspent_points: updated.unspent_points, + hp_current: updated.hp_current, + maxHp: derived.maxHp, + kills: updated.kills, + deaths: updated.deaths, + }, + derived: { + maxHp: derived.maxHp, + attack: derived.attack, + defense: derived.defense, + speed: derived.speed, + critChance: derived.critChance, + critMult: derived.critMult, + }, + }; +} + +module.exports = { createCharacter, getCharacterProfile, spendEnergy, getCharacterIdForUser, allocatePoints, rest }; diff --git a/tests/integration/step-10.10.test.js b/tests/integration/step-10.10.test.js new file mode 100644 index 0000000..839ff73 --- /dev/null +++ b/tests/integration/step-10.10.test.js @@ -0,0 +1,226 @@ +// Step 10.10 — Rest endpoint: POST /api/character/rest +// Costs level*5 gold, heals to full max HP. +// Errors: NOTHING_TO_HEAL (422) if HP already full, INSUFFICIENT_GOLD (422). +const { describe, it, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const { setup, teardown, cleanup } = require('../helpers'); + +describe('Step 10.10 — Rest endpoint', () => { + let baseUrl; + let loginCookie; + + function registerAndLogin(username) { + return fetch(`${baseUrl}/api/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password: 'password123' }), + }).then(() => fetch(`${baseUrl}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password: 'password123' }), + })); + } + + function createHero(cookie, name) { + return fetch(`${baseUrl}/api/character`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Cookie: cookie }, + body: JSON.stringify({ name, class: 'warrior' }), + }); + } + + function getCharacter(cookie) { + return fetch(`${baseUrl}/api/character`, { + method: 'GET', + headers: { Cookie: cookie }, + }); + } + + function rest(cookie) { + return fetch(`${baseUrl}/api/character/rest`, { + method: 'POST', + headers: { Cookie: cookie }, + }); + } + + function setupHero(username, name, hp, gold) { + return registerAndLogin(username) + .then((res) => { + loginCookie = res.headers.get('set-cookie'); + return createHero(loginCookie, name); + }) + .then(() => { + const db = require('../../src/db').getDb(); + const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get(username).id); + const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId); + db.prepare('UPDATE characters SET hp_current = ?, gold = ? WHERE id = ?').run(hp, gold, char.id); + }); + } + + before(async () => { + baseUrl = await setup(); + }); + + after(async () => { + const { resetNow } = require('../../src/utils/time'); + resetNow(); + await teardown(); + cleanup(); + }); + + it('rest heals to full max HP and deducts level*5 gold', async () => { + await setupHero('rest1', 'RestHero1', 10, 100); + + const charRes = await getCharacter(loginCookie); + assert.equal(charRes.status, 200); + const charData = await charRes.json(); + const level = charData.data.character.level; + const maxHp = charData.data.derived.maxHp; + const goldBefore = charData.data.character.gold; + const cost = level * 5; + + const restRes = await rest(loginCookie); + assert.equal(restRes.status, 200); + const restData = await restRes.json(); + assert.equal(restData.ok, true); + + // HP should be at max + assert.equal(restData.data.character.hp_current, maxHp); + // Gold should be reduced by cost + assert.equal(restData.data.character.gold, goldBefore - cost); + // Character data and derived included + assert.ok(restData.data.character, 'response includes updated character'); + assert.ok(restData.data.derived, 'response includes updated derived stats'); + + // Verify in DB + const db = require('../../src/db').getDb(); + const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest1').id); + const char = db.prepare('SELECT hp_current, gold FROM characters WHERE user_id = ?').get(userId); + assert.equal(char.hp_current, maxHp); + assert.equal(char.gold, goldBefore - cost); + }); + + it('rest at full HP returns 422 NOTHING_TO_HEAL and gold unchanged', async () => { + // Create a hero at level 5 (maxHp = 50 + 8*10 + 5*5 = 165), set hp to max + await setupHero('rest2', 'RestHero2', 10, 100); + // Boost to level 5 so maxHp is higher, then set hp to match maxHp + const db = require('../../src/db').getDb(); + const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest2').id); + const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId); + db.prepare('UPDATE characters SET level = 5, hp_current = 165 WHERE id = ?').run(char.id); + + const charRes = await getCharacter(loginCookie); + assert.equal(charRes.status, 200); + const charData = await charRes.json(); + const goldBefore = charData.data.character.gold; + + const restRes = await rest(loginCookie); + assert.equal(restRes.status, 422); + const restData = await restRes.json(); + assert.equal(restData.ok, false); + assert.equal(restData.error.code, 'NOTHING_TO_HEAL'); + + // Gold should be unchanged + assert.equal(goldBefore, charData.data.character.gold); + + // Verify in DB + const db2 = require('../../src/db').getDb(); + const charAfter = db2.prepare('SELECT hp_current, gold FROM characters WHERE id = ?').get(char.id); + assert.equal(charAfter.hp_current, 165); + assert.equal(charAfter.gold, 100); + }); + + it('rest with insufficient gold returns 422 INSUFFICIENT_GOLD and HP unchanged', async () => { + // Level 5 hero costs 25 gold to rest; give only 5 gold + await setupHero('rest3', 'RestHero3', 10, 5); + const db = require('../../src/db').getDb(); + const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest3').id); + const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId); + db.prepare('UPDATE characters SET level = 5, hp_current = 10, gold = 5 WHERE id = ?').run(char.id); + + const charRes = await getCharacter(loginCookie); + assert.equal(charRes.status, 200); + const charData = await charRes.json(); + const hpBefore = charData.data.character.hp_current; + + const restRes = await rest(loginCookie); + assert.equal(restRes.status, 422); + const restData = await restRes.json(); + assert.equal(restData.ok, false); + assert.equal(restData.error.code, 'INSUFFICIENT_GOLD'); + + // HP should be unchanged + assert.equal(hpBefore, charData.data.character.hp_current); + + // Verify in DB + const db2 = require('../../src/db').getDb(); + const charAfter = db2.prepare('SELECT hp_current, gold FROM characters WHERE id = ?').get(char.id); + assert.equal(charAfter.hp_current, 10); + assert.equal(charAfter.gold, 5); + }); + + it('rest on another user\'s character is rejected (cross-user isolation)', async () => { + // Create a second user (the "victim") + const victimRes = await registerAndLogin('rest4victim'); + const victimCookie = victimRes.headers.get('set-cookie'); + await createHero(victimCookie, 'VictimHero'); + const db = require('../../src/db').getDb(); + const victimUserId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest4victim').id); + const victimChar = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(victimUserId); + db.prepare('UPDATE characters SET hp_current = 10, gold = 100 WHERE id = ?').run(victimChar.id); + + // Also set up rest1 with enough gold to succeed if it worked on their own char + const r1 = await registerAndLogin('rest1'); + loginCookie = r1.headers.get('set-cookie'); + await createHero(loginCookie, 'RestHero1'); + const r1UserId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest1').id); + const r1Char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(r1UserId); + db.prepare('UPDATE characters SET hp_current = 10, gold = 100 WHERE id = ?').run(r1Char.id); + + // Try to rest with rest1's session — should succeed for rest1, NOT affect victim + const restRes = await rest(loginCookie); + assert.equal(restRes.status, 200); + + // Victim's HP should be unchanged (rest1 can't rest on victim) + const db2 = require('../../src/db').getDb(); + const charAfter = db2.prepare('SELECT hp_current FROM characters WHERE id = ?').get(victimChar.id); + assert.equal(charAfter.hp_current, 10); + }); + + it('rest cost scales with level: level 1 costs 5g, level 5 costs 25g', async () => { + // Level 1 hero — verify cost = 1 * 5 = 5 + await setupHero('rest5a', 'RestHero5A', 10, 50); + const charResA = await getCharacter(loginCookie); + assert.equal(charResA.status, 200); + const charDataA = await charResA.json(); + assert.ok(charDataA.data, 'response must have data'); + assert.ok(charDataA.data.character, 'response data must have character'); + const goldBeforeA = charDataA.data.character.gold; + + const restResA = await rest(loginCookie); + assert.equal(restResA.status, 200); + const restDataA = await restResA.json(); + assert.equal(restDataA.data.character.gold, goldBeforeA - 5); + + // Level 5 hero — verify cost = 5 * 5 = 25 + const resB = await registerAndLogin('rest5b'); + loginCookie = resB.headers.get('set-cookie'); + await createHero(loginCookie, 'RestHero5B'); + const db = require('../../src/db').getDb(); + const userIdB = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest5b').id); + const charB = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userIdB); + db.prepare('UPDATE characters SET level = 5, hp_current = 10, gold = 50 WHERE id = ?').run(charB.id); + + const charResB = await getCharacter(loginCookie); + assert.equal(charResB.status, 200); + const charDataB = await charResB.json(); + assert.ok(charDataB.data, 'response must have data'); + assert.ok(charDataB.data.character, 'response data must have character'); + const goldBeforeB = charDataB.data.character.gold; + + const restResB = await rest(loginCookie); + assert.equal(restResB.status, 200); + const restDataB = await restResB.json(); + assert.equal(restDataB.data.character.gold, goldBeforeB - 25); + }); +});