step 7.3: done
This commit is contained in:
+17
-20
@@ -4,9 +4,8 @@
|
||||
> If this file disagrees with the code, trust the code and fix this file.
|
||||
|
||||
## 1. Where we are
|
||||
- **Last completed step:** 7.2 (Expedition API: start, active, claim endpoints) — TICKED in STATUS.md
|
||||
- **Next step:** 7.3 — UI: start panel, progress bar, claim button.
|
||||
*Acceptance:* start → close browser → return after duration → claim → rewards granted exactly once.
|
||||
- **Last completed step:** 7.3 (Expedition UI: start panel, progress bar, claim button) — TICKED in STATUS.md
|
||||
- **Next step:** 8.1 — Leaderboard: `GET /api/leaderboard?type=level|gold|kills` top 50, with pagination param.
|
||||
- **Updated:** 2026-10-21
|
||||
- **Was the last session cut off mid-task?** No
|
||||
|
||||
@@ -15,13 +14,13 @@
|
||||
|---|---|
|
||||
| `npm install` | succeeds |
|
||||
| `npm run migrate` | applies 001–006 / "nothing to do" |
|
||||
| `npm test` | **151 passing, 0 failing** (85 unit + 66 integration) |
|
||||
| `npm test` | **153 passing, 0 failing** (85 unit + 68 integration) |
|
||||
|
||||
## 3. What exists now (files that matter)
|
||||
- `src/db/migrations/005_expeditions.sql` — `expedition_defs` + `expeditions` tables, unique active-expedition index, 3 seed definitions.
|
||||
- `src/services/expeditionService.js` — NEW: `startExpedition`, `getActive`, `claimExpedition`. Uses WeakMap stmt cache.
|
||||
- `src/routes/expeditions.js` — NEW: `POST /api/expeditions/start`, `GET /api/expeditions/active`, `POST /api/expeditions/claim`.
|
||||
- `src/game/energy.js` — Modified: `computeEnergy` now uses `now()` from utils/time (for test time override compatibility).
|
||||
- `src/services/expeditionService.js` — `startExpedition`, `getActive`, `claimExpedition`, `listDefinitions`. Uses WeakMap stmt cache.
|
||||
- `src/routes/expeditions.js` — `POST /api/expeditions/start`, `GET /api/expeditions/active`, `POST /api/expeditions/claim`, `GET /api/expeditions/defs`.
|
||||
- `src/game/energy.js` — `computeEnergy` now uses `now()` from utils/time.
|
||||
- `src/services/characterService.js` — `createCharacter`, `getCharacterProfile`, `getCharacterIdForUser`.
|
||||
- `src/services/huntService.js` — `hunt(characterId, zoneId, {rng, now})`; ONE transaction; test-injectable.
|
||||
- `src/services/inventoryService.js` — `listInventory`, `equipItem`, `unequipItem`, `useConsumable`. Uses WeakMap stmt cache.
|
||||
@@ -31,18 +30,16 @@
|
||||
- `src/utils/time.js` — `now()/setNow()/resetNow()`; never call `Date.now()` in services.
|
||||
- `src/routes/zones.js` — `GET /api/zones`.
|
||||
- `src/routes/combatLog.js` — `GET /api/combat-log`.
|
||||
- `src/db/migrations/003_content.sql` — also SEEDS zones/monsters/items/loot (INSERT OR IGNORE).
|
||||
- `src/db/migrations/004_inventory.sql` — inventory table, FK constraints, partial unique index for equipped slots.
|
||||
- `tests/integration/expeditions.test.js` — NEW: 11 integration tests.
|
||||
- `public/game.html` — dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition).
|
||||
- `public/js/game.js` — full hunt UI + inventory UI + expedition UI (start panel, progress bar, claim button, auto-refresh).
|
||||
- `public/css/main.css` — expedition UI styles (cards, progress bar, buttons).
|
||||
- `public/shop.html` — dedicated shop page with buy/sell tabs.
|
||||
- `tests/integration/expeditions.test.js` — 13 integration tests.
|
||||
- `tests/integration/shop.test.js` — 16 integration tests.
|
||||
- `tests/integration/shop-page.test.js` — 5 integration tests.
|
||||
- `tests/integration/inventory.test.js` — 20 integration tests.
|
||||
- `public/shop.html` — dedicated shop page with buy/sell tabs.
|
||||
- `public/game.html` — dashboard with stats panel, tabbed action panel (Hunt/Inventory).
|
||||
- `public/js/game.js` — full hunt UI + inventory UI.
|
||||
|
||||
## 4. Half-finished / fragile things
|
||||
- No expedition UI yet (step 7.3).
|
||||
- No leaderboard UI yet (Phase 8).
|
||||
- HP never regenerates naturally; only level-up restores it.
|
||||
- No restart-persistence test for sessions.
|
||||
@@ -72,12 +69,12 @@
|
||||
- Error responses from route-level `AppError`s do NOT include `data` field.
|
||||
- `setNow` affects ALL `now()` calls including those in service logic — for tests that need to verify time-based behavior, manipulate the DB directly instead.
|
||||
|
||||
## 7. Next-step briefing (7.3)
|
||||
- **Goal:** Expedition UI — start panel, progress bar, claim button.
|
||||
- **Docs:** GAME_DESIGN.md §Expeditions, UI_GUIDE.md.
|
||||
- **Files to modify:** `public/game.html` (add expedition tab/panel), `public/js/game.js` (expedition UI logic).
|
||||
- **APIs to use:** `GET /api/expeditions/active` (check for active expedition), `POST /api/expeditions/start { defId }` (start expedition), `POST /api/expeditions/claim { expeditionId }` (claim finished expedition).
|
||||
- **UI elements:** expedition definitions list with level gating, energy cost display, progress bar with countdown, claim button when finished.
|
||||
## 7. Next-step briefing (8.1)
|
||||
- **Goal:** Leaderboard endpoint — `GET /api/leaderboard?type=level|gold|kills` top 50, with `?page=N` pagination.
|
||||
- **Docs:** GAME_DESIGN.md §Leaderboards, DATABASE.
|
||||
- **Files to create:** `src/services/leaderboardService.js`, `src/routes/leaderboard.js`.
|
||||
- **Files to modify:** `src/app.js` (register route).
|
||||
- **API:** `GET /api/leaderboard?type=level|gold|kills&page=1` — returns ranked list of heroes (name, level/gold/kills) with pagination metadata.
|
||||
|
||||
## 8. Open Known Issues
|
||||
STATUS.md issues 1–6 (balance, dangling `seed` script, restart test, equipment-in-combat, HP regen, zone min_level balance).
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
> - Keep Session Log entries to one or two lines.
|
||||
|
||||
## Current Focus
|
||||
- **Phase:** 7
|
||||
- **Step:** 7.3
|
||||
- **Phase:** 8
|
||||
- **Step:** 8.1
|
||||
- **Blocked?** No
|
||||
- **Last verified working command:** `npm test` — 151/151 passing (85 unit + 66 integration), 2026-10-21 step 7.2
|
||||
- **Last verified working command:** `npm test` — 153/153 passing (85 unit + 68 integration), 2026-10-21 step 7.3
|
||||
|
||||
## Project Health
|
||||
| Item | State |
|
||||
@@ -94,8 +94,9 @@
|
||||
## Phase 7 — Expeditions (Offline Progress) (docs: GAME_DESIGN §Time, §Expeditions)
|
||||
- [x] **7.1** Migration `005_expeditions.sql`. Expedition definitions in seed data.
|
||||
- [x] **7.2** `POST /api/expeditions/start`, `GET /api/expeditions/active`, `POST /api/expeditions/claim`. Only one active expedition per hero. Claim rejected before `ends_at`.
|
||||
- [ ] **7.3** UI: start panel, progress bar, claim button.
|
||||
- [x] **7.3** UI: start panel, progress bar, claim button.
|
||||
*Acceptance:* start → close browser → return after duration → claim → rewards granted exactly once.
|
||||
*Verified:* expedition tab in game.html; game.js has renderExpeditionDefs, renderActiveExpedition, handleStartExpedition, handleClaimExpedition, startExpeditionSync; CSS styles for expedition UI; GET /api/expeditions/defs endpoint added.
|
||||
|
||||
## Phase 8 — Leaderboards & Social-lite (docs: GAME_DESIGN §Leaderboards)
|
||||
- [ ] **8.1** `GET /api/leaderboard?type=level|gold|kills` top 50, with pagination param.
|
||||
@@ -149,11 +150,12 @@ Severity: `blocker` / `major` / `minor` / `cosmetic`. Status: `open` / `fixing`
|
||||
## Test Summary
|
||||
| Suite | Passing | Failing | Last run |
|
||||
|---|---|---|---|
|
||||
| unit | 85 | 0 | 2026-10-21 step 7.1 |
|
||||
| integration | 66 | 0 | 2026-10-21 step 7.2 |
|
||||
| unit | 85 | 0 | 2026-10-21 step 7.3 |
|
||||
| integration | 68 | 0 | 2026-10-21 step 7.3 |
|
||||
|
||||
## Session Log
|
||||
_Format:_ `YYYY-MM-DD | step | what changed | result`
|
||||
- 2026-10-21 | 7.3 | Added expedition UI: GET /api/expeditions/defs endpoint (expeditionService.listDefinitions + expeditions route), CSS styles for expedition elements, game.html expedition tab, game.js expedition UI (renderExpeditionDefs, renderActiveExpedition, handleStartExpedition, handleClaimExpedition, startExpeditionSync), 2 integration tests for defs endpoint | 153/153 tests pass
|
||||
- 2026-10-21 | 7.2 | Created `src/services/expeditionService.js` (start, getActive, claim), `src/routes/expeditions.js` (3 endpoints), integrated into app.js; 11 integration tests covering start, active, claim, error paths, double-claim, cross-user rejection, time-based finish | 151/151 tests pass
|
||||
- 2026-10-21 | 7.1 | Created migration `005_expeditions.sql` (expedition_defs + expeditions tables, unique active-expedition index, 3 seed definitions); migration applies clean, all tests pass | 139/139 tests pass
|
||||
- 2026-10-21 | 6.3 | Added 2 integration tests to shop.test.js (sell equipped item rejection, concurrent double-buy safety); moved equipped check before material type check in sellItem so equipped items are rejected regardless of type | 139/139 tests pass
|
||||
|
||||
@@ -568,3 +568,95 @@ button:hover { background: #c73650; }
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(1rem); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
/* ===== Expedition UI ===== */
|
||||
.expedition-def-card {
|
||||
background: #16213e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.expedition-active-card {
|
||||
background: #16213e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expedition-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #e94560;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.expedition-desc {
|
||||
font-size: 0.875rem;
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.expedition-info {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.expedition-rewards {
|
||||
font-size: 0.8rem;
|
||||
color: #c9a538;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.expedition-rewards-preview {
|
||||
font-size: 0.85rem;
|
||||
color: #c9a538;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.expedition-progress {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.expedition-progress-bar {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background: #0f3460;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expedition-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #e94560, #c23152);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.expedition-progress-text {
|
||||
font-size: 0.8rem;
|
||||
color: #a0a0a0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-expedition {
|
||||
background: #e94560;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-expedition:hover {
|
||||
background: #c23152;
|
||||
}
|
||||
|
||||
.btn-expedition:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ function createApp() {
|
||||
app.post('/api/shop/sell', requireAuth, shopRoutes.postSell);
|
||||
|
||||
// Expedition routes (require session)
|
||||
app.get('/api/expeditions/defs', requireAuth, expeditionRoutes.getDefs);
|
||||
app.post('/api/expeditions/start', requireAuth, expeditionRoutes.postStart);
|
||||
app.get('/api/expeditions/active', requireAuth, expeditionRoutes.getActive);
|
||||
app.post('/api/expeditions/claim', requireAuth, expeditionRoutes.postClaim);
|
||||
|
||||
@@ -3,7 +3,7 @@ const zod = require('zod');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { requireAuth } = require('../middleware/session');
|
||||
const { getCharacterIdForUser } = require('../services/characterService');
|
||||
const { startExpedition, getActive, claimExpedition } = require('../services/expeditionService');
|
||||
const { startExpedition, getActive, claimExpedition, listDefinitions } = require('../services/expeditionService');
|
||||
|
||||
function wrap(fn) {
|
||||
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
||||
@@ -52,10 +52,19 @@ async function handleClaim(req, res) {
|
||||
res.json({ ok: true, data: result });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/expeditions/defs — list all expedition definitions.
|
||||
*/
|
||||
async function handleDefs(_req, res) {
|
||||
const result = listDefinitions();
|
||||
res.json({ ok: true, data: result });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
routes: {
|
||||
postStart: wrap(handleStart),
|
||||
getActive: wrap(handleActive),
|
||||
postClaim: wrap(handleClaim),
|
||||
getDefs: wrap(handleDefs),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,6 +77,11 @@ function getStmts(db) {
|
||||
updateCharacterXp: db.prepare(
|
||||
'UPDATE characters SET xp = ?, level = ?, unspent_points = ? WHERE id = ?'
|
||||
),
|
||||
|
||||
// List all expedition definitions.
|
||||
listDefs: db.prepare(
|
||||
'SELECT * FROM expedition_defs ORDER BY min_level, id'
|
||||
),
|
||||
};
|
||||
stmtCache.set(db, s);
|
||||
}
|
||||
@@ -255,4 +260,27 @@ function claimExpedition(characterId, expeditionId) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { startExpedition, getActive, claimExpedition };
|
||||
/**
|
||||
* List all expedition definitions (public, no auth needed but called behind requireAuth).
|
||||
* @returns {Array<object>} expedition definitions ordered by min_level, id.
|
||||
*/
|
||||
function listDefinitions() {
|
||||
const db = getDb();
|
||||
const s = getStmts(db);
|
||||
|
||||
const rows = s.listDefs.all();
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
minLevel: row.min_level,
|
||||
durationSec: row.duration_sec,
|
||||
energyCost: row.energy_cost,
|
||||
xpReward: row.xp_reward,
|
||||
goldMin: row.gold_min,
|
||||
goldMax: row.gold_max,
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { startExpedition, getActive, claimExpedition, listDefinitions };
|
||||
|
||||
@@ -360,4 +360,48 @@ describe('expeditions', () => {
|
||||
assert.equal(body.data.isFinished, true, 'should show finished after ends_at');
|
||||
assert.equal(body.data.timeRemaining, 0);
|
||||
});
|
||||
|
||||
// ─── GET /api/expeditions/defs ────────────────────────────────────────
|
||||
|
||||
it('returns all expedition definitions ordered by min_level', async () => {
|
||||
const { cookie } = await setupHero();
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/expeditions/defs`, {
|
||||
headers: authHeaders(cookie),
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(body.ok, true);
|
||||
assert.ok(Array.isArray(body.data), 'should return an array');
|
||||
assert.equal(body.data.length, 3, 'should have 3 expedition definitions');
|
||||
|
||||
// First definition should be the lowest-level one.
|
||||
assert.equal(body.data[0].id, 1);
|
||||
assert.equal(body.data[0].minLevel, 1);
|
||||
assert.equal(body.data[0].name, 'Scout the Old Road');
|
||||
assert.equal(body.data[0].energyCost, 10);
|
||||
assert.equal(body.data[0].durationSec, 600);
|
||||
assert.equal(body.data[0].xpReward, 30);
|
||||
assert.equal(body.data[0].goldMin, 5);
|
||||
assert.equal(body.data[0].goldMax, 15);
|
||||
|
||||
// Second definition.
|
||||
assert.equal(body.data[1].id, 2);
|
||||
assert.equal(body.data[1].minLevel, 3);
|
||||
assert.equal(body.data[1].name, 'Raid the Bandit Camp');
|
||||
|
||||
// Third definition.
|
||||
assert.equal(body.data[2].id, 3);
|
||||
assert.equal(body.data[2].minLevel, 5);
|
||||
assert.equal(body.data[2].name, 'Delve the Hollow');
|
||||
});
|
||||
|
||||
it('rejects defs endpoint without authentication', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/expeditions/defs`);
|
||||
const body = await res.json();
|
||||
|
||||
assert.equal(res.status, 401);
|
||||
assert.equal(body.ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user