You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
15 KiB
SQL

-- Migration 000006: Quest system — towns, NPCs, quests, hero quest tracking.
-- ============================================================
-- Towns: fixed settlements along the hero's travel road.
-- ============================================================
CREATE TABLE IF NOT EXISTS towns (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
biome TEXT NOT NULL,
world_x DOUBLE PRECISION NOT NULL,
world_y DOUBLE PRECISION NOT NULL,
radius DOUBLE PRECISION NOT NULL DEFAULT 8.0,
level_min INT NOT NULL DEFAULT 1,
level_max INT NOT NULL DEFAULT 100,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- NPCs: non-hostile characters in towns.
-- ============================================================
CREATE TABLE IF NOT EXISTS npcs (
id BIGSERIAL PRIMARY KEY,
town_id BIGINT NOT NULL REFERENCES towns(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('quest_giver', 'merchant', 'healer')),
offset_x DOUBLE PRECISION NOT NULL DEFAULT 0,
offset_y DOUBLE PRECISION NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_npcs_town ON npcs(town_id);
-- ============================================================
-- Quests: template definitions offered by quest-giver NPCs.
-- ============================================================
CREATE TABLE IF NOT EXISTS quests (
id BIGSERIAL PRIMARY KEY,
npc_id BIGINT NOT NULL REFERENCES npcs(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL CHECK (type IN ('kill_count', 'visit_town', 'collect_item')),
target_count INT NOT NULL DEFAULT 1,
target_enemy_type TEXT, -- NULL = any enemy (for kill_count)
target_town_id BIGINT REFERENCES towns(id), -- for visit_town quests
drop_chance DOUBLE PRECISION NOT NULL DEFAULT 0.3, -- for collect_item
min_level INT NOT NULL DEFAULT 1,
max_level INT NOT NULL DEFAULT 100,
reward_xp BIGINT NOT NULL DEFAULT 0,
reward_gold BIGINT NOT NULL DEFAULT 0,
reward_potions INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_quests_npc ON quests(npc_id);
-- ============================================================
-- Hero quests: per-hero progress tracking.
-- ============================================================
CREATE TABLE IF NOT EXISTS hero_quests (
id BIGSERIAL PRIMARY KEY,
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
quest_id BIGINT NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'accepted'
CHECK (status IN ('accepted', 'completed', 'claimed')),
progress INT NOT NULL DEFAULT 0,
accepted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
completed_at TIMESTAMPTZ,
claimed_at TIMESTAMPTZ,
UNIQUE (hero_id, quest_id)
);
CREATE INDEX IF NOT EXISTS idx_hero_quests_hero ON hero_quests(hero_id);
CREATE INDEX IF NOT EXISTS idx_hero_quests_status ON hero_quests(hero_id, status);
-- ============================================================
-- Seed data: towns (idempotent — DB may already have these names)
-- ============================================================
INSERT INTO towns (name, biome, world_x, world_y, radius, level_min, level_max) VALUES
('Willowdale', 'meadow', 50, 15, 8.0, 1, 5),
('Thornwatch', 'forest', 200, 60, 8.0, 5, 10),
('Ashengard', 'ruins', 400, 120, 8.0, 10, 16),
('Redcliff', 'canyon', 650, 195, 8.0, 16, 22),
('Boghollow', 'swamp', 900, 270, 8.0, 22, 28),
('Cinderkeep', 'volcanic', 1200, 360, 8.0, 28, 34),
('Starfall', 'astral', 1550, 465, 8.0, 34, 40)
ON CONFLICT (name) DO NOTHING;
-- ============================================================
-- Seed data: NPCs (2-3 per town; resolve town_id by name)
-- ============================================================
INSERT INTO npcs (town_id, name, type, offset_x, offset_y)
SELECT t.id, v.npc_name, v.npc_type, v.ox, v.oy
FROM (VALUES
('Willowdale', 'Elder Maren', 'quest_giver', -2.0::double precision, 1.0::double precision),
('Willowdale', 'Peddler Finn', 'merchant', 3.0, 0.0),
('Willowdale', 'Sister Asha', 'healer', 0.0, -2.5),
('Thornwatch', 'Guard Halric', 'quest_giver', -3.0, 0.5),
('Thornwatch', 'Trader Wynn', 'merchant', 2.0, 2.0),
('Ashengard', 'Scholar Orin', 'quest_giver', 1.0, -2.0),
('Ashengard', 'Bone Merchant', 'merchant', -2.0, 3.0),
('Ashengard', 'Priestess Liora', 'healer', 3.0, 1.0),
('Redcliff', 'Foreman Brak', 'quest_giver', -1.0, 2.0),
('Redcliff', 'Miner Supplies', 'merchant', 2.5, -1.0),
('Boghollow', 'Witch Nessa', 'quest_giver', 0.0, 3.0),
('Boghollow', 'Swamp Trader', 'merchant', -3.0, -1.0),
('Boghollow', 'Marsh Healer Ren', 'healer', 2.0, 0.0),
('Cinderkeep', 'Forge-master Kael', 'quest_giver', -2.5, 0.0),
('Cinderkeep', 'Ember Merchant', 'merchant', 1.0, 2.5),
('Starfall', 'Seer Aelith', 'quest_giver', 0.0, -3.0),
('Starfall', 'Void Trader', 'merchant', 3.0, 1.0),
('Starfall', 'Astral Mender', 'healer', -2.0, 2.0)
) AS v(town_name, npc_name, npc_type, ox, oy)
JOIN towns t ON t.name = v.town_name
WHERE NOT EXISTS (
SELECT 1 FROM npcs n WHERE n.town_id = t.id AND n.name = v.npc_name
);
-- ============================================================
-- Seed data: quests (resolve npc_id / target_town_id by name; skip duplicates)
-- ============================================================
-- Willowdale — Elder Maren
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Wolf Cull',
'The wolves near Willowdale are getting bolder. Thin their numbers.',
'kill_count', 5, 'wolf'::text, NULL::bigint, 0.0::double precision, 1, 5, 30::bigint, 15::bigint, 0),
('Boar Hunt',
'Wild boars are trampling the crops. Take care of them.',
'kill_count', 8, 'boar', NULL, 0.0, 2, 6, 50::bigint, 25::bigint, 1),
('Deliver to Thornwatch',
'Carry this supply manifest to Guard Halric in Thornwatch.',
'visit_town', 1, NULL, (SELECT id FROM towns WHERE name = 'Thornwatch' LIMIT 1), 0.0, 1, 10, 40::bigint, 20::bigint, 0)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Willowdale' AND n.name = 'Elder Maren'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Thornwatch — Guard Halric
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Spider Infestation',
'Cave spiders have overrun the logging trails. Clear them out.',
'kill_count', 12, 'spider'::text, NULL::bigint, 0.0::double precision, 5, 10, 80::bigint, 40::bigint, 1),
('Spider Fang Collection',
'We need spider fangs for antivenom. Collect them from slain spiders.',
'collect_item', 5, 'spider', NULL, 0.3, 5, 10, 100::bigint, 60::bigint, 1),
('Forest Patrol',
'Slay any 15 creatures along the forest road to keep it safe.',
'kill_count', 15, NULL::text, NULL::bigint, 0.0, 5, 12, 120::bigint, 70::bigint, 1)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Thornwatch' AND n.name = 'Guard Halric'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Ashengard — Scholar Orin
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Undead Purge',
'The ruins are crawling with undead. Destroy the zombies.',
'kill_count', 15, 'zombie'::text, NULL::bigint, 0.0::double precision, 10, 16, 150::bigint, 80::bigint, 1),
('Ancient Relics',
'Search fallen enemies for fragments of the old kingdom.',
'collect_item', 8, NULL::text, NULL::bigint, 0.25, 10, 16, 200::bigint, 120::bigint, 2),
('Report to Redcliff',
'Warn Foreman Brak about the growing undead threat.',
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Redcliff' LIMIT 1), 0.0, 10, 20, 120::bigint, 60::bigint, 0)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Ashengard' AND n.name = 'Scholar Orin'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Redcliff — Foreman Brak
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Orc Raider Cleanup',
'Orc warriors are raiding the mine carts. Stop them.',
'kill_count', 20, 'orc'::text, NULL::bigint, 0.0::double precision, 16, 22, 250::bigint, 150::bigint, 2),
('Ore Samples',
'Collect glowing ore fragments from defeated enemies near the canyon.',
'collect_item', 6, NULL::text, NULL::bigint, 0.3, 16, 22, 200::bigint, 120::bigint, 1)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Redcliff' AND n.name = 'Foreman Brak'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Boghollow — Witch Nessa
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Swamp Creatures',
'The swamp beasts grow more aggressive by the day. Cull 25.',
'kill_count', 25, NULL::text, NULL::bigint, 0.0::double precision, 22, 28, 350::bigint, 200::bigint, 2),
('Venomous Harvest',
'Collect venom sacs from swamp creatures for my brews.',
'collect_item', 10, NULL::text, NULL::bigint, 0.25, 22, 28, 400::bigint, 250::bigint, 2),
('Message to Cinderkeep',
'The forgemaster needs to know about the corruption spreading here.',
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Cinderkeep' LIMIT 1), 0.0, 22, 34, 200::bigint, 100::bigint, 1)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Boghollow' AND n.name = 'Witch Nessa'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Cinderkeep — Forge-master Kael
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Demon Slayer',
'Fire demons are emerging from the vents. Destroy them.',
'kill_count', 10, 'fire_demon'::text, NULL::bigint, 0.0::double precision, 28, 34, 500::bigint, 300::bigint, 2),
('Infernal Cores',
'Retrieve smoldering cores from defeated fire demons.',
'collect_item', 5, 'fire_demon'::text, NULL::bigint, 0.3::double precision, 28, 34, 600::bigint, 350::bigint, 3)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Cinderkeep' AND n.name = 'Forge-master Kael'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
-- Starfall — Seer Aelith
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
FROM npcs n
JOIN towns t ON t.id = n.town_id
CROSS JOIN (VALUES
('Titan''s Challenge',
'The Lightning Titans must be stopped before they breach the gate.',
'kill_count', 8, 'lightning_titan'::text, NULL::bigint, 0.0::double precision, 34, 40, 800::bigint, 500::bigint, 3),
('Void Fragments',
'Gather crystallized void energy from the astral enemies.',
'collect_item', 8, NULL::text, NULL::bigint, 0.2, 34, 40, 1000::bigint, 600::bigint, 3),
('Full Circle',
'Return to Willowdale and tell Elder Maren of your journey.',
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Willowdale' LIMIT 1), 0.0, 34, 40, 500::bigint, 300::bigint, 2)
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
WHERE t.name = 'Starfall' AND n.name = 'Seer Aelith'
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);