feat: complete Phase 4 combat/hunting system
- Add content migration (003_content.sql): zones, monsters, items, loot tables - Add inventory migration (004_inventory.sql) and logs migration (006_logs.sql) - Add Mulberry32 PRNG (src/utils/rng.js) for deterministic combat - Add combat system (src/game/combat.js) with turn order, crit, mage bonus, 20-round cap - Add rewards/loot system (src/game/rewards.js) and leveling (src/game/leveling.js) - Add hunt endpoint (POST /api/hunt) with full combat flow in DB transaction - Add hunt integration tests and unit tests for rewards/leveling/combat - Fix session middleware to use memory store in tests (avoids cleanup hang) - Fix huntService transaction (db.transaction() returns wrapper, must call separately) - Add huntService to helpers.js cache invalidation for test isolation
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
-- 003_content.sql — Static game content: zones, monsters, items, loot tables.
|
||||
-- Seeds use INSERT ... ON CONFLICT DO UPDATE so re-running is safe.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS zones (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
min_level INTEGER NOT NULL,
|
||||
energy_cost INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS monsters (
|
||||
id INTEGER PRIMARY KEY,
|
||||
zone_id INTEGER NOT NULL REFERENCES zones(id),
|
||||
name TEXT NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
hp INTEGER NOT NULL,
|
||||
attack INTEGER NOT NULL,
|
||||
defense INTEGER NOT NULL,
|
||||
speed INTEGER NOT NULL,
|
||||
xp INTEGER NOT NULL,
|
||||
gold_min INTEGER NOT NULL,
|
||||
gold_max INTEGER NOT NULL,
|
||||
weight INTEGER NOT NULL DEFAULT 10
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('weapon','armor','trinket','consumable','material')),
|
||||
rarity TEXT NOT NULL CHECK (rarity IN ('common','uncommon','rare','epic','legendary')),
|
||||
min_level INTEGER NOT NULL DEFAULT 1,
|
||||
buy_price INTEGER NOT NULL DEFAULT 0,
|
||||
stackable INTEGER NOT NULL DEFAULT 0 CHECK (stackable IN (0,1)),
|
||||
bonus_str INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_agi INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_int INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_vit INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_attack INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_defense INTEGER NOT NULL DEFAULT 0,
|
||||
effect TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS loot_table (
|
||||
monster_id INTEGER NOT NULL REFERENCES monsters(id),
|
||||
item_id INTEGER NOT NULL REFERENCES items(id),
|
||||
chance_pct REAL NOT NULL CHECK (chance_pct > 0 AND chance_pct <= 100),
|
||||
qty_min INTEGER NOT NULL DEFAULT 1,
|
||||
qty_max INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (monster_id, item_id)
|
||||
);
|
||||
|
||||
-- ===== ZONES =====
|
||||
INSERT OR IGNORE INTO zones (id, slug, name, description, min_level, energy_cost) VALUES
|
||||
(1, 'whispering-meadows', 'Whispering Meadows', 'Rolling green hills dotted with wildflowers — and hidden goblin burrows.', 1, 5),
|
||||
(2, 'gloomroot-forest', 'Gloomroot Forest', 'A dark, ancient forest where the canopy blocks out most of the sunlight.', 5, 8),
|
||||
(3, 'ashfang-crag', 'Ashfang Crags', 'Volcanic rock and ash-filled winds. Dragons make their homes in these crags.', 10, 12);
|
||||
|
||||
-- ===== MONSTERS =====
|
||||
INSERT OR IGNORE INTO monsters (id, zone_id, name, level, hp, attack, defense, speed, xp, gold_min, gold_max, weight) VALUES
|
||||
-- Whispering Meadows
|
||||
(1, 1, 'Giant Rat', 1, 20, 6, 1, 12, 8, 1, 3, 30),
|
||||
(2, 1, 'Meadow Wolf', 2, 32, 9, 3, 16, 14, 2, 5, 20),
|
||||
(3, 1, 'Goblin Scrapper', 3, 45, 12, 5, 11, 22, 4, 9, 15),
|
||||
-- Gloomroot Forest
|
||||
(4, 2, 'Webspinner', 5, 70, 18, 8, 15, 40, 8, 16, 25),
|
||||
(5, 2, 'Bandit Cutthroat', 6, 90, 22, 12, 14, 55, 12, 24, 20),
|
||||
(6, 2, 'Mossback Treant', 8, 150, 26, 22, 7, 85, 15, 30, 10),
|
||||
-- Ashfang Crags
|
||||
(7, 3, 'Ember Imp', 10, 130, 34, 15, 20, 120, 25, 45, 25),
|
||||
(8, 3, 'Stone Golem', 12, 260, 38, 40, 6, 170, 30, 60, 10),
|
||||
(9, 3, 'Ashfang Drake', 14, 300, 50, 30, 18, 260, 50, 100, 5);
|
||||
|
||||
-- ===== ITEMS =====
|
||||
INSERT OR IGNORE INTO items (id, slug, name, description, type, rarity, min_level, buy_price, stackable, bonus_str, bonus_agi, bonus_int, bonus_vit, bonus_attack, bonus_defense, effect) VALUES
|
||||
-- Weapons (common)
|
||||
(1, 'rusty-sword', 'Rusty Sword', 'A worn blade, but still sharp enough.', 'weapon', 'common', 1, 20, 0, 0, 0, 0, 0, 3, 0, NULL),
|
||||
(2, 'apprentice-staff', 'Apprentice Staff', 'Carved by a fledgling mage.', 'weapon', 'common', 1, 20, 0, 0, 0, 0, 1, 2, 0, NULL),
|
||||
(3, 'hunters-shortbow', 'Hunters Shortbow', 'Reliable and lightweight.', 'weapon', 'common', 1, 20, 0, 0, 2, 0, 1, 0, 0, NULL),
|
||||
-- Weapons (uncommon)
|
||||
(4, 'iron-blade', 'Iron Blade', 'Sturdy and well-balanced.', 'weapon', 'uncommon', 5, 120, 0, 2, 0, 0, 0, 8, 0, NULL),
|
||||
(5, 'oakwood-staff', 'Oakwood Staff', 'Ancient oak, imbued with subtle magic.', 'weapon', 'uncommon', 5, 120, 0, 0, 0, 3, 0, 6, 0, NULL),
|
||||
(6, 'yew-longbow', 'Yew Longbow', 'Fast and accurate.', 'weapon', 'uncommon', 5, 120, 0, 0, 3, 0, 0, 6, 0, NULL),
|
||||
-- Weapons (rare)
|
||||
(7, 'emberforged-sword', 'Emberforged Greatsword', 'Forged in dragonfire.', 'weapon', 'rare', 10, 600, 0, 4, 0, 0, 0, 18, 0, NULL),
|
||||
-- Armor (common)
|
||||
(8, 'leather-jerkin', 'Leather Jerkin', 'Basic leather protection.', 'armor', 'common', 1, 25, 0, 0, 0, 0, 0, 0, 3, NULL),
|
||||
-- Armor (uncommon)
|
||||
(9, 'chainmail-vest', 'Chainmail Vest', 'Linked steel rings provide solid defense.', 'armor', 'uncommon', 5, 140, 0, 0, 0, 0, 2, 0, 9, NULL),
|
||||
-- Armor (rare)
|
||||
(10, 'drake-scale-armor', 'Drake-scale Armor', 'Scales tempered in dragonfire.', 'armor', 'rare', 12, 700, 0, 0, 0, 0, 4, 0, 22, NULL),
|
||||
-- Trinkets (common)
|
||||
(11, 'wooden-charm', 'Wooden Charm', 'A simple carved talisman.', 'trinket','common', 1, 30, 0, 0, 0, 0, 1, 0, 0, NULL),
|
||||
-- Trinkets (uncommon)
|
||||
(12, 'wolf-fang-pendant', 'Wolf-fang Pendant', 'A fearsome trophy.', 'trinket','uncommon', 3, 90, 0, 0, 2, 0, 0, 0, 0, NULL),
|
||||
-- Trinkets (rare)
|
||||
(13, 'ember-ring', 'Ember Ring', 'Warm to the touch.', 'trinket','rare', 10, 450, 0, 3, 0, 3, 0, 0, 0, NULL),
|
||||
-- Consumables
|
||||
(14, 'minor-healing-potion', 'Minor Healing Potion', 'Restores 50 HP.', 'consumable','common', 1, 15, 1, 0, 0, 0, 0, 0, 0, 'heal:50'),
|
||||
(15, 'greater-healing-potion', 'Greater Healing Potion', 'Restores 150 HP.', 'consumable','uncommon', 8, 60, 1, 0, 0, 0, 0, 0, 0, 'heal:150'),
|
||||
(16, 'energy-tonic', 'Energy Tonic', 'Restores 25 energy.', 'consumable','uncommon', 1, 80, 1, 0, 0, 0, 0, 0, 0, 'energy:25'),
|
||||
-- Materials
|
||||
(17, 'rat-tail', 'Rat Tail', 'A slimy rat tail.', 'material','common', 1, 0, 1, 0, 0, 0, 0, 0, 0, NULL),
|
||||
(18, 'wolf-pelt', 'Wolf Pelt', 'A thick wolf pelt.', 'material','common', 2, 0, 1, 0, 0, 0, 0, 0, 0, NULL),
|
||||
(19, 'spider-silk','Spider Silk', 'Gossamer strands of silk.', 'material','uncommon', 5, 0, 1, 0, 0, 0, 0, 0, 0, NULL),
|
||||
(20, 'imp-horn', 'Imp Horn', 'A curled imp horn.', 'material','uncommon', 10, 0, 1, 0, 0, 0, 0, 0, 0, NULL),
|
||||
(21, 'drake-scale','Drake Scale', 'A single scales from an Ashfang Drake.', 'material','rare', 14, 0, 1, 0, 0, 0, 0, 0, 0, NULL);
|
||||
|
||||
-- ===== LOOT TABLE =====
|
||||
-- Common materials (40% from weakest monsters in zone)
|
||||
INSERT OR IGNORE INTO loot_table (monster_id, item_id, chance_pct, qty_min, qty_max) VALUES
|
||||
(1, 17, 40, 1, 2), -- Giant Rat → Rat Tail
|
||||
(2, 18, 40, 1, 2), -- Meadow Wolf → Wolf Pelt
|
||||
(4, 19, 40, 1, 2), -- Webspinner → Spider Silk
|
||||
(7, 20, 40, 1, 2), -- Ember Imp → Imp Horn
|
||||
(9, 21, 40, 1, 2); -- Ashfang Drake → Drake Scale
|
||||
|
||||
-- Uncommon gear drops (3-5%)
|
||||
INSERT OR IGNORE INTO loot_table (monster_id, item_id, chance_pct, qty_min, qty_max) VALUES
|
||||
(3, 4, 5, 1, 1), -- Goblin Scrapper → Iron Blade
|
||||
(3, 5, 5, 1, 1), -- Goblin Scrapper → Oakwood Staff
|
||||
(3, 6, 5, 1, 1), -- Goblin Scrapper → Yew Longbow
|
||||
(6, 7, 5, 1, 1), -- Mossback Treant → Emberforged Greatsword
|
||||
(6, 9, 4, 1, 1), -- Mossback Treant → Chainmail Vest
|
||||
(9, 10, 3, 1, 1), -- Ashfang Drake → Drake-scale Armor
|
||||
(9, 13, 3, 1, 1); -- Ashfang Drake → Ember Ring
|
||||
|
||||
-- Rare gear drops (0.5-1%)
|
||||
INSERT OR IGNORE INTO loot_table (monster_id, item_id, chance_pct, qty_min, qty_max) VALUES
|
||||
(9, 7, 1, 1, 1); -- Ashfang Drake → Emberforged Greatsword (rare drop)
|
||||
|
||||
-- Healing potions (8% anywhere)
|
||||
INSERT OR IGNORE INTO loot_table (monster_id, item_id, chance_pct, qty_min, qty_max) VALUES
|
||||
(1, 14, 8, 1, 1),
|
||||
(2, 14, 8, 1, 1),
|
||||
(3, 14, 8, 1, 1),
|
||||
(4, 14, 8, 1, 1),
|
||||
(5, 14, 8, 1, 1),
|
||||
(6, 15, 8, 1, 1),
|
||||
(7, 15, 8, 1, 1),
|
||||
(8, 15, 8, 1, 1),
|
||||
(9, 15, 8, 1, 1);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 004_inventory.sql — Player inventory and equipment slots.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
item_id INTEGER NOT NULL REFERENCES items(id),
|
||||
quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity >= 1),
|
||||
equipped_slot TEXT CHECK (equipped_slot IN ('weapon', 'armor', 'trinket')),
|
||||
acquired_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_char ON inventory(character_id);
|
||||
|
||||
-- One equipped item per slot per character (partial unique index).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_equipped
|
||||
ON inventory(character_id, equipped_slot) WHERE equipped_slot IS NOT NULL;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 006_logs.sql — Combat log and audit log for security/replay.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS combat_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
monster_id INTEGER NOT NULL REFERENCES monsters(id),
|
||||
won INTEGER NOT NULL CHECK (won IN (0, 1)),
|
||||
rounds INTEGER NOT NULL,
|
||||
xp_gained INTEGER NOT NULL,
|
||||
gold_gained INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_combat_log_char ON combat_log(character_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
event TEXT NOT NULL,
|
||||
ip TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
Reference in New Issue
Block a user