96 lines
4.2 KiB
SQL
96 lines
4.2 KiB
SQL
-- Narrative layer, first slice: a mystery (campaign) wrapping ordered level
|
|||
|
|
-- template versions, an authored NPC cast with named poses, scripted cutscenes,
|
||
|
|
-- and a per-user playthrough that owns game state. Dialogue content is fixed and
|
||
|
|
-- owned by the mystery; progress is a reference recorded in seen_dialogue.
|
||
|
|
|
||
|
|
CREATE TABLE osint.mysteries (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
slug TEXT NOT NULL UNIQUE,
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE osint.mystery_chapters (
|
||
|
|
mystery_id UUID NOT NULL REFERENCES osint.mysteries(id) ON DELETE CASCADE,
|
||
|
|
chapter_index INTEGER NOT NULL CHECK (chapter_index >= 1),
|
||
|
|
level_template_version_id UUID NOT NULL REFERENCES osint.level_template_versions(id),
|
||
|
|
PRIMARY KEY (mystery_id, chapter_index)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE osint.npcs (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
mystery_id UUID NOT NULL REFERENCES osint.mysteries(id) ON DELETE CASCADE,
|
||
|
|
npc_key TEXT NOT NULL,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
role TEXT NOT NULL DEFAULT '',
|
||
|
|
default_pose_key TEXT,
|
||
|
|
UNIQUE (mystery_id, npc_key)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- A pose is a named portrait variant backed by an immutable shared asset. A
|
||
|
|
-- missing pose is never an error; the client falls back to the NPC default pose
|
||
|
|
-- and then to no artwork.
|
||
|
|
CREATE TABLE osint.npc_poses (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
npc_id UUID NOT NULL REFERENCES osint.npcs(id) ON DELETE CASCADE,
|
||
|
|
pose_key TEXT NOT NULL,
|
||
|
|
asset_id UUID REFERENCES osint.assets(id),
|
||
|
|
UNIQUE (npc_id, pose_key)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE osint.cutscenes (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
mystery_id UUID NOT NULL REFERENCES osint.mysteries(id) ON DELETE CASCADE,
|
||
|
|
chapter_index INTEGER,
|
||
|
|
slot TEXT NOT NULL CHECK (slot IN ('mystery_intro','level_intro','level_debrief','mystery_resolution')),
|
||
|
|
title TEXT NOT NULL DEFAULT '',
|
||
|
|
-- Chapter-scoped slots carry a chapter; mystery-scoped slots do not.
|
||
|
|
CHECK (
|
||
|
|
(slot IN ('mystery_intro','mystery_resolution') AND chapter_index IS NULL)
|
||
|
|
OR (slot IN ('level_intro','level_debrief') AND chapter_index IS NOT NULL)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
CREATE INDEX cutscenes_mystery_idx ON osint.cutscenes (mystery_id);
|
||
|
|
|
||
|
|
-- One utterance. `kind` reserves the LLM path; `body_text` is required for
|
||
|
|
-- scripted steps and null for generated ones. `advances_to` reserves branching;
|
||
|
|
-- null means "next by sort_order".
|
||
|
|
CREATE TABLE osint.dialogue_steps (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
cutscene_id UUID NOT NULL REFERENCES osint.cutscenes(id) ON DELETE CASCADE,
|
||
|
|
step_key TEXT NOT NULL,
|
||
|
|
sort_order INTEGER NOT NULL,
|
||
|
|
npc_id UUID NOT NULL REFERENCES osint.npcs(id),
|
||
|
|
pose_key TEXT,
|
||
|
|
kind TEXT NOT NULL DEFAULT 'scripted' CHECK (kind IN ('scripted','generated')),
|
||
|
|
body_text TEXT,
|
||
|
|
advances_to TEXT,
|
||
|
|
UNIQUE (cutscene_id, sort_order),
|
||
|
|
UNIQUE (cutscene_id, step_key),
|
||
|
|
CHECK ((kind = 'scripted' AND body_text IS NOT NULL) OR kind = 'generated')
|
||
|
|
);
|
||
|
|
|
||
|
|
-- The single object that owns a player's game state, bound to the JWT identity.
|
||
|
|
CREATE TABLE osint.playthroughs (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
mystery_id UUID NOT NULL REFERENCES osint.mysteries(id) ON DELETE CASCADE,
|
||
|
|
current_chapter_index INTEGER NOT NULL DEFAULT 1 CHECK (current_chapter_index >= 1),
|
||
|
|
current_level_id UUID REFERENCES osint.levels(id) ON DELETE SET NULL,
|
||
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','finished')),
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
|
|
);
|
||
|
|
CREATE INDEX playthroughs_user_idx ON osint.playthroughs (user_id, updated_at DESC);
|
||
|
|
|
||
|
|
-- Progress as a reference to fixed authored dialogue, never a copy of its text.
|
||
|
|
CREATE TABLE osint.seen_dialogue (
|
||
|
|
playthrough_id UUID NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||
|
|
cutscene_id UUID NOT NULL REFERENCES osint.cutscenes(id) ON DELETE CASCADE,
|
||
|
|
seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
PRIMARY KEY (playthrough_id, cutscene_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
COMMENT ON TABLE osint.playthroughs IS 'Per-user instance of a mystery; owns current chapter and live level. Bound to the JWT sub (or the development test user).';
|
||
|
|
COMMENT ON TABLE osint.dialogue_steps IS 'Fixed authored utterances owned by the immutable mystery. seen_dialogue references these ids; they are never cloned per playthrough.';
|