huge combat update and wipe
parent
9b5af1f93c
commit
8ecb3981ac
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
-- Free revive quota for non-subscribers (MVP: 2 lifetime revives unless subscription_active).
|
|
||||||
|
|
||||||
ALTER TABLE heroes
|
|
||||||
ADD COLUMN IF NOT EXISTS revive_count INT NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN IF NOT EXISTS subscription_active BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.revive_count IS 'Number of revives consumed (free tier capped at 2 without subscription).';
|
|
||||||
COMMENT ON COLUMN heroes.subscription_active IS 'When true, revive limit does not apply.';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
-- Free-tier buff activations: 3 per rolling 24h window (spec daily task "Use 3 Buffs").
|
|
||||||
-- Subscribers ignore quota (subscription_active).
|
|
||||||
|
|
||||||
ALTER TABLE heroes
|
|
||||||
ADD COLUMN IF NOT EXISTS buff_free_charges_remaining INT NOT NULL DEFAULT 3,
|
|
||||||
ADD COLUMN IF NOT EXISTS buff_quota_period_end TIMESTAMPTZ NULL;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.buff_free_charges_remaining IS 'Free buff activations left in current window (non-subscribers; resets when period rolls).';
|
|
||||||
COMMENT ON COLUMN heroes.buff_quota_period_end IS 'End of current 24h buff quota window; NULL until first activation in a session.';
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
-- Migration: add hero position, potions, and adventure log.
|
|
||||||
|
|
||||||
-- Hero position persists across sessions so the client can restore the visual location.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS position_x DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS position_y DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Potions inventory (healing potions from monster drops).
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS potions INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Adventure log: a chronological list of notable in-game events per hero.
|
|
||||||
CREATE TABLE IF NOT EXISTS adventure_log (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_adventure_log_hero_created
|
|
||||||
ON adventure_log (hero_id, created_at DESC);
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
-- Replace shared buff quota with per-buff quotas.
|
|
||||||
-- Each buff type gets its own charge counter and period window.
|
|
||||||
-- buff_charges stores: {"rush": {"remaining": 5, "periodEnd": "2026-03-29T00:00:00Z"}, ...}
|
|
||||||
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS buff_charges JSONB NOT NULL DEFAULT '{}';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.buff_charges IS 'Per-buff-type free charge state: map of buff_type -> {remaining, periodEnd}. Replaces shared buff_free_charges_remaining.';
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Make hero name unique (case-insensitive)
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_heroes_name_lower ON heroes(LOWER(name)) WHERE name != '' AND name != 'Hero';
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE weapons ADD COLUMN IF NOT EXISTS ilvl INT NOT NULL DEFAULT 1;
|
|
||||||
ALTER TABLE armor ADD COLUMN IF NOT EXISTS ilvl INT NOT NULL DEFAULT 1;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS payments (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
type TEXT NOT NULL, -- 'buff_replenish', 'resurrection_replenish'
|
|
||||||
buff_type TEXT, -- specific buff type if applicable
|
|
||||||
amount_rub INT NOT NULL, -- price in rubles
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending', -- pending, completed, failed
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
completed_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_hero ON payments(hero_id);
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
-- Migration 000010: Extended equipment slots (head, feet, neck).
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Equipment items table (all slots beyond legacy weapon/armor).
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS equipment_items (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
slot TEXT NOT NULL, -- gear.slot.head, gear.slot.feet, gear.slot.neck, etc.
|
|
||||||
form_id TEXT NOT NULL DEFAULT '',
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
rarity TEXT NOT NULL DEFAULT 'common',
|
|
||||||
ilvl INT NOT NULL DEFAULT 1,
|
|
||||||
base_primary INT NOT NULL DEFAULT 0,
|
|
||||||
primary_stat INT NOT NULL DEFAULT 0, -- computed: ScalePrimary(base_primary, ilvl, rarity)
|
|
||||||
stat_type TEXT NOT NULL DEFAULT 'defense', -- attack, defense, speed, mixed
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Hero equipment (one row per equipped slot).
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_equipment (
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
slot TEXT NOT NULL,
|
|
||||||
item_id BIGINT NOT NULL REFERENCES equipment_items(id),
|
|
||||||
equipped_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
PRIMARY KEY (hero_id, slot)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hero_equipment_hero ON hero_equipment(hero_id);
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
-- 000011: Achievements, Daily/Weekly Tasks, and Shared World foundation.
|
|
||||||
|
|
||||||
-- Hero stat tracking columns for achievement conditions.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS total_kills INT NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS elite_kills INT NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS total_deaths INT NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS kills_since_death INT NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS legendary_drops INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Shared world: track hero online status for nearby-heroes queries.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS last_online_at TIMESTAMPTZ;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_heroes_online ON heroes(last_online_at) WHERE last_online_at IS NOT NULL;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Achievements
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS achievements (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
condition_type TEXT NOT NULL, -- 'level', 'kills', 'gold', 'elite_kills', 'deaths', 'loot_legendary', 'kills_no_death'
|
|
||||||
condition_value INT NOT NULL DEFAULT 0,
|
|
||||||
reward_type TEXT NOT NULL DEFAULT 'gold', -- 'gold', 'potion', 'title'
|
|
||||||
reward_amount INT NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_achievements (
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
achievement_id TEXT NOT NULL REFERENCES achievements(id),
|
|
||||||
unlocked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
PRIMARY KEY (hero_id, achievement_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Seed achievements.
|
|
||||||
INSERT INTO achievements (id, title, description, condition_type, condition_value, reward_type, reward_amount) VALUES
|
|
||||||
('first_blood', 'First Blood', 'Defeat your first enemy', 'kills', 1, 'gold', 50),
|
|
||||||
('warrior', 'Warrior', 'Reach level 50', 'level', 50, 'gold', 5000),
|
|
||||||
('legend', 'Legend', 'Reach level 100', 'level', 100, 'gold', 50000),
|
|
||||||
('hunter', 'Hunter', 'Defeat 100 enemies', 'kills', 100, 'gold', 500),
|
|
||||||
('slayer', 'Slayer', 'Defeat 1000 enemies', 'kills', 1000, 'gold', 5000),
|
|
||||||
('rich', 'Rich', 'Accumulate 10000 gold', 'gold', 10000, 'gold', 1000),
|
|
||||||
('lucky', 'Lucky', 'Find a Legendary item', 'loot_legendary', 1, 'potion', 5),
|
|
||||||
('undying', 'Undying', 'Defeat 50 enemies without dying', 'kills_no_death', 50, 'gold', 2000),
|
|
||||||
('elite_hunter', 'Elite Hunter', 'Defeat 10 elite enemies', 'elite_kills', 10, 'gold', 3000)
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Daily / Weekly Tasks
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS daily_tasks (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
objective_type TEXT NOT NULL, -- 'kill_count', 'elite_kill', 'collect_gold', 'use_buff', 'reach_level'
|
|
||||||
objective_count INT NOT NULL,
|
|
||||||
reward_type TEXT NOT NULL DEFAULT 'gold',
|
|
||||||
reward_amount INT NOT NULL DEFAULT 0,
|
|
||||||
period TEXT NOT NULL DEFAULT 'daily' -- 'daily', 'weekly'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_daily_tasks (
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
task_id TEXT NOT NULL REFERENCES daily_tasks(id),
|
|
||||||
progress INT NOT NULL DEFAULT 0,
|
|
||||||
completed BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
claimed BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
period_start TIMESTAMPTZ NOT NULL,
|
|
||||||
PRIMARY KEY (hero_id, task_id, period_start)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Seed daily tasks.
|
|
||||||
INSERT INTO daily_tasks VALUES
|
|
||||||
('daily_kill_10', 'Monster Slayer', 'Kill 10 enemies', 'kill_count', 10, 'gold', 100, 'daily'),
|
|
||||||
('daily_elite', 'Elite Hunter', 'Defeat an Elite enemy', 'elite_kill', 1, 'gold', 200, 'daily'),
|
|
||||||
('daily_gold_500', 'Gold Rush', 'Collect 500 Gold', 'collect_gold', 500, 'potion', 2, 'daily'),
|
|
||||||
('daily_buff_3', 'Buff Master', 'Use 3 Buffs', 'use_buff', 3, 'gold', 150, 'daily'),
|
|
||||||
('weekly_kill_100', 'Weekly Warrior', 'Kill 100 enemies', 'kill_count', 100, 'gold', 1000, 'weekly'),
|
|
||||||
('weekly_elite_5', 'Elite Slayer', 'Defeat 5 Elites', 'elite_kill', 5, 'gold', 2000, 'weekly'),
|
|
||||||
('weekly_gold_5000', 'Wealthy', 'Collect 5000 Gold', 'collect_gold', 5000, 'potion', 5, 'weekly')
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
-- Migration 000012: Increase town radii and vary by settlement size.
|
|
||||||
|
|
||||||
UPDATE towns SET radius = 18 WHERE name = 'Willowdale';
|
|
||||||
UPDATE towns SET radius = 14 WHERE name = 'Thornwatch';
|
|
||||||
UPDATE towns SET radius = 16 WHERE name = 'Ashengard';
|
|
||||||
UPDATE towns SET radius = 14 WHERE name = 'Redcliff';
|
|
||||||
UPDATE towns SET radius = 12 WHERE name = 'Boghollow';
|
|
||||||
UPDATE towns SET radius = 16 WHERE name = 'Cinderkeep';
|
|
||||||
UPDATE towns SET radius = 18 WHERE name = 'Starfall';
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
-- Server-authoritative movement: hero movement state + roads graph.
|
|
||||||
|
|
||||||
-- Hero movement columns.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS destination_town_id BIGINT REFERENCES towns(id);
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS current_town_id BIGINT REFERENCES towns(id);
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS move_state TEXT NOT NULL DEFAULT 'walking';
|
|
||||||
-- move_state: 'walking', 'resting', 'in_town', 'fighting', 'dead'
|
|
||||||
|
|
||||||
-- Roads connect towns in a linear chain.
|
|
||||||
CREATE TABLE IF NOT EXISTS roads (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
from_town_id BIGINT NOT NULL REFERENCES towns(id),
|
|
||||||
to_town_id BIGINT NOT NULL REFERENCES towns(id),
|
|
||||||
distance DOUBLE PRECISION NOT NULL,
|
|
||||||
UNIQUE(from_town_id, to_town_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Pre-computed waypoints along each road.
|
|
||||||
CREATE TABLE IF NOT EXISTS road_waypoints (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
road_id BIGINT NOT NULL REFERENCES roads(id) ON DELETE CASCADE,
|
|
||||||
seq INT NOT NULL,
|
|
||||||
x DOUBLE PRECISION NOT NULL,
|
|
||||||
y DOUBLE PRECISION NOT NULL,
|
|
||||||
UNIQUE(road_id, seq)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Seed roads between the 7 towns in order.
|
|
||||||
-- Town positions (from 000006_quest_system.sql):
|
|
||||||
-- Willowdale (50, 15) id=1
|
|
||||||
-- Thornwatch (200, 60) id=2
|
|
||||||
-- Ashengard (400, 120) id=3
|
|
||||||
-- Redcliff (650, 195) id=4
|
|
||||||
-- Boghollow (900, 270) id=5
|
|
||||||
-- Cinderkeep (1200, 360) id=6
|
|
||||||
-- Starfall (1550, 465) id=7
|
|
||||||
|
|
||||||
-- Forward roads (1->2, 2->3, ... 6->7).
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance) VALUES
|
|
||||||
(1, 2, 156.0),
|
|
||||||
(2, 3, 210.0),
|
|
||||||
(3, 4, 260.0),
|
|
||||||
(4, 5, 260.0),
|
|
||||||
(5, 6, 312.0),
|
|
||||||
(6, 7, 365.0)
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- Reverse roads (2->1, 3->2, ... 7->6).
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance) VALUES
|
|
||||||
(2, 1, 156.0),
|
|
||||||
(3, 2, 210.0),
|
|
||||||
(4, 3, 260.0),
|
|
||||||
(5, 4, 260.0),
|
|
||||||
(6, 5, 312.0),
|
|
||||||
(7, 6, 365.0)
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- Waypoints are generated at application startup via the RoadGraph loader
|
|
||||||
-- using interpolation between town positions with jitter. This avoids
|
|
||||||
-- storing thousands of rows and keeps generation deterministic per road seed.
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
-- Unified gear table replacing weapons, armor, and equipment_items
|
|
||||||
CREATE TABLE IF NOT EXISTS gear (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
slot TEXT NOT NULL,
|
|
||||||
form_id TEXT NOT NULL DEFAULT '',
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
subtype TEXT NOT NULL DEFAULT '',
|
|
||||||
rarity TEXT NOT NULL DEFAULT 'common',
|
|
||||||
ilvl INT NOT NULL DEFAULT 1,
|
|
||||||
base_primary INT NOT NULL DEFAULT 0,
|
|
||||||
primary_stat INT NOT NULL DEFAULT 0,
|
|
||||||
stat_type TEXT NOT NULL DEFAULT 'mixed',
|
|
||||||
speed_modifier DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
|
||||||
crit_chance DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
|
||||||
agility_bonus INT NOT NULL DEFAULT 0,
|
|
||||||
set_name TEXT NOT NULL DEFAULT '',
|
|
||||||
special_effect TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Hero gear: one row per equipped slot (replaces weapon_id, armor_id, and hero_equipment)
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_gear (
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
slot TEXT NOT NULL,
|
|
||||||
gear_id BIGINT NOT NULL REFERENCES gear(id),
|
|
||||||
PRIMARY KEY (hero_id, slot)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hero_gear_hero ON hero_gear(hero_id);
|
|
||||||
|
|
||||||
-- Migrate existing weapon data to gear table (safe to re-run if migration retried)
|
|
||||||
INSERT INTO gear (id, slot, name, subtype, rarity, ilvl, base_primary, primary_stat, stat_type, speed_modifier, crit_chance, special_effect)
|
|
||||||
SELECT id, 'main_hand', name, type, rarity, ilvl, damage, damage, 'attack', speed, crit_chance, special_effect
|
|
||||||
FROM weapons
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
-- Migrate existing armor data to gear table (offset IDs by 1000 to avoid conflicts)
|
|
||||||
INSERT INTO gear (id, slot, name, subtype, rarity, ilvl, base_primary, primary_stat, stat_type, speed_modifier, agility_bonus, set_name, special_effect)
|
|
||||||
SELECT id + 1000, 'chest', name, type, rarity, ilvl, defense, defense, 'defense', speed_modifier, agility_bonus, set_name, special_effect
|
|
||||||
FROM armor
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
-- Migrate equipment_items to gear (offset by 2000)
|
|
||||||
INSERT INTO gear (id, slot, form_id, name, rarity, ilvl, base_primary, primary_stat, stat_type)
|
|
||||||
SELECT id + 2000, slot, form_id, name, rarity, ilvl, base_primary, primary_stat, stat_type
|
|
||||||
FROM equipment_items
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
-- Migrate hero weapon/armor refs to hero_gear
|
|
||||||
INSERT INTO hero_gear (hero_id, slot, gear_id)
|
|
||||||
SELECT id, 'main_hand', weapon_id FROM heroes WHERE weapon_id IS NOT NULL
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
INSERT INTO hero_gear (hero_id, slot, gear_id)
|
|
||||||
SELECT id, 'chest', armor_id + 1000 FROM heroes WHERE armor_id IS NOT NULL
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- Migrate hero_equipment to hero_gear
|
|
||||||
INSERT INTO hero_gear (hero_id, slot, gear_id)
|
|
||||||
SELECT hero_id, slot, item_id + 2000 FROM hero_equipment
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- Reset gear sequence to avoid ID conflicts
|
|
||||||
SELECT setval('gear_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM gear));
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
-- Subscription system: weekly subscription with expiry date.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS subscription_expires_at TIMESTAMPTZ;
|
|
||||||
|
|
||||||
-- Payment type for subscription purchases.
|
|
||||||
-- Existing payments table is reused with type = 'subscription_weekly'.
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
-- Migration 000015: Spread towns further apart on the world map (longer roads between stops).
|
|
||||||
-- Waypoints are regenerated at startup from town positions; roads.distance is overwritten in memory.
|
|
||||||
|
|
||||||
UPDATE towns SET world_x = 125, world_y = 38 WHERE name = 'Willowdale';
|
|
||||||
UPDATE towns SET world_x = 500, world_y = 150 WHERE name = 'Thornwatch';
|
|
||||||
UPDATE towns SET world_x = 1000, world_y = 300 WHERE name = 'Ashengard';
|
|
||||||
UPDATE towns SET world_x = 1625, world_y = 488 WHERE name = 'Redcliff';
|
|
||||||
UPDATE towns SET world_x = 2250, world_y = 675 WHERE name = 'Boghollow';
|
|
||||||
UPDATE towns SET world_x = 3000, world_y = 900 WHERE name = 'Cinderkeep';
|
|
||||||
UPDATE towns SET world_x = 3875, world_y = 1163 WHERE name = 'Starfall';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
-- Close the world road loop: last town connects back to the first (and reverse).
|
|
||||||
-- Town ids: 1 Willowdale .. 7 Starfall (see 000006 / 000015). Distance is approximate; runtime recomputes from waypoints.
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance) VALUES
|
|
||||||
(7, 1, 4000.0),
|
|
||||||
(1, 7, 4000.0)
|
|
||||||
ON CONFLICT (from_town_id, to_town_id) DO NOTHING;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
-- Migration 000017: Populate road_waypoints for every row in roads.
|
|
||||||
--
|
|
||||||
-- Review (why the table was empty):
|
|
||||||
-- 000013 created road_waypoints but never inserted rows; comments there say waypoints are
|
|
||||||
-- generated at runtime in Go (internal/game/road_graph.go → generateWaypoints). The server
|
|
||||||
-- still does NOT read this table — it joins towns + roads and builds jittered polylines in memory.
|
|
||||||
-- This migration stores a canonical polyline per road for analytics, admin maps, exports, or a
|
|
||||||
-- future loader. Points use the same segment count rule as Go (≈20 world units per segment,
|
|
||||||
-- GREATEST(1, floor(dist/20))), linear interpolation only — no ±2 jitter (that stays code-only).
|
|
||||||
--
|
|
||||||
-- Idempotent: clears existing waypoint rows then re-seeds from current towns.world_x/y.
|
|
||||||
|
|
||||||
DELETE FROM road_waypoints;
|
|
||||||
|
|
||||||
INSERT INTO road_waypoints (road_id, seq, x, y)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
gs.seq,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_x
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_x
|
|
||||||
ELSE f.world_x + (t.world_x - f.world_x) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_y
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_y
|
|
||||||
ELSE f.world_y + (t.world_y - f.world_y) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END
|
|
||||||
FROM roads r
|
|
||||||
INNER JOIN towns f ON f.id = r.from_town_id
|
|
||||||
INNER JOIN towns t ON t.id = r.to_town_id
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT GREATEST(
|
|
||||||
1,
|
|
||||||
FLOOR(
|
|
||||||
SQRT(POWER(t.world_x - f.world_x, 2) + POWER(t.world_y - f.world_y, 2)) / 20.0
|
|
||||||
)::integer
|
|
||||||
) AS nseg
|
|
||||||
) seg
|
|
||||||
CROSS JOIN LATERAL generate_series(0, seg.nseg) AS gs(seq);
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
-- Migration 000018: Place towns on an approximate Archimedean spiral (not collinear).
|
|
||||||
-- Order by level_min is unchanged — ring roads still follow progression Willowdale → … → Starfall → wrap.
|
|
||||||
-- Waypoints regenerate at server startup from town centers (see road_graph / 000016).
|
|
||||||
|
|
||||||
UPDATE towns SET world_x = 2620, world_y = 800 WHERE name = 'Willowdale';
|
|
||||||
UPDATE towns SET world_x = 2926, world_y = 1058 WHERE name = 'Thornwatch';
|
|
||||||
UPDATE towns SET world_x = 2899, world_y = 1584 WHERE name = 'Ashengard';
|
|
||||||
UPDATE towns SET world_x = 2399, world_y = 2056 WHERE name = 'Redcliff';
|
|
||||||
UPDATE towns SET world_x = 1535, world_y = 2126 WHERE name = 'Boghollow';
|
|
||||||
UPDATE towns SET world_x = 633, world_y = 1571 WHERE name = 'Cinderkeep';
|
|
||||||
UPDATE towns SET world_x = 131, world_y = 660 WHERE name = 'Starfall';
|
|
||||||
|
|
||||||
-- Keep road_waypoints (if populated by 000017) aligned with new town centers.
|
|
||||||
DELETE FROM road_waypoints;
|
|
||||||
|
|
||||||
INSERT INTO road_waypoints (road_id, seq, x, y)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
gs.seq,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_x
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_x
|
|
||||||
ELSE f.world_x + (t.world_x - f.world_x) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_y
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_y
|
|
||||||
ELSE f.world_y + (t.world_y - f.world_y) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END
|
|
||||||
FROM roads r
|
|
||||||
INNER JOIN towns f ON f.id = r.from_town_id
|
|
||||||
INNER JOIN towns t ON t.id = r.to_town_id
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT GREATEST(
|
|
||||||
1,
|
|
||||||
FLOOR(
|
|
||||||
SQRT(POWER(t.world_x - f.world_x, 2) + POWER(t.world_y - f.world_y, 2)) / 20.0
|
|
||||||
)::integer
|
|
||||||
) AS nseg
|
|
||||||
) seg
|
|
||||||
CROSS JOIN LATERAL generate_series(0, seg.nseg) AS gs(seq);
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
-- Backpack: unequipped gear (max 40 slots per hero).
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_inventory (
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
slot_index SMALLINT NOT NULL CHECK (slot_index >= 0 AND slot_index < 40),
|
|
||||||
gear_id BIGINT NOT NULL REFERENCES gear(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (hero_id, slot_index),
|
|
||||||
UNIQUE (gear_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hero_inventory_hero ON hero_inventory(hero_id);
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
-- Align heroes.state CHECK with model.GameState (resting / in_town used by town arrival & admin teleport).
|
|
||||||
ALTER TABLE heroes DROP CONSTRAINT IF EXISTS heroes_state_check;
|
|
||||||
ALTER TABLE heroes ADD CONSTRAINT heroes_state_check CHECK (
|
|
||||||
state IN ('walking', 'fighting', 'dead', 'resting', 'in_town')
|
|
||||||
);
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Persist movement timers / in-town NPC tour state so offline simulation can advance resting & town visits.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS town_pause JSONB NULL;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS runtime_config (
|
|
||||||
id BOOLEAN PRIMARY KEY DEFAULT TRUE,
|
|
||||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
CONSTRAINT runtime_config_single_row CHECK (id = TRUE)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO runtime_config (id, payload)
|
|
||||||
VALUES (TRUE, '{}'::jsonb)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS buff_debuff_config (
|
|
||||||
id BOOLEAN PRIMARY KEY DEFAULT TRUE,
|
|
||||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
CONSTRAINT buff_debuff_config_single_row CHECK (id = TRUE)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO buff_debuff_config (id, payload)
|
|
||||||
VALUES (TRUE, '{}'::jsonb)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
-- Migration 000026: Town buildings — server-driven layout for towns.
|
|
||||||
-- Each NPC gets an assigned building; buildings have typed appearances per NPC role.
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Town buildings: persistent structures placed in towns.
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS town_buildings (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
town_id BIGINT NOT NULL REFERENCES towns(id) ON DELETE CASCADE,
|
|
||||||
building_type TEXT NOT NULL CHECK (building_type IN (
|
|
||||||
'house.quest_giver', 'house.merchant', 'house.healer',
|
|
||||||
'decoration.well', 'decoration.stall', 'decoration.signpost'
|
|
||||||
)),
|
|
||||||
offset_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
||||||
offset_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
||||||
facing TEXT NOT NULL DEFAULT 'south' CHECK (facing IN ('north','south','east','west')),
|
|
||||||
footprint_w DOUBLE PRECISION NOT NULL DEFAULT 2.0,
|
|
||||||
footprint_h DOUBLE PRECISION NOT NULL DEFAULT 2.0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_town_buildings_town ON town_buildings(town_id);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Link NPCs to their buildings (nullable for migration transition).
|
|
||||||
-- ============================================================
|
|
||||||
ALTER TABLE npcs ADD COLUMN IF NOT EXISTS building_id BIGINT REFERENCES town_buildings(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Seed buildings for all existing towns, then link NPCs.
|
|
||||||
-- Layout strategy per town:
|
|
||||||
-- - NPC buildings are placed in a semicircle around the town center
|
|
||||||
-- - quest_giver at ~10 o'clock, merchant at ~2 o'clock, healer at ~6 o'clock
|
|
||||||
-- - A well decoration at center, signpost near entrance
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Helper: create buildings for each town with NPCs, using deterministic offsets by NPC type.
|
|
||||||
-- quest_giver houses: upper-left zone
|
|
||||||
-- merchant houses: upper-right zone
|
|
||||||
-- healer houses: lower-center zone
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
t RECORD;
|
|
||||||
n RECORD;
|
|
||||||
new_building_id BIGINT;
|
|
||||||
btype TEXT;
|
|
||||||
ox DOUBLE PRECISION;
|
|
||||||
oy DOUBLE PRECISION;
|
|
||||||
npc_idx INTEGER;
|
|
||||||
BEGIN
|
|
||||||
FOR t IN SELECT id, radius FROM towns ORDER BY id LOOP
|
|
||||||
npc_idx := 0;
|
|
||||||
FOR n IN SELECT id, type FROM npcs WHERE town_id = t.id ORDER BY id LOOP
|
|
||||||
-- Determine building type from NPC type
|
|
||||||
btype := 'house.' || n.type;
|
|
||||||
|
|
||||||
-- Spread NPCs in a semicircle; scale offset by town radius
|
|
||||||
-- Each NPC gets a distinct angular position
|
|
||||||
CASE n.type
|
|
||||||
WHEN 'quest_giver' THEN
|
|
||||||
ox := -0.45 * t.radius;
|
|
||||||
oy := -0.25 * t.radius;
|
|
||||||
WHEN 'merchant' THEN
|
|
||||||
ox := 0.45 * t.radius;
|
|
||||||
oy := -0.25 * t.radius;
|
|
||||||
WHEN 'healer' THEN
|
|
||||||
ox := 0.0;
|
|
||||||
oy := 0.45 * t.radius;
|
|
||||||
ELSE
|
|
||||||
ox := npc_idx * 2.0;
|
|
||||||
oy := 0.0;
|
|
||||||
END CASE;
|
|
||||||
|
|
||||||
-- Stagger if multiple NPCs of same type (add small offset per index)
|
|
||||||
ox := ox + (npc_idx % 3) * 1.5;
|
|
||||||
|
|
||||||
INSERT INTO town_buildings (town_id, building_type, offset_x, offset_y, facing, footprint_w, footprint_h)
|
|
||||||
VALUES (t.id, btype, ox, oy, 'south', 2.5, 2.0)
|
|
||||||
RETURNING id INTO new_building_id;
|
|
||||||
|
|
||||||
-- Link NPC to their building
|
|
||||||
UPDATE npcs SET building_id = new_building_id WHERE id = n.id;
|
|
||||||
|
|
||||||
-- Move NPC offset to be at the building entrance (slightly in front)
|
|
||||||
UPDATE npcs SET offset_x = ox, offset_y = oy + 1.2 WHERE id = n.id;
|
|
||||||
|
|
||||||
npc_idx := npc_idx + 1;
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
-- Add a well decoration at town center
|
|
||||||
INSERT INTO town_buildings (town_id, building_type, offset_x, offset_y, facing, footprint_w, footprint_h)
|
|
||||||
VALUES (t.id, 'decoration.well', 0, 0, 'south', 1.5, 1.5);
|
|
||||||
|
|
||||||
-- Add a signpost near the entrance (south edge)
|
|
||||||
INSERT INTO town_buildings (town_id, building_type, offset_x, offset_y, facing, footprint_w, footprint_h)
|
|
||||||
VALUES (t.id, 'decoration.signpost', 0, 0.6 * t.radius, 'south', 0.5, 0.5);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
-- Migration 000027: Cross-roads — add shortcut roads between non-adjacent towns
|
|
||||||
-- so that from some towns there are multiple destination choices.
|
|
||||||
|
|
||||||
-- Shortcut 1: Willowdale <-> Ashengard (bypasses Mossharbor + Thornwatch + Emberwell)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1500.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Willowdale' AND t.name = 'Ashengard'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1500.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Ashengard' AND t.name = 'Willowdale'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Shortcut 2: Thornwatch <-> Frostmark (bypasses Emberwell + Ashengard)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1200.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Thornwatch' AND t.name = 'Frostmark'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1200.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Frostmark' AND t.name = 'Thornwatch'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Shortcut 3: Redcliff <-> Cinderkeep (bypasses Duskwatch + Boghollow)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1400.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Redcliff' AND t.name = 'Cinderkeep'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1400.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Cinderkeep' AND t.name = 'Redcliff'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Shortcut 4: Mossharbor <-> Emberwell (bypasses Thornwatch)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1100.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Mossharbor' AND t.name = 'Emberwell'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1100.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Emberwell' AND t.name = 'Mossharbor'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Generate waypoints for the new cross-roads (same rule as migration 000019).
|
|
||||||
INSERT INTO road_waypoints (road_id, seq, x, y)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
gs.seq,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_x
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_x
|
|
||||||
ELSE f.world_x + (t.world_x - f.world_x) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_y
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_y
|
|
||||||
ELSE f.world_y + (t.world_y - f.world_y) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END
|
|
||||||
FROM roads r
|
|
||||||
INNER JOIN towns f ON f.id = r.from_town_id
|
|
||||||
INNER JOIN towns t ON t.id = r.to_town_id
|
|
||||||
LEFT JOIN road_waypoints rw ON rw.road_id = r.id
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT GREATEST(
|
|
||||||
1,
|
|
||||||
FLOOR(
|
|
||||||
SQRT(POWER(t.world_x - f.world_x, 2) + POWER(t.world_y - f.world_y, 2)) / 20.0
|
|
||||||
)::integer
|
|
||||||
) AS nseg
|
|
||||||
) seg
|
|
||||||
CROSS JOIN LATERAL generate_series(0, seg.nseg) AS gs(seq)
|
|
||||||
WHERE rw.road_id IS NULL;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
-- Seed excursion / roadside-rest tuning into runtime_config.payload (merged with existing keys).
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"adventureStartChance": 0.0001,
|
|
||||||
"adventureCooldownMs": 300000,
|
|
||||||
"adventureOutDurationMs": 20000,
|
|
||||||
"adventureWildMinMs": 560000,
|
|
||||||
"adventureWildMaxMs": 2960000,
|
|
||||||
"adventureReturnDurationMs": 20000,
|
|
||||||
"adventureDepthWorldUnits": 20,
|
|
||||||
"adventureEncounterCooldownMs": 6000,
|
|
||||||
"adventureReturnEncounterEnabled": true,
|
|
||||||
"lowHpThreshold": 0.25,
|
|
||||||
"roadsideRestExitHp": 0.7,
|
|
||||||
"adventureRestTargetHp": 0.7,
|
|
||||||
"roadsideRestMinMs": 240000,
|
|
||||||
"roadsideRestMaxMs": 600000,
|
|
||||||
"roadsideRestHpPerSecond": 0.003,
|
|
||||||
"adventureRestHpPerSecond": 0.004
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
-- Migration 000029: More cross-roads so every town has at least three direct neighbors
|
|
||||||
-- (ring + shortcuts). Complements 000027 for hubs that still had only two outgoing roads
|
|
||||||
-- (Starfall, Duskwatch, Boghollow).
|
|
||||||
|
|
||||||
-- Starfall <-> Mossharbor (Starfall otherwise only: Cinderkeep, Willowdale)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1600.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Starfall' AND t.name = 'Mossharbor'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1600.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Mossharbor' AND t.name = 'Starfall'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Duskwatch <-> Frostmark (Duskwatch otherwise only: Redcliff, Boghollow)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1400.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Duskwatch' AND t.name = 'Frostmark'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1400.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Frostmark' AND t.name = 'Duskwatch'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Boghollow <-> Ashengard (Boghollow otherwise only: Duskwatch, Cinderkeep)
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1500.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Boghollow' AND t.name = 'Ashengard'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
INSERT INTO roads (from_town_id, to_town_id, distance)
|
|
||||||
SELECT f.id, t.id, 1500.0
|
|
||||||
FROM towns f, towns t
|
|
||||||
WHERE f.name = 'Ashengard' AND t.name = 'Boghollow'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM roads r WHERE r.from_town_id = f.id AND r.to_town_id = t.id);
|
|
||||||
|
|
||||||
-- Waypoints for new roads only (same rule as 000019 / 000027).
|
|
||||||
INSERT INTO road_waypoints (road_id, seq, x, y)
|
|
||||||
SELECT
|
|
||||||
r.id,
|
|
||||||
gs.seq,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_x
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_x
|
|
||||||
ELSE f.world_x + (t.world_x - f.world_x) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN gs.seq = 0 THEN f.world_y
|
|
||||||
WHEN gs.seq = seg.nseg THEN t.world_y
|
|
||||||
ELSE f.world_y + (t.world_y - f.world_y) * (gs.seq::double precision / seg.nseg::double precision)
|
|
||||||
END
|
|
||||||
FROM roads r
|
|
||||||
INNER JOIN towns f ON f.id = r.from_town_id
|
|
||||||
INNER JOIN towns t ON t.id = r.to_town_id
|
|
||||||
LEFT JOIN road_waypoints rw ON rw.road_id = r.id
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT GREATEST(
|
|
||||||
1,
|
|
||||||
FLOOR(
|
|
||||||
SQRT(POWER(t.world_x - f.world_x, 2) + POWER(t.world_y - f.world_y, 2)) / 20.0
|
|
||||||
)::integer
|
|
||||||
) AS nseg
|
|
||||||
) seg
|
|
||||||
CROSS JOIN LATERAL generate_series(0, seg.nseg) AS gs(seq)
|
|
||||||
WHERE rw.road_id IS NULL;
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
-- Seed combat roll + crit/block tuning into runtime_config.payload (merged with existing keys).
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"combatDamageRollMin": 0.6,
|
|
||||||
"combatDamageRollMax": 1.1,
|
|
||||||
"enemyCritChanceCap": 0.2,
|
|
||||||
"heroCritChanceCap": 0.12,
|
|
||||||
"heroBlockChancePerDefense": 0.0025,
|
|
||||||
"heroBlockChanceCap": 0.2
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
-- Adjust battle lizard regen in runtime_config.payload.
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"enemyRegenBattleLizard": 0.01
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
-- Seed buff_debuff_config.payload from model seedBuffMap / seedDebuffMap (backend/internal/model/buff_catalog.go).
|
|
||||||
-- Durations are stored in milliseconds per BuffJSON / DebuffJSON.
|
|
||||||
|
|
||||||
UPDATE buff_debuff_config
|
|
||||||
SET
|
|
||||||
payload = '{
|
|
||||||
"buffs": {
|
|
||||||
"rush": {"name": "Rush", "durationMs": 300000, "magnitude": 0.5, "cooldownMs": 900000},
|
|
||||||
"rage": {"name": "Rage", "durationMs": 180000, "magnitude": 1.0, "cooldownMs": 600000},
|
|
||||||
"shield": {"name": "Shield", "durationMs": 300000, "magnitude": 0.5, "cooldownMs": 720000},
|
|
||||||
"luck": {"name": "Luck", "durationMs": 1800000, "magnitude": 1.0, "cooldownMs": 7200000},
|
|
||||||
"resurrection": {"name": "Resurrection", "durationMs": 600000, "magnitude": 0.5, "cooldownMs": 1800000},
|
|
||||||
"heal": {"name": "Heal", "durationMs": 1000, "magnitude": 0.5, "cooldownMs": 300000},
|
|
||||||
"power_potion": {"name": "Power Potion", "durationMs": 300000, "magnitude": 1.5, "cooldownMs": 1200000},
|
|
||||||
"war_cry": {"name": "War Cry", "durationMs": 180000, "magnitude": 1.0, "cooldownMs": 600000}
|
|
||||||
},
|
|
||||||
"debuffs": {
|
|
||||||
"poison": {"name": "Poison", "durationMs": 50000, "magnitude": 0.02},
|
|
||||||
"freeze": {"name": "Freeze", "durationMs": 30000, "magnitude": 0.5},
|
|
||||||
"burn": {"name": "Burn", "durationMs": 40000, "magnitude": 0.03},
|
|
||||||
"stun": {"name": "Stun", "durationMs": 5000, "magnitude": 1.0},
|
|
||||||
"slow": {"name": "Slow", "durationMs": 40000, "magnitude": 0.4},
|
|
||||||
"weaken": {"name": "Weaken", "durationMs": 50000, "magnitude": 0.3},
|
|
||||||
"ice_slow": {"name": "Ice Slow", "durationMs": 40000, "magnitude": 0.2}
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
-- Combat balance defaults (hero scaling, pace, enemy damage, level-up cadence) + burn DoT magnitude.
|
|
||||||
-- Merges into existing JSON so other keys are preserved.
|
|
||||||
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"combatDamageScale": 0.432,
|
|
||||||
"combatDamageRollMin": 0.60,
|
|
||||||
"combatDamageRollMax": 1.10,
|
|
||||||
"enemyCombatDamageScale": 1.34,
|
|
||||||
"enemyCombatDamageRollMin": 0.82,
|
|
||||||
"enemyCombatDamageRollMax": 1.0,
|
|
||||||
"enemyDodgeChance": 0.14,
|
|
||||||
"combatPaceMultiplier": 9,
|
|
||||||
"minAttackIntervalMs": 250,
|
|
||||||
"levelUpHpEvery": 4,
|
|
||||||
"levelUpHpBase": 10,
|
|
||||||
"levelUpAtkEvery": 4,
|
|
||||||
"levelUpDefEvery": 5,
|
|
||||||
"levelUpStrEvery": 12,
|
|
||||||
"levelUpConEvery": 14,
|
|
||||||
"levelUpAgiEvery": 20,
|
|
||||||
"levelUpLuckEvery": 100,
|
|
||||||
"enemyScaleBandHp": 0.062,
|
|
||||||
"enemyScaleOvercapHp": 0.031,
|
|
||||||
"enemyScaleBandAtk": 0.044,
|
|
||||||
"enemyScaleOvercapAtk": 0.024,
|
|
||||||
"enemyScaleBandDef": 0.038,
|
|
||||||
"enemyScaleOvercapDef": 0.020
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
|
|
||||||
UPDATE buff_debuff_config
|
|
||||||
SET
|
|
||||||
payload = jsonb_set(
|
|
||||||
payload::jsonb,
|
|
||||||
'{debuffs,burn,magnitude}',
|
|
||||||
'0.018'::jsonb,
|
|
||||||
true
|
|
||||||
),
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
-- Skeleton King regen: seed runtime_config so production DB overrides legacy 0.10; in-code default is tuning.DefaultEnemyRegenSkeletonKing.
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"enemyRegenSkeletonKing": 0.04
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
-- Track which server version the player last acknowledged in the changelog UI.
|
|
||||||
ALTER TABLE heroes
|
|
||||||
ADD COLUMN IF NOT EXISTS changelog_ack_version TEXT NOT NULL DEFAULT '';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
-- Lower wandering merchant encounter weights (relative to monster weight).
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"merchantEncounterWeightBase": 0.02,
|
|
||||||
"merchantEncounterWeightRoadBonus": 0.05
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
-- Sync enemies table with server defaults (model/enemy.go): stats, narrower level bands, correct abilities.
|
|
||||||
-- Apply on staging/production so DB matches code-used templates after LoadEnemyTemplates at startup.
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Forest Wolf',
|
|
||||||
hp = 60, max_hp = 60, attack = 11, defense = 5, speed = 1.8, crit_chance = 0.05,
|
|
||||||
min_level = 1, max_level = 3, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{}', is_elite = false
|
|
||||||
WHERE type = 'wolf';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Wild Boar',
|
|
||||||
hp = 74, max_hp = 74, attack = 19, defense = 8, speed = 0.8, crit_chance = 0.08,
|
|
||||||
min_level = 2, max_level = 4, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{}', is_elite = false
|
|
||||||
WHERE type = 'boar';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Rotting Zombie',
|
|
||||||
hp = 108, max_hp = 108, attack = 17, defense = 8, speed = 0.5, crit_chance = 0.00,
|
|
||||||
min_level = 3, max_level = 6, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{poison}', is_elite = false
|
|
||||||
WHERE type = 'zombie';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Cave Spider',
|
|
||||||
hp = 44, max_hp = 44, attack = 17, defense = 4, speed = 2.0, crit_chance = 0.15,
|
|
||||||
min_level = 4, max_level = 7, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{critical}', is_elite = false
|
|
||||||
WHERE type = 'spider';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Orc Warrior',
|
|
||||||
hp = 118, max_hp = 118, attack = 22, defense = 13, speed = 1.0, crit_chance = 0.05,
|
|
||||||
min_level = 5, max_level = 9, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{burst}', is_elite = false
|
|
||||||
WHERE type = 'orc';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Skeleton Archer',
|
|
||||||
hp = 96, max_hp = 96, attack = 24, defense = 11, speed = 1.3, crit_chance = 0.06,
|
|
||||||
min_level = 6, max_level = 11, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{dodge}', is_elite = false
|
|
||||||
WHERE type = 'skeleton_archer';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Battle Lizard',
|
|
||||||
hp = 148, max_hp = 148, attack = 25, defense = 19, speed = 0.7, crit_chance = 0.03,
|
|
||||||
min_level = 7, max_level = 13, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{regen}', is_elite = false
|
|
||||||
WHERE type = 'battle_lizard';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Fire Demon',
|
|
||||||
hp = 128, max_hp = 128, attack = 24, defense = 13, speed = 1.2, crit_chance = 0.10,
|
|
||||||
min_level = 10, max_level = 15, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{burn}', is_elite = true
|
|
||||||
WHERE type = 'fire_demon';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Ice Guardian',
|
|
||||||
hp = 245, max_hp = 245, attack = 28, defense = 26, speed = 0.7, crit_chance = 0.04,
|
|
||||||
min_level = 12, max_level = 17, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{ice_slow}', is_elite = true
|
|
||||||
WHERE type = 'ice_guardian';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Skeleton King',
|
|
||||||
hp = 365, max_hp = 365, attack = 42, defense = 28, speed = 0.9, crit_chance = 0.08,
|
|
||||||
min_level = 15, max_level = 21, xp_reward = 1, gold_reward = 1,
|
|
||||||
special_abilities = '{regen,summon}', is_elite = true
|
|
||||||
WHERE type = 'skeleton_king';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Water Element',
|
|
||||||
hp = 455, max_hp = 455, attack = 37, defense = 22, speed = 0.8, crit_chance = 0.05,
|
|
||||||
min_level = 18, max_level = 24, xp_reward = 2, gold_reward = 1,
|
|
||||||
special_abilities = '{slow}', is_elite = true
|
|
||||||
WHERE type = 'water_element';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Forest Warden',
|
|
||||||
hp = 610, max_hp = 610, attack = 34, defense = 37, speed = 0.5, crit_chance = 0.03,
|
|
||||||
min_level = 20, max_level = 26, xp_reward = 2, gold_reward = 1,
|
|
||||||
special_abilities = '{regen}', is_elite = true
|
|
||||||
WHERE type = 'forest_warden';
|
|
||||||
|
|
||||||
UPDATE enemies SET
|
|
||||||
name = 'Lightning Titan',
|
|
||||||
hp = 565, max_hp = 565, attack = 49, defense = 28, speed = 1.5, crit_chance = 0.12,
|
|
||||||
min_level = 25, max_level = 32, xp_reward = 3, gold_reward = 2,
|
|
||||||
special_abilities = '{stun,chain_lightning}', is_elite = true
|
|
||||||
WHERE type = 'lightning_titan';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
-- Rebalance enemy regen: old values (e.g. 4%/s Skeleton King) healed a large fraction of MaxHP
|
|
||||||
-- between slow hero attacks; net damage could go negative. Align DB payload with tuning defaults.
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"enemyRegenDefault": 0.006,
|
|
||||||
"enemyRegenSkeletonKing": 0.003,
|
|
||||||
"enemyRegenForestWarden": 0.003,
|
|
||||||
"enemyRegenBattleLizard": 0.004
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
-- Snappier combat: halve combatPaceMultiplier (more frequent attacks) and halve hero/enemy damage scales
|
|
||||||
-- so DPS and median fight time stay in the same ballpark (DPS ~ damageScale/pace).
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"combatPaceMultiplier": 14,
|
|
||||||
"combatDamageScale": 0.216,
|
|
||||||
"enemyCombatDamageScale": 0.67
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
-- Enemy swings: longer interval only for monsters, stronger per-hit damage (~same incoming DPS).
|
|
||||||
UPDATE runtime_config
|
|
||||||
SET
|
|
||||||
payload = payload || '{
|
|
||||||
"enemyAttackIntervalMultiplier": 1.5,
|
|
||||||
"enemyCombatDamageScale": 1.0
|
|
||||||
}'::jsonb,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = TRUE;
|
|
||||||
Loading…
Reference in New Issue