xyvera/migrations/001_initial_schema.sql
caoimhinr fb00954954 Initial scaffold: Nakama backend, Godot client, content schema
Sets up the Step 1 (Project Foundation) and Step 2 (Content Schemas) scaffold
from the implementation sequence. Includes Docker Compose stack, Nakama
TypeScript module stubs, PostgreSQL migration, Godot 4 project shell with
scene stubs and NakamaClient singleton, YAML content schemas for all entity
types, and example data for 3 characters, 2 jobs, and all 7 core resources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 23:33:06 +02:00

185 lines
9.4 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 001_initial_schema.sql
-- Xyvera game database — initial schema
-- Applied automatically to the xyvera PostgreSQL container on first boot.
--
-- This schema covers Steps 13 of the implementation sequence:
-- - player profiles and accounts
-- - resource inventory and currency ledger
-- - character roster
-- - gacha pull history and pity counters
--
-- Nakama has its own tables in CockroachDB. This Postgres DB stores
-- game-specific state that Nakama does not model natively:
-- - economy ledger (full audit trail)
-- - character progression state
-- - village assignment history
-- - content configuration cache (loaded from YAML by the content importer)
BEGIN;
-- ── Extensions ──────────────────────────────────────────────────────────────
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
-- ── Players ──────────────────────────────────────────────────────────────────
-- Mirrors Nakama user IDs. All economy and progression data links here.
CREATE TABLE players (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nakama_user_id TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT 'Traveller',
region TEXT NOT NULL DEFAULT 'heartwood_vale',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_players_nakama_user_id ON players(nakama_user_id);
-- ── Resource Ledger ───────────────────────────────────────────────────────────
-- Every grant and spend recorded. Never update; only insert.
-- This makes the economy fully auditable (Economy Rule from spec).
CREATE TABLE resource_ledger (
id BIGSERIAL PRIMARY KEY,
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
resource_id TEXT NOT NULL, -- matches resource schema id
delta INTEGER NOT NULL, -- positive = grant, negative = spend
balance_after INTEGER NOT NULL, -- denormalized for easy dashboards
source TEXT NOT NULL, -- e.g. 'lumber_camp_collect', 'gacha_pull'
source_ref TEXT, -- optional FK to another table (e.g. pull_id)
notes JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_resource_ledger_player ON resource_ledger(player_id, resource_id);
CREATE INDEX idx_resource_ledger_source ON resource_ledger(source);
CREATE INDEX idx_resource_ledger_created ON resource_ledger(created_at);
-- ── Character Roster ─────────────────────────────────────────────────────────
CREATE TABLE roster (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
character_id TEXT NOT NULL, -- matches character schema id
presentation TEXT NOT NULL DEFAULT 'female' CHECK (presentation IN ('female', 'male')),
level INTEGER NOT NULL DEFAULT 1 CHECK (level >= 1 AND level <= 100),
ascension_tier INTEGER NOT NULL DEFAULT 0 CHECK (ascension_tier >= 0 AND ascension_tier <= 6),
bond_level INTEGER NOT NULL DEFAULT 0,
assignment_mastery JSONB NOT NULL DEFAULT '{}', -- {job_id: hours_accumulated}
skills JSONB NOT NULL DEFAULT '{}', -- {skill_id: current_level}
obtained_at TIMESTAMPTZ NOT NULL DEFAULT now(),
obtained_source TEXT NOT NULL, -- 'gacha_standard', 'gacha_event', 'starter_seed'
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_roster_player_char ON roster(player_id, character_id);
CREATE INDEX idx_roster_player ON roster(player_id);
-- ── Village Assignment State ──────────────────────────────────────────────────
CREATE TABLE village_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
roster_id UUID NOT NULL REFERENCES roster(id) ON DELETE CASCADE,
job_id TEXT NOT NULL, -- matches job schema id
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_collected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (player_id, roster_id) -- one assignment per character
);
CREATE INDEX idx_village_assignments_player ON village_assignments(player_id);
-- ── Manual Work State ────────────────────────────────────────────────────────
-- Tracks daily manual work action counts for diminishing-returns enforcement.
CREATE TABLE manual_work_state (
player_id UUID PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
actions_today INTEGER NOT NULL DEFAULT 0,
actions_reset_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- midnight UTC of current day
total_actions_lifetime BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ── Gacha Pull History ────────────────────────────────────────────────────────
CREATE TABLE pulls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
banner_id TEXT NOT NULL,
character_id TEXT NOT NULL,
rarity TEXT NOT NULL CHECK (rarity IN ('R', 'SR', 'SSR', 'UR')),
pity_count_at_pull INTEGER NOT NULL,
was_soft_pity BOOLEAN NOT NULL DEFAULT false,
was_hard_pity BOOLEAN NOT NULL DEFAULT false,
was_featured BOOLEAN NOT NULL DEFAULT false,
pulled_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_pulls_player ON pulls(player_id, pulled_at DESC);
CREATE INDEX idx_pulls_banner ON pulls(banner_id, pulled_at DESC);
-- ── Pity Counters ─────────────────────────────────────────────────────────────
-- One row per (player, banner). Updated after every pull.
CREATE TABLE pity_counters (
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
banner_id TEXT NOT NULL,
current_pity INTEGER NOT NULL DEFAULT 0,
featured_guarantee_due BOOLEAN NOT NULL DEFAULT false, -- lost the 50/50; next SSR guaranteed
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (player_id, banner_id)
);
-- ── Content Cache Tables ──────────────────────────────────────────────────────
-- These are populated by tools/import_content.py from the YAML content files.
-- They are NOT the source of truth (YAML files are); they are the runtime cache.
CREATE TABLE content_characters (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
schema_version TEXT NOT NULL DEFAULT 'v1',
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE content_jobs (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
schema_version TEXT NOT NULL DEFAULT 'v1',
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE content_resources (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
schema_version TEXT NOT NULL DEFAULT 'v1',
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE content_banners (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
active BOOLEAN NOT NULL DEFAULT false,
schema_version TEXT NOT NULL DEFAULT 'v1',
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE content_events (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
active BOOLEAN NOT NULL DEFAULT false,
schema_version TEXT NOT NULL DEFAULT 'v1',
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ── Schema Version Tracking ───────────────────────────────────────────────────
CREATE TABLE schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO schema_migrations(version) VALUES ('001_initial_schema');
COMMIT;