diff --git a/.gitignore b/.gitignore index 856360a..581cd7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ # Dragon Knight test/build files /dk -/dk.db -/dk.db-* /sessions.json -/tmp \ No newline at end of file +/data/users.json \ No newline at end of file diff --git a/data/babble.json b/data/babble.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/babble.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/control.json b/data/control.json new file mode 100644 index 0000000..853319b --- /dev/null +++ b/data/control.json @@ -0,0 +1,8 @@ +{ + "world_size": 200, + "open": 1, + "admin_email": "", + "class_1_name": "Mage", + "class_2_name": "Warrior", + "class_3_name": "Paladin" +} \ No newline at end of file diff --git a/data/drops.json b/data/drops.json index 01f3491..bd5c0da 100644 --- a/data/drops.json +++ b/data/drops.json @@ -1,226 +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" - } + { + "id": 5, + "name": "Magic Stone", + "level": 10, + "type": 1, + "att": "maxmp,25" + }, + { + "id": 27, + "name": "Emerald", + "level": 50, + "type": 1, + "att": "strength,150" + }, + { + "id": 30, + "name": "Diamond", + "level": 50, + "type": 1, + "att": "defensepower,150" + }, + { + "id": 24, + "name": "Seraph's Love", + "level": 40, + "type": 1, + "att": "maxmp,100,dexterity,100" + }, + { + "id": 13, + "name": "Demon's Sin", + "level": 35, + "type": 1, + "att": "maxhp,-50,strength,50" + }, + { + "id": 4, + "name": "Magic Pebble", + "level": 1, + "type": 1, + "att": "maxmp,10" + }, + { + "id": 11, + "name": "Dragon's Tear", + "level": 35, + "type": 1, + "att": "strength,50" + }, + { + "id": 9, + "name": "Dragon's Claw", + "level": 10, + "type": 1, + "att": "attackpower,25" + }, + { + "id": 20, + "name": "Angel's Love", + "level": 40, + "type": 1, + "att": "maxhp,100,strength,100" + }, + { + "id": 2, + "name": "Life Stone", + "level": 10, + "type": 1, + "att": "maxhp,25" + }, + { + "id": 21, + "name": "Seraph's Joy", + "level": 25, + "type": 1, + "att": "maxmp,25,dexterity,25" + }, + { + "id": 10, + "name": "Dragon's Tooth", + "level": 30, + "type": 1, + "att": "attackpower,50" + }, + { + "id": 16, + "name": "Demon's Hate", + "level": 45, + "type": 1, + "att": "maxmp,-100,strength,100" + }, + { + "id": 26, + "name": "Pearl", + "level": 50, + "type": 1, + "att": "maxmp,150" + }, + { + "id": 31, + "name": "Memory Drop", + "level": 5, + "type": 1, + "att": "expbonus,10" + }, + { + "id": 14, + "name": "Demon's Fall", + "level": 35, + "type": 1, + "att": "maxmp,-50,strength,50" + }, + { + "id": 28, + "name": "Topaz", + "level": 50, + "type": 1, + "att": "dexterity,150" + }, + { + "id": 22, + "name": "Seraph's Rise", + "level": 30, + "type": 1, + "att": "maxmp,50,dexterity,50" + }, + { + "id": 8, + "name": "Dragon's Plate", + "level": 30, + "type": 1, + "att": "defensepower,50" + }, + { + "id": 3, + "name": "Life Rock", + "level": 25, + "type": 1, + "att": "maxhp,50" + }, + { + "id": 17, + "name": "Angel's Joy", + "level": 25, + "type": 1, + "att": "maxhp,25,strength,25" + }, + { + "id": 19, + "name": "Angel's Truth", + "level": 35, + "type": 1, + "att": "maxhp,75,strength,75" + }, + { + "id": 6, + "name": "Magic Rock", + "level": 25, + "type": 1, + "att": "maxmp,50" + }, + { + "id": 15, + "name": "Demon's Lie", + "level": 45, + "type": 1, + "att": "maxhp,-100,strength,100" + }, + { + "id": 7, + "name": "Dragon's Scale", + "level": 10, + "type": 1, + "att": "defensepower,25" + }, + { + "id": 29, + "name": "Obsidian", + "level": 50, + "type": 1, + "att": "attackpower,150" + }, + { + "id": 25, + "name": "Ruby", + "level": 50, + "type": 1, + "att": "maxhp,150" + }, + { + "id": 18, + "name": "Angel's Rise", + "level": 30, + "type": 1, + "att": "maxhp,50,strength,50" + }, + { + "id": 23, + "name": "Seraph's Truth", + "level": 35, + "type": 1, + "att": "maxmp,75,dexterity,75" + }, + { + "id": 12, + "name": "Dragon's Wing", + "level": 35, + "type": 1, + "att": "dexterity,50" + }, + { + "id": 32, + "name": "Fortune Drop", + "level": 5, + "type": 1, + "att": "goldbonus,10" + }, + { + "id": 1, + "name": "Life Pebble", + "level": 1, + "type": 1, + "att": "maxhp,10" + } ] \ No newline at end of file diff --git a/data/forum.json b/data/forum.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/forum.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/items.json b/data/items.json index e185f4d..cc7ea53 100644 --- a/data/items.json +++ b/data/items.json @@ -1,266 +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" - } + { + "id": 17, + "type": 2, + "name": "Skivvies", + "value": 25, + "att": 2, + "special": "goldbonus,10" + }, + { + "id": 11, + "type": 1, + "name": "Claymore", + "value": 2000, + "att": 60, + "special": "" + }, + { + "id": 28, + "type": 3, + "name": "Reed Shield", + "value": 50, + "att": 2, + "special": "" + }, + { + "id": 8, + "type": 1, + "name": "Poleaxe", + "value": 500, + "att": 35, + "special": "" + }, + { + "id": 25, + "type": 2, + "name": "Dark Armor", + "value": 5000, + "att": 150, + "special": "expbonus,-10" + }, + { + "id": 20, + "type": 2, + "name": "Hard Leather Armor", + "value": 150, + "att": 25, + "special": "" + }, + { + "id": 12, + "type": 1, + "name": "Dark Axe", + "value": 3000, + "att": 100, + "special": "expbonus,-5" + }, + { + "id": 22, + "type": 2, + "name": "Bronze Plate", + "value": 900, + "att": 50, + "special": "" + }, + { + "id": 19, + "type": 2, + "name": "Leather Armor", + "value": 75, + "att": 10, + "special": "" + }, + { + "id": 21, + "type": 2, + "name": "Chain Mail", + "value": 300, + "att": 30, + "special": "" + }, + { + "id": 26, + "type": 2, + "name": "Bright Armor", + "value": 10000, + "att": 175, + "special": "expbonus,10" + }, + { + "id": 23, + "type": 2, + "name": "Iron Plate", + "value": 2000, + "att": 100, + "special": "" + }, + { + "id": 4, + "type": 1, + "name": "Dagger", + "value": 90, + "att": 8, + "special": "" + }, + { + "id": 7, + "type": 1, + "name": "Brand", + "value": 300, + "att": 25, + "special": "" + }, + { + "id": 29, + "type": 3, + "name": "Buckler", + "value": 100, + "att": 4, + "special": "" + }, + { + "id": 18, + "type": 2, + "name": "Clothes", + "value": 50, + "att": 5, + "special": "" + }, + { + "id": 3, + "type": 1, + "name": "Club", + "value": 40, + "att": 5, + "special": "" + }, + { + "id": 32, + "type": 3, + "name": "Silver Shield", + "value": 10000, + "att": 60, + "special": "" + }, + { + "id": 24, + "type": 2, + "name": "Magic Armor", + "value": 4000, + "att": 125, + "special": "maxmp,50" + }, + { + "id": 16, + "type": 1, + "name": "Destiny Blade", + "value": 50000, + "att": 250, + "special": "strength,50" + }, + { + "id": 14, + "type": 1, + "name": "Bright Sword", + "value": 6000, + "att": 100, + "special": "expbonus,10" + }, + { + "id": 5, + "type": 1, + "name": "Hatchet", + "value": 150, + "att": 12, + "special": "" + }, + { + "id": 1, + "type": 1, + "name": "Stick", + "value": 10, + "att": 2, + "special": "" + }, + { + "id": 27, + "type": 2, + "name": "Destiny Raiment", + "value": 50000, + "att": 200, + "special": "dexterity,50" + }, + { + "id": 13, + "type": 1, + "name": "Dark Sword", + "value": 4500, + "att": 125, + "special": "expbonus,-10" + }, + { + "id": 9, + "type": 1, + "name": "Broadsword", + "value": 800, + "att": 45, + "special": "" + }, + { + "id": 30, + "type": 3, + "name": "Small Shield", + "value": 500, + "att": 10, + "special": "" + }, + { + "id": 6, + "type": 1, + "name": "Axe", + "value": 200, + "att": 16, + "special": "" + }, + { + "id": 33, + "type": 3, + "name": "Destiny Aegis", + "value": 25000, + "att": 100, + "special": "maxhp,50" + }, + { + "id": 10, + "type": 1, + "name": "Battle Axe", + "value": 1200, + "att": 50, + "special": "" + }, + { + "id": 31, + "type": 3, + "name": "Large Shield", + "value": 2500, + "att": 30, + "special": "" + }, + { + "id": 2, + "type": 1, + "name": "Branch", + "value": 30, + "att": 4, + "special": "" + }, + { + "id": 15, + "type": 1, + "name": "Magic Sword", + "value": 10000, + "att": 150, + "special": "maxmp,50" + } ] \ No newline at end of file diff --git a/data/monsters.json b/data/monsters.json index 8383a29..a53a869 100644 --- a/data/monsters.json +++ b/data/monsters.json @@ -1,1663 +1,1663 @@ [ - { - "id": 1, - "name": "Blue Slime", - "max_hp": 4, - "max_dmg": 3, - "armor": 1, - "level": 1, - "max_exp": 1, - "max_gold": 1, - "immune": 0 - }, - { - "id": 2, - "name": "Red Slime", - "max_hp": 6, - "max_dmg": 5, - "armor": 1, - "level": 1, - "max_exp": 2, - "max_gold": 1, - "immune": 0 - }, - { - "id": 3, - "name": "Critter", - "max_hp": 6, - "max_dmg": 5, - "armor": 2, - "level": 1, - "max_exp": 4, - "max_gold": 2, - "immune": 0 - }, - { - "id": 4, - "name": "Creature", - "max_hp": 10, - "max_dmg": 8, - "armor": 2, - "level": 2, - "max_exp": 4, - "max_gold": 2, - "immune": 0 - }, - { - "id": 5, - "name": "Shadow", - "max_hp": 10, - "max_dmg": 9, - "armor": 3, - "level": 2, - "max_exp": 6, - "max_gold": 2, - "immune": 1 - }, - { - "id": 6, - "name": "Drake", - "max_hp": 11, - "max_dmg": 10, - "armor": 3, - "level": 2, - "max_exp": 8, - "max_gold": 3, - "immune": 0 - }, - { - "id": 7, - "name": "Shade", - "max_hp": 12, - "max_dmg": 10, - "armor": 3, - "level": 3, - "max_exp": 10, - "max_gold": 3, - "immune": 1 - }, - { - "id": 8, - "name": "Drakelor", - "max_hp": 14, - "max_dmg": 12, - "armor": 4, - "level": 3, - "max_exp": 10, - "max_gold": 3, - "immune": 0 - }, - { - "id": 9, - "name": "Silver Slime", - "max_hp": 15, - "max_dmg": 100, - "armor": 200, - "level": 30, - "max_exp": 15, - "max_gold": 1000, - "immune": 2 - }, - { - "id": 10, - "name": "Scamp", - "max_hp": 16, - "max_dmg": 13, - "armor": 5, - "level": 4, - "max_exp": 15, - "max_gold": 5, - "immune": 0 - }, - { - "id": 11, - "name": "Raven", - "max_hp": 16, - "max_dmg": 13, - "armor": 5, - "level": 4, - "max_exp": 18, - "max_gold": 6, - "immune": 0 - }, - { - "id": 12, - "name": "Scorpion", - "max_hp": 18, - "max_dmg": 14, - "armor": 6, - "level": 5, - "max_exp": 20, - "max_gold": 7, - "immune": 0 - }, - { - "id": 13, - "name": "Illusion", - "max_hp": 20, - "max_dmg": 15, - "armor": 6, - "level": 5, - "max_exp": 20, - "max_gold": 7, - "immune": 1 - }, - { - "id": 14, - "name": "Nightshade", - "max_hp": 22, - "max_dmg": 16, - "armor": 6, - "level": 6, - "max_exp": 24, - "max_gold": 8, - "immune": 0 - }, - { - "id": 15, - "name": "Drakemal", - "max_hp": 22, - "max_dmg": 18, - "armor": 7, - "level": 6, - "max_exp": 24, - "max_gold": 8, - "immune": 0 - }, - { - "id": 16, - "name": "Shadow Raven", - "max_hp": 24, - "max_dmg": 18, - "armor": 7, - "level": 6, - "max_exp": 26, - "max_gold": 9, - "immune": 1 - }, - { - "id": 17, - "name": "Ghost", - "max_hp": 24, - "max_dmg": 20, - "armor": 8, - "level": 6, - "max_exp": 28, - "max_gold": 9, - "immune": 0 - }, - { - "id": 18, - "name": "Frost Raven", - "max_hp": 26, - "max_dmg": 20, - "armor": 8, - "level": 7, - "max_exp": 30, - "max_gold": 10, - "immune": 0 - }, - { - "id": 19, - "name": "Rogue Scorpion", - "max_hp": 28, - "max_dmg": 22, - "armor": 9, - "level": 7, - "max_exp": 32, - "max_gold": 11, - "immune": 0 - }, - { - "id": 20, - "name": "Ghoul", - "max_hp": 29, - "max_dmg": 24, - "armor": 9, - "level": 7, - "max_exp": 34, - "max_gold": 11, - "immune": 0 - }, - { - "id": 21, - "name": "Magician", - "max_hp": 30, - "max_dmg": 24, - "armor": 10, - "level": 8, - "max_exp": 36, - "max_gold": 12, - "immune": 0 - }, - { - "id": 22, - "name": "Rogue", - "max_hp": 30, - "max_dmg": 25, - "armor": 12, - "level": 8, - "max_exp": 40, - "max_gold": 13, - "immune": 0 - }, - { - "id": 23, - "name": "Drakefin", - "max_hp": 32, - "max_dmg": 26, - "armor": 12, - "level": 8, - "max_exp": 40, - "max_gold": 13, - "immune": 0 - }, - { - "id": 24, - "name": "Shimmer", - "max_hp": 32, - "max_dmg": 26, - "armor": 14, - "level": 8, - "max_exp": 45, - "max_gold": 15, - "immune": 1 - }, - { - "id": 25, - "name": "Fire Raven", - "max_hp": 34, - "max_dmg": 28, - "armor": 14, - "level": 9, - "max_exp": 45, - "max_gold": 15, - "immune": 0 - }, - { - "id": 26, - "name": "Dybbuk", - "max_hp": 34, - "max_dmg": 28, - "armor": 14, - "level": 9, - "max_exp": 50, - "max_gold": 17, - "immune": 0 - }, - { - "id": 27, - "name": "Knave", - "max_hp": 36, - "max_dmg": 30, - "armor": 15, - "level": 9, - "max_exp": 52, - "max_gold": 17, - "immune": 0 - }, - { - "id": 28, - "name": "Goblin", - "max_hp": 36, - "max_dmg": 30, - "armor": 15, - "level": 10, - "max_exp": 54, - "max_gold": 18, - "immune": 0 - }, - { - "id": 29, - "name": "Skeleton", - "max_hp": 38, - "max_dmg": 30, - "armor": 18, - "level": 10, - "max_exp": 58, - "max_gold": 19, - "immune": 0 - }, - { - "id": 30, - "name": "Dark Slime", - "max_hp": 38, - "max_dmg": 32, - "armor": 18, - "level": 10, - "max_exp": 62, - "max_gold": 21, - "immune": 0 - }, - { - "id": 31, - "name": "Silver Scorpion", - "max_hp": 30, - "max_dmg": 160, - "armor": 350, - "level": 40, - "max_exp": 63, - "max_gold": 2000, - "immune": 2 - }, - { - "id": 32, - "name": "Mirage", - "max_hp": 40, - "max_dmg": 32, - "armor": 20, - "level": 11, - "max_exp": 64, - "max_gold": 21, - "immune": 1 - }, - { - "id": 33, - "name": "Sorceror", - "max_hp": 41, - "max_dmg": 33, - "armor": 22, - "level": 11, - "max_exp": 68, - "max_gold": 23, - "immune": 0 - }, - { - "id": 34, - "name": "Imp", - "max_hp": 42, - "max_dmg": 34, - "armor": 22, - "level": 12, - "max_exp": 70, - "max_gold": 23, - "immune": 0 - }, - { - "id": 35, - "name": "Nymph", - "max_hp": 43, - "max_dmg": 35, - "armor": 22, - "level": 12, - "max_exp": 70, - "max_gold": 23, - "immune": 0 - }, - { - "id": 36, - "name": "Scoundrel", - "max_hp": 43, - "max_dmg": 35, - "armor": 22, - "level": 12, - "max_exp": 75, - "max_gold": 25, - "immune": 0 - }, - { - "id": 37, - "name": "Megaskeleton", - "max_hp": 44, - "max_dmg": 36, - "armor": 24, - "level": 13, - "max_exp": 78, - "max_gold": 26, - "immune": 0 - }, - { - "id": 38, - "name": "Grey Wolf", - "max_hp": 44, - "max_dmg": 36, - "armor": 24, - "level": 13, - "max_exp": 82, - "max_gold": 27, - "immune": 0 - }, - { - "id": 39, - "name": "Phantom", - "max_hp": 46, - "max_dmg": 38, - "armor": 24, - "level": 14, - "max_exp": 85, - "max_gold": 28, - "immune": 1 - }, - { - "id": 40, - "name": "Specter", - "max_hp": 46, - "max_dmg": 38, - "armor": 24, - "level": 14, - "max_exp": 90, - "max_gold": 30, - "immune": 0 - }, - { - "id": 41, - "name": "Dark Scorpion", - "max_hp": 48, - "max_dmg": 40, - "armor": 26, - "level": 15, - "max_exp": 95, - "max_gold": 32, - "immune": 1 - }, - { - "id": 42, - "name": "Warlock", - "max_hp": 48, - "max_dmg": 40, - "armor": 26, - "level": 15, - "max_exp": 100, - "max_gold": 33, - "immune": 1 - }, - { - "id": 43, - "name": "Orc", - "max_hp": 49, - "max_dmg": 42, - "armor": 28, - "level": 15, - "max_exp": 104, - "max_gold": 35, - "immune": 0 - }, - { - "id": 44, - "name": "Sylph", - "max_hp": 49, - "max_dmg": 42, - "armor": 28, - "level": 15, - "max_exp": 106, - "max_gold": 35, - "immune": 0 - }, - { - "id": 45, - "name": "Wraith", - "max_hp": 50, - "max_dmg": 45, - "armor": 30, - "level": 16, - "max_exp": 108, - "max_gold": 36, - "immune": 0 - }, - { - "id": 46, - "name": "Hellion", - "max_hp": 50, - "max_dmg": 45, - "armor": 30, - "level": 16, - "max_exp": 110, - "max_gold": 37, - "immune": 0 - }, - { - "id": 47, - "name": "Bandit", - "max_hp": 52, - "max_dmg": 45, - "armor": 30, - "level": 16, - "max_exp": 114, - "max_gold": 38, - "immune": 0 - }, - { - "id": 48, - "name": "Ultraskeleton", - "max_hp": 52, - "max_dmg": 46, - "armor": 32, - "level": 16, - "max_exp": 116, - "max_gold": 39, - "immune": 0 - }, - { - "id": 49, - "name": "Dark Wolf", - "max_hp": 54, - "max_dmg": 47, - "armor": 36, - "level": 17, - "max_exp": 120, - "max_gold": 40, - "immune": 1 - }, - { - "id": 50, - "name": "Troll", - "max_hp": 56, - "max_dmg": 48, - "armor": 36, - "level": 17, - "max_exp": 120, - "max_gold": 40, - "immune": 0 - }, - { - "id": 51, - "name": "Werewolf", - "max_hp": 56, - "max_dmg": 48, - "armor": 38, - "level": 17, - "max_exp": 124, - "max_gold": 41, - "immune": 0 - }, - { - "id": 52, - "name": "Hellcat", - "max_hp": 58, - "max_dmg": 50, - "armor": 38, - "level": 18, - "max_exp": 128, - "max_gold": 43, - "immune": 0 - }, - { - "id": 53, - "name": "Spirit", - "max_hp": 58, - "max_dmg": 50, - "armor": 38, - "level": 18, - "max_exp": 132, - "max_gold": 44, - "immune": 0 - }, - { - "id": 54, - "name": "Nisse", - "max_hp": 60, - "max_dmg": 52, - "armor": 40, - "level": 19, - "max_exp": 132, - "max_gold": 44, - "immune": 0 - }, - { - "id": 55, - "name": "Dawk", - "max_hp": 60, - "max_dmg": 54, - "armor": 40, - "level": 19, - "max_exp": 136, - "max_gold": 45, - "immune": 0 - }, - { - "id": 56, - "name": "Figment", - "max_hp": 64, - "max_dmg": 55, - "armor": 42, - "level": 19, - "max_exp": 140, - "max_gold": 47, - "immune": 1 - }, - { - "id": 57, - "name": "Hellhound", - "max_hp": 66, - "max_dmg": 56, - "armor": 44, - "level": 20, - "max_exp": 140, - "max_gold": 47, - "immune": 0 - }, - { - "id": 58, - "name": "Wizard", - "max_hp": 66, - "max_dmg": 56, - "armor": 44, - "level": 20, - "max_exp": 144, - "max_gold": 48, - "immune": 0 - }, - { - "id": 59, - "name": "Uruk", - "max_hp": 68, - "max_dmg": 58, - "armor": 44, - "level": 20, - "max_exp": 146, - "max_gold": 49, - "immune": 0 - }, - { - "id": 60, - "name": "Siren", - "max_hp": 68, - "max_dmg": 400, - "armor": 800, - "level": 50, - "max_exp": 10000, - "max_gold": 50, - "immune": 2 - }, - { - "id": 61, - "name": "Megawraith", - "max_hp": 70, - "max_dmg": 60, - "armor": 46, - "level": 21, - "max_exp": 155, - "max_gold": 52, - "immune": 0 - }, - { - "id": 62, - "name": "Dawkin", - "max_hp": 70, - "max_dmg": 60, - "armor": 46, - "level": 21, - "max_exp": 155, - "max_gold": 52, - "immune": 0 - }, - { - "id": 63, - "name": "Grey Bear", - "max_hp": 70, - "max_dmg": 62, - "armor": 48, - "level": 21, - "max_exp": 160, - "max_gold": 53, - "immune": 0 - }, - { - "id": 64, - "name": "Haunt", - "max_hp": 72, - "max_dmg": 62, - "armor": 48, - "level": 22, - "max_exp": 160, - "max_gold": 53, - "immune": 0 - }, - { - "id": 65, - "name": "Hellbeast", - "max_hp": 74, - "max_dmg": 64, - "armor": 50, - "level": 22, - "max_exp": 165, - "max_gold": 55, - "immune": 0 - }, - { - "id": 66, - "name": "Fear", - "max_hp": 76, - "max_dmg": 66, - "armor": 52, - "level": 23, - "max_exp": 165, - "max_gold": 55, - "immune": 0 - }, - { - "id": 67, - "name": "Beast", - "max_hp": 76, - "max_dmg": 66, - "armor": 52, - "level": 23, - "max_exp": 170, - "max_gold": 57, - "immune": 0 - }, - { - "id": 68, - "name": "Ogre", - "max_hp": 78, - "max_dmg": 68, - "armor": 54, - "level": 23, - "max_exp": 170, - "max_gold": 57, - "immune": 0 - }, - { - "id": 69, - "name": "Dark Bear", - "max_hp": 80, - "max_dmg": 70, - "armor": 56, - "level": 24, - "max_exp": 175, - "max_gold": 58, - "immune": 1 - }, - { - "id": 70, - "name": "Fire", - "max_hp": 80, - "max_dmg": 72, - "armor": 56, - "level": 24, - "max_exp": 175, - "max_gold": 58, - "immune": 0 - }, - { - "id": 71, - "name": "Polgergeist", - "max_hp": 84, - "max_dmg": 74, - "armor": 58, - "level": 25, - "max_exp": 180, - "max_gold": 60, - "immune": 0 - }, - { - "id": 72, - "name": "Fright", - "max_hp": 86, - "max_dmg": 76, - "armor": 58, - "level": 25, - "max_exp": 180, - "max_gold": 60, - "immune": 0 - }, - { - "id": 73, - "name": "Lycan", - "max_hp": 88, - "max_dmg": 78, - "armor": 60, - "level": 25, - "max_exp": 185, - "max_gold": 62, - "immune": 0 - }, - { - "id": 74, - "name": "Terra Elemental", - "max_hp": 88, - "max_dmg": 80, - "armor": 62, - "level": 25, - "max_exp": 185, - "max_gold": 62, - "immune": 1 - }, - { - "id": 75, - "name": "Necromancer", - "max_hp": 90, - "max_dmg": 80, - "armor": 62, - "level": 26, - "max_exp": 190, - "max_gold": 63, - "immune": 0 - }, - { - "id": 76, - "name": "Ultrawraith", - "max_hp": 90, - "max_dmg": 82, - "armor": 64, - "level": 26, - "max_exp": 190, - "max_gold": 63, - "immune": 0 - }, - { - "id": 77, - "name": "Dawkor", - "max_hp": 92, - "max_dmg": 82, - "armor": 64, - "level": 26, - "max_exp": 195, - "max_gold": 65, - "immune": 0 - }, - { - "id": 78, - "name": "Werebear", - "max_hp": 92, - "max_dmg": 84, - "armor": 65, - "level": 26, - "max_exp": 195, - "max_gold": 65, - "immune": 0 - }, - { - "id": 79, - "name": "Brute", - "max_hp": 94, - "max_dmg": 84, - "armor": 65, - "level": 27, - "max_exp": 200, - "max_gold": 67, - "immune": 0 - }, - { - "id": 80, - "name": "Large Beast", - "max_hp": 96, - "max_dmg": 88, - "armor": 66, - "level": 27, - "max_exp": 200, - "max_gold": 67, - "immune": 0 - }, - { - "id": 81, - "name": "Horror", - "max_hp": 96, - "max_dmg": 88, - "armor": 68, - "level": 27, - "max_exp": 210, - "max_gold": 70, - "immune": 0 - }, - { - "id": 82, - "name": "Flame", - "max_hp": 100, - "max_dmg": 90, - "armor": 70, - "level": 28, - "max_exp": 210, - "max_gold": 70, - "immune": 0 - }, - { - "id": 83, - "name": "Lycanthor", - "max_hp": 100, - "max_dmg": 90, - "armor": 70, - "level": 28, - "max_exp": 210, - "max_gold": 70, - "immune": 0 - }, - { - "id": 84, - "name": "Wyrm", - "max_hp": 100, - "max_dmg": 92, - "armor": 72, - "level": 28, - "max_exp": 220, - "max_gold": 73, - "immune": 0 - }, - { - "id": 85, - "name": "Aero Elemental", - "max_hp": 104, - "max_dmg": 94, - "armor": 74, - "level": 29, - "max_exp": 220, - "max_gold": 73, - "immune": 1 - }, - { - "id": 86, - "name": "Dawkare", - "max_hp": 106, - "max_dmg": 96, - "armor": 76, - "level": 29, - "max_exp": 220, - "max_gold": 73, - "immune": 0 - }, - { - "id": 87, - "name": "Large Brute", - "max_hp": 108, - "max_dmg": 98, - "armor": 78, - "level": 29, - "max_exp": 230, - "max_gold": 77, - "immune": 0 - }, - { - "id": 88, - "name": "Frost Wyrm", - "max_hp": 110, - "max_dmg": 100, - "armor": 80, - "level": 30, - "max_exp": 230, - "max_gold": 77, - "immune": 0 - }, - { - "id": 89, - "name": "Knight", - "max_hp": 110, - "max_dmg": 102, - "armor": 80, - "level": 30, - "max_exp": 240, - "max_gold": 80, - "immune": 0 - }, - { - "id": 90, - "name": "Lycanthra", - "max_hp": 112, - "max_dmg": 104, - "armor": 82, - "level": 30, - "max_exp": 240, - "max_gold": 80, - "immune": 0 - }, - { - "id": 91, - "name": "Terror", - "max_hp": 115, - "max_dmg": 108, - "armor": 84, - "level": 31, - "max_exp": 250, - "max_gold": 83, - "immune": 0 - }, - { - "id": 92, - "name": "Blaze", - "max_hp": 118, - "max_dmg": 108, - "armor": 84, - "level": 31, - "max_exp": 250, - "max_gold": 83, - "immune": 0 - }, - { - "id": 93, - "name": "Aqua Elemental", - "max_hp": 120, - "max_dmg": 110, - "armor": 90, - "level": 31, - "max_exp": 260, - "max_gold": 87, - "immune": 1 - }, - { - "id": 94, - "name": "Fire Wyrm", - "max_hp": 120, - "max_dmg": 110, - "armor": 90, - "level": 32, - "max_exp": 260, - "max_gold": 87, - "immune": 0 - }, - { - "id": 95, - "name": "Lesser Wyvern", - "max_hp": 122, - "max_dmg": 110, - "armor": 92, - "level": 32, - "max_exp": 270, - "max_gold": 90, - "immune": 0 - }, - { - "id": 96, - "name": "Doomer", - "max_hp": 124, - "max_dmg": 112, - "armor": 92, - "level": 32, - "max_exp": 270, - "max_gold": 90, - "immune": 0 - }, - { - "id": 97, - "name": "Armor Knight", - "max_hp": 130, - "max_dmg": 115, - "armor": 95, - "level": 33, - "max_exp": 280, - "max_gold": 93, - "immune": 0 - }, - { - "id": 98, - "name": "Wyvern", - "max_hp": 134, - "max_dmg": 120, - "armor": 95, - "level": 33, - "max_exp": 290, - "max_gold": 97, - "immune": 0 - }, - { - "id": 99, - "name": "Nightmare", - "max_hp": 138, - "max_dmg": 125, - "armor": 100, - "level": 33, - "max_exp": 300, - "max_gold": 100, - "immune": 0 - }, - { - "id": 100, - "name": "Fira Elemental", - "max_hp": 140, - "max_dmg": 125, - "armor": 100, - "level": 34, - "max_exp": 310, - "max_gold": 103, - "immune": 1 - }, - { - "id": 101, - "name": "Megadoomer", - "max_hp": 140, - "max_dmg": 128, - "armor": 105, - "level": 34, - "max_exp": 320, - "max_gold": 107, - "immune": 0 - }, - { - "id": 102, - "name": "Greater Wyvern", - "max_hp": 145, - "max_dmg": 130, - "armor": 105, - "level": 34, - "max_exp": 335, - "max_gold": 112, - "immune": 0 - }, - { - "id": 103, - "name": "Advocate", - "max_hp": 148, - "max_dmg": 132, - "armor": 108, - "level": 35, - "max_exp": 350, - "max_gold": 117, - "immune": 0 - }, - { - "id": 104, - "name": "Strong Knight", - "max_hp": 150, - "max_dmg": 135, - "armor": 110, - "level": 35, - "max_exp": 365, - "max_gold": 122, - "immune": 0 - }, - { - "id": 105, - "name": "Liche", - "max_hp": 150, - "max_dmg": 135, - "armor": 110, - "level": 35, - "max_exp": 380, - "max_gold": 127, - "immune": 0 - }, - { - "id": 106, - "name": "Ultradoomer", - "max_hp": 155, - "max_dmg": 140, - "armor": 115, - "level": 36, - "max_exp": 395, - "max_gold": 132, - "immune": 0 - }, - { - "id": 107, - "name": "Fanatic", - "max_hp": 160, - "max_dmg": 140, - "armor": 115, - "level": 36, - "max_exp": 410, - "max_gold": 137, - "immune": 0 - }, - { - "id": 108, - "name": "Green Dragon", - "max_hp": 160, - "max_dmg": 140, - "armor": 115, - "level": 36, - "max_exp": 425, - "max_gold": 142, - "immune": 0 - }, - { - "id": 109, - "name": "Fiend", - "max_hp": 160, - "max_dmg": 145, - "armor": 120, - "level": 37, - "max_exp": 445, - "max_gold": 148, - "immune": 0 - }, - { - "id": 110, - "name": "Greatest Wyvern", - "max_hp": 162, - "max_dmg": 150, - "armor": 120, - "level": 37, - "max_exp": 465, - "max_gold": 155, - "immune": 0 - }, - { - "id": 111, - "name": "Lesser Devil", - "max_hp": 164, - "max_dmg": 150, - "armor": 120, - "level": 37, - "max_exp": 485, - "max_gold": 162, - "immune": 0 - }, - { - "id": 112, - "name": "Liche Master", - "max_hp": 168, - "max_dmg": 155, - "armor": 125, - "level": 38, - "max_exp": 505, - "max_gold": 168, - "immune": 0 - }, - { - "id": 113, - "name": "Zealot", - "max_hp": 168, - "max_dmg": 155, - "armor": 125, - "level": 38, - "max_exp": 530, - "max_gold": 177, - "immune": 0 - }, - { - "id": 114, - "name": "Serafiend", - "max_hp": 170, - "max_dmg": 155, - "armor": 125, - "level": 38, - "max_exp": 555, - "max_gold": 185, - "immune": 0 - }, - { - "id": 115, - "name": "Pale Knight", - "max_hp": 175, - "max_dmg": 160, - "armor": 130, - "level": 39, - "max_exp": 580, - "max_gold": 193, - "immune": 0 - }, - { - "id": 116, - "name": "Blue Dragon", - "max_hp": 180, - "max_dmg": 160, - "armor": 130, - "level": 39, - "max_exp": 605, - "max_gold": 202, - "immune": 0 - }, - { - "id": 117, - "name": "Obsessive", - "max_hp": 180, - "max_dmg": 160, - "armor": 135, - "level": 40, - "max_exp": 630, - "max_gold": 210, - "immune": 0 - }, - { - "id": 118, - "name": "Devil", - "max_hp": 184, - "max_dmg": 164, - "armor": 135, - "level": 40, - "max_exp": 666, - "max_gold": 222, - "immune": 0 - }, - { - "id": 119, - "name": "Liche Prince", - "max_hp": 190, - "max_dmg": 168, - "armor": 138, - "level": 40, - "max_exp": 660, - "max_gold": 220, - "immune": 0 - }, - { - "id": 120, - "name": "Cherufiend", - "max_hp": 195, - "max_dmg": 170, - "armor": 140, - "level": 41, - "max_exp": 690, - "max_gold": 230, - "immune": 0 - }, - { - "id": 121, - "name": "Red Dragon", - "max_hp": 200, - "max_dmg": 180, - "armor": 145, - "level": 41, - "max_exp": 720, - "max_gold": 240, - "immune": 0 - }, - { - "id": 122, - "name": "Greater Devil", - "max_hp": 200, - "max_dmg": 180, - "armor": 145, - "level": 41, - "max_exp": 750, - "max_gold": 250, - "immune": 0 - }, - { - "id": 123, - "name": "Renegade", - "max_hp": 205, - "max_dmg": 185, - "armor": 150, - "level": 42, - "max_exp": 780, - "max_gold": 260, - "immune": 0 - }, - { - "id": 124, - "name": "Archfiend", - "max_hp": 210, - "max_dmg": 190, - "armor": 150, - "level": 42, - "max_exp": 810, - "max_gold": 270, - "immune": 0 - }, - { - "id": 125, - "name": "Liche Lord", - "max_hp": 210, - "max_dmg": 190, - "armor": 155, - "level": 42, - "max_exp": 850, - "max_gold": 283, - "immune": 0 - }, - { - "id": 126, - "name": "Greatest Devil", - "max_hp": 215, - "max_dmg": 195, - "armor": 160, - "level": 43, - "max_exp": 890, - "max_gold": 297, - "immune": 0 - }, - { - "id": 127, - "name": "Dark Knight", - "max_hp": 220, - "max_dmg": 200, - "armor": 160, - "level": 43, - "max_exp": 930, - "max_gold": 310, - "immune": 0 - }, - { - "id": 128, - "name": "Giant", - "max_hp": 220, - "max_dmg": 200, - "armor": 165, - "level": 43, - "max_exp": 970, - "max_gold": 323, - "immune": 0 - }, - { - "id": 129, - "name": "Shadow Dragon", - "max_hp": 225, - "max_dmg": 200, - "armor": 170, - "level": 44, - "max_exp": 1010, - "max_gold": 337, - "immune": 0 - }, - { - "id": 130, - "name": "Liche King", - "max_hp": 225, - "max_dmg": 205, - "armor": 170, - "level": 44, - "max_exp": 1050, - "max_gold": 350, - "immune": 0 - }, - { - "id": 131, - "name": "Incubus", - "max_hp": 230, - "max_dmg": 205, - "armor": 175, - "level": 44, - "max_exp": 1100, - "max_gold": 367, - "immune": 1 - }, - { - "id": 132, - "name": "Traitor", - "max_hp": 230, - "max_dmg": 205, - "armor": 175, - "level": 45, - "max_exp": 1150, - "max_gold": 383, - "immune": 0 - }, - { - "id": 133, - "name": "Demon", - "max_hp": 240, - "max_dmg": 210, - "armor": 180, - "level": 45, - "max_exp": 1200, - "max_gold": 400, - "immune": 0 - }, - { - "id": 134, - "name": "Dark Dragon", - "max_hp": 245, - "max_dmg": 215, - "armor": 180, - "level": 45, - "max_exp": 1250, - "max_gold": 417, - "immune": 1 - }, - { - "id": 135, - "name": "Insurgent", - "max_hp": 250, - "max_dmg": 220, - "armor": 190, - "level": 46, - "max_exp": 1300, - "max_gold": 433, - "immune": 0 - }, - { - "id": 136, - "name": "Leviathan", - "max_hp": 255, - "max_dmg": 225, - "armor": 190, - "level": 46, - "max_exp": 1350, - "max_gold": 450, - "immune": 0 - }, - { - "id": 137, - "name": "Grey Daemon", - "max_hp": 260, - "max_dmg": 230, - "armor": 190, - "level": 46, - "max_exp": 1400, - "max_gold": 467, - "immune": 0 - }, - { - "id": 138, - "name": "Succubus", - "max_hp": 265, - "max_dmg": 240, - "armor": 200, - "level": 47, - "max_exp": 1460, - "max_gold": 487, - "immune": 1 - }, - { - "id": 139, - "name": "Demon Prince", - "max_hp": 270, - "max_dmg": 240, - "armor": 200, - "level": 47, - "max_exp": 1520, - "max_gold": 507, - "immune": 0 - }, - { - "id": 140, - "name": "Black Dragon", - "max_hp": 275, - "max_dmg": 250, - "armor": 205, - "level": 47, - "max_exp": 1580, - "max_gold": 527, - "immune": 1 - }, - { - "id": 141, - "name": "Nihilist", - "max_hp": 280, - "max_dmg": 250, - "armor": 205, - "level": 47, - "max_exp": 1640, - "max_gold": 547, - "immune": 0 - }, - { - "id": 142, - "name": "Behemoth", - "max_hp": 285, - "max_dmg": 260, - "armor": 210, - "level": 48, - "max_exp": 1700, - "max_gold": 567, - "immune": 0 - }, - { - "id": 143, - "name": "Demagogue", - "max_hp": 290, - "max_dmg": 260, - "armor": 210, - "level": 48, - "max_exp": 1760, - "max_gold": 587, - "immune": 0 - }, - { - "id": 144, - "name": "Demon Lord", - "max_hp": 300, - "max_dmg": 270, - "armor": 220, - "level": 48, - "max_exp": 1820, - "max_gold": 607, - "immune": 0 - }, - { - "id": 145, - "name": "Red Daemon", - "max_hp": 310, - "max_dmg": 280, - "armor": 230, - "level": 48, - "max_exp": 1880, - "max_gold": 627, - "immune": 0 - }, - { - "id": 146, - "name": "Colossus", - "max_hp": 320, - "max_dmg": 300, - "armor": 240, - "level": 49, - "max_exp": 1940, - "max_gold": 647, - "immune": 0 - }, - { - "id": 147, - "name": "Demon King", - "max_hp": 330, - "max_dmg": 300, - "armor": 250, - "level": 49, - "max_exp": 2000, - "max_gold": 667, - "immune": 0 - }, - { - "id": 148, - "name": "Dark Daemon", - "max_hp": 340, - "max_dmg": 320, - "armor": 260, - "level": 49, - "max_exp": 2200, - "max_gold": 733, - "immune": 1 - }, - { - "id": 149, - "name": "Titan", - "max_hp": 360, - "max_dmg": 340, - "armor": 270, - "level": 50, - "max_exp": 2400, - "max_gold": 800, - "immune": 0 - }, - { - "id": 150, - "name": "Black Daemon", - "max_hp": 400, - "max_dmg": 400, - "armor": 280, - "level": 50, - "max_exp": 3000, - "max_gold": 1000, - "immune": 1 - }, - { - "id": 151, - "name": "Lucifuge", - "max_hp": 600, - "max_dmg": 600, - "armor": 400, - "level": 50, - "max_exp": 10000, - "max_gold": 10000, - "immune": 2 - } + { + "id": 31, + "name": "Silver Scorpion", + "max_hp": 30, + "max_dmg": 160, + "armor": 350, + "level": 40, + "max_exp": 63, + "max_gold": 2000, + "immune": 2 + }, + { + "id": 75, + "name": "Necromancer", + "max_hp": 90, + "max_dmg": 80, + "armor": 62, + "level": 26, + "max_exp": 190, + "max_gold": 63, + "immune": 0 + }, + { + "id": 21, + "name": "Magician", + "max_hp": 30, + "max_dmg": 24, + "armor": 10, + "level": 8, + "max_exp": 36, + "max_gold": 12, + "immune": 0 + }, + { + "id": 36, + "name": "Scoundrel", + "max_hp": 43, + "max_dmg": 35, + "armor": 22, + "level": 12, + "max_exp": 75, + "max_gold": 25, + "immune": 0 + }, + { + "id": 60, + "name": "Siren", + "max_hp": 68, + "max_dmg": 400, + "armor": 800, + "level": 50, + "max_exp": 10000, + "max_gold": 50, + "immune": 2 + }, + { + "id": 88, + "name": "Frost Wyrm", + "max_hp": 110, + "max_dmg": 100, + "armor": 80, + "level": 30, + "max_exp": 230, + "max_gold": 77, + "immune": 0 + }, + { + "id": 120, + "name": "Cherufiend", + "max_hp": 195, + "max_dmg": 170, + "armor": 140, + "level": 41, + "max_exp": 690, + "max_gold": 230, + "immune": 0 + }, + { + "id": 126, + "name": "Greatest Devil", + "max_hp": 215, + "max_dmg": 195, + "armor": 160, + "level": 43, + "max_exp": 890, + "max_gold": 297, + "immune": 0 + }, + { + "id": 111, + "name": "Lesser Devil", + "max_hp": 164, + "max_dmg": 150, + "armor": 120, + "level": 37, + "max_exp": 485, + "max_gold": 162, + "immune": 0 + }, + { + "id": 39, + "name": "Phantom", + "max_hp": 46, + "max_dmg": 38, + "armor": 24, + "level": 14, + "max_exp": 85, + "max_gold": 28, + "immune": 1 + }, + { + "id": 17, + "name": "Ghost", + "max_hp": 24, + "max_dmg": 20, + "armor": 8, + "level": 6, + "max_exp": 28, + "max_gold": 9, + "immune": 0 + }, + { + "id": 103, + "name": "Advocate", + "max_hp": 148, + "max_dmg": 132, + "armor": 108, + "level": 35, + "max_exp": 350, + "max_gold": 117, + "immune": 0 + }, + { + "id": 11, + "name": "Raven", + "max_hp": 16, + "max_dmg": 13, + "armor": 5, + "level": 4, + "max_exp": 18, + "max_gold": 6, + "immune": 0 + }, + { + "id": 73, + "name": "Lycan", + "max_hp": 88, + "max_dmg": 78, + "armor": 60, + "level": 25, + "max_exp": 185, + "max_gold": 62, + "immune": 0 + }, + { + "id": 23, + "name": "Drakefin", + "max_hp": 32, + "max_dmg": 26, + "armor": 12, + "level": 8, + "max_exp": 40, + "max_gold": 13, + "immune": 0 + }, + { + "id": 57, + "name": "Hellhound", + "max_hp": 66, + "max_dmg": 56, + "armor": 44, + "level": 20, + "max_exp": 140, + "max_gold": 47, + "immune": 0 + }, + { + "id": 104, + "name": "Strong Knight", + "max_hp": 150, + "max_dmg": 135, + "armor": 110, + "level": 35, + "max_exp": 365, + "max_gold": 122, + "immune": 0 + }, + { + "id": 13, + "name": "Illusion", + "max_hp": 20, + "max_dmg": 15, + "armor": 6, + "level": 5, + "max_exp": 20, + "max_gold": 7, + "immune": 1 + }, + { + "id": 72, + "name": "Fright", + "max_hp": 86, + "max_dmg": 76, + "armor": 58, + "level": 25, + "max_exp": 180, + "max_gold": 60, + "immune": 0 + }, + { + "id": 122, + "name": "Greater Devil", + "max_hp": 200, + "max_dmg": 180, + "armor": 145, + "level": 41, + "max_exp": 750, + "max_gold": 250, + "immune": 0 + }, + { + "id": 33, + "name": "Sorceror", + "max_hp": 41, + "max_dmg": 33, + "armor": 22, + "level": 11, + "max_exp": 68, + "max_gold": 23, + "immune": 0 + }, + { + "id": 8, + "name": "Drakelor", + "max_hp": 14, + "max_dmg": 12, + "armor": 4, + "level": 3, + "max_exp": 10, + "max_gold": 3, + "immune": 0 + }, + { + "id": 116, + "name": "Blue Dragon", + "max_hp": 180, + "max_dmg": 160, + "armor": 130, + "level": 39, + "max_exp": 605, + "max_gold": 202, + "immune": 0 + }, + { + "id": 15, + "name": "Drakemal", + "max_hp": 22, + "max_dmg": 18, + "armor": 7, + "level": 6, + "max_exp": 24, + "max_gold": 8, + "immune": 0 + }, + { + "id": 6, + "name": "Drake", + "max_hp": 11, + "max_dmg": 10, + "armor": 3, + "level": 2, + "max_exp": 8, + "max_gold": 3, + "immune": 0 + }, + { + "id": 27, + "name": "Knave", + "max_hp": 36, + "max_dmg": 30, + "armor": 15, + "level": 9, + "max_exp": 52, + "max_gold": 17, + "immune": 0 + }, + { + "id": 137, + "name": "Grey Daemon", + "max_hp": 260, + "max_dmg": 230, + "armor": 190, + "level": 46, + "max_exp": 1400, + "max_gold": 467, + "immune": 0 + }, + { + "id": 151, + "name": "Lucifuge", + "max_hp": 600, + "max_dmg": 600, + "armor": 400, + "level": 50, + "max_exp": 10000, + "max_gold": 10000, + "immune": 2 + }, + { + "id": 42, + "name": "Warlock", + "max_hp": 48, + "max_dmg": 40, + "armor": 26, + "level": 15, + "max_exp": 100, + "max_gold": 33, + "immune": 1 + }, + { + "id": 78, + "name": "Werebear", + "max_hp": 92, + "max_dmg": 84, + "armor": 65, + "level": 26, + "max_exp": 195, + "max_gold": 65, + "immune": 0 + }, + { + "id": 46, + "name": "Hellion", + "max_hp": 50, + "max_dmg": 45, + "armor": 30, + "level": 16, + "max_exp": 110, + "max_gold": 37, + "immune": 0 + }, + { + "id": 56, + "name": "Figment", + "max_hp": 64, + "max_dmg": 55, + "armor": 42, + "level": 19, + "max_exp": 140, + "max_gold": 47, + "immune": 1 + }, + { + "id": 2, + "name": "Red Slime", + "max_hp": 6, + "max_dmg": 5, + "armor": 1, + "level": 1, + "max_exp": 2, + "max_gold": 1, + "immune": 0 + }, + { + "id": 76, + "name": "Ultrawraith", + "max_hp": 90, + "max_dmg": 82, + "armor": 64, + "level": 26, + "max_exp": 190, + "max_gold": 63, + "immune": 0 + }, + { + "id": 109, + "name": "Fiend", + "max_hp": 160, + "max_dmg": 145, + "armor": 120, + "level": 37, + "max_exp": 445, + "max_gold": 148, + "immune": 0 + }, + { + "id": 81, + "name": "Horror", + "max_hp": 96, + "max_dmg": 88, + "armor": 68, + "level": 27, + "max_exp": 210, + "max_gold": 70, + "immune": 0 + }, + { + "id": 142, + "name": "Behemoth", + "max_hp": 285, + "max_dmg": 260, + "armor": 210, + "level": 48, + "max_exp": 1700, + "max_gold": 567, + "immune": 0 + }, + { + "id": 37, + "name": "Megaskeleton", + "max_hp": 44, + "max_dmg": 36, + "armor": 24, + "level": 13, + "max_exp": 78, + "max_gold": 26, + "immune": 0 + }, + { + "id": 91, + "name": "Terror", + "max_hp": 115, + "max_dmg": 108, + "armor": 84, + "level": 31, + "max_exp": 250, + "max_gold": 83, + "immune": 0 + }, + { + "id": 117, + "name": "Obsessive", + "max_hp": 180, + "max_dmg": 160, + "armor": 135, + "level": 40, + "max_exp": 630, + "max_gold": 210, + "immune": 0 + }, + { + "id": 123, + "name": "Renegade", + "max_hp": 205, + "max_dmg": 185, + "armor": 150, + "level": 42, + "max_exp": 780, + "max_gold": 260, + "immune": 0 + }, + { + "id": 133, + "name": "Demon", + "max_hp": 240, + "max_dmg": 210, + "armor": 180, + "level": 45, + "max_exp": 1200, + "max_gold": 400, + "immune": 0 + }, + { + "id": 49, + "name": "Dark Wolf", + "max_hp": 54, + "max_dmg": 47, + "armor": 36, + "level": 17, + "max_exp": 120, + "max_gold": 40, + "immune": 1 + }, + { + "id": 32, + "name": "Mirage", + "max_hp": 40, + "max_dmg": 32, + "armor": 20, + "level": 11, + "max_exp": 64, + "max_gold": 21, + "immune": 1 + }, + { + "id": 143, + "name": "Demagogue", + "max_hp": 290, + "max_dmg": 260, + "armor": 210, + "level": 48, + "max_exp": 1760, + "max_gold": 587, + "immune": 0 + }, + { + "id": 95, + "name": "Lesser Wyvern", + "max_hp": 122, + "max_dmg": 110, + "armor": 92, + "level": 32, + "max_exp": 270, + "max_gold": 90, + "immune": 0 + }, + { + "id": 141, + "name": "Nihilist", + "max_hp": 280, + "max_dmg": 250, + "armor": 205, + "level": 47, + "max_exp": 1640, + "max_gold": 547, + "immune": 0 + }, + { + "id": 101, + "name": "Megadoomer", + "max_hp": 140, + "max_dmg": 128, + "armor": 105, + "level": 34, + "max_exp": 320, + "max_gold": 107, + "immune": 0 + }, + { + "id": 96, + "name": "Doomer", + "max_hp": 124, + "max_dmg": 112, + "armor": 92, + "level": 32, + "max_exp": 270, + "max_gold": 90, + "immune": 0 + }, + { + "id": 85, + "name": "Aero Elemental", + "max_hp": 104, + "max_dmg": 94, + "armor": 74, + "level": 29, + "max_exp": 220, + "max_gold": 73, + "immune": 1 + }, + { + "id": 79, + "name": "Brute", + "max_hp": 94, + "max_dmg": 84, + "armor": 65, + "level": 27, + "max_exp": 200, + "max_gold": 67, + "immune": 0 + }, + { + "id": 29, + "name": "Skeleton", + "max_hp": 38, + "max_dmg": 30, + "armor": 18, + "level": 10, + "max_exp": 58, + "max_gold": 19, + "immune": 0 + }, + { + "id": 7, + "name": "Shade", + "max_hp": 12, + "max_dmg": 10, + "armor": 3, + "level": 3, + "max_exp": 10, + "max_gold": 3, + "immune": 1 + }, + { + "id": 130, + "name": "Liche King", + "max_hp": 225, + "max_dmg": 205, + "armor": 170, + "level": 44, + "max_exp": 1050, + "max_gold": 350, + "immune": 0 + }, + { + "id": 71, + "name": "Polgergeist", + "max_hp": 84, + "max_dmg": 74, + "armor": 58, + "level": 25, + "max_exp": 180, + "max_gold": 60, + "immune": 0 + }, + { + "id": 38, + "name": "Grey Wolf", + "max_hp": 44, + "max_dmg": 36, + "armor": 24, + "level": 13, + "max_exp": 82, + "max_gold": 27, + "immune": 0 + }, + { + "id": 3, + "name": "Critter", + "max_hp": 6, + "max_dmg": 5, + "armor": 2, + "level": 1, + "max_exp": 4, + "max_gold": 2, + "immune": 0 + }, + { + "id": 25, + "name": "Fire Raven", + "max_hp": 34, + "max_dmg": 28, + "armor": 14, + "level": 9, + "max_exp": 45, + "max_gold": 15, + "immune": 0 + }, + { + "id": 61, + "name": "Megawraith", + "max_hp": 70, + "max_dmg": 60, + "armor": 46, + "level": 21, + "max_exp": 155, + "max_gold": 52, + "immune": 0 + }, + { + "id": 132, + "name": "Traitor", + "max_hp": 230, + "max_dmg": 205, + "armor": 175, + "level": 45, + "max_exp": 1150, + "max_gold": 383, + "immune": 0 + }, + { + "id": 16, + "name": "Shadow Raven", + "max_hp": 24, + "max_dmg": 18, + "armor": 7, + "level": 6, + "max_exp": 26, + "max_gold": 9, + "immune": 1 + }, + { + "id": 20, + "name": "Ghoul", + "max_hp": 29, + "max_dmg": 24, + "armor": 9, + "level": 7, + "max_exp": 34, + "max_gold": 11, + "immune": 0 + }, + { + "id": 82, + "name": "Flame", + "max_hp": 100, + "max_dmg": 90, + "armor": 70, + "level": 28, + "max_exp": 210, + "max_gold": 70, + "immune": 0 + }, + { + "id": 131, + "name": "Incubus", + "max_hp": 230, + "max_dmg": 205, + "armor": 175, + "level": 44, + "max_exp": 1100, + "max_gold": 367, + "immune": 1 + }, + { + "id": 48, + "name": "Ultraskeleton", + "max_hp": 52, + "max_dmg": 46, + "armor": 32, + "level": 16, + "max_exp": 116, + "max_gold": 39, + "immune": 0 + }, + { + "id": 124, + "name": "Archfiend", + "max_hp": 210, + "max_dmg": 190, + "armor": 150, + "level": 42, + "max_exp": 810, + "max_gold": 270, + "immune": 0 + }, + { + "id": 105, + "name": "Liche", + "max_hp": 150, + "max_dmg": 135, + "armor": 110, + "level": 35, + "max_exp": 380, + "max_gold": 127, + "immune": 0 + }, + { + "id": 134, + "name": "Dark Dragon", + "max_hp": 245, + "max_dmg": 215, + "armor": 180, + "level": 45, + "max_exp": 1250, + "max_gold": 417, + "immune": 1 + }, + { + "id": 63, + "name": "Grey Bear", + "max_hp": 70, + "max_dmg": 62, + "armor": 48, + "level": 21, + "max_exp": 160, + "max_gold": 53, + "immune": 0 + }, + { + "id": 50, + "name": "Troll", + "max_hp": 56, + "max_dmg": 48, + "armor": 36, + "level": 17, + "max_exp": 120, + "max_gold": 40, + "immune": 0 + }, + { + "id": 12, + "name": "Scorpion", + "max_hp": 18, + "max_dmg": 14, + "armor": 6, + "level": 5, + "max_exp": 20, + "max_gold": 7, + "immune": 0 + }, + { + "id": 113, + "name": "Zealot", + "max_hp": 168, + "max_dmg": 155, + "armor": 125, + "level": 38, + "max_exp": 530, + "max_gold": 177, + "immune": 0 + }, + { + "id": 139, + "name": "Demon Prince", + "max_hp": 270, + "max_dmg": 240, + "armor": 200, + "level": 47, + "max_exp": 1520, + "max_gold": 507, + "immune": 0 + }, + { + "id": 9, + "name": "Silver Slime", + "max_hp": 15, + "max_dmg": 100, + "armor": 200, + "level": 30, + "max_exp": 15, + "max_gold": 1000, + "immune": 2 + }, + { + "id": 150, + "name": "Black Daemon", + "max_hp": 400, + "max_dmg": 400, + "armor": 280, + "level": 50, + "max_exp": 3000, + "max_gold": 1000, + "immune": 1 + }, + { + "id": 107, + "name": "Fanatic", + "max_hp": 160, + "max_dmg": 140, + "armor": 115, + "level": 36, + "max_exp": 410, + "max_gold": 137, + "immune": 0 + }, + { + "id": 55, + "name": "Dawk", + "max_hp": 60, + "max_dmg": 54, + "armor": 40, + "level": 19, + "max_exp": 136, + "max_gold": 45, + "immune": 0 + }, + { + "id": 148, + "name": "Dark Daemon", + "max_hp": 340, + "max_dmg": 320, + "armor": 260, + "level": 49, + "max_exp": 2200, + "max_gold": 733, + "immune": 1 + }, + { + "id": 87, + "name": "Large Brute", + "max_hp": 108, + "max_dmg": 98, + "armor": 78, + "level": 29, + "max_exp": 230, + "max_gold": 77, + "immune": 0 + }, + { + "id": 119, + "name": "Liche Prince", + "max_hp": 190, + "max_dmg": 168, + "armor": 138, + "level": 40, + "max_exp": 660, + "max_gold": 220, + "immune": 0 + }, + { + "id": 51, + "name": "Werewolf", + "max_hp": 56, + "max_dmg": 48, + "armor": 38, + "level": 17, + "max_exp": 124, + "max_gold": 41, + "immune": 0 + }, + { + "id": 53, + "name": "Spirit", + "max_hp": 58, + "max_dmg": 50, + "armor": 38, + "level": 18, + "max_exp": 132, + "max_gold": 44, + "immune": 0 + }, + { + "id": 34, + "name": "Imp", + "max_hp": 42, + "max_dmg": 34, + "armor": 22, + "level": 12, + "max_exp": 70, + "max_gold": 23, + "immune": 0 + }, + { + "id": 112, + "name": "Liche Master", + "max_hp": 168, + "max_dmg": 155, + "armor": 125, + "level": 38, + "max_exp": 505, + "max_gold": 168, + "immune": 0 + }, + { + "id": 114, + "name": "Serafiend", + "max_hp": 170, + "max_dmg": 155, + "armor": 125, + "level": 38, + "max_exp": 555, + "max_gold": 185, + "immune": 0 + }, + { + "id": 35, + "name": "Nymph", + "max_hp": 43, + "max_dmg": 35, + "armor": 22, + "level": 12, + "max_exp": 70, + "max_gold": 23, + "immune": 0 + }, + { + "id": 140, + "name": "Black Dragon", + "max_hp": 275, + "max_dmg": 250, + "armor": 205, + "level": 47, + "max_exp": 1580, + "max_gold": 527, + "immune": 1 + }, + { + "id": 86, + "name": "Dawkare", + "max_hp": 106, + "max_dmg": 96, + "armor": 76, + "level": 29, + "max_exp": 220, + "max_gold": 73, + "immune": 0 + }, + { + "id": 97, + "name": "Armor Knight", + "max_hp": 130, + "max_dmg": 115, + "armor": 95, + "level": 33, + "max_exp": 280, + "max_gold": 93, + "immune": 0 + }, + { + "id": 77, + "name": "Dawkor", + "max_hp": 92, + "max_dmg": 82, + "armor": 64, + "level": 26, + "max_exp": 195, + "max_gold": 65, + "immune": 0 + }, + { + "id": 66, + "name": "Fear", + "max_hp": 76, + "max_dmg": 66, + "armor": 52, + "level": 23, + "max_exp": 165, + "max_gold": 55, + "immune": 0 + }, + { + "id": 54, + "name": "Nisse", + "max_hp": 60, + "max_dmg": 52, + "armor": 40, + "level": 19, + "max_exp": 132, + "max_gold": 44, + "immune": 0 + }, + { + "id": 19, + "name": "Rogue Scorpion", + "max_hp": 28, + "max_dmg": 22, + "armor": 9, + "level": 7, + "max_exp": 32, + "max_gold": 11, + "immune": 0 + }, + { + "id": 90, + "name": "Lycanthra", + "max_hp": 112, + "max_dmg": 104, + "armor": 82, + "level": 30, + "max_exp": 240, + "max_gold": 80, + "immune": 0 + }, + { + "id": 70, + "name": "Fire", + "max_hp": 80, + "max_dmg": 72, + "armor": 56, + "level": 24, + "max_exp": 175, + "max_gold": 58, + "immune": 0 + }, + { + "id": 67, + "name": "Beast", + "max_hp": 76, + "max_dmg": 66, + "armor": 52, + "level": 23, + "max_exp": 170, + "max_gold": 57, + "immune": 0 + }, + { + "id": 108, + "name": "Green Dragon", + "max_hp": 160, + "max_dmg": 140, + "armor": 115, + "level": 36, + "max_exp": 425, + "max_gold": 142, + "immune": 0 + }, + { + "id": 125, + "name": "Liche Lord", + "max_hp": 210, + "max_dmg": 190, + "armor": 155, + "level": 42, + "max_exp": 850, + "max_gold": 283, + "immune": 0 + }, + { + "id": 115, + "name": "Pale Knight", + "max_hp": 175, + "max_dmg": 160, + "armor": 130, + "level": 39, + "max_exp": 580, + "max_gold": 193, + "immune": 0 + }, + { + "id": 74, + "name": "Terra Elemental", + "max_hp": 88, + "max_dmg": 80, + "armor": 62, + "level": 25, + "max_exp": 185, + "max_gold": 62, + "immune": 1 + }, + { + "id": 59, + "name": "Uruk", + "max_hp": 68, + "max_dmg": 58, + "armor": 44, + "level": 20, + "max_exp": 146, + "max_gold": 49, + "immune": 0 + }, + { + "id": 135, + "name": "Insurgent", + "max_hp": 250, + "max_dmg": 220, + "armor": 190, + "level": 46, + "max_exp": 1300, + "max_gold": 433, + "immune": 0 + }, + { + "id": 26, + "name": "Dybbuk", + "max_hp": 34, + "max_dmg": 28, + "armor": 14, + "level": 9, + "max_exp": 50, + "max_gold": 17, + "immune": 0 + }, + { + "id": 99, + "name": "Nightmare", + "max_hp": 138, + "max_dmg": 125, + "armor": 100, + "level": 33, + "max_exp": 300, + "max_gold": 100, + "immune": 0 + }, + { + "id": 45, + "name": "Wraith", + "max_hp": 50, + "max_dmg": 45, + "armor": 30, + "level": 16, + "max_exp": 108, + "max_gold": 36, + "immune": 0 + }, + { + "id": 100, + "name": "Fira Elemental", + "max_hp": 140, + "max_dmg": 125, + "armor": 100, + "level": 34, + "max_exp": 310, + "max_gold": 103, + "immune": 1 + }, + { + "id": 110, + "name": "Greatest Wyvern", + "max_hp": 162, + "max_dmg": 150, + "armor": 120, + "level": 37, + "max_exp": 465, + "max_gold": 155, + "immune": 0 + }, + { + "id": 64, + "name": "Haunt", + "max_hp": 72, + "max_dmg": 62, + "armor": 48, + "level": 22, + "max_exp": 160, + "max_gold": 53, + "immune": 0 + }, + { + "id": 1, + "name": "Blue Slime", + "max_hp": 4, + "max_dmg": 3, + "armor": 1, + "level": 1, + "max_exp": 1, + "max_gold": 1, + "immune": 0 + }, + { + "id": 40, + "name": "Specter", + "max_hp": 46, + "max_dmg": 38, + "armor": 24, + "level": 14, + "max_exp": 90, + "max_gold": 30, + "immune": 0 + }, + { + "id": 83, + "name": "Lycanthor", + "max_hp": 100, + "max_dmg": 90, + "armor": 70, + "level": 28, + "max_exp": 210, + "max_gold": 70, + "immune": 0 + }, + { + "id": 14, + "name": "Nightshade", + "max_hp": 22, + "max_dmg": 16, + "armor": 6, + "level": 6, + "max_exp": 24, + "max_gold": 8, + "immune": 0 + }, + { + "id": 69, + "name": "Dark Bear", + "max_hp": 80, + "max_dmg": 70, + "armor": 56, + "level": 24, + "max_exp": 175, + "max_gold": 58, + "immune": 1 + }, + { + "id": 129, + "name": "Shadow Dragon", + "max_hp": 225, + "max_dmg": 200, + "armor": 170, + "level": 44, + "max_exp": 1010, + "max_gold": 337, + "immune": 0 + }, + { + "id": 92, + "name": "Blaze", + "max_hp": 118, + "max_dmg": 108, + "armor": 84, + "level": 31, + "max_exp": 250, + "max_gold": 83, + "immune": 0 + }, + { + "id": 24, + "name": "Shimmer", + "max_hp": 32, + "max_dmg": 26, + "armor": 14, + "level": 8, + "max_exp": 45, + "max_gold": 15, + "immune": 1 + }, + { + "id": 136, + "name": "Leviathan", + "max_hp": 255, + "max_dmg": 225, + "armor": 190, + "level": 46, + "max_exp": 1350, + "max_gold": 450, + "immune": 0 + }, + { + "id": 4, + "name": "Creature", + "max_hp": 10, + "max_dmg": 8, + "armor": 2, + "level": 2, + "max_exp": 4, + "max_gold": 2, + "immune": 0 + }, + { + "id": 5, + "name": "Shadow", + "max_hp": 10, + "max_dmg": 9, + "armor": 3, + "level": 2, + "max_exp": 6, + "max_gold": 2, + "immune": 1 + }, + { + "id": 106, + "name": "Ultradoomer", + "max_hp": 155, + "max_dmg": 140, + "armor": 115, + "level": 36, + "max_exp": 395, + "max_gold": 132, + "immune": 0 + }, + { + "id": 147, + "name": "Demon King", + "max_hp": 330, + "max_dmg": 300, + "armor": 250, + "level": 49, + "max_exp": 2000, + "max_gold": 667, + "immune": 0 + }, + { + "id": 62, + "name": "Dawkin", + "max_hp": 70, + "max_dmg": 60, + "armor": 46, + "level": 21, + "max_exp": 155, + "max_gold": 52, + "immune": 0 + }, + { + "id": 128, + "name": "Giant", + "max_hp": 220, + "max_dmg": 200, + "armor": 165, + "level": 43, + "max_exp": 970, + "max_gold": 323, + "immune": 0 + }, + { + "id": 58, + "name": "Wizard", + "max_hp": 66, + "max_dmg": 56, + "armor": 44, + "level": 20, + "max_exp": 144, + "max_gold": 48, + "immune": 0 + }, + { + "id": 121, + "name": "Red Dragon", + "max_hp": 200, + "max_dmg": 180, + "armor": 145, + "level": 41, + "max_exp": 720, + "max_gold": 240, + "immune": 0 + }, + { + "id": 80, + "name": "Large Beast", + "max_hp": 96, + "max_dmg": 88, + "armor": 66, + "level": 27, + "max_exp": 200, + "max_gold": 67, + "immune": 0 + }, + { + "id": 28, + "name": "Goblin", + "max_hp": 36, + "max_dmg": 30, + "armor": 15, + "level": 10, + "max_exp": 54, + "max_gold": 18, + "immune": 0 + }, + { + "id": 84, + "name": "Wyrm", + "max_hp": 100, + "max_dmg": 92, + "armor": 72, + "level": 28, + "max_exp": 220, + "max_gold": 73, + "immune": 0 + }, + { + "id": 138, + "name": "Succubus", + "max_hp": 265, + "max_dmg": 240, + "armor": 200, + "level": 47, + "max_exp": 1460, + "max_gold": 487, + "immune": 1 + }, + { + "id": 149, + "name": "Titan", + "max_hp": 360, + "max_dmg": 340, + "armor": 270, + "level": 50, + "max_exp": 2400, + "max_gold": 800, + "immune": 0 + }, + { + "id": 10, + "name": "Scamp", + "max_hp": 16, + "max_dmg": 13, + "armor": 5, + "level": 4, + "max_exp": 15, + "max_gold": 5, + "immune": 0 + }, + { + "id": 94, + "name": "Fire Wyrm", + "max_hp": 120, + "max_dmg": 110, + "armor": 90, + "level": 32, + "max_exp": 260, + "max_gold": 87, + "immune": 0 + }, + { + "id": 144, + "name": "Demon Lord", + "max_hp": 300, + "max_dmg": 270, + "armor": 220, + "level": 48, + "max_exp": 1820, + "max_gold": 607, + "immune": 0 + }, + { + "id": 127, + "name": "Dark Knight", + "max_hp": 220, + "max_dmg": 200, + "armor": 160, + "level": 43, + "max_exp": 930, + "max_gold": 310, + "immune": 0 + }, + { + "id": 98, + "name": "Wyvern", + "max_hp": 134, + "max_dmg": 120, + "armor": 95, + "level": 33, + "max_exp": 290, + "max_gold": 97, + "immune": 0 + }, + { + "id": 102, + "name": "Greater Wyvern", + "max_hp": 145, + "max_dmg": 130, + "armor": 105, + "level": 34, + "max_exp": 335, + "max_gold": 112, + "immune": 0 + }, + { + "id": 146, + "name": "Colossus", + "max_hp": 320, + "max_dmg": 300, + "armor": 240, + "level": 49, + "max_exp": 1940, + "max_gold": 647, + "immune": 0 + }, + { + "id": 93, + "name": "Aqua Elemental", + "max_hp": 120, + "max_dmg": 110, + "armor": 90, + "level": 31, + "max_exp": 260, + "max_gold": 87, + "immune": 1 + }, + { + "id": 89, + "name": "Knight", + "max_hp": 110, + "max_dmg": 102, + "armor": 80, + "level": 30, + "max_exp": 240, + "max_gold": 80, + "immune": 0 + }, + { + "id": 47, + "name": "Bandit", + "max_hp": 52, + "max_dmg": 45, + "armor": 30, + "level": 16, + "max_exp": 114, + "max_gold": 38, + "immune": 0 + }, + { + "id": 65, + "name": "Hellbeast", + "max_hp": 74, + "max_dmg": 64, + "armor": 50, + "level": 22, + "max_exp": 165, + "max_gold": 55, + "immune": 0 + }, + { + "id": 43, + "name": "Orc", + "max_hp": 49, + "max_dmg": 42, + "armor": 28, + "level": 15, + "max_exp": 104, + "max_gold": 35, + "immune": 0 + }, + { + "id": 145, + "name": "Red Daemon", + "max_hp": 310, + "max_dmg": 280, + "armor": 230, + "level": 48, + "max_exp": 1880, + "max_gold": 627, + "immune": 0 + }, + { + "id": 44, + "name": "Sylph", + "max_hp": 49, + "max_dmg": 42, + "armor": 28, + "level": 15, + "max_exp": 106, + "max_gold": 35, + "immune": 0 + }, + { + "id": 30, + "name": "Dark Slime", + "max_hp": 38, + "max_dmg": 32, + "armor": 18, + "level": 10, + "max_exp": 62, + "max_gold": 21, + "immune": 0 + }, + { + "id": 18, + "name": "Frost Raven", + "max_hp": 26, + "max_dmg": 20, + "armor": 8, + "level": 7, + "max_exp": 30, + "max_gold": 10, + "immune": 0 + }, + { + "id": 52, + "name": "Hellcat", + "max_hp": 58, + "max_dmg": 50, + "armor": 38, + "level": 18, + "max_exp": 128, + "max_gold": 43, + "immune": 0 + }, + { + "id": 118, + "name": "Devil", + "max_hp": 184, + "max_dmg": 164, + "armor": 135, + "level": 40, + "max_exp": 666, + "max_gold": 222, + "immune": 0 + }, + { + "id": 22, + "name": "Rogue", + "max_hp": 30, + "max_dmg": 25, + "armor": 12, + "level": 8, + "max_exp": 40, + "max_gold": 13, + "immune": 0 + }, + { + "id": 41, + "name": "Dark Scorpion", + "max_hp": 48, + "max_dmg": 40, + "armor": 26, + "level": 15, + "max_exp": 95, + "max_gold": 32, + "immune": 1 + }, + { + "id": 68, + "name": "Ogre", + "max_hp": 78, + "max_dmg": 68, + "armor": 54, + "level": 23, + "max_exp": 170, + "max_gold": 57, + "immune": 0 + } ] \ No newline at end of file diff --git a/data/news.json b/data/news.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/news.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/spells.json b/data/spells.json index a4a0e7c..954ac2a 100644 --- a/data/spells.json +++ b/data/spells.json @@ -1,135 +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 - } + { + "id": 5, + "name": "Gaia", + "mp": 75, + "attribute": 150, + "type": 1 + }, + { + "id": 2, + "name": "Revive", + "mp": 10, + "attribute": 25, + "type": 1 + }, + { + "id": 3, + "name": "Life", + "mp": 25, + "attribute": 50, + "type": 1 + }, + { + "id": 8, + "name": "Maim", + "mp": 25, + "attribute": 70, + "type": 2 + }, + { + "id": 14, + "name": "Craze", + "mp": 10, + "attribute": 10, + "type": 4 + }, + { + "id": 1, + "name": "Heal", + "mp": 5, + "attribute": 10, + "type": 1 + }, + { + "id": 15, + "name": "Rage", + "mp": 20, + "attribute": 25, + "type": 4 + }, + { + "id": 9, + "name": "Rend", + "mp": 40, + "attribute": 100, + "type": 2 + }, + { + "id": 6, + "name": "Hurt", + "mp": 5, + "attribute": 15, + "type": 2 + }, + { + "id": 17, + "name": "Ward", + "mp": 10, + "attribute": 10, + "type": 5 + }, + { + "id": 12, + "name": "Dream", + "mp": 30, + "attribute": 9, + "type": 3 + }, + { + "id": 18, + "name": "Fend", + "mp": 20, + "attribute": 25, + "type": 5 + }, + { + "id": 19, + "name": "Barrier", + "mp": 30, + "attribute": 50, + "type": 5 + }, + { + "id": 13, + "name": "Nightmare", + "mp": 60, + "attribute": 13, + "type": 3 + }, + { + "id": 11, + "name": "Sleep", + "mp": 10, + "attribute": 5, + "type": 3 + }, + { + "id": 16, + "name": "Fury", + "mp": 30, + "attribute": 50, + "type": 4 + }, + { + "id": 10, + "name": "Chaos", + "mp": 50, + "attribute": 130, + "type": 2 + }, + { + "id": 7, + "name": "Pain", + "mp": 12, + "attribute": 35, + "type": 2 + }, + { + "id": 4, + "name": "Breath", + "mp": 50, + "attribute": 100, + "type": 1 + } ] \ No newline at end of file diff --git a/data/towns.json b/data/towns.json index 957dcf0..f031865 100644 --- a/data/towns.json +++ b/data/towns.json @@ -1,82 +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" - } + { + "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" + }, + { + "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" + } ] \ No newline at end of file diff --git a/internal/actions/move.go b/internal/actions/move.go index 3918135..b73d4a7 100644 --- a/internal/actions/move.go +++ b/internal/actions/move.go @@ -32,10 +32,7 @@ func (d Direction) String() string { } func Move(user *users.User, dir Direction) (string, int, int, error) { - control, err := control.Get() - if err != nil { - panic("Move: CONTROL ROW SHOULD EXIST") - } + control := control.Get() newX, newY := user.X, user.Y switch dir { diff --git a/internal/actions/user_item.go b/internal/actions/user_item.go index ada1ad7..f54cdbe 100644 --- a/internal/actions/user_item.go +++ b/internal/actions/user_item.go @@ -31,26 +31,26 @@ func UserEquipItem(user *users.User, item *items.Item) { if oldItem != nil { switch oldItem.Type { case items.TypeWeapon: - user.Set("Attack", user.Attack-oldItem.Att) + user.Attack -= oldItem.Att case items.TypeArmor: - user.Set("Defense", user.Defense-oldItem.Att) + user.Defense -= oldItem.Att case items.TypeShield: - user.Set("Defense", user.Defense-oldItem.Att) + user.Defense -= oldItem.Att } } switch item.Type { case items.TypeWeapon: - user.Set("Attack", user.Attack+item.Att) - user.Set("WeaponID", item.ID) - user.Set("WeaponName", item.Name) + user.Attack += item.Att + user.WeaponID = item.ID + user.WeaponName = item.Name case items.TypeArmor: - user.Set("Defense", user.Defense+item.Att) - user.Set("ArmorID", item.ID) - user.Set("ArmorName", item.Name) + user.Defense += item.Att + user.ArmorID = item.ID + user.ArmorName = item.Name case items.TypeShield: - user.Set("Defense", user.Defense+item.Att) - user.Set("ShieldID", item.ID) - user.Set("ShieldName", item.Name) + user.Defense += item.Att + user.ShieldID = item.ID + user.ShieldName = item.Name } } diff --git a/internal/babble/babble.go b/internal/babble/babble.go index 498a33c..e6be6f4 100644 --- a/internal/babble/babble.go +++ b/internal/babble/babble.go @@ -5,7 +5,6 @@ import ( "fmt" "sort" "strings" - "sync" "time" ) @@ -18,14 +17,11 @@ type Babble struct { } func (b *Babble) Save() error { - babbleStore := GetStore() - babbleStore.UpdateBabble(b) - return nil + return GetStore().UpdateWithRebuild(b.ID, b) } func (b *Babble) Delete() error { - babbleStore := GetStore() - babbleStore.RemoveBabble(b.ID) + GetStore().RemoveWithRebuild(b.ID) return nil } @@ -52,329 +48,156 @@ func (b *Babble) Validate() error { return nil } -// BabbleStore provides in-memory storage with O(1) lookups and babble-specific indices +// BabbleStore with enhanced BaseStore type BabbleStore struct { - *store.BaseStore[Babble] // Embedded generic store - byAuthor map[string][]int // Author (lowercase) -> []ID - allByPosted []int // All IDs sorted by posted DESC, id DESC - mu sync.RWMutex // Protects indices + *store.BaseStore[Babble] } -// Global in-memory store -var babbleStore *BabbleStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *BabbleStore { + bs := &BabbleStore{BaseStore: store.NewBaseStore[Babble]()} -// Initialize the in-memory store -func initStore() { - babbleStore = &BabbleStore{ - BaseStore: store.NewBaseStore[Babble](), - byAuthor: make(map[string][]int), - allByPosted: make([]int, 0), - } + // Register indices + bs.RegisterIndex("byAuthor", store.BuildStringGroupIndex(func(b *Babble) string { + return strings.ToLower(b.Author) + })) + + bs.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *Babble) bool { + if a.Posted != b.Posted { + return a.Posted > b.Posted // DESC + } + return a.ID > b.ID // DESC + })) + + return bs +}) + +// Enhanced CRUD operations +func (bs *BabbleStore) AddBabble(babble *Babble) error { + return bs.AddWithRebuild(babble.ID, babble) } -// GetStore returns the global babble store -func GetStore() *BabbleStore { - storeOnce.Do(initStore) - return babbleStore -} - -// AddBabble adds a babble message to the in-memory store and updates all indices -func (bs *BabbleStore) AddBabble(babble *Babble) { - bs.mu.Lock() - defer bs.mu.Unlock() - - // Validate babble - if err := babble.Validate(); err != nil { - return - } - - // Add to base store - bs.Add(babble.ID, babble) - - // Rebuild indices - bs.rebuildIndicesUnsafe() -} - -// RemoveBabble removes a babble message from the store and updates indices func (bs *BabbleStore) RemoveBabble(id int) { - bs.mu.Lock() - defer bs.mu.Unlock() - - // Remove from base store - bs.Remove(id) - - // Rebuild indices - bs.rebuildIndicesUnsafe() + bs.RemoveWithRebuild(id) } -// UpdateBabble updates a babble message efficiently -func (bs *BabbleStore) UpdateBabble(babble *Babble) { - bs.mu.Lock() - defer bs.mu.Unlock() - - // Validate babble - if err := babble.Validate(); err != nil { - return - } - - // Update base store - bs.Add(babble.ID, babble) - - // Rebuild indices - bs.rebuildIndicesUnsafe() +func (bs *BabbleStore) UpdateBabble(babble *Babble) error { + return bs.UpdateWithRebuild(babble.ID, babble) } -// LoadData loads babble data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { bs := GetStore() - - // Load from base store, which handles JSON loading - if err := bs.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - bs.rebuildIndices() - return nil + return bs.BaseStore.LoadData(dataPath) } -// SaveData saves babble data to JSON file func SaveData(dataPath string) error { bs := GetStore() return bs.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (bs *BabbleStore) rebuildIndicesUnsafe() { - // Clear indices - bs.byAuthor = make(map[string][]int) - bs.allByPosted = make([]int, 0) - - // Collect all babbles and build indices - allBabbles := bs.GetAll() - - for id, babble := range allBabbles { - // Author index (case-insensitive) - authorKey := strings.ToLower(babble.Author) - bs.byAuthor[authorKey] = append(bs.byAuthor[authorKey], id) - - // All IDs - bs.allByPosted = append(bs.allByPosted, id) - } - - // Sort allByPosted by posted DESC, then ID DESC - sort.Slice(bs.allByPosted, func(i, j int) bool { - babbleI, _ := bs.GetByID(bs.allByPosted[i]) - babbleJ, _ := bs.GetByID(bs.allByPosted[j]) - if babbleI.Posted != babbleJ.Posted { - return babbleI.Posted > babbleJ.Posted // DESC - } - return bs.allByPosted[i] > bs.allByPosted[j] // DESC - }) - - // Sort author indices by posted DESC, then ID DESC - for author := range bs.byAuthor { - sort.Slice(bs.byAuthor[author], func(i, j int) bool { - babbleI, _ := bs.GetByID(bs.byAuthor[author][i]) - babbleJ, _ := bs.GetByID(bs.byAuthor[author][j]) - if babbleI.Posted != babbleJ.Posted { - return babbleI.Posted > babbleJ.Posted // DESC - } - return bs.byAuthor[author][i] > bs.byAuthor[author][j] // DESC - }) - } -} - -// rebuildIndices rebuilds all babble-specific indices from base store data -func (bs *BabbleStore) rebuildIndices() { - bs.mu.Lock() - defer bs.mu.Unlock() - bs.rebuildIndicesUnsafe() -} - -// Retrieves a babble message by ID +// Query functions using enhanced store func Find(id int) (*Babble, error) { bs := GetStore() - babble, exists := bs.GetByID(id) + babble, exists := bs.Find(id) if !exists { return nil, fmt.Errorf("babble with ID %d not found", id) } return babble, nil } -// Retrieves all babble messages ordered by posted time (newest first) func All() ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - result := make([]*Babble, 0, len(bs.allByPosted)) - for _, id := range bs.allByPosted { - if babble, exists := bs.GetByID(id); exists { - result = append(result, babble) - } - } - return result, nil + return bs.AllSorted("allByPosted"), nil } -// Retrieves babble messages by a specific author func ByAuthor(author string) ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() + messages := bs.GroupByIndex("byAuthor", strings.ToLower(author)) - ids, exists := bs.byAuthor[strings.ToLower(author)] - if !exists { - return []*Babble{}, nil - } - - result := make([]*Babble, 0, len(ids)) - for _, id := range ids { - if babble, exists := bs.GetByID(id); exists { - result = append(result, babble) + // Sort by posted DESC, then ID DESC + sort.Slice(messages, func(i, j int) bool { + if messages[i].Posted != messages[j].Posted { + return messages[i].Posted > messages[j].Posted // DESC } - } - return result, nil + return messages[i].ID > messages[j].ID // DESC + }) + + return messages, nil } -// Retrieves the most recent babble messages (limited by count) func Recent(limit int) ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - if limit > len(bs.allByPosted) { - limit = len(bs.allByPosted) + all := bs.AllSorted("allByPosted") + if limit > len(all) { + limit = len(all) } - - result := make([]*Babble, 0, limit) - for i := 0; i < limit; i++ { - if babble, exists := bs.GetByID(bs.allByPosted[i]); exists { - result = append(result, babble) - } - } - return result, nil + return all[:limit], nil } -// Retrieves babble messages since a specific timestamp func Since(since int64) ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - var result []*Babble - for _, id := range bs.allByPosted { - if babble, exists := bs.GetByID(id); exists && babble.Posted >= since { - result = append(result, babble) - } - } - return result, nil + return bs.FilterByIndex("allByPosted", func(b *Babble) bool { + return b.Posted >= since + }), nil } -// Retrieves babble messages between two timestamps (inclusive) func Between(start, end int64) ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - var result []*Babble - for _, id := range bs.allByPosted { - if babble, exists := bs.GetByID(id); exists && babble.Posted >= start && babble.Posted <= end { - result = append(result, babble) - } - } - return result, nil + return bs.FilterByIndex("allByPosted", func(b *Babble) bool { + return b.Posted >= start && b.Posted <= end + }), nil } -// Retrieves babble messages containing the search term (case-insensitive) func Search(term string) ([]*Babble, error) { bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - var result []*Babble lowerTerm := strings.ToLower(term) - - for _, id := range bs.allByPosted { - if babble, exists := bs.GetByID(id); exists { - if strings.Contains(strings.ToLower(babble.Babble), lowerTerm) { - result = append(result, babble) - } - } - } - return result, nil + return bs.FilterByIndex("allByPosted", func(b *Babble) bool { + return strings.Contains(strings.ToLower(b.Babble), lowerTerm) + }), nil } -// Retrieves recent messages from a specific author func RecentByAuthor(author string, limit int) ([]*Babble, error) { - bs := GetStore() - bs.mu.RLock() - defer bs.mu.RUnlock() - - ids, exists := bs.byAuthor[strings.ToLower(author)] - if !exists { - return []*Babble{}, nil + messages, err := ByAuthor(author) + if err != nil { + return nil, err } - - if limit > len(ids) { - limit = len(ids) + if limit > len(messages) { + limit = len(messages) } - - result := make([]*Babble, 0, limit) - for i := 0; i < limit; i++ { - if babble, exists := bs.GetByID(ids[i]); exists { - result = append(result, babble) - } - } - return result, nil + return messages[:limit], nil } -// Saves a new babble to the in-memory store and sets the ID +// Insert with ID assignment func (b *Babble) Insert() error { bs := GetStore() - - // Validate before insertion - if err := b.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if b.ID == 0 { b.ID = bs.GetNextID() } - - // Add to store - bs.AddBabble(b) - return nil + return bs.AddBabble(b) } -// Returns the posted timestamp as a time.Time +// Helper methods func (b *Babble) PostedTime() time.Time { return time.Unix(b.Posted, 0) } -// Sets the posted timestamp from a time.Time func (b *Babble) SetPostedTime(t time.Time) { b.Posted = t.Unix() } -// Returns true if the babble message was posted within the last hour func (b *Babble) IsRecent() bool { return time.Since(b.PostedTime()) < time.Hour } -// Returns how long ago the babble message was posted func (b *Babble) Age() time.Duration { return time.Since(b.PostedTime()) } -// Returns true if the given username is the author of this babble message func (b *Babble) IsAuthor(username string) bool { return strings.EqualFold(b.Author, username) } -// Returns a truncated version of the babble for previews func (b *Babble) Preview(maxLength int) string { if len(b.Babble) <= maxLength { return b.Babble @@ -387,7 +210,6 @@ func (b *Babble) Preview(maxLength int) string { return b.Babble[:maxLength-3] + "..." } -// Returns the number of words in the babble message func (b *Babble) WordCount() int { if b.Babble == "" { return 0 @@ -415,27 +237,22 @@ func (b *Babble) WordCount() int { return words } -// Returns the character length of the babble message func (b *Babble) Length() int { return len(b.Babble) } -// Returns true if the babble message contains the given term (case-insensitive) func (b *Babble) Contains(term string) bool { return strings.Contains(strings.ToLower(b.Babble), strings.ToLower(term)) } -// Returns true if the babble message is empty or whitespace-only func (b *Babble) IsEmpty() bool { return strings.TrimSpace(b.Babble) == "" } -// Returns true if the message exceeds the typical chat length func (b *Babble) IsLongMessage(threshold int) bool { return b.Length() > threshold } -// Returns a slice of usernames mentioned in the message (starting with @) func (b *Babble) GetMentions() []string { words := strings.Fields(b.Babble) var mentions []string @@ -453,7 +270,6 @@ func (b *Babble) GetMentions() []string { return mentions } -// Returns true if the message mentions the given username func (b *Babble) HasMention(username string) bool { mentions := b.GetMentions() for _, mention := range mentions { diff --git a/internal/control/control.go b/internal/control/control.go index d2cc27a..b8abea1 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -1,14 +1,24 @@ package control import ( - "dk/internal/store" + "encoding/json" "fmt" + "os" "sync" ) +var ( + global *Control + configPath string + mu sync.RWMutex +) + +func init() { + global = New() +} + // Control represents the game control settings type Control struct { - ID int `json:"id"` WorldSize int `json:"world_size"` Open int `json:"open"` AdminEmail string `json:"admin_email"` @@ -17,220 +27,112 @@ type Control struct { Class3Name string `json:"class_3_name"` } -func (c *Control) Save() error { - controlStore := GetStore() - controlStore.UpdateControl(c) - return nil -} - -func (c *Control) Delete() error { - controlStore := GetStore() - controlStore.RemoveControl(c.ID) - return nil -} - -// Creates a new Control with sensible defaults +// New creates a new Control with sensible defaults func New() *Control { return &Control{ - WorldSize: 200, // Default world size - Open: 1, // Default open for registration - AdminEmail: "", // No admin email by default - Class1Name: "Mage", // Default class names + WorldSize: 200, + Open: 1, + AdminEmail: "", + Class1Name: "Mage", Class2Name: "Warrior", Class3Name: "Paladin", } } -// Validate checks if control has valid values +// Load loads control settings from JSON file into global instance +func Load(filename string) error { + mu.Lock() + configPath = filename + mu.Unlock() + + data, err := os.ReadFile(filename) + if err != nil { + if os.IsNotExist(err) { + return nil // Keep defaults + } + return fmt.Errorf("failed to read config file: %w", err) + } + + if len(data) == 0 { + return nil + } + + control := &Control{} + if err := json.Unmarshal(data, control); err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + // Apply defaults for any missing fields + defaults := New() + if control.WorldSize == 0 { + control.WorldSize = defaults.WorldSize + } + if control.Class1Name == "" { + control.Class1Name = defaults.Class1Name + } + if control.Class2Name == "" { + control.Class2Name = defaults.Class2Name + } + if control.Class3Name == "" { + control.Class3Name = defaults.Class3Name + } + + mu.Lock() + global = control + mu.Unlock() + return nil +} + +// Save saves global control settings to the loaded path +func Save() error { + mu.RLock() + defer mu.RUnlock() + + if configPath == "" { + return fmt.Errorf("no config path set, call Load() first") + } + + data, err := json.MarshalIndent(global, "", "\t") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +// Get returns the global control instance (thread-safe) +func Get() *Control { + mu.RLock() + defer mu.RUnlock() + return global +} + +// Set updates the global control instance (thread-safe) +func Set(control *Control) { + mu.Lock() + defer mu.Unlock() + global = control +} func (c *Control) Validate() error { if c.WorldSize <= 0 || c.WorldSize > 10000 { - return fmt.Errorf("control WorldSize must be between 1 and 10000") + return fmt.Errorf("WorldSize must be between 1 and 10000") } if c.Open != 0 && c.Open != 1 { - return fmt.Errorf("control Open must be 0 or 1") + return fmt.Errorf("Open must be 0 or 1") } return nil } -// ControlStore provides in-memory storage for control settings -type ControlStore struct { - *store.BaseStore[Control] // Embedded generic store - allByID []int // All IDs sorted by ID - mu sync.RWMutex // Protects indices -} - -// Global in-memory store -var controlStore *ControlStore -var storeOnce sync.Once - -// Initialize the in-memory store -func initStore() { - controlStore = &ControlStore{ - BaseStore: store.NewBaseStore[Control](), - allByID: make([]int, 0), - } -} - -// GetStore returns the global control store -func GetStore() *ControlStore { - storeOnce.Do(initStore) - return controlStore -} - -// AddControl adds a control to the in-memory store and updates all indices -func (cs *ControlStore) AddControl(control *Control) { - cs.mu.Lock() - defer cs.mu.Unlock() - - // Validate control - if err := control.Validate(); err != nil { - return - } - - // Add to base store - cs.Add(control.ID, control) - - // Rebuild indices - cs.rebuildIndicesUnsafe() -} - -// RemoveControl removes a control from the store and updates indices -func (cs *ControlStore) RemoveControl(id int) { - cs.mu.Lock() - defer cs.mu.Unlock() - - // Remove from base store - cs.Remove(id) - - // Rebuild indices - cs.rebuildIndicesUnsafe() -} - -// UpdateControl updates a control efficiently -func (cs *ControlStore) UpdateControl(control *Control) { - cs.mu.Lock() - defer cs.mu.Unlock() - - // Validate control - if err := control.Validate(); err != nil { - return - } - - // Update base store - cs.Add(control.ID, control) - - // Rebuild indices - cs.rebuildIndicesUnsafe() -} - -// LoadData loads control data from JSON file, or starts with empty store -func LoadData(dataPath string) error { - cs := GetStore() - - // Load from base store, which handles JSON loading - if err := cs.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - cs.rebuildIndices() - return nil -} - -// SaveData saves control data to JSON file -func SaveData(dataPath string) error { - cs := GetStore() - return cs.BaseStore.SaveData(dataPath) -} - -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (cs *ControlStore) rebuildIndicesUnsafe() { - // Clear indices - cs.allByID = make([]int, 0) - - // Collect all controls - allControls := cs.GetAll() - - for id := range allControls { - cs.allByID = append(cs.allByID, id) - } - - // Sort by ID (though typically only one control record exists) - for i := 0; i < len(cs.allByID); i++ { - for j := i + 1; j < len(cs.allByID); j++ { - if cs.allByID[i] > cs.allByID[j] { - cs.allByID[i], cs.allByID[j] = cs.allByID[j], cs.allByID[i] - } - } - } -} - -// rebuildIndices rebuilds all control-specific indices from base store data -func (cs *ControlStore) rebuildIndices() { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.rebuildIndicesUnsafe() -} - -// Retrieves the control record by ID (typically only ID 1 exists) -func Find(id int) (*Control, error) { - cs := GetStore() - control, exists := cs.GetByID(id) - if !exists { - return nil, fmt.Errorf("control with ID %d not found", id) - } - return control, nil -} - -// Retrieves the main control record (ID 1) -func Get() (*Control, error) { - return Find(1) -} - -// Saves a new control to the in-memory store and sets the ID -func (c *Control) Insert() error { - cs := GetStore() - - // Validate before insertion - if err := c.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set - if c.ID == 0 { - c.ID = cs.GetNextID() - } - - // Add to store - cs.AddControl(c) - return nil -} - -// Returns true if the game world is open for new players +// IsOpen returns true if the game world is open for new players func (c *Control) IsOpen() bool { return c.Open == 1 } -// Sets whether the game world is open for new players -func (c *Control) SetOpen(open bool) { - if open { - c.Open = 1 - } else { - c.Open = 0 - } -} - -// Closes the game world to new players -func (c *Control) Close() { - c.Open = 0 -} - -// Opens the game world to new players -func (c *Control) OpenWorld() { - c.Open = 1 -} - -// Returns all class names as a slice +// GetClassNames returns all class names as a slice func (c *Control) GetClassNames() []string { classes := make([]string, 0, 3) if c.Class1Name != "" { @@ -245,26 +147,7 @@ func (c *Control) GetClassNames() []string { return classes } -// Sets all class names from a slice -func (c *Control) SetClassNames(classes []string) { - // Reset all class names - c.Class1Name = "" - c.Class2Name = "" - c.Class3Name = "" - - // Set provided class names - if len(classes) > 0 { - c.Class1Name = classes[0] - } - if len(classes) > 1 { - c.Class2Name = classes[1] - } - if len(classes) > 2 { - c.Class3Name = classes[2] - } -} - -// Returns the name of a specific class (1-3) +// GetClassName returns the name of a specific class (1-3) func (c *Control) GetClassName(classNum int) string { switch classNum { case 1: @@ -278,24 +161,7 @@ func (c *Control) GetClassName(classNum int) string { } } -// Sets the name of a specific class (1-3) -func (c *Control) SetClassName(classNum int, name string) bool { - switch classNum { - case 1: - c.Class1Name = name - return true - case 2: - c.Class2Name = name - return true - case 3: - c.Class3Name = name - return true - default: - return false - } -} - -// Returns true if the given name matches one of the configured classes +// IsValidClassName returns true if the given name matches one of the configured classes func (c *Control) IsValidClassName(name string) bool { if name == "" { return false @@ -303,7 +169,7 @@ func (c *Control) IsValidClassName(name string) bool { return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name } -// Returns the class number (1-3) for a given class name, or 0 if not found +// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found func (c *Control) GetClassNumber(name string) int { if name == c.Class1Name && name != "" { return 1 @@ -317,28 +183,28 @@ func (c *Control) GetClassNumber(name string) int { return 0 } -// Returns true if an admin email is configured +// HasAdminEmail returns true if an admin email is configured func (c *Control) HasAdminEmail() bool { return c.AdminEmail != "" } -// Returns true if the world size is within reasonable bounds +// IsWorldSizeValid returns true if the world size is within reasonable bounds func (c *Control) IsWorldSizeValid() bool { return c.WorldSize > 0 && c.WorldSize <= 10000 } -// Returns the world radius (half the world size) +// GetWorldRadius returns the world radius (half the world size) func (c *Control) GetWorldRadius() int { return c.WorldSize / 2 } -// Returns true if the given coordinates are within world bounds +// IsWithinWorldBounds returns true if the given coordinates are within world bounds func (c *Control) IsWithinWorldBounds(x, y int) bool { radius := c.GetWorldRadius() return x >= -radius && x <= radius && y >= -radius && y <= radius } -// Returns the minimum and maximum coordinates for the world +// GetWorldBounds returns the minimum and maximum coordinates for the world func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) { radius := c.GetWorldRadius() return -radius, -radius, radius, radius diff --git a/internal/drops/drops.go b/internal/drops/drops.go index 085ade3..4897815 100644 --- a/internal/drops/drops.go +++ b/internal/drops/drops.go @@ -3,8 +3,6 @@ package drops import ( "dk/internal/store" "fmt" - "sort" - "sync" ) // Drop represents a drop item in the game @@ -17,14 +15,11 @@ type Drop struct { } func (d *Drop) Save() error { - dropStore := GetStore() - dropStore.UpdateDrop(d) - return nil + return GetStore().UpdateWithRebuild(d.ID, d) } func (d *Drop) Delete() error { - dropStore := GetStore() - dropStore.RemoveDrop(d.ID) + GetStore().RemoveWithRebuild(d.ID) return nil } @@ -57,239 +52,96 @@ const ( TypeConsumable = 1 ) -// DropStore provides in-memory storage with O(1) lookups and drop-specific indices +// DropStore with enhanced BaseStore type DropStore struct { - *store.BaseStore[Drop] // Embedded generic store - byLevel map[int][]int // Level -> []ID - byType map[int][]int // Type -> []ID - allByID []int // All IDs sorted by ID - mu sync.RWMutex // Protects indices + *store.BaseStore[Drop] } -// Global in-memory store -var dropStore *DropStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *DropStore { + ds := &DropStore{BaseStore: store.NewBaseStore[Drop]()} -// Initialize the in-memory store -func initStore() { - dropStore = &DropStore{ - BaseStore: store.NewBaseStore[Drop](), - byLevel: make(map[int][]int), - byType: make(map[int][]int), - allByID: make([]int, 0), - } + // Register indices + ds.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(d *Drop) int { + return d.Level + })) + + ds.RegisterIndex("byType", store.BuildIntGroupIndex(func(d *Drop) int { + return d.Type + })) + + ds.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Drop) bool { + return a.ID < b.ID + })) + + return ds +}) + +// Enhanced CRUD operations +func (ds *DropStore) AddDrop(drop *Drop) error { + return ds.AddWithRebuild(drop.ID, drop) } -// GetStore returns the global drop store -func GetStore() *DropStore { - storeOnce.Do(initStore) - return dropStore -} - -// AddDrop adds a drop to the in-memory store and updates all indices -func (ds *DropStore) AddDrop(drop *Drop) { - ds.mu.Lock() - defer ds.mu.Unlock() - - // Validate drop - if err := drop.Validate(); err != nil { - return - } - - // Add to base store - ds.Add(drop.ID, drop) - - // Rebuild indices - ds.rebuildIndicesUnsafe() -} - -// RemoveDrop removes a drop from the store and updates indices func (ds *DropStore) RemoveDrop(id int) { - ds.mu.Lock() - defer ds.mu.Unlock() - - // Remove from base store - ds.Remove(id) - - // Rebuild indices - ds.rebuildIndicesUnsafe() + ds.RemoveWithRebuild(id) } -// UpdateDrop updates a drop efficiently -func (ds *DropStore) UpdateDrop(drop *Drop) { - ds.mu.Lock() - defer ds.mu.Unlock() - - // Validate drop - if err := drop.Validate(); err != nil { - return - } - - // Update base store - ds.Add(drop.ID, drop) - - // Rebuild indices - ds.rebuildIndicesUnsafe() +func (ds *DropStore) UpdateDrop(drop *Drop) error { + return ds.UpdateWithRebuild(drop.ID, drop) } -// LoadData loads drop data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { ds := GetStore() - - // Load from base store, which handles JSON loading - if err := ds.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - ds.rebuildIndices() - return nil + return ds.BaseStore.LoadData(dataPath) } -// SaveData saves drop data to JSON file func SaveData(dataPath string) error { ds := GetStore() return ds.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (ds *DropStore) rebuildIndicesUnsafe() { - // Clear indices - ds.byLevel = make(map[int][]int) - ds.byType = make(map[int][]int) - ds.allByID = make([]int, 0) - - // Collect all drops and build indices - allDrops := ds.GetAll() - - for id, drop := range allDrops { - // Level index - ds.byLevel[drop.Level] = append(ds.byLevel[drop.Level], id) - - // Type index - ds.byType[drop.Type] = append(ds.byType[drop.Type], id) - - // All IDs - ds.allByID = append(ds.allByID, id) - } - - // Sort allByID by ID - sort.Ints(ds.allByID) - - // Sort level indices by ID - for level := range ds.byLevel { - sort.Ints(ds.byLevel[level]) - } - - // Sort type indices by level, then ID - for dropType := range ds.byType { - sort.Slice(ds.byType[dropType], func(i, j int) bool { - dropI, _ := ds.GetByID(ds.byType[dropType][i]) - dropJ, _ := ds.GetByID(ds.byType[dropType][j]) - if dropI.Level != dropJ.Level { - return dropI.Level < dropJ.Level - } - return ds.byType[dropType][i] < ds.byType[dropType][j] - }) - } -} - -// rebuildIndices rebuilds all drop-specific indices from base store data -func (ds *DropStore) rebuildIndices() { - ds.mu.Lock() - defer ds.mu.Unlock() - ds.rebuildIndicesUnsafe() -} - -// Retrieves a drop by ID +// Query functions using enhanced store func Find(id int) (*Drop, error) { ds := GetStore() - drop, exists := ds.GetByID(id) + drop, exists := ds.Find(id) if !exists { return nil, fmt.Errorf("drop with ID %d not found", id) } return drop, nil } -// Retrieves all drops func All() ([]*Drop, error) { ds := GetStore() - ds.mu.RLock() - defer ds.mu.RUnlock() - - result := make([]*Drop, 0, len(ds.allByID)) - for _, id := range ds.allByID { - if drop, exists := ds.GetByID(id); exists { - result = append(result, drop) - } - } - return result, nil + return ds.AllSorted("allByID"), nil } -// Retrieves drops by minimum level requirement func ByLevel(minLevel int) ([]*Drop, error) { ds := GetStore() - ds.mu.RLock() - defer ds.mu.RUnlock() - - var result []*Drop - for level := 1; level <= minLevel; level++ { - if ids, exists := ds.byLevel[level]; exists { - for _, id := range ids { - if drop, exists := ds.GetByID(id); exists { - result = append(result, drop) - } - } - } - } - return result, nil + return ds.FilterByIndex("allByID", func(d *Drop) bool { + return d.Level <= minLevel + }), nil } -// Retrieves drops by type func ByType(dropType int) ([]*Drop, error) { ds := GetStore() - ds.mu.RLock() - defer ds.mu.RUnlock() - - ids, exists := ds.byType[dropType] - if !exists { - return []*Drop{}, nil - } - - result := make([]*Drop, 0, len(ids)) - for _, id := range ids { - if drop, exists := ds.GetByID(id); exists { - result = append(result, drop) - } - } - return result, nil + return ds.GroupByIndex("byType", dropType), nil } -// Saves a new drop to the in-memory store and sets the ID +// Insert with ID assignment func (d *Drop) Insert() error { ds := GetStore() - - // Validate before insertion - if err := d.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if d.ID == 0 { d.ID = ds.GetNextID() } - - // Add to store - ds.AddDrop(d) - return nil + return ds.AddDrop(d) } -// Returns true if the drop is a consumable item +// Helper methods func (d *Drop) IsConsumable() bool { return d.Type == TypeConsumable } -// Returns the string representation of the drop type func (d *Drop) TypeName() string { switch d.Type { case TypeConsumable: diff --git a/internal/forum/forum.go b/internal/forum/forum.go index 3d58262..39aaffc 100644 --- a/internal/forum/forum.go +++ b/internal/forum/forum.go @@ -5,7 +5,6 @@ import ( "fmt" "sort" "strings" - "sync" "time" ) @@ -22,14 +21,11 @@ type Forum struct { } func (f *Forum) Save() error { - forumStore := GetStore() - forumStore.UpdateForum(f) - return nil + return GetStore().UpdateWithRebuild(f.ID, f) } func (f *Forum) Delete() error { - forumStore := GetStore() - forumStore.RemoveForum(f.ID) + GetStore().RemoveWithRebuild(f.ID) return nil } @@ -70,393 +66,191 @@ func (f *Forum) Validate() error { return nil } -// ForumStore provides in-memory storage with O(1) lookups and forum-specific indices +// ForumStore with enhanced BaseStore type ForumStore struct { - *store.BaseStore[Forum] // Embedded generic store - byParent map[int][]int // Parent -> []ID - byAuthor map[int][]int // Author -> []ID - threadsOnly []int // Parent=0 IDs sorted by last_post DESC, id DESC - allByLastPost []int // All IDs sorted by last_post DESC, id DESC - mu sync.RWMutex // Protects indices + *store.BaseStore[Forum] } -// Global in-memory store -var forumStore *ForumStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *ForumStore { + fs := &ForumStore{BaseStore: store.NewBaseStore[Forum]()} -// Initialize the in-memory store -func initStore() { - forumStore = &ForumStore{ - BaseStore: store.NewBaseStore[Forum](), - byParent: make(map[int][]int), - byAuthor: make(map[int][]int), - threadsOnly: make([]int, 0), - allByLastPost: make([]int, 0), - } + // Register indices + fs.RegisterIndex("byParent", store.BuildIntGroupIndex(func(f *Forum) int { + return f.Parent + })) + + fs.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(f *Forum) int { + return f.Author + })) + + fs.RegisterIndex("allByLastPost", store.BuildSortedListIndex(func(a, b *Forum) bool { + if a.LastPost != b.LastPost { + return a.LastPost > b.LastPost // DESC + } + return a.ID > b.ID // DESC + })) + + return fs +}) + +// Enhanced CRUD operations +func (fs *ForumStore) AddForum(forum *Forum) error { + return fs.AddWithRebuild(forum.ID, forum) } -// GetStore returns the global forum store -func GetStore() *ForumStore { - storeOnce.Do(initStore) - return forumStore -} - -// AddForum adds a forum post to the in-memory store and updates all indices -func (fs *ForumStore) AddForum(forum *Forum) { - fs.mu.Lock() - defer fs.mu.Unlock() - - // Validate forum - if err := forum.Validate(); err != nil { - return - } - - // Add to base store - fs.Add(forum.ID, forum) - - // Rebuild indices - fs.rebuildIndicesUnsafe() -} - -// RemoveForum removes a forum post from the store and updates indices func (fs *ForumStore) RemoveForum(id int) { - fs.mu.Lock() - defer fs.mu.Unlock() - - // Remove from base store - fs.Remove(id) - - // Rebuild indices - fs.rebuildIndicesUnsafe() + fs.RemoveWithRebuild(id) } -// UpdateForum updates a forum post efficiently -func (fs *ForumStore) UpdateForum(forum *Forum) { - fs.mu.Lock() - defer fs.mu.Unlock() - - // Validate forum - if err := forum.Validate(); err != nil { - return - } - - // Update base store - fs.Add(forum.ID, forum) - - // Rebuild indices - fs.rebuildIndicesUnsafe() +func (fs *ForumStore) UpdateForum(forum *Forum) error { + return fs.UpdateWithRebuild(forum.ID, forum) } -// LoadData loads forum data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { fs := GetStore() - - // Load from base store, which handles JSON loading - if err := fs.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - fs.rebuildIndices() - return nil + return fs.BaseStore.LoadData(dataPath) } -// SaveData saves forum data to JSON file func SaveData(dataPath string) error { fs := GetStore() return fs.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (fs *ForumStore) rebuildIndicesUnsafe() { - // Clear indices - fs.byParent = make(map[int][]int) - fs.byAuthor = make(map[int][]int) - fs.threadsOnly = make([]int, 0) - fs.allByLastPost = make([]int, 0) - - // Collect all forum posts and build indices - allForums := fs.GetAll() - - for id, forum := range allForums { - // Parent index - fs.byParent[forum.Parent] = append(fs.byParent[forum.Parent], id) - - // Author index - fs.byAuthor[forum.Author] = append(fs.byAuthor[forum.Author], id) - - // Threads only (parent = 0) - if forum.Parent == 0 { - fs.threadsOnly = append(fs.threadsOnly, id) - } - - // All posts - fs.allByLastPost = append(fs.allByLastPost, id) - } - - // Sort allByLastPost by last_post DESC, then ID DESC - sort.Slice(fs.allByLastPost, func(i, j int) bool { - forumI, _ := fs.GetByID(fs.allByLastPost[i]) - forumJ, _ := fs.GetByID(fs.allByLastPost[j]) - if forumI.LastPost != forumJ.LastPost { - return forumI.LastPost > forumJ.LastPost // DESC - } - return fs.allByLastPost[i] > fs.allByLastPost[j] // DESC - }) - - // Sort threadsOnly by last_post DESC, then ID DESC - sort.Slice(fs.threadsOnly, func(i, j int) bool { - forumI, _ := fs.GetByID(fs.threadsOnly[i]) - forumJ, _ := fs.GetByID(fs.threadsOnly[j]) - if forumI.LastPost != forumJ.LastPost { - return forumI.LastPost > forumJ.LastPost // DESC - } - return fs.threadsOnly[i] > fs.threadsOnly[j] // DESC - }) - - // Sort byParent replies by posted ASC, then ID ASC - for parent := range fs.byParent { - if parent > 0 { // Only sort replies, not threads - sort.Slice(fs.byParent[parent], func(i, j int) bool { - forumI, _ := fs.GetByID(fs.byParent[parent][i]) - forumJ, _ := fs.GetByID(fs.byParent[parent][j]) - if forumI.Posted != forumJ.Posted { - return forumI.Posted < forumJ.Posted // ASC - } - return fs.byParent[parent][i] < fs.byParent[parent][j] // ASC - }) - } - } - - // Sort byAuthor by posted DESC, then ID DESC - for author := range fs.byAuthor { - sort.Slice(fs.byAuthor[author], func(i, j int) bool { - forumI, _ := fs.GetByID(fs.byAuthor[author][i]) - forumJ, _ := fs.GetByID(fs.byAuthor[author][j]) - if forumI.Posted != forumJ.Posted { - return forumI.Posted > forumJ.Posted // DESC - } - return fs.byAuthor[author][i] > fs.byAuthor[author][j] // DESC - }) - } -} - -// rebuildIndices rebuilds all forum-specific indices from base store data -func (fs *ForumStore) rebuildIndices() { - fs.mu.Lock() - defer fs.mu.Unlock() - fs.rebuildIndicesUnsafe() -} - -// Retrieves a forum post by ID +// Query functions using enhanced store func Find(id int) (*Forum, error) { fs := GetStore() - forum, exists := fs.GetByID(id) + forum, exists := fs.Find(id) if !exists { return nil, fmt.Errorf("forum post with ID %d not found", id) } return forum, nil } -// Retrieves all forum posts ordered by last post time (most recent first) func All() ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() - - result := make([]*Forum, 0, len(fs.allByLastPost)) - for _, id := range fs.allByLastPost { - if forum, exists := fs.GetByID(id); exists { - result = append(result, forum) - } - } - return result, nil + return fs.AllSorted("allByLastPost"), nil } -// Retrieves all top-level forum threads (parent = 0) func Threads() ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() - - result := make([]*Forum, 0, len(fs.threadsOnly)) - for _, id := range fs.threadsOnly { - if forum, exists := fs.GetByID(id); exists { - result = append(result, forum) - } - } - return result, nil + return fs.FilterByIndex("allByLastPost", func(f *Forum) bool { + return f.Parent == 0 + }), nil } -// Retrieves all replies to a specific thread/post func ByParent(parentID int) ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() + replies := fs.GroupByIndex("byParent", parentID) - ids, exists := fs.byParent[parentID] - if !exists { - return []*Forum{}, nil + // Sort replies chronologically (posted ASC, then ID ASC) + if parentID > 0 && len(replies) > 1 { + sort.Slice(replies, func(i, j int) bool { + if replies[i].Posted != replies[j].Posted { + return replies[i].Posted < replies[j].Posted // ASC + } + return replies[i].ID < replies[j].ID // ASC + }) } - result := make([]*Forum, 0, len(ids)) - for _, id := range ids { - if forum, exists := fs.GetByID(id); exists { - result = append(result, forum) - } - } - return result, nil + return replies, nil } -// Retrieves forum posts by a specific author func ByAuthor(authorID int) ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() + posts := fs.GroupByIndex("byAuthor", authorID) - ids, exists := fs.byAuthor[authorID] - if !exists { - return []*Forum{}, nil - } - - result := make([]*Forum, 0, len(ids)) - for _, id := range ids { - if forum, exists := fs.GetByID(id); exists { - result = append(result, forum) + // Sort by posted DESC, then ID DESC + sort.Slice(posts, func(i, j int) bool { + if posts[i].Posted != posts[j].Posted { + return posts[i].Posted > posts[j].Posted // DESC } - } - return result, nil + return posts[i].ID > posts[j].ID // DESC + }) + + return posts, nil } -// Retrieves the most recent forum activity (limited by count) func Recent(limit int) ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() - - if limit > len(fs.allByLastPost) { - limit = len(fs.allByLastPost) + all := fs.AllSorted("allByLastPost") + if limit > len(all) { + limit = len(all) } - - result := make([]*Forum, 0, limit) - for i := 0; i < limit; i++ { - if forum, exists := fs.GetByID(fs.allByLastPost[i]); exists { - result = append(result, forum) - } - } - return result, nil + return all[:limit], nil } -// Retrieves forum posts containing the search term in title or content func Search(term string) ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() - - var result []*Forum lowerTerm := strings.ToLower(term) - - for _, id := range fs.allByLastPost { - if forum, exists := fs.GetByID(id); exists { - if strings.Contains(strings.ToLower(forum.Title), lowerTerm) || - strings.Contains(strings.ToLower(forum.Content), lowerTerm) { - result = append(result, forum) - } - } - } - return result, nil + return fs.FilterByIndex("allByLastPost", func(f *Forum) bool { + return strings.Contains(strings.ToLower(f.Title), lowerTerm) || + strings.Contains(strings.ToLower(f.Content), lowerTerm) + }), nil } -// Retrieves forum posts with activity since a specific timestamp func Since(since int64) ([]*Forum, error) { fs := GetStore() - fs.mu.RLock() - defer fs.mu.RUnlock() - - var result []*Forum - for _, id := range fs.allByLastPost { - if forum, exists := fs.GetByID(id); exists && forum.LastPost >= since { - result = append(result, forum) - } - } - return result, nil + return fs.FilterByIndex("allByLastPost", func(f *Forum) bool { + return f.LastPost >= since + }), nil } -// Saves a new forum post to the in-memory store and sets the ID +// Insert with ID assignment func (f *Forum) Insert() error { fs := GetStore() - - // Validate before insertion - if err := f.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if f.ID == 0 { f.ID = fs.GetNextID() } - - // Add to store - fs.AddForum(f) - return nil + return fs.AddForum(f) } -// Returns the posted timestamp as a time.Time +// Helper methods func (f *Forum) PostedTime() time.Time { return time.Unix(f.Posted, 0) } -// Returns the last post timestamp as a time.Time func (f *Forum) LastPostTime() time.Time { return time.Unix(f.LastPost, 0) } -// Sets the posted timestamp from a time.Time func (f *Forum) SetPostedTime(t time.Time) { f.Posted = t.Unix() } -// Sets the last post timestamp from a time.Time func (f *Forum) SetLastPostTime(t time.Time) { f.LastPost = t.Unix() } -// Returns true if this is a top-level thread (parent = 0) func (f *Forum) IsThread() bool { return f.Parent == 0 } -// Returns true if this is a reply to another post (parent > 0) func (f *Forum) IsReply() bool { return f.Parent > 0 } -// Returns true if this post has replies func (f *Forum) HasReplies() bool { return f.Replies > 0 } -// Returns true if there has been activity within the last 24 hours func (f *Forum) IsRecentActivity() bool { return time.Since(f.LastPostTime()) < 24*time.Hour } -// Returns how long ago the last activity occurred func (f *Forum) ActivityAge() time.Duration { return time.Since(f.LastPostTime()) } -// Returns how long ago the post was originally made func (f *Forum) PostAge() time.Duration { return time.Since(f.PostedTime()) } -// Returns true if the given user ID is the author of this post func (f *Forum) IsAuthor(userID int) bool { return f.Author == userID } -// Returns a truncated version of the content for previews func (f *Forum) Preview(maxLength int) string { if len(f.Content) <= maxLength { return f.Content @@ -469,7 +263,6 @@ func (f *Forum) Preview(maxLength int) string { return f.Content[:maxLength-3] + "..." } -// Returns the number of words in the content func (f *Forum) WordCount() int { if f.Content == "" { return 0 @@ -497,41 +290,34 @@ func (f *Forum) WordCount() int { return words } -// Returns the character length of the content func (f *Forum) Length() int { return len(f.Content) } -// Returns true if the title or content contains the given term (case-insensitive) func (f *Forum) Contains(term string) bool { lowerTerm := strings.ToLower(term) return strings.Contains(strings.ToLower(f.Title), lowerTerm) || strings.Contains(strings.ToLower(f.Content), lowerTerm) } -// Updates the last_post timestamp to current time func (f *Forum) UpdateLastPost() { f.LastPost = time.Now().Unix() } -// Increments the reply count func (f *Forum) IncrementReplies() { f.Replies++ } -// Decrements the reply count (minimum 0) func (f *Forum) DecrementReplies() { if f.Replies > 0 { f.Replies-- } } -// Retrieves all direct replies to this post func (f *Forum) GetReplies() ([]*Forum, error) { return ByParent(f.ID) } -// Retrieves the parent thread (if this is a reply) or returns self (if this is a thread) func (f *Forum) GetThread() (*Forum, error) { if f.IsThread() { return f, nil diff --git a/internal/items/items.go b/internal/items/items.go index 3291d3b..9486bed 100644 --- a/internal/items/items.go +++ b/internal/items/items.go @@ -3,8 +3,6 @@ package items import ( "dk/internal/store" "fmt" - "sort" - "sync" ) // Item represents an item in the game @@ -18,14 +16,11 @@ type Item struct { } func (i *Item) Save() error { - itemStore := GetStore() - itemStore.UpdateItem(i) - return nil + return GetStore().UpdateWithRebuild(i.ID, i) } func (i *Item) Delete() error { - itemStore := GetStore() - itemStore.RemoveItem(i.ID) + GetStore().RemoveWithRebuild(i.ID) return nil } @@ -64,212 +59,93 @@ const ( TypeShield = 3 ) -// ItemStore provides in-memory storage with O(1) lookups and item-specific indices +// ItemStore with enhanced BaseStore type ItemStore struct { - *store.BaseStore[Item] // Embedded generic store - byType map[int][]int // Type -> []ID - allByID []int // All IDs sorted by ID - mu sync.RWMutex // Protects indices + *store.BaseStore[Item] } -// Global in-memory store -var itemStore *ItemStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *ItemStore { + is := &ItemStore{BaseStore: store.NewBaseStore[Item]()} -// Initialize the in-memory store -func initStore() { - itemStore = &ItemStore{ - BaseStore: store.NewBaseStore[Item](), - byType: make(map[int][]int), - allByID: make([]int, 0), - } + // Register indices + is.RegisterIndex("byType", store.BuildIntGroupIndex(func(i *Item) int { + return i.Type + })) + + is.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Item) bool { + return a.ID < b.ID + })) + + return is +}) + +// Enhanced CRUD operations +func (is *ItemStore) AddItem(item *Item) error { + return is.AddWithRebuild(item.ID, item) } -// GetStore returns the global item store -func GetStore() *ItemStore { - storeOnce.Do(initStore) - return itemStore -} - -// AddItem adds an item to the in-memory store and updates all indices -func (is *ItemStore) AddItem(item *Item) { - is.mu.Lock() - defer is.mu.Unlock() - - // Validate item - if err := item.Validate(); err != nil { - return - } - - // Add to base store - is.Add(item.ID, item) - - // Rebuild indices - is.rebuildIndicesUnsafe() -} - -// RemoveItem removes an item from the store and updates indices func (is *ItemStore) RemoveItem(id int) { - is.mu.Lock() - defer is.mu.Unlock() - - // Remove from base store - is.Remove(id) - - // Rebuild indices - is.rebuildIndicesUnsafe() + is.RemoveWithRebuild(id) } -// UpdateItem updates an item efficiently -func (is *ItemStore) UpdateItem(item *Item) { - is.mu.Lock() - defer is.mu.Unlock() - - // Validate item - if err := item.Validate(); err != nil { - return - } - - // Update base store - is.Add(item.ID, item) - - // Rebuild indices - is.rebuildIndicesUnsafe() +func (is *ItemStore) UpdateItem(item *Item) error { + return is.UpdateWithRebuild(item.ID, item) } -// LoadData loads item data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { is := GetStore() - - // Load from base store, which handles JSON loading - if err := is.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - is.rebuildIndices() - return nil + return is.BaseStore.LoadData(dataPath) } -// SaveData saves item data to JSON file func SaveData(dataPath string) error { is := GetStore() return is.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (is *ItemStore) rebuildIndicesUnsafe() { - // Clear indices - is.byType = make(map[int][]int) - is.allByID = make([]int, 0) - - // Collect all items and build indices - allItems := is.GetAll() - - for id, item := range allItems { - // Type index - is.byType[item.Type] = append(is.byType[item.Type], id) - - // All IDs - is.allByID = append(is.allByID, id) - } - - // Sort allByID by ID - sort.Ints(is.allByID) - - // Sort type indices by ID - for itemType := range is.byType { - sort.Ints(is.byType[itemType]) - } -} - -// rebuildIndices rebuilds all item-specific indices from base store data -func (is *ItemStore) rebuildIndices() { - is.mu.Lock() - defer is.mu.Unlock() - is.rebuildIndicesUnsafe() -} - -// Retrieves an item by ID +// Query functions using enhanced store func Find(id int) (*Item, error) { is := GetStore() - item, exists := is.GetByID(id) + item, exists := is.Find(id) if !exists { return nil, fmt.Errorf("item with ID %d not found", id) } return item, nil } -// Retrieves all items func All() ([]*Item, error) { is := GetStore() - is.mu.RLock() - defer is.mu.RUnlock() - - result := make([]*Item, 0, len(is.allByID)) - for _, id := range is.allByID { - if item, exists := is.GetByID(id); exists { - result = append(result, item) - } - } - return result, nil + return is.AllSorted("allByID"), nil } -// Retrieves items by type func ByType(itemType int) ([]*Item, error) { is := GetStore() - is.mu.RLock() - defer is.mu.RUnlock() - - ids, exists := is.byType[itemType] - if !exists { - return []*Item{}, nil - } - - result := make([]*Item, 0, len(ids)) - for _, id := range ids { - if item, exists := is.GetByID(id); exists { - result = append(result, item) - } - } - return result, nil + return is.GroupByIndex("byType", itemType), nil } -// Saves a new item to the in-memory store and sets the ID +// Insert with ID assignment func (i *Item) Insert() error { is := GetStore() - - // Validate before insertion - if err := i.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if i.ID == 0 { i.ID = is.GetNextID() } - - // Add to store - is.AddItem(i) - return nil + return is.AddItem(i) } -// Returns true if the item is a weapon +// Helper methods func (i *Item) IsWeapon() bool { return i.Type == TypeWeapon } -// Returns true if the item is armor func (i *Item) IsArmor() bool { return i.Type == TypeArmor } -// Returns true if the item is a shield func (i *Item) IsShield() bool { return i.Type == TypeShield } -// Returns the string representation of the item type func (i *Item) TypeName() string { switch i.Type { case TypeWeapon: @@ -283,12 +159,10 @@ func (i *Item) TypeName() string { } } -// Returns true if the item has special properties func (i *Item) HasSpecial() bool { return i.Special != "" } -// Returns true if the item can be equipped func (i *Item) IsEquippable() bool { return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield } diff --git a/internal/news/news.go b/internal/news/news.go index 4a998b5..1ba3987 100644 --- a/internal/news/news.go +++ b/internal/news/news.go @@ -3,9 +3,7 @@ package news import ( "dk/internal/store" "fmt" - "sort" "strings" - "sync" "time" ) @@ -18,14 +16,11 @@ type News struct { } func (n *News) Save() error { - newsStore := GetStore() - newsStore.UpdateNews(n) - return nil + return GetStore().UpdateWithRebuild(n.ID, n) } func (n *News) Delete() error { - newsStore := GetStore() - newsStore.RemoveNews(n.ID) + GetStore().RemoveWithRebuild(n.ID) return nil } @@ -49,309 +44,139 @@ func (n *News) Validate() error { return nil } -// NewsStore provides in-memory storage with O(1) lookups and news-specific indices +// NewsStore with enhanced BaseStore type NewsStore struct { - *store.BaseStore[News] // Embedded generic store - byAuthor map[int][]int // Author -> []ID - allByPosted []int // All IDs sorted by posted DESC, id DESC - mu sync.RWMutex // Protects indices + *store.BaseStore[News] } -// Global in-memory store -var newsStore *NewsStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *NewsStore { + ns := &NewsStore{BaseStore: store.NewBaseStore[News]()} -// Initialize the in-memory store -func initStore() { - newsStore = &NewsStore{ - BaseStore: store.NewBaseStore[News](), - byAuthor: make(map[int][]int), - allByPosted: make([]int, 0), - } + // Register indices + ns.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(n *News) int { + return n.Author + })) + + ns.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *News) bool { + if a.Posted != b.Posted { + return a.Posted > b.Posted // DESC + } + return a.ID > b.ID // DESC + })) + + return ns +}) + +// Enhanced CRUD operations +func (ns *NewsStore) AddNews(news *News) error { + return ns.AddWithRebuild(news.ID, news) } -// GetStore returns the global news store -func GetStore() *NewsStore { - storeOnce.Do(initStore) - return newsStore -} - -// AddNews adds a news post to the in-memory store and updates all indices -func (ns *NewsStore) AddNews(news *News) { - ns.mu.Lock() - defer ns.mu.Unlock() - - // Validate news - if err := news.Validate(); err != nil { - return - } - - // Add to base store - ns.Add(news.ID, news) - - // Rebuild indices - ns.rebuildIndicesUnsafe() -} - -// RemoveNews removes a news post from the store and updates indices func (ns *NewsStore) RemoveNews(id int) { - ns.mu.Lock() - defer ns.mu.Unlock() - - // Remove from base store - ns.Remove(id) - - // Rebuild indices - ns.rebuildIndicesUnsafe() + ns.RemoveWithRebuild(id) } -// UpdateNews updates a news post efficiently -func (ns *NewsStore) UpdateNews(news *News) { - ns.mu.Lock() - defer ns.mu.Unlock() - - // Validate news - if err := news.Validate(); err != nil { - return - } - - // Update base store - ns.Add(news.ID, news) - - // Rebuild indices - ns.rebuildIndicesUnsafe() +func (ns *NewsStore) UpdateNews(news *News) error { + return ns.UpdateWithRebuild(news.ID, news) } -// LoadData loads news data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { ns := GetStore() - - // Load from base store, which handles JSON loading - if err := ns.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - ns.rebuildIndices() - return nil + return ns.BaseStore.LoadData(dataPath) } -// SaveData saves news data to JSON file func SaveData(dataPath string) error { ns := GetStore() return ns.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (ns *NewsStore) rebuildIndicesUnsafe() { - // Clear indices - ns.byAuthor = make(map[int][]int) - ns.allByPosted = make([]int, 0) - - // Collect all news and build indices - allNews := ns.GetAll() - - for id, news := range allNews { - // Author index - ns.byAuthor[news.Author] = append(ns.byAuthor[news.Author], id) - - // All IDs - ns.allByPosted = append(ns.allByPosted, id) - } - - // Sort allByPosted by posted DESC, then ID DESC - sort.Slice(ns.allByPosted, func(i, j int) bool { - newsI, _ := ns.GetByID(ns.allByPosted[i]) - newsJ, _ := ns.GetByID(ns.allByPosted[j]) - if newsI.Posted != newsJ.Posted { - return newsI.Posted > newsJ.Posted // DESC - } - return ns.allByPosted[i] > ns.allByPosted[j] // DESC - }) - - // Sort author indices by posted DESC, then ID DESC - for author := range ns.byAuthor { - sort.Slice(ns.byAuthor[author], func(i, j int) bool { - newsI, _ := ns.GetByID(ns.byAuthor[author][i]) - newsJ, _ := ns.GetByID(ns.byAuthor[author][j]) - if newsI.Posted != newsJ.Posted { - return newsI.Posted > newsJ.Posted // DESC - } - return ns.byAuthor[author][i] > ns.byAuthor[author][j] // DESC - }) - } -} - -// rebuildIndices rebuilds all news-specific indices from base store data -func (ns *NewsStore) rebuildIndices() { - ns.mu.Lock() - defer ns.mu.Unlock() - ns.rebuildIndicesUnsafe() -} - -// Retrieves a news post by ID +// Query functions using enhanced store func Find(id int) (*News, error) { ns := GetStore() - news, exists := ns.GetByID(id) + news, exists := ns.Find(id) if !exists { return nil, fmt.Errorf("news with ID %d not found", id) } return news, nil } -// Retrieves all news posts ordered by posted date (newest first) func All() ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - result := make([]*News, 0, len(ns.allByPosted)) - for _, id := range ns.allByPosted { - if news, exists := ns.GetByID(id); exists { - result = append(result, news) - } - } - return result, nil + return ns.AllSorted("allByPosted"), nil } -// Retrieves news posts by a specific author func ByAuthor(authorID int) ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - ids, exists := ns.byAuthor[authorID] - if !exists { - return []*News{}, nil - } - - result := make([]*News, 0, len(ids)) - for _, id := range ids { - if news, exists := ns.GetByID(id); exists { - result = append(result, news) - } - } - return result, nil + return ns.GroupByIndex("byAuthor", authorID), nil } -// Retrieves the most recent news posts (limited by count) func Recent(limit int) ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - if limit > len(ns.allByPosted) { - limit = len(ns.allByPosted) + all := ns.AllSorted("allByPosted") + if limit > len(all) { + limit = len(all) } - - result := make([]*News, 0, limit) - for i := 0; i < limit; i++ { - if news, exists := ns.GetByID(ns.allByPosted[i]); exists { - result = append(result, news) - } - } - return result, nil + return all[:limit], nil } -// Retrieves news posts since a specific timestamp func Since(since int64) ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - var result []*News - for _, id := range ns.allByPosted { - if news, exists := ns.GetByID(id); exists && news.Posted >= since { - result = append(result, news) - } - } - return result, nil + return ns.FilterByIndex("allByPosted", func(n *News) bool { + return n.Posted >= since + }), nil } -// Retrieves news posts between two timestamps (inclusive) func Between(start, end int64) ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - var result []*News - for _, id := range ns.allByPosted { - if news, exists := ns.GetByID(id); exists && news.Posted >= start && news.Posted <= end { - result = append(result, news) - } - } - return result, nil + return ns.FilterByIndex("allByPosted", func(n *News) bool { + return n.Posted >= start && n.Posted <= end + }), nil } -// Retrieves news posts containing the search term in content func Search(term string) ([]*News, error) { ns := GetStore() - ns.mu.RLock() - defer ns.mu.RUnlock() - - var result []*News lowerTerm := strings.ToLower(term) - - for _, id := range ns.allByPosted { - if news, exists := ns.GetByID(id); exists { - if strings.Contains(strings.ToLower(news.Content), lowerTerm) { - result = append(result, news) - } - } - } - return result, nil + return ns.FilterByIndex("allByPosted", func(n *News) bool { + return strings.Contains(strings.ToLower(n.Content), lowerTerm) + }), nil } -// Saves a new news post to the in-memory store and sets the ID +// Insert with ID assignment func (n *News) Insert() error { ns := GetStore() - - // Validate before insertion - if err := n.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if n.ID == 0 { n.ID = ns.GetNextID() } - - // Add to store - ns.AddNews(n) - return nil + return ns.AddNews(n) } -// Returns the posted timestamp as a time.Time +// Helper methods func (n *News) PostedTime() time.Time { return time.Unix(n.Posted, 0) } -// Sets the posted timestamp from a time.Time func (n *News) SetPostedTime(t time.Time) { n.Posted = t.Unix() } -// Returns true if the news post was made within the last 24 hours func (n *News) IsRecent() bool { return time.Since(n.PostedTime()) < 24*time.Hour } -// Returns how long ago the news post was made func (n *News) Age() time.Duration { return time.Since(n.PostedTime()) } -// Converts a time.Time to a human-readable date string func (n *News) ReadableTime() string { return n.PostedTime().Format("Jan 2, 2006 3:04 PM") } -// Returns true if the given user ID is the author of this news post func (n *News) IsAuthor(userID int) bool { return n.Author == userID } -// Returns a truncated version of the content for previews func (n *News) Preview(maxLength int) string { if len(n.Content) <= maxLength { return n.Content @@ -364,7 +189,6 @@ func (n *News) Preview(maxLength int) string { return n.Content[:maxLength-3] + "..." } -// Returns the number of words in the content func (n *News) WordCount() int { if n.Content == "" { return 0 @@ -392,17 +216,14 @@ func (n *News) WordCount() int { return words } -// Returns the character length of the content func (n *News) Length() int { return len(n.Content) } -// Returns true if the content contains the given term (case-insensitive) func (n *News) Contains(term string) bool { return strings.Contains(strings.ToLower(n.Content), strings.ToLower(term)) } -// Returns true if the content is empty or whitespace-only func (n *News) IsEmpty() bool { return strings.TrimSpace(n.Content) == "" } diff --git a/internal/routes/index.go b/internal/routes/index.go index f7e3863..4bc1656 100644 --- a/internal/routes/index.go +++ b/internal/routes/index.go @@ -36,10 +36,8 @@ func Move(ctx router.Ctx, _ []string) { return } - user.Set("Currently", currently) - user.Set("X", newX) - user.Set("Y", newY) - user.Save() + user.Currently = currently + user.X, user.Y = newX, newY if currently == "In Town" { ctx.Redirect("/town", 303) diff --git a/internal/routes/town.go b/internal/routes/town.go index 296ecd4..708a617 100644 --- a/internal/routes/town.go +++ b/internal/routes/town.go @@ -73,11 +73,8 @@ func rest(ctx router.Ctx, _ []string) { return } - user.Set("Gold", user.Gold-town.InnCost) - user.Set("HP", user.MaxHP) - user.Set("MP", user.MaxMP) - user.Set("TP", user.MaxTP) - user.Save() + user.Gold -= town.InnCost + user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{ "town": town, @@ -140,7 +137,7 @@ func buyItem(ctx router.Ctx, params []string) { return } - user.Set("Gold", user.Gold-item.Value) + user.Gold -= item.Value actions.UserEquipItem(user, item) user.Save() @@ -210,11 +207,11 @@ func buyMap(ctx router.Ctx, params []string) { return } - user.Set("Gold", user.Gold-mapped.MapCost) + user.Gold -= mapped.MapCost if user.Towns == "" { - user.Set("Towns", params[0]) + user.Towns = params[0] } else { - user.Set("Towns", user.Towns+","+params[0]) + user.Towns += "," + params[0] } user.Save() diff --git a/internal/server/server.go b/internal/server/server.go deleted file mode 100644 index dda6940..0000000 --- a/internal/server/server.go +++ /dev/null @@ -1,114 +0,0 @@ -package server - -import ( - "fmt" - "log" - "os" - "os/signal" - "path/filepath" - "syscall" - - "dk/internal/auth" - "dk/internal/middleware" - "dk/internal/monsters" - "dk/internal/router" - "dk/internal/routes" - "dk/internal/template" - - "github.com/valyala/fasthttp" -) - -func Start(port string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - // Initialize template singleton - template.InitializeCache(cwd) - - // Load monster data into memory - if err := monsters.LoadData("data/monsters.json"); err != nil { - return fmt.Errorf("failed to load monster data: %w", err) - } - - auth.Init("sessions.json") // Initialize auth.Manager - - r := router.New() - r.Use(middleware.Timing()) - r.Use(middleware.Auth(auth.Manager)) - r.Use(middleware.CSRF(auth.Manager)) - - r.Get("/", routes.Index) - r.Get("/explore", routes.Explore) - r.Post("/move", routes.Move) - routes.RegisterAuthRoutes(r) - routes.RegisterTownRoutes(r) - - // Use current working directory for static files - assetsDir := filepath.Join(cwd, "assets") - - // Static file server for /assets - fs := &fasthttp.FS{ - Root: assetsDir, - Compress: false, - } - assetsHandler := fs.NewRequestHandler() - - // Combined handler - requestHandler := func(ctx *fasthttp.RequestCtx) { - path := string(ctx.Path()) - - // Handle static assets - strip /assets prefix - if len(path) >= 7 && path[:7] == "/assets" { - // Strip the /assets prefix for the file system handler - originalPath := ctx.Path() - ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix - assetsHandler(ctx) - ctx.Request.URI().SetPathBytes(originalPath) // Restore original path - return - } - - // Handle routes - r.ServeHTTP(ctx) - } - - addr := ":" + port - log.Printf("Server starting on %s", addr) - - // Setup graceful shutdown - server := &fasthttp.Server{ - Handler: requestHandler, - } - - // Channel to listen for interrupt signal - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - - // Start server in a goroutine - go func() { - if err := server.ListenAndServe(addr); err != nil { - log.Printf("Server error: %v", err) - } - }() - - // Wait for interrupt signal - <-c - log.Println("Received shutdown signal, shutting down gracefully...") - - // Save monster data before shutdown - log.Println("Saving monster data...") - if err := monsters.SaveData("data/monsters.json"); 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 { - log.Printf("Error saving sessions: %v", err) - } - - // FastHTTP doesn't have a graceful Shutdown method like net/http - // We just let the server stop naturally when the main function exits - log.Println("Server stopped") - return nil -} diff --git a/internal/spells/spells.go b/internal/spells/spells.go index 40e5a32..f72c6b8 100644 --- a/internal/spells/spells.go +++ b/internal/spells/spells.go @@ -3,9 +3,7 @@ package spells import ( "dk/internal/store" "fmt" - "sort" "strings" - "sync" ) // Spell represents a spell in the game @@ -18,14 +16,11 @@ type Spell struct { } func (s *Spell) Save() error { - spellStore := GetStore() - spellStore.UpdateSpell(s) - return nil + return GetStore().UpdateWithRebuild(s.ID, s) } func (s *Spell) Delete() error { - spellStore := GetStore() - spellStore.RemoveSpell(s.ID) + GetStore().RemoveWithRebuild(s.ID) return nil } @@ -65,321 +60,138 @@ const ( TypeDefenseBoost = 5 ) -// SpellStore provides in-memory storage with O(1) lookups and spell-specific indices +// SpellStore with enhanced BaseStore type SpellStore struct { - *store.BaseStore[Spell] // Embedded generic store - byType map[int][]int // Type -> []ID - byName map[string]int // Name (lowercase) -> ID - byMP map[int][]int // MP -> []ID - allByTypeMP []int // All IDs sorted by type, MP, ID - mu sync.RWMutex // Protects indices + *store.BaseStore[Spell] } -// Global in-memory store -var spellStore *SpellStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *SpellStore { + ss := &SpellStore{BaseStore: store.NewBaseStore[Spell]()} -// Initialize the in-memory store -func initStore() { - spellStore = &SpellStore{ - BaseStore: store.NewBaseStore[Spell](), - byType: make(map[int][]int), - byName: make(map[string]int), - byMP: make(map[int][]int), - allByTypeMP: make([]int, 0), - } + // Register indices + ss.RegisterIndex("byType", store.BuildIntGroupIndex(func(s *Spell) int { + return s.Type + })) + + ss.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(s *Spell) string { + return s.Name + })) + + ss.RegisterIndex("byMP", store.BuildIntGroupIndex(func(s *Spell) int { + return s.MP + })) + + ss.RegisterIndex("allByTypeMP", store.BuildSortedListIndex(func(a, b *Spell) bool { + if a.Type != b.Type { + return a.Type < b.Type + } + if a.MP != b.MP { + return a.MP < b.MP + } + return a.ID < b.ID + })) + + return ss +}) + +// Enhanced CRUD operations +func (ss *SpellStore) AddSpell(spell *Spell) error { + return ss.AddWithRebuild(spell.ID, spell) } -// GetStore returns the global spell store -func GetStore() *SpellStore { - storeOnce.Do(initStore) - return spellStore -} - -// AddSpell adds a spell to the in-memory store and updates all indices -func (ss *SpellStore) AddSpell(spell *Spell) { - ss.mu.Lock() - defer ss.mu.Unlock() - - // Validate spell - if err := spell.Validate(); err != nil { - return - } - - // Add to base store - ss.Add(spell.ID, spell) - - // Rebuild indices - ss.rebuildIndicesUnsafe() -} - -// RemoveSpell removes a spell from the store and updates indices func (ss *SpellStore) RemoveSpell(id int) { - ss.mu.Lock() - defer ss.mu.Unlock() - - // Remove from base store - ss.Remove(id) - - // Rebuild indices - ss.rebuildIndicesUnsafe() + ss.RemoveWithRebuild(id) } -// UpdateSpell updates a spell efficiently -func (ss *SpellStore) UpdateSpell(spell *Spell) { - ss.mu.Lock() - defer ss.mu.Unlock() - - // Validate spell - if err := spell.Validate(); err != nil { - return - } - - // Update base store - ss.Add(spell.ID, spell) - - // Rebuild indices - ss.rebuildIndicesUnsafe() +func (ss *SpellStore) UpdateSpell(spell *Spell) error { + return ss.UpdateWithRebuild(spell.ID, spell) } -// LoadData loads spell data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { ss := GetStore() - - // Load from base store, which handles JSON loading - if err := ss.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - ss.rebuildIndices() - return nil + return ss.BaseStore.LoadData(dataPath) } -// SaveData saves spell data to JSON file func SaveData(dataPath string) error { ss := GetStore() return ss.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (ss *SpellStore) rebuildIndicesUnsafe() { - // Clear indices - ss.byType = make(map[int][]int) - ss.byName = make(map[string]int) - ss.byMP = make(map[int][]int) - ss.allByTypeMP = make([]int, 0) - - // Collect all spells and build indices - allSpells := ss.GetAll() - - for id, spell := range allSpells { - // Type index - ss.byType[spell.Type] = append(ss.byType[spell.Type], id) - - // Name index (case-insensitive) - ss.byName[strings.ToLower(spell.Name)] = id - - // MP index - ss.byMP[spell.MP] = append(ss.byMP[spell.MP], id) - - // All IDs - ss.allByTypeMP = append(ss.allByTypeMP, id) - } - - // Sort allByTypeMP by type, then MP, then ID - sort.Slice(ss.allByTypeMP, func(i, j int) bool { - spellI, _ := ss.GetByID(ss.allByTypeMP[i]) - spellJ, _ := ss.GetByID(ss.allByTypeMP[j]) - if spellI.Type != spellJ.Type { - return spellI.Type < spellJ.Type - } - if spellI.MP != spellJ.MP { - return spellI.MP < spellJ.MP - } - return ss.allByTypeMP[i] < ss.allByTypeMP[j] - }) - - // Sort type indices by MP, then ID - for spellType := range ss.byType { - sort.Slice(ss.byType[spellType], func(i, j int) bool { - spellI, _ := ss.GetByID(ss.byType[spellType][i]) - spellJ, _ := ss.GetByID(ss.byType[spellType][j]) - if spellI.MP != spellJ.MP { - return spellI.MP < spellJ.MP - } - return ss.byType[spellType][i] < ss.byType[spellType][j] - }) - } - - // Sort MP indices by type, then ID - for mp := range ss.byMP { - sort.Slice(ss.byMP[mp], func(i, j int) bool { - spellI, _ := ss.GetByID(ss.byMP[mp][i]) - spellJ, _ := ss.GetByID(ss.byMP[mp][j]) - if spellI.Type != spellJ.Type { - return spellI.Type < spellJ.Type - } - return ss.byMP[mp][i] < ss.byMP[mp][j] - }) - } -} - -// rebuildIndices rebuilds all spell-specific indices from base store data -func (ss *SpellStore) rebuildIndices() { - ss.mu.Lock() - defer ss.mu.Unlock() - ss.rebuildIndicesUnsafe() -} - -// Retrieves a spell by ID +// Query functions using enhanced store func Find(id int) (*Spell, error) { ss := GetStore() - spell, exists := ss.GetByID(id) + spell, exists := ss.Find(id) if !exists { return nil, fmt.Errorf("spell with ID %d not found", id) } return spell, nil } -// Retrieves all spells func All() ([]*Spell, error) { ss := GetStore() - ss.mu.RLock() - defer ss.mu.RUnlock() - - result := make([]*Spell, 0, len(ss.allByTypeMP)) - for _, id := range ss.allByTypeMP { - if spell, exists := ss.GetByID(id); exists { - result = append(result, spell) - } - } - return result, nil + return ss.AllSorted("allByTypeMP"), nil } -// Retrieves spells by type func ByType(spellType int) ([]*Spell, error) { ss := GetStore() - ss.mu.RLock() - defer ss.mu.RUnlock() - - ids, exists := ss.byType[spellType] - if !exists { - return []*Spell{}, nil - } - - result := make([]*Spell, 0, len(ids)) - for _, id := range ids { - if spell, exists := ss.GetByID(id); exists { - result = append(result, spell) - } - } - return result, nil + return ss.GroupByIndex("byType", spellType), nil } -// Retrieves spells that cost at most the specified MP func ByMaxMP(maxMP int) ([]*Spell, error) { ss := GetStore() - ss.mu.RLock() - defer ss.mu.RUnlock() - - var result []*Spell - for mp := 0; mp <= maxMP; mp++ { - if ids, exists := ss.byMP[mp]; exists { - for _, id := range ids { - if spell, exists := ss.GetByID(id); exists { - result = append(result, spell) - } - } - } - } - return result, nil + return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool { + return s.MP <= maxMP + }), nil } -// Retrieves spells of a specific type that cost at most the specified MP func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) { ss := GetStore() - ss.mu.RLock() - defer ss.mu.RUnlock() - - ids, exists := ss.byType[spellType] - if !exists { - return []*Spell{}, nil - } - - var result []*Spell - for _, id := range ids { - if spell, exists := ss.GetByID(id); exists && spell.MP <= maxMP { - result = append(result, spell) - } - } - return result, nil + return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool { + return s.Type == spellType && s.MP <= maxMP + }), nil } -// Retrieves a spell by name (case-insensitive) func ByName(name string) (*Spell, error) { ss := GetStore() - ss.mu.RLock() - defer ss.mu.RUnlock() - - id, exists := ss.byName[strings.ToLower(name)] + spell, exists := ss.LookupByIndex("byName", strings.ToLower(name)) if !exists { return nil, fmt.Errorf("spell with name '%s' not found", name) } - - spell, exists := ss.GetByID(id) - if !exists { - return nil, fmt.Errorf("spell with name '%s' not found", name) - } - return spell, nil } -// Saves a new spell to the in-memory store and sets the ID +// Insert with ID assignment func (s *Spell) Insert() error { ss := GetStore() - - // Validate before insertion - if err := s.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if s.ID == 0 { s.ID = ss.GetNextID() } - - // Add to store - ss.AddSpell(s) - return nil + return ss.AddSpell(s) } -// Returns true if the spell is a healing spell +// Helper methods func (s *Spell) IsHealing() bool { return s.Type == TypeHealing } -// Returns true if the spell is a hurt spell func (s *Spell) IsHurt() bool { return s.Type == TypeHurt } -// Returns true if the spell is a sleep spell func (s *Spell) IsSleep() bool { return s.Type == TypeSleep } -// Returns true if the spell boosts attack func (s *Spell) IsAttackBoost() bool { return s.Type == TypeAttackBoost } -// Returns true if the spell boosts defense func (s *Spell) IsDefenseBoost() bool { return s.Type == TypeDefenseBoost } -// Returns the string representation of the spell type func (s *Spell) TypeName() string { switch s.Type { case TypeHealing: @@ -397,12 +209,10 @@ func (s *Spell) TypeName() string { } } -// Returns true if the spell can be cast with the given MP func (s *Spell) CanCast(availableMP int) bool { return availableMP >= s.MP } -// Returns the attribute per MP ratio (higher is more efficient) func (s *Spell) Efficiency() float64 { if s.MP == 0 { return 0 @@ -410,12 +220,10 @@ func (s *Spell) Efficiency() float64 { return float64(s.Attribute) / float64(s.MP) } -// Returns true if the spell is used for attacking func (s *Spell) IsOffensive() bool { return s.Type == TypeHurt || s.Type == TypeSleep } -// Returns true if the spell is used for support/buffs func (s *Spell) IsSupport() bool { return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost } diff --git a/internal/store/store.go b/internal/store/store.go index 9f8247b..914035b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -41,8 +41,6 @@ func NewBaseStore[T any]() *BaseStore[T] { } } -// Index Management - // RegisterIndex registers an index builder function func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) { bs.mu.Lock() @@ -76,8 +74,6 @@ func (bs *BaseStore[T]) rebuildIndicesUnsafe() { } } -// Enhanced CRUD Operations - // AddWithRebuild adds item with validation and index rebuild func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error { bs.mu.Lock() @@ -234,8 +230,6 @@ func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool return result } -// Common Index Builders - // BuildStringLookupIndex creates string-to-ID mapping func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] { return func(allItems map[int]*T) any { @@ -312,8 +306,6 @@ func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] { } } -// Singleton Management Helper - // NewSingleton creates singleton store pattern with sync.Once func NewSingleton[S any](initFunc func() *S) func() *S { var store *S @@ -327,8 +319,6 @@ func NewSingleton[S any](initFunc func() *S) func() *S { } } -// Legacy Methods (backward compatibility) - // GetNextID returns the next available ID atomically func (bs *BaseStore[T]) GetNextID() int { bs.mu.Lock() @@ -379,8 +369,6 @@ func (bs *BaseStore[T]) Clear() { bs.rebuildIndicesUnsafe() } -// JSON Persistence - // LoadFromJSON loads items from JSON using reflection func (bs *BaseStore[T]) LoadFromJSON(filename string) error { bs.mu.Lock() @@ -471,7 +459,7 @@ func (bs *BaseStore[T]) LoadData(dataPath string) error { return fmt.Errorf("failed to load from JSON: %w", err) } - fmt.Printf("Loaded %d items from JSON\n", len(bs.items)) + fmt.Printf("Loaded %d items from %s\n", len(bs.items), dataPath) bs.RebuildIndices() // Rebuild indices after loading return nil } @@ -488,6 +476,6 @@ func (bs *BaseStore[T]) SaveData(dataPath string) error { return fmt.Errorf("failed to save to JSON: %w", err) } - fmt.Printf("Saved %d items to JSON\n", len(bs.items)) + fmt.Printf("Saved %d items to %s\n", len(bs.items), dataPath) return nil } diff --git a/internal/towns/towns.go b/internal/towns/towns.go index 3996b2c..e15ba14 100644 --- a/internal/towns/towns.go +++ b/internal/towns/towns.go @@ -8,7 +8,6 @@ import ( "sort" "strconv" "strings" - "sync" "dk/internal/helpers" ) @@ -26,14 +25,11 @@ type Town struct { } func (t *Town) Save() error { - townStore := GetStore() - townStore.UpdateTown(t) - return nil + return GetStore().UpdateWithRebuild(t.ID, t) } func (t *Town) Delete() error { - townStore := GetStore() - townStore.RemoveTown(t.ID) + GetStore().RemoveWithRebuild(t.ID) return nil } @@ -67,284 +63,128 @@ func (t *Town) Validate() error { return nil } -// TownStore provides in-memory storage with O(1) lookups and town-specific indices -type TownStore struct { - *store.BaseStore[Town] // Embedded generic store - byName map[string]int // Name (lowercase) -> ID - byCoords map[string]int // "x,y" -> ID - byInnCost map[int][]int // InnCost -> []ID - byTPCost map[int][]int // TPCost -> []ID - allByID []int // All IDs sorted by ID - mu sync.RWMutex // Protects indices -} - -// Global in-memory store -var townStore *TownStore -var storeOnce sync.Once - -// Initialize the in-memory store -func initStore() { - townStore = &TownStore{ - BaseStore: store.NewBaseStore[Town](), - byName: make(map[string]int), - byCoords: make(map[string]int), - byInnCost: make(map[int][]int), - byTPCost: make(map[int][]int), - allByID: make([]int, 0), - } -} - -// GetStore returns the global town store -func GetStore() *TownStore { - storeOnce.Do(initStore) - return townStore -} - -// AddTown adds a town to the in-memory store and updates all indices -func (ts *TownStore) AddTown(town *Town) { - ts.mu.Lock() - defer ts.mu.Unlock() - - // Validate town - if err := town.Validate(); err != nil { - return - } - - // Add to base store - ts.Add(town.ID, town) - - // Rebuild indices - ts.rebuildIndicesUnsafe() -} - -// RemoveTown removes a town from the store and updates indices -func (ts *TownStore) RemoveTown(id int) { - ts.mu.Lock() - defer ts.mu.Unlock() - - // Remove from base store - ts.Remove(id) - - // Rebuild indices - ts.rebuildIndicesUnsafe() -} - -// UpdateTown updates a town efficiently -func (ts *TownStore) UpdateTown(town *Town) { - ts.mu.Lock() - defer ts.mu.Unlock() - - // Validate town - if err := town.Validate(); err != nil { - return - } - - // Update base store - ts.Add(town.ID, town) - - // Rebuild indices - ts.rebuildIndicesUnsafe() -} - -// LoadData loads town data from JSON file, or starts with empty store -func LoadData(dataPath string) error { - ts := GetStore() - - // Load from base store, which handles JSON loading - if err := ts.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - ts.rebuildIndices() - return nil -} - -// SaveData saves town data to JSON file -func SaveData(dataPath string) error { - ts := GetStore() - return ts.BaseStore.SaveData(dataPath) -} - // coordsKey creates a key for coordinate-based lookup func coordsKey(x, y int) string { return strconv.Itoa(x) + "," + strconv.Itoa(y) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (ts *TownStore) rebuildIndicesUnsafe() { - // Clear indices - ts.byName = make(map[string]int) - ts.byCoords = make(map[string]int) - ts.byInnCost = make(map[int][]int) - ts.byTPCost = make(map[int][]int) - ts.allByID = make([]int, 0) - - // Collect all towns and build indices - allTowns := ts.GetAll() - - for id, town := range allTowns { - // Name index (case-insensitive) - ts.byName[strings.ToLower(town.Name)] = id - - // Coordinates index - ts.byCoords[coordsKey(town.X, town.Y)] = id - - // Cost indices - ts.byInnCost[town.InnCost] = append(ts.byInnCost[town.InnCost], id) - ts.byTPCost[town.TPCost] = append(ts.byTPCost[town.TPCost], id) - - // All IDs - ts.allByID = append(ts.allByID, id) - } - - // Sort all by ID - sort.Ints(ts.allByID) - - // Sort cost indices by ID - for innCost := range ts.byInnCost { - sort.Ints(ts.byInnCost[innCost]) - } - - for tpCost := range ts.byTPCost { - sort.Ints(ts.byTPCost[tpCost]) - } +// TownStore with enhanced BaseStore +type TownStore struct { + *store.BaseStore[Town] } -// rebuildIndices rebuilds all town-specific indices from base store data -func (ts *TownStore) rebuildIndices() { - ts.mu.Lock() - defer ts.mu.Unlock() - ts.rebuildIndicesUnsafe() +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *TownStore { + ts := &TownStore{BaseStore: store.NewBaseStore[Town]()} + + // Register indices + ts.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(t *Town) string { + return t.Name + })) + + ts.RegisterIndex("byCoords", store.BuildStringLookupIndex(func(t *Town) string { + return coordsKey(t.X, t.Y) + })) + + ts.RegisterIndex("byInnCost", store.BuildIntGroupIndex(func(t *Town) int { + return t.InnCost + })) + + ts.RegisterIndex("byTPCost", store.BuildIntGroupIndex(func(t *Town) int { + return t.TPCost + })) + + ts.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Town) bool { + return a.ID < b.ID + })) + + return ts +}) + +// Enhanced CRUD operations +func (ts *TownStore) AddTown(town *Town) error { + return ts.AddWithRebuild(town.ID, town) } -// Retrieves a town by ID +func (ts *TownStore) RemoveTown(id int) { + ts.RemoveWithRebuild(id) +} + +func (ts *TownStore) UpdateTown(town *Town) error { + return ts.UpdateWithRebuild(town.ID, town) +} + +// Data persistence +func LoadData(dataPath string) error { + ts := GetStore() + return ts.BaseStore.LoadData(dataPath) +} + +func SaveData(dataPath string) error { + ts := GetStore() + return ts.BaseStore.SaveData(dataPath) +} + +// Query functions using enhanced store func Find(id int) (*Town, error) { ts := GetStore() - town, exists := ts.GetByID(id) + town, exists := ts.Find(id) if !exists { return nil, fmt.Errorf("town with ID %d not found", id) } return town, nil } -// Retrieves all towns func All() ([]*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - result := make([]*Town, 0, len(ts.allByID)) - for _, id := range ts.allByID { - if town, exists := ts.GetByID(id); exists { - result = append(result, town) - } - } - return result, nil + return ts.AllSorted("allByID"), nil } -// Retrieves a town by name (case-insensitive) func ByName(name string) (*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - id, exists := ts.byName[strings.ToLower(name)] + town, exists := ts.LookupByIndex("byName", strings.ToLower(name)) if !exists { return nil, fmt.Errorf("town with name '%s' not found", name) } - - town, exists := ts.GetByID(id) - if !exists { - return nil, fmt.Errorf("town with name '%s' not found", name) - } - return town, nil } -// Retrieves towns with inn cost at most the specified amount func ByMaxInnCost(maxCost int) ([]*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - var result []*Town - for cost := 0; cost <= maxCost; cost++ { - if ids, exists := ts.byInnCost[cost]; exists { - for _, id := range ids { - if town, exists := ts.GetByID(id); exists { - result = append(result, town) - } - } - } - } - return result, nil + return ts.FilterByIndex("allByID", func(t *Town) bool { + return t.InnCost <= maxCost + }), nil } -// Retrieves towns with teleport cost at most the specified amount func ByMaxTPCost(maxCost int) ([]*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - var result []*Town - for cost := 0; cost <= maxCost; cost++ { - if ids, exists := ts.byTPCost[cost]; exists { - for _, id := range ids { - if town, exists := ts.GetByID(id); exists { - result = append(result, town) - } - } - } - } - return result, nil + return ts.FilterByIndex("allByID", func(t *Town) bool { + return t.TPCost <= maxCost + }), nil } -// Retrieves a town by its x, y coordinates func ByCoords(x, y int) (*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - id, exists := ts.byCoords[coordsKey(x, y)] + town, exists := ts.LookupByIndex("byCoords", coordsKey(x, y)) if !exists { return nil, nil // Return nil if not found (like original) } - - town, exists := ts.GetByID(id) - if !exists { - return nil, nil - } - return town, nil } -// ExistsAt checks for a town at the given coordinates, returning true/false func ExistsAt(x, y int) bool { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - _, exists := ts.byCoords[coordsKey(x, y)] + _, exists := ts.LookupByIndex("byCoords", coordsKey(x, y)) return exists } -// Retrieves towns within a certain distance from a point func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) { ts := GetStore() - ts.mu.RLock() - defer ts.mu.RUnlock() - - var result []*Town maxDistance2 := float64(maxDistance * maxDistance) - for _, id := range ts.allByID { - if town, exists := ts.GetByID(id); exists { - if town.DistanceFromSquared(fromX, fromY) <= maxDistance2 { - result = append(result, town) - } - } - } + result := ts.FilterByIndex("allByID", func(t *Town) bool { + return t.DistanceFromSquared(fromX, fromY) <= maxDistance2 + }) // Sort by distance, then by ID sort.Slice(result, func(i, j int) bool { @@ -359,83 +199,62 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) { return result, nil } -// Saves a new town to the in-memory store and sets the ID +// Insert with ID assignment func (t *Town) Insert() error { ts := GetStore() - - // Validate before insertion - if err := t.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if t.ID == 0 { t.ID = ts.GetNextID() } - - // Add to store - ts.AddTown(t) - return nil + return ts.AddTown(t) } -// Returns the shop items as a slice of item IDs +// Helper methods func (t *Town) GetShopItems() []int { return helpers.StringToInts(t.ShopList) } -// Sets the shop items from a slice of item IDs func (t *Town) SetShopItems(items []int) { t.ShopList = helpers.IntsToString(items) } -// Checks if the town's shop sells a specific item ID func (t *Town) HasShopItem(itemID int) bool { return slices.Contains(t.GetShopItems(), itemID) } -// Calculates the squared distance from this town to given coordinates func (t *Town) DistanceFromSquared(x, y int) float64 { dx := float64(t.X - x) dy := float64(t.Y - y) return dx*dx + dy*dy // Return squared distance for performance } -// Calculates the actual distance from this town to given coordinates func (t *Town) DistanceFrom(x, y int) float64 { return math.Sqrt(t.DistanceFromSquared(x, y)) } -// Returns true if this is the starting town (Midworld) func (t *Town) IsStartingTown() bool { return t.X == 0 && t.Y == 0 } -// Returns true if the player can afford the inn func (t *Town) CanAffordInn(gold int) bool { return gold >= t.InnCost } -// Returns true if the player can afford to buy the map func (t *Town) CanAffordMap(gold int) bool { return gold >= t.MapCost } -// Returns true if the player can afford to teleport here func (t *Town) CanAffordTeleport(gold int) bool { return gold >= t.TPCost } -// Returns true if the town has a shop with items func (t *Town) HasShop() bool { return len(t.GetShopItems()) > 0 } -// Returns the town's coordinates func (t *Town) GetPosition() (int, int) { return t.X, t.Y } -// Sets the town's coordinates func (t *Town) SetPosition(x, y int) { t.X = x t.Y = y diff --git a/internal/users/users.go b/internal/users/users.go index fdb6b04..25c4373 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -6,7 +6,6 @@ import ( "slices" "sort" "strings" - "sync" "time" "dk/internal/helpers" @@ -67,14 +66,11 @@ type User struct { } func (u *User) Save() error { - userStore := GetStore() - userStore.UpdateUser(u) - return nil + return GetStore().UpdateWithRebuild(u.ID, u) } func (u *User) Delete() error { - userStore := GetStore() - userStore.RemoveUser(u.ID) + GetStore().RemoveWithRebuild(u.ID) return nil } @@ -135,161 +131,76 @@ func (u *User) Validate() error { return nil } -// UserStore provides in-memory storage with O(1) lookups and user-specific indices +// UserStore with enhanced BaseStore type UserStore struct { - *store.BaseStore[User] // Embedded generic store - byUsername map[string]int // Username (lowercase) -> ID - byEmail map[string]int // Email -> ID - byLevel map[int][]int // Level -> []ID - allByRegistered []int // All IDs sorted by registered DESC, id DESC - mu sync.RWMutex // Protects indices + *store.BaseStore[User] } -// Global in-memory store -var userStore *UserStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *UserStore { + us := &UserStore{BaseStore: store.NewBaseStore[User]()} -// Initialize the in-memory store -func initStore() { - userStore = &UserStore{ - BaseStore: store.NewBaseStore[User](), - byUsername: make(map[string]int), - byEmail: make(map[string]int), - byLevel: make(map[int][]int), - allByRegistered: make([]int, 0), - } + // Register indices + us.RegisterIndex("byUsername", store.BuildCaseInsensitiveLookupIndex(func(u *User) string { + return u.Username + })) + + us.RegisterIndex("byEmail", store.BuildStringLookupIndex(func(u *User) string { + return u.Email + })) + + us.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(u *User) int { + return u.Level + })) + + us.RegisterIndex("allByRegistered", store.BuildSortedListIndex(func(a, b *User) bool { + if a.Registered != b.Registered { + return a.Registered > b.Registered // DESC + } + return a.ID > b.ID // DESC + })) + + us.RegisterIndex("allByLevelExp", store.BuildSortedListIndex(func(a, b *User) bool { + if a.Level != b.Level { + return a.Level > b.Level // Level DESC + } + if a.Exp != b.Exp { + return a.Exp > b.Exp // Exp DESC + } + return a.ID < b.ID // ID ASC + })) + + return us +}) + +// Enhanced CRUD operations +func (us *UserStore) AddUser(user *User) error { + return us.AddWithRebuild(user.ID, user) } -// GetStore returns the global user store -func GetStore() *UserStore { - storeOnce.Do(initStore) - return userStore -} - -// AddUser adds a user to the in-memory store and updates all indices -func (us *UserStore) AddUser(user *User) { - us.mu.Lock() - defer us.mu.Unlock() - - // Validate user - if err := user.Validate(); err != nil { - return - } - - // Add to base store - us.Add(user.ID, user) - - // Rebuild indices - us.rebuildIndicesUnsafe() -} - -// RemoveUser removes a user from the store and updates indices func (us *UserStore) RemoveUser(id int) { - us.mu.Lock() - defer us.mu.Unlock() - - // Remove from base store - us.Remove(id) - - // Rebuild indices - us.rebuildIndicesUnsafe() + us.RemoveWithRebuild(id) } -// UpdateUser updates a user efficiently -func (us *UserStore) UpdateUser(user *User) { - us.mu.Lock() - defer us.mu.Unlock() - - // Validate user - if err := user.Validate(); err != nil { - return - } - - // Update base store - us.Add(user.ID, user) - - // Rebuild indices - us.rebuildIndicesUnsafe() +func (us *UserStore) UpdateUser(user *User) error { + return us.UpdateWithRebuild(user.ID, user) } -// LoadData loads user data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { us := GetStore() - - // Load from base store, which handles JSON loading - if err := us.BaseStore.LoadData(dataPath); err != nil { - return err - } - - // Rebuild indices from loaded data - us.rebuildIndices() - return nil + return us.BaseStore.LoadData(dataPath) } -// SaveData saves user data to JSON file func SaveData(dataPath string) error { us := GetStore() return us.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -func (us *UserStore) rebuildIndicesUnsafe() { - // Clear indices - us.byUsername = make(map[string]int) - us.byEmail = make(map[string]int) - us.byLevel = make(map[int][]int) - us.allByRegistered = make([]int, 0) - - // Collect all users and build indices - allUsers := us.GetAll() - - for id, user := range allUsers { - // Username index (case-insensitive) - us.byUsername[strings.ToLower(user.Username)] = id - - // Email index - us.byEmail[user.Email] = id - - // Level index - us.byLevel[user.Level] = append(us.byLevel[user.Level], id) - - // All IDs - us.allByRegistered = append(us.allByRegistered, id) - } - - // Sort allByRegistered by registered DESC, then ID DESC - sort.Slice(us.allByRegistered, func(i, j int) bool { - userI, _ := us.GetByID(us.allByRegistered[i]) - userJ, _ := us.GetByID(us.allByRegistered[j]) - if userI.Registered != userJ.Registered { - return userI.Registered > userJ.Registered // DESC - } - return us.allByRegistered[i] > us.allByRegistered[j] // DESC - }) - - // Sort level indices by exp DESC, then ID ASC - for level := range us.byLevel { - sort.Slice(us.byLevel[level], func(i, j int) bool { - userI, _ := us.GetByID(us.byLevel[level][i]) - userJ, _ := us.GetByID(us.byLevel[level][j]) - if userI.Exp != userJ.Exp { - return userI.Exp > userJ.Exp // DESC - } - return us.byLevel[level][i] < us.byLevel[level][j] // ASC - }) - } -} - -// rebuildIndices rebuilds all user-specific indices from base store data -func (us *UserStore) rebuildIndices() { - us.mu.Lock() - defer us.mu.Unlock() - us.rebuildIndicesUnsafe() -} - +// Query functions using enhanced store func Find(id int) (*User, error) { us := GetStore() - user, exists := us.GetByID(id) + user, exists := us.Find(id) if !exists { return nil, fmt.Errorf("user with ID %d not found", id) } @@ -298,86 +209,39 @@ func Find(id int) (*User, error) { func All() ([]*User, error) { us := GetStore() - us.mu.RLock() - defer us.mu.RUnlock() - - result := make([]*User, 0, len(us.allByRegistered)) - for _, id := range us.allByRegistered { - if user, exists := us.GetByID(id); exists { - result = append(result, user) - } - } - return result, nil + return us.AllSorted("allByRegistered"), nil } func ByUsername(username string) (*User, error) { us := GetStore() - us.mu.RLock() - defer us.mu.RUnlock() - - id, exists := us.byUsername[strings.ToLower(username)] + user, exists := us.LookupByIndex("byUsername", strings.ToLower(username)) if !exists { return nil, fmt.Errorf("user with username '%s' not found", username) } - - user, exists := us.GetByID(id) - if !exists { - return nil, fmt.Errorf("user with username '%s' not found", username) - } - return user, nil } func ByEmail(email string) (*User, error) { us := GetStore() - us.mu.RLock() - defer us.mu.RUnlock() - - id, exists := us.byEmail[email] + user, exists := us.LookupByIndex("byEmail", email) if !exists { return nil, fmt.Errorf("user with email '%s' not found", email) } - - user, exists := us.GetByID(id) - if !exists { - return nil, fmt.Errorf("user with email '%s' not found", email) - } - return user, nil } func ByLevel(level int) ([]*User, error) { us := GetStore() - us.mu.RLock() - defer us.mu.RUnlock() - - ids, exists := us.byLevel[level] - if !exists { - return []*User{}, nil - } - - result := make([]*User, 0, len(ids)) - for _, id := range ids { - if user, exists := us.GetByID(id); exists { - result = append(result, user) - } - } - return result, nil + return us.GroupByIndex("byLevel", level), nil } func Online(within time.Duration) ([]*User, error) { us := GetStore() - us.mu.RLock() - defer us.mu.RUnlock() - cutoff := time.Now().Add(-within).Unix() - var result []*User - for _, id := range us.allByRegistered { - if user, exists := us.GetByID(id); exists && user.LastOnline >= cutoff { - result = append(result, user) - } - } + result := us.FilterByIndex("allByRegistered", func(u *User) bool { + return u.LastOnline >= cutoff + }) // Sort by last_online DESC, then ID ASC sort.Slice(result, func(i, j int) bool { @@ -390,24 +254,16 @@ func Online(within time.Duration) ([]*User, error) { return result, nil } +// Insert with ID assignment func (u *User) Insert() error { us := GetStore() - - // Validate before insertion - if err := u.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Assign new ID if not set if u.ID == 0 { u.ID = us.GetNextID() } - - // Add to store - us.AddUser(u) - return nil + return us.AddUser(u) } +// Helper methods func (u *User) RegisteredTime() time.Time { return time.Unix(u.Registered, 0) } diff --git a/main.go b/main.go index 91ac617..4c43458 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,27 @@ import ( "fmt" "log" "os" + "os/signal" + "path/filepath" + "syscall" - "dk/internal/server" + "dk/internal/auth" + "dk/internal/babble" + "dk/internal/control" + "dk/internal/drops" + "dk/internal/forum" + "dk/internal/items" + "dk/internal/middleware" + "dk/internal/monsters" + "dk/internal/news" + "dk/internal/router" + "dk/internal/routes" + "dk/internal/spells" + "dk/internal/template" + "dk/internal/towns" + "dk/internal/users" + + "github.com/valyala/fasthttp" ) func main() { @@ -25,16 +44,206 @@ func main() { default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1]) fmt.Fprintln(os.Stderr, "Available commands:") - fmt.Fprintln(os.Stderr, " install - Install the database") fmt.Fprintln(os.Stderr, " serve - Start the server") fmt.Fprintln(os.Stderr, " (no command) - Start the server") os.Exit(1) } } +func loadModels() error { + dataDir := "data" + + if err := os.MkdirAll(dataDir, 0755); err != nil { + return fmt.Errorf("failed to create data directory: %w", err) + } + + if err := users.LoadData(filepath.Join(dataDir, "users.json")); err != nil { + return fmt.Errorf("failed to load users data: %w", err) + } + + if err := towns.LoadData(filepath.Join(dataDir, "towns.json")); err != nil { + return fmt.Errorf("failed to load towns data: %w", err) + } + + if err := spells.LoadData(filepath.Join(dataDir, "spells.json")); err != nil { + return fmt.Errorf("failed to load spells data: %w", err) + } + + if err := news.LoadData(filepath.Join(dataDir, "news.json")); err != nil { + return fmt.Errorf("failed to load news data: %w", err) + } + + if err := monsters.LoadData(filepath.Join(dataDir, "monsters.json")); err != nil { + return fmt.Errorf("failed to load monsters data: %w", err) + } + + if err := items.LoadData(filepath.Join(dataDir, "items.json")); err != nil { + return fmt.Errorf("failed to load items data: %w", err) + } + + if err := forum.LoadData(filepath.Join(dataDir, "forum.json")); err != nil { + return fmt.Errorf("failed to load forum data: %w", err) + } + + if err := drops.LoadData(filepath.Join(dataDir, "drops.json")); err != nil { + return fmt.Errorf("failed to load drops data: %w", err) + } + + if err := babble.LoadData(filepath.Join(dataDir, "babble.json")); err != nil { + return fmt.Errorf("failed to load babble data: %w", err) + } + + if err := control.Load(filepath.Join(dataDir, "control.json")); err != nil { + return fmt.Errorf("failed to load control data: %w", err) + } + + return nil +} + +func saveModels() error { + dataDir := "data" + + if err := users.SaveData(filepath.Join(dataDir, "users.json")); err != nil { + return fmt.Errorf("failed to save users data: %w", err) + } + + if err := towns.SaveData(filepath.Join(dataDir, "towns.json")); err != nil { + return fmt.Errorf("failed to save towns data: %w", err) + } + + if err := spells.SaveData(filepath.Join(dataDir, "spells.json")); err != nil { + return fmt.Errorf("failed to save spells data: %w", err) + } + + if err := news.SaveData(filepath.Join(dataDir, "news.json")); err != nil { + return fmt.Errorf("failed to save news data: %w", err) + } + + if err := monsters.SaveData(filepath.Join(dataDir, "monsters.json")); err != nil { + return fmt.Errorf("failed to save monsters data: %w", err) + } + + if err := items.SaveData(filepath.Join(dataDir, "items.json")); err != nil { + return fmt.Errorf("failed to save items data: %w", err) + } + + if err := forum.SaveData(filepath.Join(dataDir, "forum.json")); err != nil { + return fmt.Errorf("failed to save forum data: %w", err) + } + + if err := drops.SaveData(filepath.Join(dataDir, "drops.json")); err != nil { + return fmt.Errorf("failed to save drops data: %w", err) + } + + if err := babble.SaveData(filepath.Join(dataDir, "babble.json")); err != nil { + return fmt.Errorf("failed to save babble data: %w", err) + } + + if err := control.Save(); err != nil { + return fmt.Errorf("failed to save control data: %w", err) + } + + return nil +} + func startServer(port string) { fmt.Println("Starting Dragon Knight server...") - if err := server.Start(port); err != nil { + if err := start(port); err != nil { log.Fatalf("Server failed: %v", err) } } + +func start(port string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + // Initialize template singleton + template.InitializeCache(cwd) + + // Load all model data into memory + if err := loadModels(); err != nil { + return fmt.Errorf("failed to load models: %w", err) + } + + auth.Init("sessions.json") // Initialize auth.Manager + + r := router.New() + r.Use(middleware.Timing()) + r.Use(middleware.Auth(auth.Manager)) + r.Use(middleware.CSRF(auth.Manager)) + + r.Get("/", routes.Index) + r.Get("/explore", routes.Explore) + r.Post("/move", routes.Move) + routes.RegisterAuthRoutes(r) + routes.RegisterTownRoutes(r) + + // Use current working directory for static files + assetsDir := filepath.Join(cwd, "assets") + + // Static file server for /assets + fs := &fasthttp.FS{ + Root: assetsDir, + Compress: false, + } + assetsHandler := fs.NewRequestHandler() + + // Combined handler + requestHandler := func(ctx *fasthttp.RequestCtx) { + path := string(ctx.Path()) + + // Handle static assets - strip /assets prefix + if len(path) >= 7 && path[:7] == "/assets" { + // Strip the /assets prefix for the file system handler + originalPath := ctx.Path() + ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix + assetsHandler(ctx) + ctx.Request.URI().SetPathBytes(originalPath) // Restore original path + return + } + + // Handle routes + r.ServeHTTP(ctx) + } + + addr := ":" + port + log.Printf("Server starting on %s", addr) + + // Setup graceful shutdown + server := &fasthttp.Server{ + Handler: requestHandler, + } + + // Channel to listen for interrupt signal + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + // Start server in a goroutine + go func() { + if err := server.ListenAndServe(addr); err != nil { + log.Printf("Server error: %v", err) + } + }() + + // Wait for interrupt signal + <-c + log.Println("Received shutdown signal, shutting down gracefully...") + + // Save all model data before shutdown + log.Println("Saving model data...") + if err := saveModels(); err != nil { + log.Printf("Error saving model data: %v", err) + } + + // Save sessions before shutdown + log.Println("Saving sessions...") + if err := auth.Manager.Close(); err != nil { + log.Printf("Error saving sessions: %v", err) + } + + // FastHTTP doesn't have a graceful Shutdown method like net/http + // We just let the server stop naturally when the main function exits + log.Println("Server stopped") + return nil +}