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);
+ });
+});