darkwatch/server/src/db.ts

153 lines
4.9 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,
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"],
];
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));
}
}
export default db;