Files

92 lines
4.2 KiB
Markdown

# 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.