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
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);
|