- Torch timer: 60min countdown per character, visual warnings at 10m/5m/1m - Fog overlay: CSS radial gradient layers with seamless infinite drift - Fog synced across all clients via socket, adapts to light/dark themes - DM card redesign: compact layout with HP/AC/luck/torch + modifier row - Grid changed to 3-up (from 4) with larger fonts - DiceBear avatars on cards and character sheets with style picker - Campaign name shown in header - Server: JSON.stringify fix for object fields in PATCH handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
168 lines
5.6 KiB
TypeScript
168 lines
5.6 KiB
TypeScript
import Database from "better-sqlite3";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import { SEED_ITEMS } from "./seed-items.js";
|
|
import { SEED_TALENTS } from "./seed-talents.js";
|
|
|
|
const DATA_DIR = path.join(import.meta.dirname, "..", "data");
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
const db = new Database(path.join(DATA_DIR, "shadowdark.db"));
|
|
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("foreign_keys = ON");
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS campaigns (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
created_by TEXT DEFAULT '',
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS characters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
|
|
created_by TEXT DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
class TEXT NOT NULL DEFAULT 'Fighter',
|
|
ancestry TEXT NOT NULL DEFAULT 'Human',
|
|
level INTEGER NOT NULL DEFAULT 1,
|
|
xp INTEGER NOT NULL DEFAULT 0,
|
|
hp_current INTEGER NOT NULL DEFAULT 0,
|
|
hp_max INTEGER NOT NULL DEFAULT 0,
|
|
ac INTEGER NOT NULL DEFAULT 10,
|
|
alignment TEXT NOT NULL DEFAULT 'Neutral',
|
|
title TEXT DEFAULT '',
|
|
notes TEXT DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS character_stats (
|
|
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
|
stat_name TEXT NOT NULL,
|
|
value INTEGER NOT NULL DEFAULT 10,
|
|
PRIMARY KEY (character_id, stat_name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS character_gear (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'gear',
|
|
slot_count INTEGER NOT NULL DEFAULT 1,
|
|
properties TEXT DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS character_talents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
effect TEXT DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS game_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
slot_count INTEGER NOT NULL DEFAULT 1,
|
|
effects TEXT DEFAULT '{}',
|
|
properties TEXT DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS game_talents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
effect TEXT DEFAULT '{}'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS roll_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
|
|
character_id INTEGER,
|
|
character_name TEXT NOT NULL DEFAULT 'Roll',
|
|
type TEXT NOT NULL DEFAULT 'custom',
|
|
label TEXT NOT NULL,
|
|
dice_expression TEXT NOT NULL,
|
|
rolls TEXT NOT NULL DEFAULT '[]',
|
|
modifier INTEGER NOT NULL DEFAULT 0,
|
|
total INTEGER NOT NULL DEFAULT 0,
|
|
advantage INTEGER NOT NULL DEFAULT 0,
|
|
disadvantage INTEGER NOT NULL DEFAULT 0,
|
|
nat20 INTEGER NOT NULL DEFAULT 0,
|
|
character_color TEXT DEFAULT '',
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
|
|
// --- Migrations for v2 ---
|
|
const v2Columns: Array<[string, string, string]> = [
|
|
["characters", "background", "TEXT DEFAULT ''"],
|
|
["characters", "deity", "TEXT DEFAULT ''"],
|
|
["characters", "languages", "TEXT DEFAULT ''"],
|
|
["characters", "gp", "INTEGER DEFAULT 0"],
|
|
["characters", "sp", "INTEGER DEFAULT 0"],
|
|
["characters", "cp", "INTEGER DEFAULT 0"],
|
|
["characters", "gear_slots_max", "INTEGER DEFAULT 10"],
|
|
["characters", "overrides", "TEXT DEFAULT '{}'"],
|
|
["character_gear", "game_item_id", "INTEGER"],
|
|
["character_gear", "effects", "TEXT DEFAULT '{}'"],
|
|
["character_talents", "game_talent_id", "INTEGER"],
|
|
["characters", "color", "TEXT DEFAULT ''"],
|
|
["characters", "luck_token", "INTEGER DEFAULT 1"],
|
|
["characters", "torch_lit_at", "TEXT"],
|
|
["roll_log", "nat20", "INTEGER DEFAULT 0"],
|
|
["roll_log", "character_color", "TEXT DEFAULT ''"],
|
|
];
|
|
|
|
for (const [table, column, definition] of v2Columns) {
|
|
try {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
} catch {
|
|
// Column already exists
|
|
}
|
|
}
|
|
|
|
// Seed game_items if empty
|
|
const count = (
|
|
db.prepare("SELECT COUNT(*) as c FROM game_items").get() as { c: number }
|
|
).c;
|
|
if (count === 0) {
|
|
const insert = db.prepare(
|
|
"INSERT INTO game_items (name, type, slot_count, effects, properties) VALUES (?, ?, ?, ?, ?)",
|
|
);
|
|
for (const item of SEED_ITEMS) {
|
|
insert.run(
|
|
item.name,
|
|
item.type,
|
|
item.slot_count,
|
|
JSON.stringify(item.effects),
|
|
JSON.stringify(item.properties),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Seed game_talents if empty
|
|
const talentCount = (
|
|
db.prepare("SELECT COUNT(*) as c FROM game_talents").get() as { c: number }
|
|
).c;
|
|
if (talentCount === 0) {
|
|
const insertTalent = db.prepare(
|
|
"INSERT INTO game_talents (name, source, description, effect) VALUES (?, ?, ?, ?)",
|
|
);
|
|
for (const t of SEED_TALENTS) {
|
|
insertTalent.run(t.name, t.source, t.description, JSON.stringify(t.effect));
|
|
}
|
|
}
|
|
|
|
// --- Migration: update Grit talent to include hp_per_level ---
|
|
db.prepare(
|
|
`UPDATE game_talents SET effect = '{"hp_bonus":2,"hp_per_level":1}' WHERE name = 'Grit' AND effect = '{"hp_bonus":2}'`,
|
|
).run();
|
|
db.prepare(
|
|
`UPDATE character_talents SET effect = '{"hp_bonus":2,"hp_per_level":1}' WHERE name = 'Grit' AND effect = '{"hp_bonus":2}'`,
|
|
).run();
|
|
|
|
export default db;
|