step 9.4: done

This commit is contained in:
bepis
2026-10-21 10:35:29 +11:00
parent cf725b40a2
commit 0b89b8f685
5 changed files with 141 additions and 47 deletions
+26 -33
View File
@@ -4,8 +4,8 @@
> If this file disagrees with the code, trust the code and fix this file. > If this file disagrees with the code, trust the code and fix this file.
## 1. Where we are ## 1. Where we are
- **Last completed step:** 9.3 (error pages, shared toast system, loading states) — TICKED in STATUS.md - **Last completed step:** 9.4 (Mobile-responsive pass) — TICKED in STATUS.md
- **Next step:** Phase 9 — 9.4 (Mobile-responsive pass per UI_GUIDE) - **Next step:** Phase 9 — 9.5 (Final full run of "Definition of playable" from NOTES.md §7)
- **Updated:** 2026-10-21 - **Updated:** 2026-10-21
- **Was the last session cut off mid-task?** No - **Was the last session cut off mid-task?** No
@@ -18,26 +18,23 @@
| `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear | | `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear |
## 3. What exists now (files that matter) ## 3. What exists now (files that matter)
- `public/404.html` — Custom 404 error page (branding: "Page Not Found", Emberfall theme). - `public/game.html` — Dashboard with 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), tabbed action area.
- `public/500.html` — Custom 500 error page (branding: "Something went wrong", retry button). - `public/js/game.js` — hunt UI + inventory UI + expedition UI + mobile nav init (DOMContentLoaded); uses shared toast.js.
- `public/js/toast.js` — Shared toast notification system (window.showToast, 3 types: error/info/success). - `public/css/main.css` — All styles including 3-col desktop grid, mobile bottom nav (< 700px), stacked panels on mobile, spinner, toast, button loading states.
- `public/game.html` — Dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition). - `public/404.html` — Custom 404 error page.
- `public/js/game.js` — hunt UI + inventory UI + expedition UI (uses shared toast.js). - `public/500.html` — Custom 500 error page.
- `public/shop.html` — Dedicated shop page. - `public/js/toast.js` — Shared toast notification system.
- `public/js/shop.js` — buy/sell handlers (uses shared toast.js). - `public/shop.html` / `public/js/shop.js` — Shop page (buy/sell).
- `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation (all include toast.js). - `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation.
- `public/profile.html` — Public hero profile page. - `public/profile.html` / `public/js/profile.js` — Public hero profile page.
- `public/js/profile.js` — Fetches GET /api/heroes/:name, renders profile.
- `public/js/auth.js` — Shared API wrapper + showError. - `public/js/auth.js` — Shared API wrapper + showError.
- `public/css/main.css` — All styles including spinner, toast, button loading states, error page shared CSS. - `scripts/seed-dev.js` — Creates demo account + hero + starter gear.
- `scripts/seed-dev.js` — Creates demo account + hero + starter gear (idempotent, skips in production).
- `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving. - `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving.
- `src/services/*` — All game services (auth, character, hunt, inventory, shop, expedition, leaderboard, profile). - `src/services/*` — All game services.
- `src/routes/*` — All route handlers. - `src/routes/*` — All route handlers.
- `src/game/*` — Pure game logic (combat, stats, energy, leveling, rewards). - `src/game/*` — Pure game logic.
- `tests/integration/error-pages.test.js` — 3 tests for 404/500 page serving. - `tests/integration/*.test.js` — 90 integration tests.
- `tests/integration/*.test.js` — 90 integration tests across auth, combat, hunt, inventory, shop, expeditions, profile, leaderboard, error-pages. - `tests/unit/*.test.js` — 85 unit tests.
- `tests/unit/*.test.js` — 85 unit tests for game logic.
## 4. Half-finished / fragile things ## 4. Half-finished / fragile things
- No leaderboard UI yet (Phase 8 API done but no UI). - No leaderboard UI yet (Phase 8 API done but no UI).
@@ -45,21 +42,19 @@
- No restart-persistence test for sessions. - No restart-persistence test for sessions.
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`. - `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). - Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
- Starter gear (GAME_DESIGN.md §Items) documented but not auto-created on character creation. - Starter gear documented but not auto-created on character creation.
- `GET /api/zones` documented in API.md but not implemented (Issue #2). - `GET /api/zones` documented in API.md but not implemented (Issue #2).
- Mobile nav uses emoji icons (⚔️ 🏕️ 🎒) — verify they render acceptably on all platforms.
## 5. Decisions & deviations from the docs ## 5. Decisions & deviations from the docs
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock). - D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
- **D-014:** Mobile bottom nav uses emoji icons (⚔️ Hunt / 🏕️ Trip / 🎒 Items) instead of full text labels to save horizontal space on narrow screens.
- **D-015:** Desktop tabs hidden via CSS (`display: none`) below 700px; mobile nav shown via CSS; JS `syncMobileNav()` keeps active state in sync.
- Zod query param parsing uses `zod.coerce.number()` because Express query params are strings. - Zod query param parsing uses `zod.coerce.number()` because Express query params are strings.
- Inventory response uses `itemId` (item.id) and `inventoryId` (inventory row id) to avoid confusion. - 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. - 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. - Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
- **D-008:** Shop materials with buy_price=0 sell at `floor(default_rate * character_level / item_min_level)`. - D-008..D-013 from previous steps.
- **D-009:** `sellItem` checks equipped_slot before material type.
- **D-010:** `startExpedition` checks active-expedition before energy.
- **D-011:** `computeEnergy` uses `now()` from utils/time.
- **D-012:** General `/api` rate limiter at 120 req/min per IP (SECURITY.md spec), 1000 in test mode.
- **D-013:** All `innerHTML` with dynamic data removed from game.js and profile.js — use createElement/textContent.
## 6. Gotchas learned ## 6. Gotchas learned
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`. - `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
@@ -81,10 +76,8 @@
- Express `sendFile` rejects `../` path segments — use `path.resolve()` + `path.join()` for absolute paths. - Express `sendFile` rejects `../` path segments — use `path.resolve()` + `path.join()` for absolute paths.
- `express.static` sends its own 404 responses; catch-all must be placed BEFORE static middleware. - `express.static` sends its own 404 responses; catch-all must be placed BEFORE static middleware.
## 7. Next-step briefing (9.4) ## 7. Next-step briefing (9.5)
- **Goal:** Mobile-responsive pass per UI_GUIDE (< 700px: single column, tabs become bottom nav). - **Goal:** Final full run of "Definition of playable" from NOTES.md §7.
- **Docs:** UI_GUIDE §game.html layout (Mobile section). - Walk the complete flow: register → log in → create hero → see stats/energy → hunt → gain XP/gold/loot → level up → equip items → buy/sell in shop → start and claim expedition → see leaderboard → log out → log back in (verify persistence).
- Current CSS has some `@media (max-width: 600px)` rules but needs comprehensive mobile review. - Document any gaps or broken steps.
- Should: verify all pages render correctly on mobile viewports. - No code changes expected unless a critical blocker is found.
- Should: tabs → bottom nav on mobile (per UI_GUIDE).
- Should NOT introduce new dependencies — use vanilla HTML/CSS/JS.
+6 -5
View File
@@ -8,9 +8,9 @@
## Current Focus ## Current Focus
- **Phase:** 9 - **Phase:** 9
- **Step:** 9.4 - **Step:** 9.5
- **Blocked?** No - **Blocked?** No
- **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.3 - **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.4
## Project Health ## Project Health
| Item | State | | Item | State |
@@ -110,7 +110,7 @@
- [x] **9.2** Add `npm run seed:dev` creating a demo account and hero. - [x] **9.2** Add `npm run seed:dev` creating a demo account and hero.
*Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production. *Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production.
- [x] **9.3** Global error pages / friendly UI error toasts. Loading states. - [x] **9.3** Global error pages / friendly UI error toasts. Loading states.
- [ ] **9.4** Mobile-responsive pass (see UI_GUIDE). - [x] **9.4** Mobile-responsive pass (see UI_GUIDE).
- [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below. - [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below.
- [ ] **9.6** Update README with real run instructions. - [ ] **9.6** Update README with real run instructions.
@@ -153,11 +153,12 @@ Severity: `blocker` / `major` / `minor` / `cosmetic`. Status: `open` / `fixing`
## Test Summary ## Test Summary
| Suite | Passing | Failing | Last run | | Suite | Passing | Failing | Last run |
|---|---|---|---| |---|---|---|---|
| unit | 85 | 0 | 2026-10-21 step 8.3 | | unit | 85 | 0 | 2026-10-21 step 9.4 |
| integration | 87 | 0 | 2026-10-21 step 8.3 | | integration | 90 | 0 | 2026-10-21 step 9.4 |
## Session Log ## Session Log
_Format:_ `YYYY-MM-DD | step | what changed | result` _Format:_ `YYYY-MM-DD | step | what changed | result`
- 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
- 2026-10-21 | 8.3 | Created public profile UI: `public/profile.html` + `public/js/profile.js` (fetches GET /api/heroes/:name via ?name=, displays hero name/class/level/gold/stats/equipment; 404 handling, URL-safe names); 5 integration tests | 172/172 tests pass - 2026-10-21 | 8.3 | Created public profile UI: `public/profile.html` + `public/js/profile.js` (fetches GET /api/heroes/:name via ?name=, displays hero name/class/level/gold/stats/equipment; 404 handling, URL-safe names); 5 integration tests | 172/172 tests pass
- 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.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.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
+67 -4
View File
@@ -76,9 +76,10 @@ button:hover { background: #c73650; }
/* Game dashboard layout */ /* Game dashboard layout */
.game-container { .game-container {
width: 100%; width: 100%;
max-width: 800px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
padding-bottom: 5rem; /* space for mobile bottom nav */
} }
.top-bar { .top-bar {
@@ -99,12 +100,24 @@ button:hover { background: #c73650; }
.main-content { .main-content {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1.5fr 1fr;
gap: 1rem; gap: 1rem;
} }
@media (max-width: 600px) { @media (max-width: 700px) {
.main-content { grid-template-columns: 1fr; } .main-content {
grid-template-columns: 1fr;
}
.game-container {
padding-bottom: 5rem;
}
}
/* Hide desktop tabs on mobile */
@media (max-width: 700px) {
#action-panel > .tabs {
display: none;
}
} }
.panel { .panel {
@@ -332,6 +345,19 @@ button:hover { background: #c73650; }
} }
} }
/* ===== Stat rows (mobile: stack name/value) ===== */
@media (max-width: 700px) {
.stat-row > .label {
display: flex;
flex-direction: column;
gap: 0;
text-align: left;
}
.stat-row > .label:last-child {
text-align: right;
}
}
/* ===== Equipment panel ===== */ /* ===== Equipment panel ===== */
.equipment-panel h4 { .equipment-panel h4 {
color: var(--accent, #e2822b); color: var(--accent, #e2822b);
@@ -661,6 +687,43 @@ button:hover { background: #c73650; }
cursor: not-allowed; cursor: not-allowed;
} }
/* ===== Mobile bottom navigation ===== */
.mobile-bottom-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #16213e;
border-top: 1px solid #333;
z-index: 100;
padding: 0.25rem 0.5rem;
padding-bottom: env(safe-area-inset-bottom, 0.25rem);
}
@media (max-width: 700px) {
.mobile-bottom-nav {
display: flex;
justify-content: space-around;
align-items: center;
}
}
.mobile-bottom-nav .tab {
flex: 1;
text-align: center;
padding: 0.5rem 0.25rem;
font-size: 0.75rem;
border-bottom: 2px solid transparent;
border-radius: 0;
color: var(--muted, #a59a8a);
}
.mobile-bottom-nav .tab.active {
color: var(--accent, #e2822b);
border-bottom-color: var(--accent, #e2822b);
}
/* ===== Profile page ===== */ /* ===== Profile page ===== */
.profile-header { .profile-header {
display: flex; display: flex;
+12 -5
View File
@@ -17,9 +17,9 @@
</div> </div>
</header> </header>
<!-- Main content --> <!-- Main content (3-col on desktop, stacks on mobile) -->
<main class="main-content"> <main class="main-content">
<!-- Stats panel --> <!-- Left column: Stats -->
<section class="panel stats-panel" id="stats-panel"> <section class="panel stats-panel" id="stats-panel">
<h3>Your Hero</h3> <h3>Your Hero</h3>
<div id="hero-loading" class="loading">Loading...</div> <div id="hero-loading" class="loading">Loading...</div>
@@ -72,7 +72,7 @@
</div> </div>
</section> </section>
<!-- Action panel (tabs: Hunt / Inventory) --> <!-- Center column: Tabs + action area -->
<section class="panel action-panel" id="action-panel"> <section class="panel action-panel" id="action-panel">
<div class="tabs" role="tablist"> <div class="tabs" role="tablist">
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button> <button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
@@ -104,7 +104,7 @@
</div> </div>
</section> </section>
<!-- Combat log panel --> <!-- Right column: Combat log -->
<section class="panel combat-panel" id="combat-panel"> <section class="panel combat-panel" id="combat-panel">
<h3>Combat Log</h3> <h3>Combat Log</h3>
<div id="combat-log-list" class="combat-log-list"> <div id="combat-log-list" class="combat-log-list">
@@ -112,12 +112,19 @@
</div> </div>
</section> </section>
<!-- Result panel --> <!-- Result panel (spans full width on mobile via grid-column) -->
<section class="panel result-panel" id="result-panel" style="display:none;"> <section class="panel result-panel" id="result-panel" style="display:none;">
<h3 id="result-title"></h3> <h3 id="result-title"></h3>
<div id="result-body"></div> <div id="result-body"></div>
</section> </section>
</main> </main>
<!-- Mobile bottom navigation (replaces tabs on small screens) -->
<nav class="mobile-bottom-nav" role="tablist">
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">⚔️ Hunt</button>
<button class="tab" data-tab="expedition" role="tab" aria-selected="false">🏕️ Trip</button>
<button class="tab" data-tab="inventory" role="tab" aria-selected="false">🎒 Items</button>
</nav>
</div> </div>
<script src="/js/toast.js"></script> <script src="/js/toast.js"></script>
+30
View File
@@ -21,6 +21,36 @@ function switchTab(tab) {
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none'; document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
} }
/**
* Sync mobile bottom nav active state with desktop tabs.
*/
function syncMobileNav() {
const mobileNav = document.querySelector('.mobile-bottom-nav');
if (!mobileNav) return;
mobileNav.querySelectorAll('.tab').forEach((t) => {
const isActive = t.dataset.tab === currentTab;
t.classList.toggle('active', isActive);
t.setAttribute('aria-selected', isActive);
});
}
/**
* Attach mobile bottom-nav click handlers.
*/
function initMobileNav() {
const mobileNav = document.querySelector('.mobile-bottom-nav');
if (!mobileNav) return;
mobileNav.querySelectorAll('.tab').forEach((btn) => {
btn.addEventListener('click', () => {
switchTab(btn.dataset.tab);
});
});
}
document.addEventListener('DOMContentLoaded', () => {
initMobileNav();
});
/** /**
* Refresh energy state from the server at a fixed interval. * Refresh energy state from the server at a fixed interval.
* Keeps client-side countdown in sync with the authoritative server. * Keeps client-side countdown in sync with the authoritative server.