work on converting to in-memory

This commit is contained in:
Sky Johnson 2025-08-13 19:15:01 -05:00
parent 71245b1655
commit 958a7098a2
9 changed files with 2822 additions and 617 deletions

226
data/drops.json Normal file
View File

@ -0,0 +1,226 @@
[
{
"id": 1,
"name": "Life Pebble",
"level": 1,
"type": 1,
"att": "maxhp,10"
},
{
"id": 2,
"name": "Life Stone",
"level": 10,
"type": 1,
"att": "maxhp,25"
},
{
"id": 3,
"name": "Life Rock",
"level": 25,
"type": 1,
"att": "maxhp,50"
},
{
"id": 4,
"name": "Magic Pebble",
"level": 1,
"type": 1,
"att": "maxmp,10"
},
{
"id": 5,
"name": "Magic Stone",
"level": 10,
"type": 1,
"att": "maxmp,25"
},
{
"id": 6,
"name": "Magic Rock",
"level": 25,
"type": 1,
"att": "maxmp,50"
},
{
"id": 7,
"name": "Dragon's Scale",
"level": 10,
"type": 1,
"att": "defensepower,25"
},
{
"id": 8,
"name": "Dragon's Plate",
"level": 30,
"type": 1,
"att": "defensepower,50"
},
{
"id": 9,
"name": "Dragon's Claw",
"level": 10,
"type": 1,
"att": "attackpower,25"
},
{
"id": 10,
"name": "Dragon's Tooth",
"level": 30,
"type": 1,
"att": "attackpower,50"
},
{
"id": 11,
"name": "Dragon's Tear",
"level": 35,
"type": 1,
"att": "strength,50"
},
{
"id": 12,
"name": "Dragon's Wing",
"level": 35,
"type": 1,
"att": "dexterity,50"
},
{
"id": 13,
"name": "Demon's Sin",
"level": 35,
"type": 1,
"att": "maxhp,-50,strength,50"
},
{
"id": 14,
"name": "Demon's Fall",
"level": 35,
"type": 1,
"att": "maxmp,-50,strength,50"
},
{
"id": 15,
"name": "Demon's Lie",
"level": 45,
"type": 1,
"att": "maxhp,-100,strength,100"
},
{
"id": 16,
"name": "Demon's Hate",
"level": 45,
"type": 1,
"att": "maxmp,-100,strength,100"
},
{
"id": 17,
"name": "Angel's Joy",
"level": 25,
"type": 1,
"att": "maxhp,25,strength,25"
},
{
"id": 18,
"name": "Angel's Rise",
"level": 30,
"type": 1,
"att": "maxhp,50,strength,50"
},
{
"id": 19,
"name": "Angel's Truth",
"level": 35,
"type": 1,
"att": "maxhp,75,strength,75"
},
{
"id": 20,
"name": "Angel's Love",
"level": 40,
"type": 1,
"att": "maxhp,100,strength,100"
},
{
"id": 21,
"name": "Seraph's Joy",
"level": 25,
"type": 1,
"att": "maxmp,25,dexterity,25"
},
{
"id": 22,
"name": "Seraph's Rise",
"level": 30,
"type": 1,
"att": "maxmp,50,dexterity,50"
},
{
"id": 23,
"name": "Seraph's Truth",
"level": 35,
"type": 1,
"att": "maxmp,75,dexterity,75"
},
{
"id": 24,
"name": "Seraph's Love",
"level": 40,
"type": 1,
"att": "maxmp,100,dexterity,100"
},
{
"id": 25,
"name": "Ruby",
"level": 50,
"type": 1,
"att": "maxhp,150"
},
{
"id": 26,
"name": "Pearl",
"level": 50,
"type": 1,
"att": "maxmp,150"
},
{
"id": 27,
"name": "Emerald",
"level": 50,
"type": 1,
"att": "strength,150"
},
{
"id": 28,
"name": "Topaz",
"level": 50,
"type": 1,
"att": "dexterity,150"
},
{
"id": 29,
"name": "Obsidian",
"level": 50,
"type": 1,
"att": "attackpower,150"
},
{
"id": 30,
"name": "Diamond",
"level": 50,
"type": 1,
"att": "defensepower,150"
},
{
"id": 31,
"name": "Memory Drop",
"level": 5,
"type": 1,
"att": "expbonus,10"
},
{
"id": 32,
"name": "Fortune Drop",
"level": 5,
"type": 1,
"att": "goldbonus,10"
}
]

266
data/items.json Normal file
View File

@ -0,0 +1,266 @@
[
{
"id": 1,
"type": 1,
"name": "Stick",
"value": 10,
"att": 2,
"special": ""
},
{
"id": 2,
"type": 1,
"name": "Branch",
"value": 30,
"att": 4,
"special": ""
},
{
"id": 3,
"type": 1,
"name": "Club",
"value": 40,
"att": 5,
"special": ""
},
{
"id": 4,
"type": 1,
"name": "Dagger",
"value": 90,
"att": 8,
"special": ""
},
{
"id": 5,
"type": 1,
"name": "Hatchet",
"value": 150,
"att": 12,
"special": ""
},
{
"id": 6,
"type": 1,
"name": "Axe",
"value": 200,
"att": 16,
"special": ""
},
{
"id": 7,
"type": 1,
"name": "Brand",
"value": 300,
"att": 25,
"special": ""
},
{
"id": 8,
"type": 1,
"name": "Poleaxe",
"value": 500,
"att": 35,
"special": ""
},
{
"id": 9,
"type": 1,
"name": "Broadsword",
"value": 800,
"att": 45,
"special": ""
},
{
"id": 10,
"type": 1,
"name": "Battle Axe",
"value": 1200,
"att": 50,
"special": ""
},
{
"id": 11,
"type": 1,
"name": "Claymore",
"value": 2000,
"att": 60,
"special": ""
},
{
"id": 12,
"type": 1,
"name": "Dark Axe",
"value": 3000,
"att": 100,
"special": "expbonus,-5"
},
{
"id": 13,
"type": 1,
"name": "Dark Sword",
"value": 4500,
"att": 125,
"special": "expbonus,-10"
},
{
"id": 14,
"type": 1,
"name": "Bright Sword",
"value": 6000,
"att": 100,
"special": "expbonus,10"
},
{
"id": 15,
"type": 1,
"name": "Magic Sword",
"value": 10000,
"att": 150,
"special": "maxmp,50"
},
{
"id": 16,
"type": 1,
"name": "Destiny Blade",
"value": 50000,
"att": 250,
"special": "strength,50"
},
{
"id": 17,
"type": 2,
"name": "Skivvies",
"value": 25,
"att": 2,
"special": "goldbonus,10"
},
{
"id": 18,
"type": 2,
"name": "Clothes",
"value": 50,
"att": 5,
"special": ""
},
{
"id": 19,
"type": 2,
"name": "Leather Armor",
"value": 75,
"att": 10,
"special": ""
},
{
"id": 20,
"type": 2,
"name": "Hard Leather Armor",
"value": 150,
"att": 25,
"special": ""
},
{
"id": 21,
"type": 2,
"name": "Chain Mail",
"value": 300,
"att": 30,
"special": ""
},
{
"id": 22,
"type": 2,
"name": "Bronze Plate",
"value": 900,
"att": 50,
"special": ""
},
{
"id": 23,
"type": 2,
"name": "Iron Plate",
"value": 2000,
"att": 100,
"special": ""
},
{
"id": 24,
"type": 2,
"name": "Magic Armor",
"value": 4000,
"att": 125,
"special": "maxmp,50"
},
{
"id": 25,
"type": 2,
"name": "Dark Armor",
"value": 5000,
"att": 150,
"special": "expbonus,-10"
},
{
"id": 26,
"type": 2,
"name": "Bright Armor",
"value": 10000,
"att": 175,
"special": "expbonus,10"
},
{
"id": 27,
"type": 2,
"name": "Destiny Raiment",
"value": 50000,
"att": 200,
"special": "dexterity,50"
},
{
"id": 28,
"type": 3,
"name": "Reed Shield",
"value": 50,
"att": 2,
"special": ""
},
{
"id": 29,
"type": 3,
"name": "Buckler",
"value": 100,
"att": 4,
"special": ""
},
{
"id": 30,
"type": 3,
"name": "Small Shield",
"value": 500,
"att": 10,
"special": ""
},
{
"id": 31,
"type": 3,
"name": "Large Shield",
"value": 2500,
"att": 30,
"special": ""
},
{
"id": 32,
"type": 3,
"name": "Silver Shield",
"value": 10000,
"att": 60,
"special": ""
},
{
"id": 33,
"type": 3,
"name": "Destiny Aegis",
"value": 25000,
"att": 100,
"special": "maxhp,50"
}
]

1663
data/monsters.json Normal file

File diff suppressed because it is too large Load Diff

135
data/spells.json Normal file
View File

@ -0,0 +1,135 @@
[
{
"id": 1,
"name": "Heal",
"mp": 5,
"attribute": 10,
"type": 1
},
{
"id": 2,
"name": "Revive",
"mp": 10,
"attribute": 25,
"type": 1
},
{
"id": 3,
"name": "Life",
"mp": 25,
"attribute": 50,
"type": 1
},
{
"id": 4,
"name": "Breath",
"mp": 50,
"attribute": 100,
"type": 1
},
{
"id": 5,
"name": "Gaia",
"mp": 75,
"attribute": 150,
"type": 1
},
{
"id": 6,
"name": "Hurt",
"mp": 5,
"attribute": 15,
"type": 2
},
{
"id": 7,
"name": "Pain",
"mp": 12,
"attribute": 35,
"type": 2
},
{
"id": 8,
"name": "Maim",
"mp": 25,
"attribute": 70,
"type": 2
},
{
"id": 9,
"name": "Rend",
"mp": 40,
"attribute": 100,
"type": 2
},
{
"id": 10,
"name": "Chaos",
"mp": 50,
"attribute": 130,
"type": 2
},
{
"id": 11,
"name": "Sleep",
"mp": 10,
"attribute": 5,
"type": 3
},
{
"id": 12,
"name": "Dream",
"mp": 30,
"attribute": 9,
"type": 3
},
{
"id": 13,
"name": "Nightmare",
"mp": 60,
"attribute": 13,
"type": 3
},
{
"id": 14,
"name": "Craze",
"mp": 10,
"attribute": 10,
"type": 4
},
{
"id": 15,
"name": "Rage",
"mp": 20,
"attribute": 25,
"type": 4
},
{
"id": 16,
"name": "Fury",
"mp": 30,
"attribute": 50,
"type": 4
},
{
"id": 17,
"name": "Ward",
"mp": 10,
"attribute": 10,
"type": 5
},
{
"id": 18,
"name": "Fend",
"mp": 20,
"attribute": 25,
"type": 5
},
{
"id": 19,
"name": "Barrier",
"mp": 30,
"attribute": 50,
"type": 5
}
]

82
data/towns.json Normal file
View File

@ -0,0 +1,82 @@
[
{
"id": 1,
"name": "Midworld",
"x": 0,
"y": 0,
"inn_cost": 5,
"map_cost": 0,
"tp_cost": 0,
"shop_list": "1,2,3,17,18,19,28,29"
},
{
"id": 2,
"name": "Roma",
"x": 30,
"y": 30,
"inn_cost": 10,
"map_cost": 25,
"tp_cost": 5,
"shop_list": "2,3,4,18,19,29"
},
{
"id": 3,
"name": "Bris",
"x": 70,
"y": -70,
"inn_cost": 25,
"map_cost": 50,
"tp_cost": 15,
"shop_list": "2,3,4,5,18,19,20,29.30"
},
{
"id": 4,
"name": "Kalle",
"x": -100,
"y": 100,
"inn_cost": 40,
"map_cost": 100,
"tp_cost": 30,
"shop_list": "5,6,8,10,12,21,22,23,29,30"
},
{
"id": 5,
"name": "Narcissa",
"x": -130,
"y": -130,
"inn_cost": 60,
"map_cost": 500,
"tp_cost": 50,
"shop_list": "4,7,9,11,13,21,22,23,29,30,31"
},
{
"id": 6,
"name": "Hambry",
"x": 170,
"y": 170,
"inn_cost": 90,
"map_cost": 1000,
"tp_cost": 80,
"shop_list": "10,11,12,13,14,23,24,30,31"
},
{
"id": 7,
"name": "Gilead",
"x": 200,
"y": -200,
"inn_cost": 100,
"map_cost": 3000,
"tp_cost": 110,
"shop_list": "12,13,14,15,24,25,26,32"
},
{
"id": 8,
"name": "Endworld",
"x": -250,
"y": -250,
"inn_cost": 125,
"map_cost": 9000,
"tp_cost": 160,
"shop_list": "16,27,33"
}
]

View File

@ -1,500 +0,0 @@
// package install is the home of the install command
//
// Its purpose is to set up the intial database structure and data,
// then create a "demo" user to act as the initial admin account.
//
// At the moment, it simply creates a static structure and admin user;
// in the future I'd like to add migrations and prompt for account
// creation.
package install
import (
"fmt"
"time"
"dk/internal/database"
"dk/internal/password"
"dk/internal/users"
)
const dbPath = "dk.db"
func Run() error {
fmt.Println("Dragon Knight Installation")
fmt.Println("==========================")
start := time.Now()
if err := database.Init("dk.db"); err != nil {
return fmt.Errorf("failed to initialize database: %w", err)
}
defer database.Close()
if err := createTables(); err != nil {
return fmt.Errorf("failed to create tables: %w", err)
}
if err := populateData(); err != nil {
return fmt.Errorf("failed to populate data: %w", err)
}
if err := createDemoUser(); err != nil {
return fmt.Errorf("failed to create demo user: %w", err)
}
elapsed := time.Since(start)
fmt.Printf("\nDatabase setup complete in %.4f seconds.\n", elapsed.Seconds())
fmt.Println("Demo user created:")
fmt.Println(" Username: demo")
fmt.Println(" Password: Demo123!")
fmt.Println(" Email: demo@demo.com")
fmt.Println("\nInstallation complete!")
return nil
}
func createTables() error {
tables := []struct {
name string
sql string
}{
{"babble", `CREATE TABLE babble (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
author TEXT NOT NULL DEFAULT '',
babble TEXT NOT NULL DEFAULT ''
)`},
{"control", `CREATE TABLE control (
id INTEGER PRIMARY KEY AUTOINCREMENT,
world_size INTEGER NOT NULL DEFAULT 250,
open INTEGER NOT NULL DEFAULT 1,
admin_email TEXT NOT NULL DEFAULT '',
class_1_name TEXT NOT NULL DEFAULT '',
class_2_name TEXT NOT NULL DEFAULT '',
class_3_name TEXT NOT NULL DEFAULT ''
)`},
{"drops", `CREATE TABLE drops (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
level INTEGER NOT NULL DEFAULT 0,
type INTEGER NOT NULL DEFAULT 0,
att TEXT NOT NULL DEFAULT ''
)`},
{"forum", `CREATE TABLE forum (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
last_post INTEGER NOT NULL DEFAULT (unixepoch()),
author INTEGER NOT NULL,
parent INTEGER NOT NULL DEFAULT 0,
replies INTEGER NOT NULL DEFAULT 0,
title TEXT NOT NULL,
content TEXT NOT NULL
)`},
{"items", `CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL,
value INTEGER NOT NULL DEFAULT 0,
att INTEGER NOT NULL DEFAULT 0,
special TEXT NOT NULL DEFAULT ''
)`},
{"monsters", `CREATE TABLE monsters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
max_hp INTEGER NOT NULL DEFAULT 0,
max_dmg INTEGER NOT NULL DEFAULT 0,
armor INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
max_exp INTEGER NOT NULL DEFAULT 0,
max_gold INTEGER NOT NULL DEFAULT 0,
immune INTEGER NOT NULL DEFAULT 0
)`},
{"news", `CREATE TABLE news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author INTEGER NOT NULL,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
content TEXT NOT NULL
)`},
{"spells", `CREATE TABLE spells (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
mp INTEGER NOT NULL DEFAULT 0,
attribute INTEGER NOT NULL DEFAULT 0,
type INTEGER NOT NULL DEFAULT 0
)`},
{"towns", `CREATE TABLE towns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
x INTEGER NOT NULL DEFAULT 0,
y INTEGER NOT NULL DEFAULT 0,
inn_cost INTEGER NOT NULL DEFAULT 0,
map_cost INTEGER NOT NULL DEFAULT 0,
tp_cost INTEGER NOT NULL DEFAULT 0,
shop_list TEXT NOT NULL DEFAULT ''
)`},
{"users", `CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
verified INTEGER NOT NULL DEFAULT 0,
token TEXT NOT NULL DEFAULT '',
registered INTEGER NOT NULL DEFAULT (unixepoch()),
last_online INTEGER NOT NULL DEFAULT (unixepoch()),
auth INTEGER NOT NULL DEFAULT 0,
x INTEGER NOT NULL DEFAULT 0,
y INTEGER NOT NULL DEFAULT 0,
class_id INTEGER NOT NULL DEFAULT 0,
currently TEXT NOT NULL DEFAULT 'In Town',
fighting INTEGER NOT NULL DEFAULT 0,
monster_id INTEGER NOT NULL DEFAULT 0,
monster_hp INTEGER NOT NULL DEFAULT 0,
monster_sleep INTEGER NOT NULL DEFAULT 0,
monster_immune INTEGER NOT NULL DEFAULT 0,
uber_damage INTEGER NOT NULL DEFAULT 0,
uber_defense INTEGER NOT NULL DEFAULT 0,
hp INTEGER NOT NULL DEFAULT 15,
mp INTEGER NOT NULL DEFAULT 0,
tp INTEGER NOT NULL DEFAULT 10,
max_hp INTEGER NOT NULL DEFAULT 15,
max_mp INTEGER NOT NULL DEFAULT 0,
max_tp INTEGER NOT NULL DEFAULT 10,
level INTEGER NOT NULL DEFAULT 1,
gold INTEGER NOT NULL DEFAULT 100,
exp INTEGER NOT NULL DEFAULT 0,
gold_bonus INTEGER NOT NULL DEFAULT 0,
exp_bonus INTEGER NOT NULL DEFAULT 0,
strength INTEGER NOT NULL DEFAULT 5,
dexterity INTEGER NOT NULL DEFAULT 5,
attack INTEGER NOT NULL DEFAULT 5,
defense INTEGER NOT NULL DEFAULT 5,
weapon_id INTEGER NOT NULL DEFAULT 0,
armor_id INTEGER NOT NULL DEFAULT 0,
shield_id INTEGER NOT NULL DEFAULT 0,
slot_1_id INTEGER NOT NULL DEFAULT 0,
slot_2_id INTEGER NOT NULL DEFAULT 0,
slot_3_id INTEGER NOT NULL DEFAULT 0,
weapon_name TEXT NOT NULL DEFAULT '',
armor_name TEXT NOT NULL DEFAULT '',
shield_name TEXT NOT NULL DEFAULT '',
slot_1_name TEXT NOT NULL DEFAULT '',
slot_2_name TEXT NOT NULL DEFAULT '',
slot_3_name TEXT NOT NULL DEFAULT '',
drop_code INTEGER NOT NULL DEFAULT 0,
spells TEXT NOT NULL DEFAULT '',
towns TEXT NOT NULL DEFAULT ''
)`},
}
for _, table := range tables {
if err := database.Exec(table.sql); err != nil {
return fmt.Errorf("failed to create %s table: %w", table.name, err)
}
fmt.Printf("✓ %s table created\n", table.name)
}
return nil
}
func populateData() error {
if err := database.Exec("INSERT INTO control VALUES (1, 250, 1, '', 'Mage', 'Warrior', 'Paladin')"); err != nil {
return fmt.Errorf("failed to populate control table: %w", err)
}
fmt.Println("✓ control table populated")
dropsSQL := `INSERT INTO drops VALUES
(1, 'Life Pebble', 1, 1, 'maxhp,10'),
(2, 'Life Stone', 10, 1, 'maxhp,25'),
(3, 'Life Rock', 25, 1, 'maxhp,50'),
(4, 'Magic Pebble', 1, 1, 'maxmp,10'),
(5, 'Magic Stone', 10, 1, 'maxmp,25'),
(6, 'Magic Rock', 25, 1, 'maxmp,50'),
(7, 'Dragon''s Scale', 10, 1, 'defensepower,25'),
(8, 'Dragon''s Plate', 30, 1, 'defensepower,50'),
(9, 'Dragon''s Claw', 10, 1, 'attackpower,25'),
(10, 'Dragon''s Tooth', 30, 1, 'attackpower,50'),
(11, 'Dragon''s Tear', 35, 1, 'strength,50'),
(12, 'Dragon''s Wing', 35, 1, 'dexterity,50'),
(13, 'Demon''s Sin', 35, 1, 'maxhp,-50,strength,50'),
(14, 'Demon''s Fall', 35, 1, 'maxmp,-50,strength,50'),
(15, 'Demon''s Lie', 45, 1, 'maxhp,-100,strength,100'),
(16, 'Demon''s Hate', 45, 1, 'maxmp,-100,strength,100'),
(17, 'Angel''s Joy', 25, 1, 'maxhp,25,strength,25'),
(18, 'Angel''s Rise', 30, 1, 'maxhp,50,strength,50'),
(19, 'Angel''s Truth', 35, 1, 'maxhp,75,strength,75'),
(20, 'Angel''s Love', 40, 1, 'maxhp,100,strength,100'),
(21, 'Seraph''s Joy', 25, 1, 'maxmp,25,dexterity,25'),
(22, 'Seraph''s Rise', 30, 1, 'maxmp,50,dexterity,50'),
(23, 'Seraph''s Truth', 35, 1, 'maxmp,75,dexterity,75'),
(24, 'Seraph''s Love', 40, 1, 'maxmp,100,dexterity,100'),
(25, 'Ruby', 50, 1, 'maxhp,150'),
(26, 'Pearl', 50, 1, 'maxmp,150'),
(27, 'Emerald', 50, 1, 'strength,150'),
(28, 'Topaz', 50, 1, 'dexterity,150'),
(29, 'Obsidian', 50, 1, 'attackpower,150'),
(30, 'Diamond', 50, 1, 'defensepower,150'),
(31, 'Memory Drop', 5, 1, 'expbonus,10'),
(32, 'Fortune Drop', 5, 1, 'goldbonus,10')`
if err := database.Exec(dropsSQL); err != nil {
return fmt.Errorf("failed to populate drops table: %w", err)
}
fmt.Println("✓ drops table populated")
itemsSQL := `INSERT INTO items VALUES
(1, 1, 'Stick', 10, 2, ''),
(2, 1, 'Branch', 30, 4, ''),
(3, 1, 'Club', 40, 5, ''),
(4, 1, 'Dagger', 90, 8, ''),
(5, 1, 'Hatchet', 150, 12, ''),
(6, 1, 'Axe', 200, 16, ''),
(7, 1, 'Brand', 300, 25, ''),
(8, 1, 'Poleaxe', 500, 35, ''),
(9, 1, 'Broadsword', 800, 45, ''),
(10, 1, 'Battle Axe', 1200, 50, ''),
(11, 1, 'Claymore', 2000, 60, ''),
(12, 1, 'Dark Axe', 3000, 100, 'expbonus,-5'),
(13, 1, 'Dark Sword', 4500, 125, 'expbonus,-10'),
(14, 1, 'Bright Sword', 6000, 100, 'expbonus,10'),
(15, 1, 'Magic Sword', 10000, 150, 'maxmp,50'),
(16, 1, 'Destiny Blade', 50000, 250, 'strength,50'),
(17, 2, 'Skivvies', 25, 2, 'goldbonus,10'),
(18, 2, 'Clothes', 50, 5, ''),
(19, 2, 'Leather Armor', 75, 10, ''),
(20, 2, 'Hard Leather Armor', 150, 25, ''),
(21, 2, 'Chain Mail', 300, 30, ''),
(22, 2, 'Bronze Plate', 900, 50, ''),
(23, 2, 'Iron Plate', 2000, 100, ''),
(24, 2, 'Magic Armor', 4000, 125, 'maxmp,50'),
(25, 2, 'Dark Armor', 5000, 150, 'expbonus,-10'),
(26, 2, 'Bright Armor', 10000, 175, 'expbonus,10'),
(27, 2, 'Destiny Raiment', 50000, 200, 'dexterity,50'),
(28, 3, 'Reed Shield', 50, 2, ''),
(29, 3, 'Buckler', 100, 4, ''),
(30, 3, 'Small Shield', 500, 10, ''),
(31, 3, 'Large Shield', 2500, 30, ''),
(32, 3, 'Silver Shield', 10000, 60, ''),
(33, 3, 'Destiny Aegis', 25000, 100, 'maxhp,50')`
if err := database.Exec(itemsSQL); err != nil {
return fmt.Errorf("failed to populate items table: %w", err)
}
fmt.Println("✓ items table populated")
monstersSQL := `INSERT INTO monsters VALUES
(1, 'Blue Slime', 4, 3, 1, 1, 1, 1, 0),
(2, 'Red Slime', 6, 5, 1, 1, 2, 1, 0),
(3, 'Critter', 6, 5, 2, 1, 4, 2, 0),
(4, 'Creature', 10, 8, 2, 2, 4, 2, 0),
(5, 'Shadow', 10, 9, 3, 2, 6, 2, 1),
(6, 'Drake', 11, 10, 3, 2, 8, 3, 0),
(7, 'Shade', 12, 10, 3, 3, 10, 3, 1),
(8, 'Drakelor', 14, 12, 4, 3, 10, 3, 0),
(9, 'Silver Slime', 15, 100, 200, 30, 15, 1000, 2),
(10, 'Scamp', 16, 13, 5, 4, 15, 5, 0),
(11, 'Raven', 16, 13, 5, 4, 18, 6, 0),
(12, 'Scorpion', 18, 14, 6, 5, 20, 7, 0),
(13, 'Illusion', 20, 15, 6, 5, 20, 7, 1),
(14, 'Nightshade', 22, 16, 6, 6, 24, 8, 0),
(15, 'Drakemal', 22, 18, 7, 6, 24, 8, 0),
(16, 'Shadow Raven', 24, 18, 7, 6, 26, 9, 1),
(17, 'Ghost', 24, 20, 8, 6, 28, 9, 0),
(18, 'Frost Raven', 26, 20, 8, 7, 30, 10, 0),
(19, 'Rogue Scorpion', 28, 22, 9, 7, 32, 11, 0),
(20, 'Ghoul', 29, 24, 9, 7, 34, 11, 0),
(21, 'Magician', 30, 24, 10, 8, 36, 12, 0),
(22, 'Rogue', 30, 25, 12, 8, 40, 13, 0),
(23, 'Drakefin', 32, 26, 12, 8, 40, 13, 0),
(24, 'Shimmer', 32, 26, 14, 8, 45, 15, 1),
(25, 'Fire Raven', 34, 28, 14, 9, 45, 15, 0),
(26, 'Dybbuk', 34, 28, 14, 9, 50, 17, 0),
(27, 'Knave', 36, 30, 15, 9, 52, 17, 0),
(28, 'Goblin', 36, 30, 15, 10, 54, 18, 0),
(29, 'Skeleton', 38, 30, 18, 10, 58, 19, 0),
(30, 'Dark Slime', 38, 32, 18, 10, 62, 21, 0),
(31, 'Silver Scorpion', 30, 160, 350, 40, 63, 2000, 2),
(32, 'Mirage', 40, 32, 20, 11, 64, 21, 1),
(33, 'Sorceror', 41, 33, 22, 11, 68, 23, 0),
(34, 'Imp', 42, 34, 22, 12, 70, 23, 0),
(35, 'Nymph', 43, 35, 22, 12, 70, 23, 0),
(36, 'Scoundrel', 43, 35, 22, 12, 75, 25, 0),
(37, 'Megaskeleton', 44, 36, 24, 13, 78, 26, 0),
(38, 'Grey Wolf', 44, 36, 24, 13, 82, 27, 0),
(39, 'Phantom', 46, 38, 24, 14, 85, 28, 1),
(40, 'Specter', 46, 38, 24, 14, 90, 30, 0),
(41, 'Dark Scorpion', 48, 40, 26, 15, 95, 32, 1),
(42, 'Warlock', 48, 40, 26, 15, 100, 33, 1),
(43, 'Orc', 49, 42, 28, 15, 104, 35, 0),
(44, 'Sylph', 49, 42, 28, 15, 106, 35, 0),
(45, 'Wraith', 50, 45, 30, 16, 108, 36, 0),
(46, 'Hellion', 50, 45, 30, 16, 110, 37, 0),
(47, 'Bandit', 52, 45, 30, 16, 114, 38, 0),
(48, 'Ultraskeleton', 52, 46, 32, 16, 116, 39, 0),
(49, 'Dark Wolf', 54, 47, 36, 17, 120, 40, 1),
(50, 'Troll', 56, 48, 36, 17, 120, 40, 0),
(51, 'Werewolf', 56, 48, 38, 17, 124, 41, 0),
(52, 'Hellcat', 58, 50, 38, 18, 128, 43, 0),
(53, 'Spirit', 58, 50, 38, 18, 132, 44, 0),
(54, 'Nisse', 60, 52, 40, 19, 132, 44, 0),
(55, 'Dawk', 60, 54, 40, 19, 136, 45, 0),
(56, 'Figment', 64, 55, 42, 19, 140, 47, 1),
(57, 'Hellhound', 66, 56, 44, 20, 140, 47, 0),
(58, 'Wizard', 66, 56, 44, 20, 144, 48, 0),
(59, 'Uruk', 68, 58, 44, 20, 146, 49, 0),
(60, 'Siren', 68, 400, 800, 50, 10000, 50, 2),
(61, 'Megawraith', 70, 60, 46, 21, 155, 52, 0),
(62, 'Dawkin', 70, 60, 46, 21, 155, 52, 0),
(63, 'Grey Bear', 70, 62, 48, 21, 160, 53, 0),
(64, 'Haunt', 72, 62, 48, 22, 160, 53, 0),
(65, 'Hellbeast', 74, 64, 50, 22, 165, 55, 0),
(66, 'Fear', 76, 66, 52, 23, 165, 55, 0),
(67, 'Beast', 76, 66, 52, 23, 170, 57, 0),
(68, 'Ogre', 78, 68, 54, 23, 170, 57, 0),
(69, 'Dark Bear', 80, 70, 56, 24, 175, 58, 1),
(70, 'Fire', 80, 72, 56, 24, 175, 58, 0),
(71, 'Polgergeist', 84, 74, 58, 25, 180, 60, 0),
(72, 'Fright', 86, 76, 58, 25, 180, 60, 0),
(73, 'Lycan', 88, 78, 60, 25, 185, 62, 0),
(74, 'Terra Elemental', 88, 80, 62, 25, 185, 62, 1),
(75, 'Necromancer', 90, 80, 62, 26, 190, 63, 0),
(76, 'Ultrawraith', 90, 82, 64, 26, 190, 63, 0),
(77, 'Dawkor', 92, 82, 64, 26, 195, 65, 0),
(78, 'Werebear', 92, 84, 65, 26, 195, 65, 0),
(79, 'Brute', 94, 84, 65, 27, 200, 67, 0),
(80, 'Large Beast', 96, 88, 66, 27, 200, 67, 0),
(81, 'Horror', 96, 88, 68, 27, 210, 70, 0),
(82, 'Flame', 100, 90, 70, 28, 210, 70, 0),
(83, 'Lycanthor', 100, 90, 70, 28, 210, 70, 0),
(84, 'Wyrm', 100, 92, 72, 28, 220, 73, 0),
(85, 'Aero Elemental', 104, 94, 74, 29, 220, 73, 1),
(86, 'Dawkare', 106, 96, 76, 29, 220, 73, 0),
(87, 'Large Brute', 108, 98, 78, 29, 230, 77, 0),
(88, 'Frost Wyrm', 110, 100, 80, 30, 230, 77, 0),
(89, 'Knight', 110, 102, 80, 30, 240, 80, 0),
(90, 'Lycanthra', 112, 104, 82, 30, 240, 80, 0),
(91, 'Terror', 115, 108, 84, 31, 250, 83, 0),
(92, 'Blaze', 118, 108, 84, 31, 250, 83, 0),
(93, 'Aqua Elemental', 120, 110, 90, 31, 260, 87, 1),
(94, 'Fire Wyrm', 120, 110, 90, 32, 260, 87, 0),
(95, 'Lesser Wyvern', 122, 110, 92, 32, 270, 90, 0),
(96, 'Doomer', 124, 112, 92, 32, 270, 90, 0),
(97, 'Armor Knight', 130, 115, 95, 33, 280, 93, 0),
(98, 'Wyvern', 134, 120, 95, 33, 290, 97, 0),
(99, 'Nightmare', 138, 125, 100, 33, 300, 100, 0),
(100, 'Fira Elemental', 140, 125, 100, 34, 310, 103, 1),
(101, 'Megadoomer', 140, 128, 105, 34, 320, 107, 0),
(102, 'Greater Wyvern', 145, 130, 105, 34, 335, 112, 0),
(103, 'Advocate', 148, 132, 108, 35, 350, 117, 0),
(104, 'Strong Knight', 150, 135, 110, 35, 365, 122, 0),
(105, 'Liche', 150, 135, 110, 35, 380, 127, 0),
(106, 'Ultradoomer', 155, 140, 115, 36, 395, 132, 0),
(107, 'Fanatic', 160, 140, 115, 36, 410, 137, 0),
(108, 'Green Dragon', 160, 140, 115, 36, 425, 142, 0),
(109, 'Fiend', 160, 145, 120, 37, 445, 148, 0),
(110, 'Greatest Wyvern', 162, 150, 120, 37, 465, 155, 0),
(111, 'Lesser Devil', 164, 150, 120, 37, 485, 162, 0),
(112, 'Liche Master', 168, 155, 125, 38, 505, 168, 0),
(113, 'Zealot', 168, 155, 125, 38, 530, 177, 0),
(114, 'Serafiend', 170, 155, 125, 38, 555, 185, 0),
(115, 'Pale Knight', 175, 160, 130, 39, 580, 193, 0),
(116, 'Blue Dragon', 180, 160, 130, 39, 605, 202, 0),
(117, 'Obsessive', 180, 160, 135, 40, 630, 210, 0),
(118, 'Devil', 184, 164, 135, 40, 666, 222, 0),
(119, 'Liche Prince', 190, 168, 138, 40, 660, 220, 0),
(120, 'Cherufiend', 195, 170, 140, 41, 690, 230, 0),
(121, 'Red Dragon', 200, 180, 145, 41, 720, 240, 0),
(122, 'Greater Devil', 200, 180, 145, 41, 750, 250, 0),
(123, 'Renegade', 205, 185, 150, 42, 780, 260, 0),
(124, 'Archfiend', 210, 190, 150, 42, 810, 270, 0),
(125, 'Liche Lord', 210, 190, 155, 42, 850, 283, 0),
(126, 'Greatest Devil', 215, 195, 160, 43, 890, 297, 0),
(127, 'Dark Knight', 220, 200, 160, 43, 930, 310, 0),
(128, 'Giant', 220, 200, 165, 43, 970, 323, 0),
(129, 'Shadow Dragon', 225, 200, 170, 44, 1010, 337, 0),
(130, 'Liche King', 225, 205, 170, 44, 1050, 350, 0),
(131, 'Incubus', 230, 205, 175, 44, 1100, 367, 1),
(132, 'Traitor', 230, 205, 175, 45, 1150, 383, 0),
(133, 'Demon', 240, 210, 180, 45, 1200, 400, 0),
(134, 'Dark Dragon', 245, 215, 180, 45, 1250, 417, 1),
(135, 'Insurgent', 250, 220, 190, 46, 1300, 433, 0),
(136, 'Leviathan', 255, 225, 190, 46, 1350, 450, 0),
(137, 'Grey Daemon', 260, 230, 190, 46, 1400, 467, 0),
(138, 'Succubus', 265, 240, 200, 47, 1460, 487, 1),
(139, 'Demon Prince', 270, 240, 200, 47, 1520, 507, 0),
(140, 'Black Dragon', 275, 250, 205, 47, 1580, 527, 1),
(141, 'Nihilist', 280, 250, 205, 47, 1640, 547, 0),
(142, 'Behemoth', 285, 260, 210, 48, 1700, 567, 0),
(143, 'Demagogue', 290, 260, 210, 48, 1760, 587, 0),
(144, 'Demon Lord', 300, 270, 220, 48, 1820, 607, 0),
(145, 'Red Daemon', 310, 280, 230, 48, 1880, 627, 0),
(146, 'Colossus', 320, 300, 240, 49, 1940, 647, 0),
(147, 'Demon King', 330, 300, 250, 49, 2000, 667, 0),
(148, 'Dark Daemon', 340, 320, 260, 49, 2200, 733, 1),
(149, 'Titan', 360, 340, 270, 50, 2400, 800, 0),
(150, 'Black Daemon', 400, 400, 280, 50, 3000, 1000, 1),
(151, 'Lucifuge', 600, 600, 400, 50, 10000, 10000, 2)`
if err := database.Exec(monstersSQL); err != nil {
return fmt.Errorf("failed to populate monsters table: %w", err)
}
fmt.Println("✓ monsters table populated")
if err := database.Exec("INSERT INTO news (author, content) VALUES (1, 'Welcome to Dragon Knight! This is your first news post.')"); err != nil {
return fmt.Errorf("failed to populate news table: %w", err)
}
fmt.Println("✓ news table populated")
spellsSQL := `INSERT INTO spells VALUES
(1, 'Heal', 5, 10, 1),
(2, 'Revive', 10, 25, 1),
(3, 'Life', 25, 50, 1),
(4, 'Breath', 50, 100, 1),
(5, 'Gaia', 75, 150, 1),
(6, 'Hurt', 5, 15, 2),
(7, 'Pain', 12, 35, 2),
(8, 'Maim', 25, 70, 2),
(9, 'Rend', 40, 100, 2),
(10, 'Chaos', 50, 130, 2),
(11, 'Sleep', 10, 5, 3),
(12, 'Dream', 30, 9, 3),
(13, 'Nightmare', 60, 13, 3),
(14, 'Craze', 10, 10, 4),
(15, 'Rage', 20, 25, 4),
(16, 'Fury', 30, 50, 4),
(17, 'Ward', 10, 10, 5),
(18, 'Fend', 20, 25, 5),
(19, 'Barrier', 30, 50, 5)`
if err := database.Exec(spellsSQL); err != nil {
return fmt.Errorf("failed to populate spells table: %w", err)
}
fmt.Println("✓ spells table populated")
townsSQL := `INSERT INTO towns VALUES
(1, 'Midworld', 0, 0, 5, 0, 0, '1,2,3,17,18,19,28,29'),
(2, 'Roma', 30, 30, 10, 25, 5, '2,3,4,18,19,29'),
(3, 'Bris', 70, -70, 25, 50, 15, '2,3,4,5,18,19,20,29.30'),
(4, 'Kalle', -100, 100, 40, 100, 30, '5,6,8,10,12,21,22,23,29,30'),
(5, 'Narcissa', -130, -130, 60, 500, 50, '4,7,9,11,13,21,22,23,29,30,31'),
(6, 'Hambry', 170, 170, 90, 1000, 80, '10,11,12,13,14,23,24,30,31'),
(7, 'Gilead', 200, -200, 100, 3000, 110, '12,13,14,15,24,25,26,32'),
(8, 'Endworld', -250, -250, 125, 9000, 160, '16,27,33')`
if err := database.Exec(townsSQL); err != nil {
return fmt.Errorf("failed to populate towns table: %w", err)
}
fmt.Println("✓ towns table populated")
return nil
}
func createDemoUser() error {
user := users.New()
user.Username = "demo"
user.Email = "demo@demo.com"
user.Password = password.Hash("Demo123!")
user.ClassID = 1
user.Auth = 4
if err := user.Insert(); err != nil {
return fmt.Errorf("failed to create demo user: %w", err)
}
fmt.Println("✓ Demo user created")
return nil
}

View File

@ -1,51 +1,37 @@
package monsters
import (
"encoding/json"
"fmt"
"dk/internal/database"
"dk/internal/helpers/scanner"
"zombiezen.com/go/sqlite"
"os"
"path/filepath"
"sort"
"sync"
)
// Monster represents a monster in the database
// Monster represents a monster in the game
type Monster struct {
database.BaseModel
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
MaxHP int `db:"max_hp" json:"max_hp"`
MaxDmg int `db:"max_dmg" json:"max_dmg"`
Armor int `db:"armor" json:"armor"`
Level int `db:"level" json:"level"`
MaxExp int `db:"max_exp" json:"max_exp"`
MaxGold int `db:"max_gold" json:"max_gold"`
Immune int `db:"immune" json:"immune"`
}
func (m *Monster) GetTableName() string {
return "monsters"
}
func (m *Monster) GetID() int {
return m.ID
}
func (m *Monster) SetID(id int) {
m.ID = id
}
func (m *Monster) Set(field string, value any) error {
return database.Set(m, field, value)
ID int `json:"id"`
Name string `json:"name"`
MaxHP int `json:"max_hp"`
MaxDmg int `json:"max_dmg"`
Armor int `json:"armor"`
Level int `json:"level"`
MaxExp int `json:"max_exp"`
MaxGold int `json:"max_gold"`
Immune int `json:"immune"`
}
func (m *Monster) Save() error {
return database.Save(m)
store := GetStore()
store.UpdateMonster(m)
return nil
}
func (m *Monster) Delete() error {
return database.Delete(m)
store := GetStore()
store.RemoveMonster(m.ID)
return nil
}
// Creates a new Monster with sensible defaults
@ -62,19 +48,6 @@ func New() *Monster {
}
}
var monsterScanner = scanner.New[Monster]()
// Returns the column list for monster queries
func monsterColumns() string {
return monsterScanner.Columns()
}
// Populates a Monster struct using the fast scanner
func scanMonster(stmt *sqlite.Stmt) *Monster {
monster := &Monster{}
monsterScanner.Scan(stmt, monster)
return monster
}
// Immunity constants for monster immunity types
const (
@ -83,109 +56,423 @@ const (
ImmuneSleep = 2 // Immune to Sleep spells
)
// Retrieves a monster by ID
func Find(id int) (*Monster, error) {
var monster *Monster
query := `SELECT ` + monsterColumns() + ` FROM monsters WHERE id = ?`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
monster = scanMonster(stmt)
return nil
}, id)
if err != nil {
return nil, fmt.Errorf("failed to find monster: %w", err)
// MonsterStore provides in-memory storage with O(1) lookups
type MonsterStore struct {
monsters map[int]*Monster // ID -> Monster (O(1))
byLevel map[int][]*Monster // Level -> []*Monster (O(1) to get slice)
byImmunity map[int][]*Monster // Immunity -> []*Monster (O(1) to get slice)
allByLevel []*Monster // Pre-sorted by level, id
maxID int
mu sync.RWMutex
}
if monster == nil {
// Global in-memory store
var store *MonsterStore
var storeOnce sync.Once
// Initialize the in-memory store
func initStore() {
store = &MonsterStore{
monsters: make(map[int]*Monster),
byLevel: make(map[int][]*Monster),
byImmunity: make(map[int][]*Monster),
allByLevel: make([]*Monster, 0),
maxID: 0,
}
}
// GetStore returns the global monster store
func GetStore() *MonsterStore {
storeOnce.Do(initStore)
return store
}
// AddMonster adds a monster to the in-memory store and updates all indices
func (ms *MonsterStore) AddMonster(monster *Monster) {
ms.mu.Lock()
defer ms.mu.Unlock()
// Add to primary store
ms.monsters[monster.ID] = monster
// Update max ID
if monster.ID > ms.maxID {
ms.maxID = monster.ID
}
// Add to level index
ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], monster)
// Add to immunity index
ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], monster)
// Add to sorted list and re-sort
ms.allByLevel = append(ms.allByLevel, monster)
sort.Slice(ms.allByLevel, func(i, j int) bool {
if ms.allByLevel[i].Level == ms.allByLevel[j].Level {
return ms.allByLevel[i].ID < ms.allByLevel[j].ID
}
return ms.allByLevel[i].Level < ms.allByLevel[j].Level
})
// Sort level index
sort.Slice(ms.byLevel[monster.Level], func(i, j int) bool {
return ms.byLevel[monster.Level][i].ID < ms.byLevel[monster.Level][j].ID
})
// Sort immunity index
sort.Slice(ms.byImmunity[monster.Immune], func(i, j int) bool {
if ms.byImmunity[monster.Immune][i].Level == ms.byImmunity[monster.Immune][j].Level {
return ms.byImmunity[monster.Immune][i].ID < ms.byImmunity[monster.Immune][j].ID
}
return ms.byImmunity[monster.Immune][i].Level < ms.byImmunity[monster.Immune][j].Level
})
}
// RemoveMonster removes a monster from the store and updates indices
func (ms *MonsterStore) RemoveMonster(id int) {
ms.mu.Lock()
defer ms.mu.Unlock()
monster, exists := ms.monsters[id]
if !exists {
return
}
// Remove from primary store
delete(ms.monsters, id)
// Remove from level index
levelMonsters := ms.byLevel[monster.Level]
for i, m := range levelMonsters {
if m.ID == id {
ms.byLevel[monster.Level] = append(levelMonsters[:i], levelMonsters[i+1:]...)
break
}
}
// Remove from immunity index
immunityMonsters := ms.byImmunity[monster.Immune]
for i, m := range immunityMonsters {
if m.ID == id {
ms.byImmunity[monster.Immune] = append(immunityMonsters[:i], immunityMonsters[i+1:]...)
break
}
}
// Remove from sorted list
for i, m := range ms.allByLevel {
if m.ID == id {
ms.allByLevel = append(ms.allByLevel[:i], ms.allByLevel[i+1:]...)
break
}
}
}
// UpdateMonster updates a monster and rebuilds indices
func (ms *MonsterStore) UpdateMonster(monster *Monster) {
ms.RemoveMonster(monster.ID)
ms.AddMonster(monster)
}
// GetNextID returns the next available ID
func (ms *MonsterStore) GetNextID() int {
ms.mu.RLock()
defer ms.mu.RUnlock()
return ms.maxID + 1
}
// LoadFromJSON loads monster data from a JSON file
func (ms *MonsterStore) LoadFromJSON(filename string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
data, err := os.ReadFile(filename)
if err != nil {
if os.IsNotExist(err) {
return nil // File doesn't exist, start with empty store
}
return fmt.Errorf("failed to read monsters JSON: %w", err)
}
// Handle empty file
if len(data) == 0 {
return nil // Empty file, start with empty store
}
var monsters []*Monster
if err := json.Unmarshal(data, &monsters); err != nil {
return fmt.Errorf("failed to unmarshal monsters JSON: %w", err)
}
// Clear existing data
ms.monsters = make(map[int]*Monster)
ms.byLevel = make(map[int][]*Monster)
ms.byImmunity = make(map[int][]*Monster)
ms.allByLevel = make([]*Monster, 0)
ms.maxID = 0
// Add all monsters
for _, monster := range monsters {
ms.monsters[monster.ID] = monster
if monster.ID > ms.maxID {
ms.maxID = monster.ID
}
ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], monster)
ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], monster)
ms.allByLevel = append(ms.allByLevel, monster)
}
// Sort all indices
sort.Slice(ms.allByLevel, func(i, j int) bool {
if ms.allByLevel[i].Level == ms.allByLevel[j].Level {
return ms.allByLevel[i].ID < ms.allByLevel[j].ID
}
return ms.allByLevel[i].Level < ms.allByLevel[j].Level
})
for level := range ms.byLevel {
sort.Slice(ms.byLevel[level], func(i, j int) bool {
return ms.byLevel[level][i].ID < ms.byLevel[level][j].ID
})
}
for immunity := range ms.byImmunity {
sort.Slice(ms.byImmunity[immunity], func(i, j int) bool {
if ms.byImmunity[immunity][i].Level == ms.byImmunity[immunity][j].Level {
return ms.byImmunity[immunity][i].ID < ms.byImmunity[immunity][j].ID
}
return ms.byImmunity[immunity][i].Level < ms.byImmunity[immunity][j].Level
})
}
return nil
}
// SaveToJSON saves monster data to a JSON file
func (ms *MonsterStore) SaveToJSON(filename string) error {
ms.mu.RLock()
defer ms.mu.RUnlock()
monsters := make([]*Monster, 0, len(ms.monsters))
for _, monster := range ms.monsters {
monsters = append(monsters, monster)
}
// Sort by ID for consistent output
sort.Slice(monsters, func(i, j int) bool {
return monsters[i].ID < monsters[j].ID
})
data, err := json.MarshalIndent(monsters, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal monsters to JSON: %w", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write monsters JSON: %w", err)
}
return nil
}
// findMonstersDataPath finds the monsters.json file relative to the current working directory
func findMonstersDataPath() (string, error) {
// Try current directory first (cwd/data/monsters.json)
if _, err := os.Stat("data/monsters.json"); err == nil {
return "data/monsters.json", nil
}
// Walk up directories to find the data folder
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
dataPath := filepath.Join(dir, "data", "monsters.json")
if _, err := os.Stat(dataPath); err == nil {
return dataPath, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break // reached root
}
dir = parent
}
// Default to current directory if not found
return "data/monsters.json", nil
}
// LoadData loads monster data from JSON file, or initializes with default data
func LoadData() error {
store := GetStore()
dataPath, err := findMonstersDataPath()
if err != nil {
return fmt.Errorf("failed to find monsters data path: %w", err)
}
if err := store.LoadFromJSON(dataPath); err != nil {
// If JSON doesn't exist, initialize with default monsters
if os.IsNotExist(err) {
fmt.Println("No existing monster data found, initializing with defaults...")
if err := initializeDefaultMonsters(); err != nil {
return fmt.Errorf("failed to initialize default monsters: %w", err)
}
// Save the default data
if err := SaveData(); err != nil {
return fmt.Errorf("failed to save default monster data: %w", err)
}
fmt.Printf("Initialized %d default monsters\n", len(store.monsters))
} else {
return fmt.Errorf("failed to load from JSON: %w", err)
}
} else {
fmt.Printf("Loaded %d monsters from JSON\n", len(store.monsters))
}
return nil
}
// initializeDefaultMonsters creates the default monster set
func initializeDefaultMonsters() error {
store := GetStore()
// Default monsters from the original SQL data
defaultMonsters := []*Monster{
{ID: 1, Name: "Blue Slime", MaxHP: 4, MaxDmg: 3, Armor: 1, Level: 1, MaxExp: 1, MaxGold: 1, Immune: ImmuneNone},
{ID: 2, Name: "Red Slime", MaxHP: 6, MaxDmg: 5, Armor: 1, Level: 1, MaxExp: 2, MaxGold: 1, Immune: ImmuneNone},
{ID: 3, Name: "Critter", MaxHP: 6, MaxDmg: 5, Armor: 2, Level: 1, MaxExp: 4, MaxGold: 2, Immune: ImmuneNone},
{ID: 4, Name: "Creature", MaxHP: 10, MaxDmg: 8, Armor: 2, Level: 2, MaxExp: 4, MaxGold: 2, Immune: ImmuneNone},
{ID: 5, Name: "Shadow", MaxHP: 10, MaxDmg: 9, Armor: 3, Level: 2, MaxExp: 6, MaxGold: 2, Immune: ImmuneHurt},
{ID: 6, Name: "Drake", MaxHP: 11, MaxDmg: 10, Armor: 3, Level: 2, MaxExp: 8, MaxGold: 3, Immune: ImmuneNone},
{ID: 7, Name: "Shade", MaxHP: 12, MaxDmg: 10, Armor: 3, Level: 3, MaxExp: 10, MaxGold: 3, Immune: ImmuneHurt},
{ID: 8, Name: "Drakelor", MaxHP: 14, MaxDmg: 12, Armor: 4, Level: 3, MaxExp: 10, MaxGold: 3, Immune: ImmuneNone},
{ID: 9, Name: "Silver Slime", MaxHP: 15, MaxDmg: 100, Armor: 200, Level: 30, MaxExp: 15, MaxGold: 1000, Immune: ImmuneSleep},
{ID: 10, Name: "Scamp", MaxHP: 16, MaxDmg: 13, Armor: 5, Level: 4, MaxExp: 15, MaxGold: 5, Immune: ImmuneNone},
}
for _, monster := range defaultMonsters {
store.AddMonster(monster)
}
return nil
}
// SaveData saves monster data to JSON file
func SaveData() error {
store := GetStore()
dataPath, err := findMonstersDataPath()
if err != nil {
return fmt.Errorf("failed to find monsters data path: %w", err)
}
// Ensure data directory exists
dataDir := filepath.Dir(dataPath)
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
if err := store.SaveToJSON(dataPath); err != nil {
return fmt.Errorf("failed to save monsters to JSON: %w", err)
}
fmt.Printf("Saved %d monsters to JSON\n", len(store.monsters))
return nil
}
// Retrieves a monster by ID - O(1) lookup
func Find(id int) (*Monster, error) {
store := GetStore()
store.mu.RLock()
defer store.mu.RUnlock()
monster, exists := store.monsters[id]
if !exists {
return nil, fmt.Errorf("monster with ID %d not found", id)
}
return monster, nil
}
// Retrieves all monsters
// Retrieves all monsters - O(1) lookup (returns pre-sorted slice)
func All() ([]*Monster, error) {
var monsters []*Monster
store := GetStore()
store.mu.RLock()
defer store.mu.RUnlock()
query := `SELECT ` + monsterColumns() + ` FROM monsters ORDER BY level, id`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
monster := scanMonster(stmt)
monsters = append(monsters, monster)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve all monsters: %w", err)
// Return a copy of the slice to prevent external modifications
result := make([]*Monster, len(store.allByLevel))
copy(result, store.allByLevel)
return result, nil
}
return monsters, nil
}
// Retrieves monsters by level
// Retrieves monsters by level - O(1) lookup
func ByLevel(level int) ([]*Monster, error) {
var monsters []*Monster
store := GetStore()
store.mu.RLock()
defer store.mu.RUnlock()
query := `SELECT ` + monsterColumns() + ` FROM monsters WHERE level = ? ORDER BY id`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
monster := scanMonster(stmt)
monsters = append(monsters, monster)
return nil
}, level)
if err != nil {
return nil, fmt.Errorf("failed to retrieve monsters by level: %w", err)
monsters, exists := store.byLevel[level]
if !exists {
return []*Monster{}, nil
}
return monsters, nil
// Return a copy of the slice to prevent external modifications
result := make([]*Monster, len(monsters))
copy(result, monsters)
return result, nil
}
// Retrieves monsters within a level range (inclusive)
// Retrieves monsters within a level range (inclusive) - O(k) where k is result size
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
var monsters []*Monster
store := GetStore()
store.mu.RLock()
defer store.mu.RUnlock()
query := `SELECT ` + monsterColumns() + ` FROM monsters WHERE level BETWEEN ? AND ? ORDER BY level, id`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
monster := scanMonster(stmt)
monsters = append(monsters, monster)
return nil
}, minLevel, maxLevel)
if err != nil {
return nil, fmt.Errorf("failed to retrieve monsters by level range: %w", err)
var result []*Monster
for level := minLevel; level <= maxLevel; level++ {
if monsters, exists := store.byLevel[level]; exists {
result = append(result, monsters...)
}
}
return monsters, nil
return result, nil
}
// Retrieves monsters by immunity type
// Retrieves monsters by immunity type - O(1) lookup
func ByImmunity(immunityType int) ([]*Monster, error) {
var monsters []*Monster
store := GetStore()
store.mu.RLock()
defer store.mu.RUnlock()
query := `SELECT ` + monsterColumns() + ` FROM monsters WHERE immune = ? ORDER BY level, id`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
monster := scanMonster(stmt)
monsters = append(monsters, monster)
return nil
}, immunityType)
if err != nil {
return nil, fmt.Errorf("failed to retrieve monsters by immunity: %w", err)
monsters, exists := store.byImmunity[immunityType]
if !exists {
return []*Monster{}, nil
}
return monsters, nil
// Return a copy of the slice to prevent external modifications
result := make([]*Monster, len(monsters))
copy(result, monsters)
return result, nil
}
// Saves a new monster to the database and sets the ID
// Saves a new monster to the in-memory store and sets the ID
func (m *Monster) Insert() error {
columns := `name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune`
values := []any{m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune}
return database.Insert(m, columns, values...)
store := GetStore()
// Assign new ID if not set
if m.ID == 0 {
m.ID = store.GetNextID()
}
// Add to store
store.AddMonster(m)
return nil
}
// Returns true if the monster is immune to Hurt spells

View File

@ -11,6 +11,7 @@ import (
"dk/internal/auth"
"dk/internal/database"
"dk/internal/middleware"
"dk/internal/monsters"
"dk/internal/router"
"dk/internal/routes"
"dk/internal/template"
@ -32,6 +33,11 @@ func Start(port string) error {
}
defer database.Close()
// Load monster data into memory
if err := monsters.LoadData(); err != nil {
return fmt.Errorf("failed to load monster data: %w", err)
}
auth.Init("sessions.json") // Initialize auth.Manager
r := router.New()
@ -96,6 +102,12 @@ func Start(port string) error {
<-c
log.Println("Received shutdown signal, shutting down gracefully...")
// Save monster data before shutdown
log.Println("Saving monster data...")
if err := monsters.SaveData(); err != nil {
log.Printf("Error saving monster data: %v", err)
}
// Save sessions before shutdown
log.Println("Saving sessions...")
if err := auth.Manager.Close(); err != nil {

34
test_load_data.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"fmt"
"dk/internal/monsters"
)
func main() {
fmt.Println("Testing LoadData() function...")
err := monsters.LoadData()
if err != nil {
fmt.Printf("LoadData() failed: %v\n", err)
return
}
// Test that we can find a monster
monster, err := monsters.Find(1)
if err != nil {
fmt.Printf("Find(1) failed: %v\n", err)
return
}
fmt.Printf("Successfully loaded data! Found monster: %s (Level %d)\n", monster.Name, monster.Level)
// Test getting all monsters
all, err := monsters.All()
if err != nil {
fmt.Printf("All() failed: %v\n", err)
return
}
fmt.Printf("Total monsters loaded: %d\n", len(all))
}