First commit, phased 1 and 2 created but untested
This commit is contained in:
@@ -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`.
|
||||
Reference in New Issue
Block a user