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>
185 lines
9.4 KiB
PL/PgSQL
185 lines
9.4 KiB
PL/PgSQL
-- 001_initial_schema.sql
|
||
-- Xyvera game database — initial schema
|
||
-- Applied automatically to the xyvera PostgreSQL container on first boot.
|
||
--
|
||
-- This schema covers Steps 1–3 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;
|