First commit, phased 1 and 2 created but untested
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
# API.md
|
||||
|
||||
Base path: `/api`. JSON in, JSON out. Auth via session cookie.
|
||||
|
||||
## Response envelope
|
||||
Success: `{ "ok": true, "data": { ... } }`
|
||||
Failure: `{ "ok": false, "error": { "code": "INSUFFICIENT_ENERGY", "message": "Not enough energy." } }`
|
||||
|
||||
## Status codes
|
||||
| Code | Use |
|
||||
|---|---|
|
||||
| 200 | success |
|
||||
| 201 | created (register, create hero) |
|
||||
| 400 | validation failed (`VALIDATION_ERROR`) |
|
||||
| 401 | not logged in (`UNAUTHENTICATED`) |
|
||||
| 403 | logged in but not allowed (`FORBIDDEN`, `BANNED`) |
|
||||
| 404 | not found (`NOT_FOUND`) |
|
||||
| 409 | conflict (`USERNAME_TAKEN`, `HERO_EXISTS`, `EXPEDITION_ACTIVE`) |
|
||||
| 422 | rule violation (`INSUFFICIENT_ENERGY`, `INSUFFICIENT_GOLD`, `LEVEL_TOO_LOW`, `NOT_READY`) |
|
||||
| 429 | rate limited |
|
||||
| 500 | unexpected (never leak stack traces) |
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Auth (public unless noted)
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| POST | `/auth/register` | `{username, password}` | `{user:{id,username}}` + sets session |
|
||||
| POST | `/auth/login` | `{username, password}` | `{user:{id,username}, hasHero}` |
|
||||
| POST | `/auth/logout` 🔒 | – | `{}` |
|
||||
| GET | `/auth/me` 🔒 | – | `{user:{id,username}, hasHero}` |
|
||||
|
||||
Validation: username 3–20 chars `[A-Za-z0-9_]`; password 8–72 chars.
|
||||
Login failure always returns the same generic message (`INVALID_CREDENTIALS`) — don't reveal whether the username exists.
|
||||
|
||||
### Character 🔒
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| POST | `/character` | `{name, class}` | `{character}` |
|
||||
| GET | `/character` | – | `{character, derived, equipment, energy:{current,max,nextTickInSec}}` |
|
||||
| POST | `/character/allocate` | `{str,agi,int,vit}` (points to add) | `{character, derived}` |
|
||||
| POST | `/character/rest` | – | heals to full for gold cost (see BALANCE) |
|
||||
|
||||
### Hunting 🔒
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/zones` | – | zones with `unlocked` flag |
|
||||
| POST | `/hunt` | `{zoneId}` | `{result:{won,rounds[],xp,gold,loot[],leveledUp,newLevel}, character}` |
|
||||
| GET | `/combat-log?limit=20` | – | recent fights |
|
||||
|
||||
### Inventory 🔒
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/inventory` | – | `{items:[{inventoryId,item,quantity,equippedSlot}]}` |
|
||||
| POST | `/inventory/equip` | `{inventoryId}` | `{equipment, derived}` |
|
||||
| POST | `/inventory/unequip` | `{slot}` | `{equipment, derived}` |
|
||||
| POST | `/inventory/use` | `{inventoryId}` | `{character, message}` |
|
||||
|
||||
### Shop 🔒
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/shop` | – | stock list with prices and `canAfford`, `levelOk` |
|
||||
| POST | `/shop/buy` | `{itemId, quantity}` (1–99) | `{gold, inventory}` |
|
||||
| POST | `/shop/sell` | `{inventoryId, quantity}` | `{gold, inventory}` |
|
||||
|
||||
### Expeditions 🔒
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/expeditions` | – | available defs + active one (with `remainingSec`) |
|
||||
| POST | `/expeditions/start` | `{defId}` | `{expedition}` |
|
||||
| POST | `/expeditions/claim` | – | `{rewards, character}` |
|
||||
|
||||
### Leaderboard & heroes (public read)
|
||||
| Method | Path | Query | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/leaderboard` | `type=level|gold|kills`, `page=1` | top 50 per page |
|
||||
| GET | `/heroes/:name` | – | public profile (name, class, level, kills, equipped item names) |
|
||||
|
||||
### Misc
|
||||
| GET | `/health` | – | `{ok:true}` |
|
||||
|
||||
## Rules for every route
|
||||
1. Validate with zod; on failure → 400 `VALIDATION_ERROR` with field messages.
|
||||
2. Never trust client-supplied ids for ownership: always scope by `req.session.userId` → character.
|
||||
3. Responses never include `password_hash` or session data.
|
||||
4. State-changing endpoints are POST. GET must never modify state.
|
||||
5. Return the *updated* character where relevant so the UI can refresh without a second call.
|
||||
@@ -0,0 +1,91 @@
|
||||
# ARCHITECTURE.md
|
||||
|
||||
## Layering (strict, one-way)
|
||||
```
|
||||
routes → services → game (pure rules)
|
||||
↓ ↓
|
||||
middleware db (prepared statements)
|
||||
```
|
||||
- **routes/**: parse + validate request, call a service, shape the response. No business logic. No SQL.
|
||||
- **services/**: orchestrate. Open transactions, read/write DB, call `game/` functions, return plain objects.
|
||||
- **game/**: pure functions. No `require` of db/express. Deterministic given inputs (RNG is passed in).
|
||||
- **db/**: connection, migrations, seeds. Only services import `db`.
|
||||
- **middleware/**: auth guard, rate limiting, error handler, request validation helper.
|
||||
- **utils/**: tiny helpers (rng, time, ids, logger).
|
||||
|
||||
Dependencies only point downward. `game/` never imports from `services/`.
|
||||
|
||||
## Folder structure
|
||||
```
|
||||
emberfall/
|
||||
├── NOTES.md STATUS.md README.md
|
||||
├── package.json .env .env.example .gitignore
|
||||
├── docs/ # specs (this folder)
|
||||
├── data/ # SQLite file lives here (gitignored)
|
||||
├── scripts/
|
||||
│ ├── migrate.js # runs src/db/migrations/*.sql in order
|
||||
│ ├── seed.js # runs src/db/seeds/*.js (idempotent)
|
||||
│ └── seed-dev.js # demo account + hero
|
||||
├── src/
|
||||
│ ├── server.js # listen()
|
||||
│ ├── app.js # builds express app (exported for tests)
|
||||
│ ├── config/index.js
|
||||
│ ├── db/
|
||||
│ │ ├── index.js # better-sqlite3 handle
|
||||
│ │ ├── migrations/ # 001_users.sql, 002_characters.sql ...
|
||||
│ │ └── seeds/ # zones.js, monsters.js, items.js, expeditions.js
|
||||
│ ├── middleware/
|
||||
│ │ ├── requireAuth.js
|
||||
│ │ ├── rateLimiters.js
|
||||
│ │ ├── validate.js # zod wrapper
|
||||
│ │ └── errorHandler.js
|
||||
│ ├── routes/
|
||||
│ │ ├── index.js # mounts all routers under /api
|
||||
│ │ ├── auth.js character.js hunt.js inventory.js
|
||||
│ │ ├── shop.js expeditions.js leaderboard.js heroes.js
|
||||
│ ├── services/
|
||||
│ │ ├── authService.js characterService.js huntService.js
|
||||
│ │ ├── inventoryService.js shopService.js
|
||||
│ │ ├── expeditionService.js leaderboardService.js
|
||||
│ ├── game/
|
||||
│ │ ├── constants.js # ALL tunable numbers live here
|
||||
│ │ ├── classes.js stats.js energy.js combat.js
|
||||
│ │ ├── rewards.js leveling.js loot.js
|
||||
│ └── utils/
|
||||
│ ├── rng.js time.js errors.js logger.js
|
||||
├── public/
|
||||
│ ├── index.html login.html register.html
|
||||
│ ├── create-hero.html game.html shop.html leaderboard.html
|
||||
│ ├── css/ (base.css, theme.css, layout.css, components.css)
|
||||
│ ├── js/ (api.js, auth.js, game.js, ui.js, ...)
|
||||
│ └── img/
|
||||
└── tests/
|
||||
├── unit/ # src/game/* and utils
|
||||
└── integration/ # HTTP-level tests using a temp DB
|
||||
```
|
||||
|
||||
## Module format
|
||||
Use **CommonJS** (`require` / `module.exports`) everywhere on the server, to keep
|
||||
tooling simple. Frontend JS uses plain `<script>` tags or ES modules (`type="module"`) — pick one and stay consistent (log in DECISIONS.md).
|
||||
|
||||
## Request lifecycle
|
||||
1. `helmet` → JSON parser → session → static files
|
||||
2. `/api/*` routers; protected routers use `requireAuth`
|
||||
3. Route validates body/query with zod
|
||||
4. Service runs (transaction for anything that writes more than one row)
|
||||
5. Route sends `{ ok: true, data: ... }`
|
||||
6. Any thrown `AppError` → `errorHandler` → `{ ok: false, error: {code, message} }`
|
||||
|
||||
## Transactions
|
||||
Any action that spends a resource AND grants a result (hunt, buy, claim) is ONE
|
||||
`db.transaction(() => { ... })()`. better-sqlite3 is synchronous, so this also
|
||||
prevents double-spend races from parallel requests.
|
||||
|
||||
## Time
|
||||
All timestamps are **integer milliseconds since epoch (UTC)**, stored as INTEGER.
|
||||
`src/utils/time.js` exports `now()` so tests can stub it. Never call `Date.now()`
|
||||
directly in services or game code.
|
||||
|
||||
## Config
|
||||
Gameplay numbers → `src/game/constants.js`. Environment/infra → `src/config/index.js`.
|
||||
Never hardcode magic numbers inline.
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# BALANCE.md — Formulas & Starter Numbers
|
||||
|
||||
All numbers below go in `src/game/constants.js`. Tune there, not inline.
|
||||
|
||||
## Energy
|
||||
```
|
||||
ENERGY_MAX = 100
|
||||
ENERGY_REGEN_SECONDS = 180 // +1 per 3 min
|
||||
ENERGY_TONIC_RESTORE = 25
|
||||
```
|
||||
```
|
||||
computeEnergy(stored, updatedAtMs, nowMs):
|
||||
if nowMs <= updatedAtMs: return {energy: stored, updatedAtMs}
|
||||
gained = floor((nowMs - updatedAtMs) / (REGEN_SECONDS*1000))
|
||||
if stored + gained >= MAX: return {energy: MAX, updatedAtMs: nowMs}
|
||||
return {energy: stored + gained, updatedAtMs: updatedAtMs + gained*REGEN_SECONDS*1000}
|
||||
```
|
||||
(Keep leftover partial interval by advancing the timestamp only by whole ticks.)
|
||||
|
||||
## Leveling
|
||||
```
|
||||
xpToNext(level) = floor(100 * level^1.5) // L1→2: 100, L2→3: 282, L5→6: 1118, L10→11: 3162
|
||||
MAX_LEVEL = 50
|
||||
STAT_POINTS_PER_LEVEL = 3
|
||||
```
|
||||
On level-up: `hp_current = maxHp`, loop to handle multiple level-ups from one reward.
|
||||
|
||||
## Derived stats
|
||||
```
|
||||
maxHp = 50 + vit*10 + level*5 + itemBonusVit*10
|
||||
primary = STR | INT | AGI (by class)
|
||||
attack = 5 + primary*2 + level + equipment.bonus_attack
|
||||
defense = floor(vit*1.5) + equipment.bonus_defense (+10% if Warrior)
|
||||
speed = 10 + agi
|
||||
critChance% = min(40, 5 + floor(agi*0.5)) (+8 if Ranger)
|
||||
critMult = 1.5
|
||||
```
|
||||
Equipment `bonus_str/agi/int/vit` are added to the base stats BEFORE the derived formulas.
|
||||
|
||||
## Damage
|
||||
```
|
||||
mitigation = 100 / (100 + defenderDefense)
|
||||
raw = attackerAttack * mitigation
|
||||
variance = rng.range(0.9, 1.1)
|
||||
dmg = max(1, round(raw * variance * (crit ? critMult : 1)))
|
||||
```
|
||||
Mage bonus: if defenderDefense >= 15, multiply `raw` by 1.15.
|
||||
Monsters crit at a flat 5%.
|
||||
|
||||
## Rewards
|
||||
```
|
||||
xp = monster.xp * levelDiffMult
|
||||
gold = rng.int(monster.gold_min, monster.gold_max) * levelDiffMult
|
||||
levelDiffMult = clamp(1 + 0.1*(monster.level - hero.level), 0.2, 1.5)
|
||||
```
|
||||
Loot: for each row in `loot_table` for that monster, roll `rng.float()*100 < chance_pct`.
|
||||
Max 2 distinct loot drops per fight.
|
||||
|
||||
## Defeat penalty
|
||||
`goldLost = floor(gold * 0.10)`; `hp_current = 1`; `deaths += 1`.
|
||||
|
||||
## Rest
|
||||
`restCost = level * 5` gold → heals to full. Free if hp already full is rejected (`NOTHING_TO_HEAL`).
|
||||
|
||||
## Starter monsters (use or tweak)
|
||||
| Zone | Name | Lv | HP | Atk | Def | Spd | XP | Gold |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Meadows | Giant Rat | 1 | 20 | 6 | 1 | 12 | 8 | 1–3 |
|
||||
| Meadows | Meadow Wolf | 2 | 32 | 9 | 3 | 16 | 14 | 2–5 |
|
||||
| Meadows | Goblin Scrapper | 3 | 45 | 12 | 5 | 11 | 22 | 4–9 |
|
||||
| Forest | Webspinner | 5 | 70 | 18 | 8 | 15 | 40 | 8–16 |
|
||||
| Forest | Bandit Cutthroat | 6 | 90 | 22 | 12 | 14 | 55 | 12–24 |
|
||||
| Forest | Mossback Treant | 8 | 150 | 26 | 22 | 7 | 85 | 15–30 |
|
||||
| Crags | Ember Imp | 10 | 130 | 34 | 15 | 20 | 120 | 25–45 |
|
||||
| Crags | Stone Golem | 12 | 260 | 38 | 40 | 6 | 170 | 30–60 |
|
||||
| Crags | Ashfang Drake | 14 | 300 | 50 | 30 | 18 | 260 | 50–100 |
|
||||
|
||||
## Starter items (≥ 20 required; this is a minimum seed list)
|
||||
| Item | Type | Rarity | Lv | Buy | Bonus |
|
||||
|---|---|---|---|---|---|
|
||||
| Rusty Sword | weapon | common | 1 | 20 | +3 atk |
|
||||
| Apprentice Staff | weapon | common | 1 | 20 | +2 int, +1 atk |
|
||||
| Hunter's Shortbow | weapon | common | 1 | 20 | +2 agi, +1 atk |
|
||||
| Iron Blade | weapon | uncommon | 5 | 120 | +8 atk, +2 str |
|
||||
| Oakwood Staff | weapon | uncommon | 5 | 120 | +6 atk, +3 int |
|
||||
| Yew Longbow | weapon | uncommon | 5 | 120 | +6 atk, +3 agi |
|
||||
| Emberforged Greatsword | weapon | rare | 10 | 600 | +18 atk, +4 str |
|
||||
| Leather Jerkin | armor | common | 1 | 25 | +3 def |
|
||||
| Chainmail Vest | armor | uncommon | 5 | 140 | +9 def, +2 vit |
|
||||
| Drake-scale Armor | armor | rare | 12 | 700 | +22 def, +4 vit |
|
||||
| Wooden Charm | trinket | common | 1 | 30 | +1 vit |
|
||||
| Wolf-fang Pendant | trinket | uncommon | 3 | 90 | +2 agi |
|
||||
| Ember Ring | trinket | rare | 10 | 450 | +3 str,+3 int |
|
||||
| Minor Healing Potion | consumable | common | 1 | 15 | effect heal:50 |
|
||||
| Greater Healing Potion | consumable | uncommon | 8 | 60 | effect heal:150 |
|
||||
| Energy Tonic | consumable | uncommon | 1 | 80 | effect energy:25 |
|
||||
| Rat Tail | material | common | 1 | 0 | sell 2g |
|
||||
| Wolf Pelt | material | common | 2 | 0 | sell 5g |
|
||||
| Spider Silk | material | uncommon | 5 | 0 | sell 12g |
|
||||
| Imp Horn | material | uncommon | 10 | 0 | sell 30g |
|
||||
| Drake Scale | material | rare | 14 | 0 | sell 90g |
|
||||
|
||||
Materials with `buy_price = 0` sell at: common 2×level, uncommon 4×level, rare 8×level (round down, min 1).
|
||||
|
||||
## Starter loot chances
|
||||
Common material from its zone's weakest monsters: 40%. Uncommon gear drops: 3–5%.
|
||||
Rare gear drops: 0.5–1% from top-tier zone monsters. Healing potion: 8% anywhere.
|
||||
|
||||
## Expedition starters
|
||||
| Name | Min Lv | Duration | Energy | XP | Gold |
|
||||
|---|---|---|---|---|---|
|
||||
| Scout the Old Road | 1 | 10 min | 10 | 40 | 10–25 |
|
||||
| Raid the Bandit Camp | 5 | 1 h | 25 | 300 | 80–160 |
|
||||
| Delve the Hollow | 10 | 4 h | 40 | 1400 | 300–600 |
|
||||
(Use short durations such as 20–60 s via an env flag `DEV_FAST_TIMERS=1` for testing.)
|
||||
@@ -0,0 +1,45 @@
|
||||
# CODING_STANDARDS.md
|
||||
|
||||
## Language & style
|
||||
- Node 20+, CommonJS, `'use strict'` not needed in CJS modules but harmless.
|
||||
- 2-space indent, semicolons, single quotes, trailing commas in multiline literals.
|
||||
- `const` by default; `let` only if reassigned; never `var`.
|
||||
- Names: `camelCase` functions/vars, `PascalCase` classes, `UPPER_SNAKE` constants, `snake_case` DB columns.
|
||||
- Convert DB `snake_case` rows → `camelCase` objects at the service boundary (one small mapper per entity).
|
||||
- Functions do one thing and fit on a screen (< 40 lines). Early returns over deep nesting.
|
||||
- Comments explain *why*, not *what*. Add a one-line header comment to each file describing its purpose.
|
||||
|
||||
## Errors
|
||||
Create `src/utils/errors.js`:
|
||||
```js
|
||||
class AppError extends Error {
|
||||
constructor(code, message, status = 400) { super(message); this.code = code; this.status = status; }
|
||||
}
|
||||
```
|
||||
- Services throw `AppError` for expected failures (`new AppError('INSUFFICIENT_ENERGY','Not enough energy.',422)`).
|
||||
- Routes use an async wrapper so thrown errors reach `errorHandler`:
|
||||
```js
|
||||
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
||||
```
|
||||
- `errorHandler` maps `AppError` → envelope; `ZodError` → 400 `VALIDATION_ERROR`; anything else → 500 generic message + server log.
|
||||
|
||||
## Pure game code
|
||||
- `src/game/*` exports plain functions; no I/O; no `Date.now()`; no `Math.random()` — take `rng` / `nowMs` params.
|
||||
- Return new objects instead of mutating inputs.
|
||||
|
||||
## Database access
|
||||
- One prepared statement per query, created once at module load (or lazily cached), not inside loops.
|
||||
- Wrap multi-write operations in `db.transaction`.
|
||||
- Never `SELECT *` in services for public output; select what you return.
|
||||
|
||||
## Frontend JS
|
||||
- `public/js/api.js` exports a small `api(method, path, body)` wrapper that handles JSON, error envelope, and 401 → redirect to login.
|
||||
- DOM updates via `textContent` / `createElement`. No `innerHTML` with dynamic data.
|
||||
- One JS file per page + shared `api.js` and `ui.js` (toasts, formatting).
|
||||
- No global state beyond a single `state` object per page.
|
||||
|
||||
## Logging
|
||||
`src/utils/logger.js`: tiny wrapper over `console` with levels; silent in tests unless `LOG=1`.
|
||||
|
||||
## Dependencies
|
||||
Pinned in package.json via `npm install --save-exact`. No dependency without need (NOTES.md §2.3).
|
||||
@@ -0,0 +1,185 @@
|
||||
# DATABASE.md
|
||||
|
||||
SQLite via `better-sqlite3`. Pragmas at connect: `journal_mode=WAL`, `foreign_keys=ON`, `busy_timeout=5000`.
|
||||
|
||||
## Migration rules
|
||||
- Files in `src/db/migrations/` named `NNN_description.sql` (zero-padded).
|
||||
- Applied in order; tracked in `_migrations(name TEXT PRIMARY KEY, applied_at INTEGER)`.
|
||||
- **Never edit an applied migration.** Add a new one.
|
||||
- Each file runs inside a transaction.
|
||||
|
||||
## Conventions
|
||||
- Primary keys: `id INTEGER PRIMARY KEY AUTOINCREMENT`.
|
||||
- Timestamps: `INTEGER` ms epoch UTC. Column names end in `_at`.
|
||||
- Booleans: `INTEGER` 0/1 with `CHECK`.
|
||||
- Money/stat columns: `INTEGER` with `CHECK (>= 0)` where negatives are illegal.
|
||||
- Add indexes for every foreign key and every leaderboard sort column.
|
||||
|
||||
## Schema (v1)
|
||||
|
||||
### 001_users.sql
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_login_at INTEGER,
|
||||
is_banned INTEGER NOT NULL DEFAULT 0 CHECK (is_banned IN (0,1))
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_users_username_nocase ON users(username COLLATE NOCASE);
|
||||
```
|
||||
|
||||
### 002_characters.sql
|
||||
```sql
|
||||
CREATE TABLE characters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
class TEXT NOT NULL CHECK (class IN ('warrior','mage','ranger')),
|
||||
level INTEGER NOT NULL DEFAULT 1 CHECK (level >= 1),
|
||||
xp INTEGER NOT NULL DEFAULT 0 CHECK (xp >= 0),
|
||||
gold INTEGER NOT NULL DEFAULT 50 CHECK (gold >= 0),
|
||||
str INTEGER NOT NULL,
|
||||
agi INTEGER NOT NULL,
|
||||
int INTEGER NOT NULL,
|
||||
vit INTEGER NOT NULL,
|
||||
unspent_points INTEGER NOT NULL DEFAULT 0 CHECK (unspent_points >= 0),
|
||||
hp_current INTEGER NOT NULL CHECK (hp_current >= 0),
|
||||
energy INTEGER NOT NULL CHECK (energy >= 0),
|
||||
energy_updated_at INTEGER NOT NULL,
|
||||
kills INTEGER NOT NULL DEFAULT 0,
|
||||
deaths INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_characters_name_nocase ON characters(name COLLATE NOCASE);
|
||||
CREATE INDEX idx_characters_level ON characters(level DESC, xp DESC);
|
||||
CREATE INDEX idx_characters_gold ON characters(gold DESC);
|
||||
CREATE INDEX idx_characters_kills ON characters(kills DESC);
|
||||
```
|
||||
|
||||
### 003_content.sql (static game data, loaded by seeds)
|
||||
```sql
|
||||
CREATE TABLE zones (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
min_level INTEGER NOT NULL,
|
||||
energy_cost INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE monsters (
|
||||
id INTEGER PRIMARY KEY,
|
||||
zone_id INTEGER NOT NULL REFERENCES zones(id),
|
||||
name TEXT NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
hp INTEGER NOT NULL,
|
||||
attack INTEGER NOT NULL,
|
||||
defense INTEGER NOT NULL,
|
||||
speed INTEGER NOT NULL,
|
||||
xp INTEGER NOT NULL,
|
||||
gold_min INTEGER NOT NULL,
|
||||
gold_max INTEGER NOT NULL,
|
||||
weight INTEGER NOT NULL DEFAULT 10 -- spawn weight within zone
|
||||
);
|
||||
CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('weapon','armor','trinket','consumable','material')),
|
||||
rarity TEXT NOT NULL CHECK (rarity IN ('common','uncommon','rare','epic','legendary')),
|
||||
min_level INTEGER NOT NULL DEFAULT 1,
|
||||
buy_price INTEGER NOT NULL DEFAULT 0, -- 0 = not sold in shop
|
||||
stackable INTEGER NOT NULL DEFAULT 0 CHECK (stackable IN (0,1)),
|
||||
bonus_str INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_agi INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_int INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_vit INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_attack INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_defense INTEGER NOT NULL DEFAULT 0,
|
||||
effect TEXT -- consumables: 'heal:50' | 'energy:25'
|
||||
);
|
||||
CREATE TABLE loot_table (
|
||||
monster_id INTEGER NOT NULL REFERENCES monsters(id),
|
||||
item_id INTEGER NOT NULL REFERENCES items(id),
|
||||
chance_pct REAL NOT NULL CHECK (chance_pct > 0 AND chance_pct <= 100),
|
||||
qty_min INTEGER NOT NULL DEFAULT 1,
|
||||
qty_max INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (monster_id, item_id)
|
||||
);
|
||||
```
|
||||
Seeds use `INSERT ... ON CONFLICT(id) DO UPDATE` so re-running is safe.
|
||||
|
||||
### 004_inventory.sql
|
||||
```sql
|
||||
CREATE TABLE inventory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
item_id INTEGER NOT NULL REFERENCES items(id),
|
||||
quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
||||
equipped_slot TEXT CHECK (equipped_slot IN ('weapon','armor','trinket')),
|
||||
acquired_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_inventory_char ON inventory(character_id);
|
||||
-- one equipped item per slot per character
|
||||
CREATE UNIQUE INDEX idx_inventory_equipped
|
||||
ON inventory(character_id, equipped_slot) WHERE equipped_slot IS NOT NULL;
|
||||
```
|
||||
Rules: stackable items merge into one row per (character, item). Non-stackable items get one row each.
|
||||
|
||||
### 005_expeditions.sql
|
||||
```sql
|
||||
CREATE TABLE expedition_defs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
min_level INTEGER NOT NULL,
|
||||
duration_sec INTEGER NOT NULL,
|
||||
energy_cost INTEGER NOT NULL,
|
||||
xp_reward INTEGER NOT NULL,
|
||||
gold_min INTEGER NOT NULL,
|
||||
gold_max INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE expeditions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
def_id INTEGER NOT NULL REFERENCES expedition_defs(id),
|
||||
started_at INTEGER NOT NULL,
|
||||
ends_at INTEGER NOT NULL,
|
||||
claimed_at INTEGER -- NULL until claimed
|
||||
);
|
||||
-- at most one unclaimed expedition per character
|
||||
CREATE UNIQUE INDEX idx_one_active_expedition
|
||||
ON expeditions(character_id) WHERE claimed_at IS NULL;
|
||||
```
|
||||
|
||||
### 006_logs.sql
|
||||
```sql
|
||||
CREATE TABLE combat_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
monster_id INTEGER NOT NULL REFERENCES monsters(id),
|
||||
won INTEGER NOT NULL CHECK (won IN (0,1)),
|
||||
rounds INTEGER NOT NULL,
|
||||
xp_gained INTEGER NOT NULL,
|
||||
gold_gained INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_combat_log_char ON combat_log(character_id, created_at DESC);
|
||||
|
||||
CREATE TABLE audit_log ( -- security-relevant events
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
event TEXT NOT NULL, -- 'login_ok','login_fail','register','logout'
|
||||
ip TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Sessions
|
||||
Created automatically by the session store package. Don't hand-write that table.
|
||||
|
||||
## Stretch-goal tables (do NOT create until needed)
|
||||
`guilds`, `guild_members`, `market_listings`, `pvp_matches`, `chat_messages`, `achievements`, `daily_rewards`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# DECISIONS.md — Architecture Decision Log
|
||||
|
||||
Record any choice that deviates from or fills a gap in the docs. Newest at the bottom.
|
||||
|
||||
Format:
|
||||
```
|
||||
## D-NNN: Title
|
||||
- Date:
|
||||
- Context: (what problem / ambiguity)
|
||||
- Decision: (what you chose)
|
||||
- Alternatives considered:
|
||||
- Consequences:
|
||||
```
|
||||
|
||||
## D-001: SQLite + better-sqlite3 as the datastore
|
||||
- Date: (project start)
|
||||
- Context: Needs persistent accounts and game state with zero setup for a local build.
|
||||
- Decision: SQLite file in `data/`, synchronous driver, WAL mode.
|
||||
- Alternatives considered: Postgres (heavier setup), JSON files (no integrity/transactions).
|
||||
- Consequences: Single-process deployment; synchronous transactions give simple double-spend protection. Migrate to Postgres only if scaling beyond one server.
|
||||
|
||||
## D-002: Lazy time evaluation instead of tick loops
|
||||
- Date: (project start)
|
||||
- Context: Game must progress while players are offline.
|
||||
- Decision: Store timestamps; compute energy/timers on demand.
|
||||
- Consequences: No background workers, trivially testable with injected clocks.
|
||||
|
||||
## D-003: Vanilla frontend, no build step
|
||||
- Date: (project start)
|
||||
- Context: Reduce tooling surface area for a local-model build.
|
||||
- Decision: Static HTML/CSS/JS served by Express.
|
||||
- Consequences: Slightly more DOM boilerplate; far fewer moving parts.
|
||||
|
||||
<!-- Add D-004 onward below -->
|
||||
@@ -0,0 +1,89 @@
|
||||
# GAME_DESIGN.md — Emberfall
|
||||
|
||||
## Setting & tone
|
||||
The Ashen Realm: an old kingdom whose great Ember-Throne has gone dark. Monsters
|
||||
creep out of the wilds, and heroes gather at the border town of **Cindergate** to
|
||||
rekindle the Throne. Tone: classic high fantasy, slightly wry. Keep flavour text
|
||||
short (1–2 sentences). Names should feel Tolkien/D&D-ish but original.
|
||||
|
||||
## Core loop (target session: 5–10 min, several times a day)
|
||||
1. Log in → see energy (regenerated while away) and any finished expedition.
|
||||
2. Spend energy: hunt monsters, or start an expedition.
|
||||
3. Collect XP, gold, loot → level up → equip/upgrade → unlock harder zones.
|
||||
4. Spend leftover gold in shop. Check leaderboard. Log out.
|
||||
Energy limits session length; timers give reasons to return.
|
||||
|
||||
## Time (lazy evaluation — IMPORTANT)
|
||||
- **Energy**: max 100, +1 per 180 s (3 min). Stored as `(energy, energy_updated_at)`.
|
||||
On any request that needs energy: `computeEnergy()` → write back the new pair.
|
||||
At max, regen stops (timestamp resets to now so no "banked" regen).
|
||||
- **Expeditions**: stored with `ends_at`. "Finished" is just `now >= ends_at`.
|
||||
Nothing runs at that moment; the player claims it later.
|
||||
- **HP**: stored as `hp_current`. Natural regen: +1% max HP per minute, computed lazily
|
||||
the same way as energy (add `hp_updated_at` column if you implement it; otherwise
|
||||
HP fully restores on level-up and via Rest/potions — v1 acceptable).
|
||||
- No `setInterval` for game logic. Ever.
|
||||
|
||||
## Classes (pick one at creation; permanent in v1)
|
||||
| Class | Fantasy | Starting STR/AGI/INT/VIT | Bonus |
|
||||
|---|---|---|---|
|
||||
| Warrior | Armoured frontliner | 8 / 4 / 2 / 8 | +10% defense |
|
||||
| Mage | Glass-cannon spellcaster | 2 / 4 / 9 / 5 | +15% damage vs. high-defense foes |
|
||||
| Ranger | Fast skirmisher | 5 / 9 / 3 / 5 | +8% crit chance |
|
||||
|
||||
Primary stat for attack: Warrior→STR, Mage→INT, Ranger→AGI.
|
||||
Each level grants **3 unspent stat points** (player allocates via API).
|
||||
|
||||
## Stats
|
||||
- **STR** physical power, **AGI** speed & crit, **INT** magic power, **VIT** health.
|
||||
- Derived stats formulas: see BALANCE.md §Derived.
|
||||
|
||||
## Combat (auto-resolved, server-side, turn based)
|
||||
- Hero vs one monster, alternating attacks. Higher `speed` acts first each round.
|
||||
- Max 20 rounds; if no one dies, the one with higher HP% wins.
|
||||
- Each hit: damage formula in BALANCE.md, small random variance, crit chance.
|
||||
- Output a **round-by-round log** (array of `{round, actor, damage, crit, targetHpAfter}`) for the UI to replay.
|
||||
- Win: XP + gold + loot rolls. Lose: lose 10% of carried gold (min 0), HP set to 1,
|
||||
no XP; deaths +1. (Never lose items in v1.)
|
||||
- Hero HP persists between fights (`hp_current`). Hunting with HP < 20% of max is
|
||||
blocked: `HP_TOO_LOW` (422) → player must Rest or use a potion.
|
||||
- Combat resolution takes an injected `rng` so tests are deterministic.
|
||||
|
||||
## Zones (v1: 3 required, add more if time permits)
|
||||
| Zone | Level | Energy | Theme |
|
||||
|---|---|---|---|
|
||||
| Whispering Meadows | 1+ | 5 | goblins, wolves, giant rats |
|
||||
| Gloomroot Forest | 5+ | 8 | spiders, bandits, treants |
|
||||
| Ashfang Crags | 10+ | 12 | drakes, fire imps, stone golems |
|
||||
Optional: Sunken Catacombs (15+), Emberthrone Ruins (20+, boss).
|
||||
|
||||
## Items
|
||||
Types: `weapon`, `armor`, `trinket` (equippable, one per slot), `consumable`
|
||||
(stackable), `material` (stackable, sell-only in v1).
|
||||
Rarity: common → uncommon → rare → epic → legendary (colour-coded in UI).
|
||||
Items give flat stat/attack/defense bonuses. Class restrictions: none in v1.
|
||||
Starter gear: new heroes get one common weapon appropriate to class (auto-equipped) and 2 Minor Healing Potions.
|
||||
|
||||
## Economy
|
||||
- Gold sources: monster drops, expeditions, selling items.
|
||||
- Gold sinks: shop purchases, Rest (heal to full), (stretch) market tax.
|
||||
- Sell price = 40% of `buy_price` (items with `buy_price` 0 sell for rarity default: see BALANCE).
|
||||
- Prices must make the 1st hour feel rewarding but not trivialise gear. See BALANCE.
|
||||
- Gold can never go below 0; enforced by DB CHECK and service checks.
|
||||
|
||||
## Expeditions (the "offline" mechanic)
|
||||
- Choose one of 3–4 definitions (e.g. "Scout the Old Road" 10 min, "Raid the Bandit Camp" 1 h, "Delve the Hollow" 4 h).
|
||||
- Costs energy up-front. One active at a time. Hero cannot hunt while on expedition? **No** — hunting is still allowed (keeps it simple).
|
||||
- Claim after `ends_at`: grants XP + gold (+ a loot roll for 1h+ ones). Double-claim impossible (`claimed_at` set inside the same transaction).
|
||||
|
||||
## Leaderboards
|
||||
Top 50 by: level (tie-break xp), gold, kills. Public. Query on indexed columns. Optionally cache 30 s in memory.
|
||||
|
||||
## Names & content rules
|
||||
- Hero names: 3–16 chars `[A-Za-z0-9 '-]`, unique case-insensitive, trimmed, no double spaces.
|
||||
- Basic profanity blocklist: small array in `src/game/constants.js` (keep it short; log extension as a TODO).
|
||||
|
||||
## Balance targets (sanity check when testing)
|
||||
- Level 1 → 5 in roughly 40–60 hunts (~2–3 energy refills).
|
||||
- A fresh Level-1 hero should win ≈ 80% of Meadow fights and lose ≈ 90% against a Crag monster.
|
||||
- No single item should raise power by > 25% at its tier.
|
||||
@@ -0,0 +1,71 @@
|
||||
# SECURITY.md
|
||||
|
||||
PBBGs are cheat targets. Assume every request is hostile.
|
||||
|
||||
## Authentication
|
||||
- Hash with `bcryptjs`, cost 10–12. Never store or log plaintext.
|
||||
- Case-insensitive unique usernames (DB index uses `COLLATE NOCASE`).
|
||||
- Login errors are generic: `INVALID_CREDENTIALS`. Do a dummy hash compare when the user doesn't exist (timing).
|
||||
- Regenerate the session ID on login (`req.session.regenerate`) to prevent fixation.
|
||||
- Logout destroys the session server-side and clears the cookie.
|
||||
- Banned users (`is_banned=1`) are rejected at login and in `requireAuth`.
|
||||
|
||||
## Sessions / cookies
|
||||
- `httpOnly: true`, `sameSite: 'lax'`, `secure: true` when `NODE_ENV=production`.
|
||||
- `maxAge` 7 days, rolling. Secret from `SESSION_SECRET` env (refuse to boot in production without it).
|
||||
- Store sessions in SQLite, not memory (so restarts don't log everyone out).
|
||||
|
||||
## CSRF
|
||||
Cookie-based sessions + state-changing POSTs ⇒ CSRF matters.
|
||||
- `sameSite=lax` blocks most cross-site POSTs.
|
||||
- Additionally require header `Content-Type: application/json` on all POSTs and reject others (browsers can't send cross-site JSON without CORS preflight). **Do not enable CORS.**
|
||||
|
||||
## Input validation
|
||||
- Validate every body/query/param with zod. Reject unknown keys (`.strict()`).
|
||||
- Clamp numerics (quantities 1–99). Reject NaN/negative/float where ints are expected.
|
||||
- Never render user-supplied text with `innerHTML`. Use `textContent`. (Usernames and hero names are user-supplied!)
|
||||
|
||||
## Anti-cheat / game integrity
|
||||
- Server decides all outcomes; client sends only intent.
|
||||
- Every multi-step state change is one DB transaction (spend + grant).
|
||||
- Ownership: derive the character from the session — never accept `characterId` from the client.
|
||||
- Check `inventory.character_id = ?` on every inventory operation.
|
||||
- Spam: rate-limit actions (e.g., 30 game actions / minute / user) — energy is the real limit, this is a safety net.
|
||||
- Expedition claim: set `claimed_at` and grant rewards in one transaction; the partial unique index prevents duplicates.
|
||||
- Never trust client clocks. All timing from `utils/time.now()`.
|
||||
|
||||
## Rate limiting (`express-rate-limit`)
|
||||
| Scope | Limit |
|
||||
|---|---|
|
||||
| `POST /auth/login` | 10 / 15 min / IP |
|
||||
| `POST /auth/register` | 5 / hour / IP |
|
||||
| other `/api` | 120 / min / IP |
|
||||
|
||||
## Headers & transport
|
||||
- `helmet()` defaults. Provide a CSP: `default-src 'self'` (no inline scripts — put JS in files).
|
||||
- Serve behind HTTPS in production (note in README; reverse proxy is out of scope).
|
||||
- `app.set('trust proxy', 1)` only if running behind a proxy.
|
||||
|
||||
## Data exposure
|
||||
- Public endpoints (`/leaderboard`, `/heroes/:name`) return only: name, class, level, kills, gold(optional). Never user ids, usernames, or emails.
|
||||
- Hero name ≠ login username (so leaderboard doesn't reveal login names).
|
||||
- Error handler returns generic message for unexpected errors; stack traces go to the server log only.
|
||||
|
||||
## SQL
|
||||
- Prepared statements with `?` / named params only. No string-built SQL, ever. Table/column names are never user-controlled (use allowlist for leaderboard sort `type`).
|
||||
|
||||
## Secrets & logging
|
||||
- `.env` is gitignored. `.env.example` has placeholders only.
|
||||
- Don't log request bodies on auth routes.
|
||||
- `audit_log` records login ok/fail, register, logout with IP + timestamp.
|
||||
|
||||
## Checklist (Phase 9.1) — tick each in STATUS Session Log
|
||||
- [ ] No `innerHTML` with dynamic data
|
||||
- [ ] All routes validated by zod
|
||||
- [ ] All writes in transactions
|
||||
- [ ] Ownership checks on every inventory/expedition op
|
||||
- [ ] Rate limiters mounted
|
||||
- [ ] Cookie flags verified in browser dev tools
|
||||
- [ ] Password hash never in any response (grep for `password_hash` in routes)
|
||||
- [ ] Leaderboard sort allowlisted
|
||||
- [ ] Production boot fails without SESSION_SECRET
|
||||
@@ -0,0 +1,42 @@
|
||||
# TESTING.md
|
||||
|
||||
Runner: built-in `node:test` + `node:assert/strict`. No Jest/Mocha.
|
||||
Script: `"test": "node --test tests/"` (add `--test-concurrency=1` if DB tests collide).
|
||||
|
||||
## Layout
|
||||
- `tests/unit/` — pure functions in `src/game` and `src/utils`. No DB, no HTTP.
|
||||
- `tests/integration/` — boot `app` with a **temporary SQLite file**, run migrations + seeds, call the real HTTP API.
|
||||
|
||||
## Test DB setup helper (`tests/helpers.js`)
|
||||
- Create temp path via `os.tmpdir()`; set `process.env.DB_PATH` BEFORE requiring `src/db`.
|
||||
- Run migrate + seed programmatically.
|
||||
- Start the app on port 0 (`app.listen(0)`), return base URL and a `close()` fn.
|
||||
- Provide `registerAndLogin(base)` which returns a cookie string to reuse in `fetch` headers.
|
||||
- Delete the temp file in `after()`.
|
||||
|
||||
## What MUST be tested
|
||||
| Area | Cases |
|
||||
|---|---|
|
||||
| energy | no time passed; partial tick; full cap; clock went backwards; large gap |
|
||||
| leveling | exact XP threshold; multi-level jump; max level cap |
|
||||
| stats | each class; equipment bonuses applied |
|
||||
| combat | fixed seed → identical log; hero win; hero loss; round cap; min damage 1 |
|
||||
| rewards | level-diff multiplier clamps; loot chance 0% / 100% |
|
||||
| auth | register ok; duplicate (case-insensitive); weak password; login ok/fail; session persists; logout |
|
||||
| hunt | spends energy; insufficient energy rejected; HP too low rejected; locked zone rejected; rewards persisted |
|
||||
| inventory | equip swaps slot; can't equip others' items; use potion consumes 1 |
|
||||
| shop | insufficient gold; level gate; sell equipped rejected; quantities validated |
|
||||
| expedition | claim before end rejected; claim once only; second start while active rejected |
|
||||
| leaderboard | sort allowlist; ordering correct |
|
||||
|
||||
## Determinism
|
||||
Inject RNG: `createRng(seed)` (mulberry32) in `src/utils/rng.js` exposing `.float()`, `.int(min,max)`, `.range(min,max)`. In integration tests, allow overriding the RNG via an `app` factory option or env seed.
|
||||
Inject time: tests set `time.setNow(fn)` to fast-forward energy/expeditions instead of sleeping.
|
||||
|
||||
## Naming
|
||||
`describe('energy.computeEnergy', () => { it('caps at max', ...) })`.
|
||||
|
||||
## Rules
|
||||
- A failing test is never deleted to make the suite green. Fix the code or log a Known Issue.
|
||||
- New bug → write the failing test first, then fix.
|
||||
- Update the Test Summary table in STATUS.md after each phase.
|
||||
@@ -0,0 +1,51 @@
|
||||
# UI_GUIDE.md
|
||||
|
||||
## Pages
|
||||
| File | Purpose | Auth |
|
||||
|---|---|---|
|
||||
| `index.html` | Landing: title, tagline, Login/Register buttons | public |
|
||||
| `register.html` / `login.html` | forms | public |
|
||||
| `create-hero.html` | name + class picker with class descriptions | needs login, no hero |
|
||||
| `game.html` | main dashboard (see layout) | needs login + hero |
|
||||
| `shop.html` | buy/sell tabs | needs hero |
|
||||
| `leaderboard.html` | tabs: Level / Gold / Kills | public |
|
||||
|
||||
Auth guard on the client: on page load call `/api/auth/me`; redirect as needed. (Server still enforces — client redirect is UX only.)
|
||||
|
||||
## game.html layout
|
||||
```
|
||||
┌ Header: logo • hero name • Lv • gold • Logout
|
||||
├ Left column: Hero card (HP bar, XP bar, Energy bar + regen timer), stats, unspent points [+]
|
||||
├ Centre: Tabs → Hunt | Expedition | Inventory
|
||||
│ Hunt: zone cards (locked ones greyed with "Reach Lv X")
|
||||
│ Combat log panel: replays rounds with short delay, then shows summary
|
||||
└ Right column: Equipment slots (weapon/armor/trinket), recent combat history
|
||||
```
|
||||
Mobile (< 700px): single column; tabs become a bottom nav.
|
||||
|
||||
## Theme: "Ember & Parchment" (dark fantasy)
|
||||
CSS variables in `theme.css`:
|
||||
```
|
||||
--bg: #14110f; --panel: #211b17; --panel-2: #2c231d;
|
||||
--text: #eadfce; --muted: #a59a8a;
|
||||
--accent: #e2822b (ember orange); --accent-2: #f2c14e (gold);
|
||||
--danger: #c0392b; --success: #4f9d69; --info: #4a7fb0;
|
||||
--border: #3a2f27;
|
||||
Rarity: common #b8b0a3, uncommon #5fb36b, rare #4a8fe0, epic #a45ee0, legendary #f2a23a
|
||||
```
|
||||
Fonts: system serif stack for headings (`Georgia, 'Times New Roman', serif`), system sans for body. No external font/CDN requests (CSP + offline friendly).
|
||||
|
||||
## Components (build once in components.css)
|
||||
`.panel`, `.btn` (+ `.btn-primary`, `.btn-danger`, `:disabled`), `.bar` with `.bar-fill` (hp/xp/energy variants), `.tabs`, `.card`, `.toast`, `.tooltip`, `.badge` (rarity), `.table`, `.modal`.
|
||||
|
||||
## UX rules
|
||||
- Every action button shows a loading state and is disabled while the request is in flight (prevents double-clicks).
|
||||
- Show energy cost on the button: `Hunt (5⚡)`. Disable if not enough energy, and say why.
|
||||
- Errors → toast from `error.message`. Successes → toast or inline result.
|
||||
- Numbers formatted with thousands separators.
|
||||
- Countdowns update every second client-side but **re-sync with the server** after each API call.
|
||||
- Accessible basics: labels on inputs, buttons are `<button>`, colour is not the only signal, focus outlines kept.
|
||||
- No images required: use emoji/CSS shapes for icons if no art is available (⚔️ 🛡️ 💍 🧪 ⚡ 💰).
|
||||
|
||||
## Copy tone
|
||||
Short, flavourful, never longer than 2 lines. e.g. "A Meadow Wolf lunges from the tall grass!"
|
||||
@@ -0,0 +1,39 @@
|
||||
# WORKFLOW.md — How the agent should work
|
||||
|
||||
## The loop (repeat for every step)
|
||||
1. **Orient:** read `STATUS.md` → find the next unchecked step.
|
||||
2. **Scope:** read only the docs named in that phase heading.
|
||||
3. **Plan:** write a 3–6 line plan (files to create/edit, what to test).
|
||||
4. **Build small:** one file or function at a time. Don't write the whole phase in one go.
|
||||
5. **Verify:** run `npm test`, and start the server and hit the endpoint with `curl` (or a test) before moving on.
|
||||
6. **Record:** tick the step, update Project Health, add a Session Log line, list new Known Issues.
|
||||
7. **Re-orient:** go back to 1.
|
||||
|
||||
## Definition of Done (per step)
|
||||
- [ ] Code is in the right folder per ARCHITECTURE.md
|
||||
- [ ] Inputs validated; errors return the standard error shape (API.md)
|
||||
- [ ] At least one test covers the happy path and one covers a failure path
|
||||
- [ ] `npm test` is green
|
||||
- [ ] No leftover `console.log` debugging; no commented-out code blocks
|
||||
- [ ] STATUS.md updated
|
||||
|
||||
## When something breaks
|
||||
1. Read the **full** error message and stack trace.
|
||||
2. Reproduce with the smallest possible command or test.
|
||||
3. Change **one** thing at a time. Re-run.
|
||||
4. If stuck after 3 genuine attempts: log it under Known Issues with what you tried, then move to a step that isn't blocked.
|
||||
|
||||
## Things that waste time — avoid
|
||||
- Rewriting working files "to clean them up".
|
||||
- Adding libraries to dodge writing 10 lines of code.
|
||||
- Building UI polish before the API works.
|
||||
- Inventing game mechanics not in GAME_DESIGN.md (add to Stretch Goals instead).
|
||||
- Editing earlier migrations after they've been run. **Add a new migration.**
|
||||
|
||||
## Editing files
|
||||
- Prefer targeted edits over regenerating whole files.
|
||||
- After editing, re-read the changed region to confirm it is what you intended.
|
||||
- Never delete the user's `data/*.db` unless a step says to.
|
||||
|
||||
## Commit style (if git is in use)
|
||||
`phase.step: short description` — e.g. `1.2: add authService register/login`.
|
||||
Reference in New Issue
Block a user