finish migration back to sqlite, update routes/actions et cetera

This commit is contained in:
Sky Johnson 2025-08-22 07:47:01 -05:00
parent 3df8f29a4c
commit 75a1927d3a
23 changed files with 557 additions and 3738 deletions

View File

@ -1 +0,0 @@
[]

View File

@ -1,11 +0,0 @@
[
{
"id": 1,
"world_size": 200,
"open": 1,
"admin_email": "",
"class_1_name": "Mage",
"class_2_name": "Warrior",
"class_3_name": "Paladin"
}
]

BIN
data/dk.db Normal file

Binary file not shown.

View File

@ -1,226 +0,0 @@
[
{
"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"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
[]

View File

@ -1,266 +0,0 @@
[
{
"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"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
[]

View File

@ -1,135 +0,0 @@
[
{
"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
}
]

View File

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

View File

@ -12,7 +12,7 @@ CREATE TABLE drops (
`name` TEXT NOT NULL, `name` TEXT NOT NULL,
`level` INTEGER NOT NULL DEFAULT 0, `level` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0,
`att` TEXT NOT NULL DEFAULT '', `att` TEXT NOT NULL DEFAULT ''
); );
INSERT INTO drops VALUES INSERT INTO drops VALUES
(1, 'Life Pebble', 1, 1, 'maxhp,10'), (1, 'Life Pebble', 1, 1, 'maxhp,10'),
@ -117,7 +117,7 @@ CREATE TABLE classes (
'hp_rate' INTEGER NOT NULL DEFAULT 2, 'hp_rate' INTEGER NOT NULL DEFAULT 2,
'mp_rate' INTEGER NOT NULL DEFAULT 2, 'mp_rate' INTEGER NOT NULL DEFAULT 2,
'str_rate' INTEGER NOT NULL DEFAULT 2, 'str_rate' INTEGER NOT NULL DEFAULT 2,
'dex_rate' INTEGER NOT NULL DEFAULT 2, 'dex_rate' INTEGER NOT NULL DEFAULT 2
); );
INSERT INTO classes VALUES INSERT INTO classes VALUES
(1, 'Adventurer', '', 3, 15, 10, 4, 4, 2, 2, 2, 2), (1, 'Adventurer', '', 3, 15, 10, 4, 4, 2, 2, 2, 2),
@ -348,3 +348,82 @@ INSERT INTO towns VALUES
(6, 'Hambry', 170, 170, 90, 1000, 80, '10,11,12,13,14,23,24,30,31'), (6, 'Hambry', 170, 170, 90, 1000, 80, '10,11,12,13,14,23,24,30,31'),
(7, 'Gilead', 200, -200, 100, 3000, 110, '12,13,14,15,24,25,26,32'), (7, 'Gilead', 200, -200, 100, 3000, 110, '12,13,14,15,24,25,26,32'),
(8, 'Endworld', -250, -250, 125, 9000, 160, '16,27,33'); (8, 'Endworld', -250, -250, 125, 9000, 160, '16,27,33');
DROP TABLE IF EXISTS users;
CREATE TABLE users (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`username` TEXT NOT NULL,
`password` TEXT NOT NULL,
`email` TEXT NOT NULL,
`verified` INTEGER NOT NULL DEFAULT 0,
`token` TEXT NOT NULL DEFAULT '',
`registered` INTEGER NOT NULL DEFAULT (unixepoch()),
`last_online` INTEGER NOT NULL DEFAULT (unixepoch()),
`auth` INTEGER NOT NULL DEFAULT 0,
`x` INTEGER NOT NULL DEFAULT 0,
`y` INTEGER NOT NULL DEFAULT 0,
`class_id` INTEGER NOT NULL DEFAULT 1,
`currently` TEXT NOT NULL DEFAULT 'In Town',
`fight_id` INTEGER NOT NULL DEFAULT 0,
`hp` INTEGER NOT NULL DEFAULT 10,
`mp` INTEGER NOT NULL DEFAULT 10,
`tp` INTEGER NOT NULL DEFAULT 10,
`max_hp` INTEGER NOT NULL DEFAULT 10,
`max_mp` INTEGER NOT NULL DEFAULT 10,
`max_tp` INTEGER NOT NULL DEFAULT 10,
`level` INTEGER NOT NULL DEFAULT 1,
`gold` INTEGER NOT NULL DEFAULT 100,
`exp` INTEGER NOT NULL DEFAULT 0,
`gold_bonus` INTEGER NOT NULL DEFAULT 0,
`exp_bonus` INTEGER NOT NULL DEFAULT 0,
`strength` INTEGER NOT NULL DEFAULT 0,
`dexterity` INTEGER NOT NULL DEFAULT 0,
`attack` INTEGER NOT NULL DEFAULT 0,
`defense` INTEGER NOT NULL DEFAULT 0,
`weapon_id` INTEGER NOT NULL DEFAULT 0,
`armor_id` INTEGER NOT NULL DEFAULT 0,
`shield_id` INTEGER NOT NULL DEFAULT 0,
`slot_1_id` INTEGER NOT NULL DEFAULT 0,
`slot_2_id` INTEGER NOT NULL DEFAULT 0,
`slot_3_id` INTEGER NOT NULL DEFAULT 0,
`weapon_name` TEXT NOT NULL DEFAULT '',
`armor_name` TEXT NOT NULL DEFAULT '',
`shield_name` TEXT NOT NULL DEFAULT '',
`slot_1_name` TEXT NOT NULL DEFAULT '',
`slot_2_name` TEXT NOT NULL DEFAULT '',
`slot_3_name` TEXT NOT NULL DEFAULT '',
`spells` TEXT NOT NULL DEFAULT '',
`towns` TEXT NOT NULL DEFAULT ''
);
DROP TABLE IF EXISTS fights;
CREATE TABLE fights (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`user_id` INTEGER NOT NULL,
`monster_id` INTEGER NOT NULL,
`monster_hp` INTEGER NOT NULL DEFAULT 0,
`monster_max_hp` INTEGER NOT NULL DEFAULT 0,
`monster_sleep` INTEGER NOT NULL DEFAULT 0,
`monster_immune` INTEGER NOT NULL DEFAULT 0,
`uber_damage` INTEGER NOT NULL DEFAULT 0,
`uber_defense` INTEGER NOT NULL DEFAULT 0,
`first_strike` INTEGER NOT NULL DEFAULT 0,
`turn` INTEGER NOT NULL DEFAULT 0,
`ran_away` INTEGER NOT NULL DEFAULT 0,
`victory` INTEGER NOT NULL DEFAULT 0,
`won` INTEGER NOT NULL DEFAULT 0,
`reward_gold` INTEGER NOT NULL DEFAULT 0,
`reward_exp` INTEGER NOT NULL DEFAULT 0,
`created` INTEGER NOT NULL DEFAULT (unixepoch()),
`updated` INTEGER NOT NULL DEFAULT (unixepoch())
);
DROP TABLE IF EXISTS fight_logs;
CREATE TABLE fight_logs (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`fight_id` INTEGER NOT NULL,
`type` INTEGER NOT NULL DEFAULT 7,
`data` INTEGER NOT NULL DEFAULT 0,
`name` TEXT NOT NULL DEFAULT '',
`created` INTEGER NOT NULL DEFAULT (unixepoch())
);

3
go.mod
View File

@ -3,9 +3,9 @@ module dk
go 1.25.0 go 1.25.0
require ( require (
git.sharkk.net/Sharkk/Nigiri v1.0.0
git.sharkk.net/Sharkk/Sushi v1.1.1 git.sharkk.net/Sharkk/Sushi v1.1.1
github.com/valyala/fasthttp v1.65.0 github.com/valyala/fasthttp v1.65.0
zombiezen.com/go/sqlite v1.4.2
) )
require ( require (
@ -24,5 +24,4 @@ require (
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.37.1 // indirect modernc.org/sqlite v1.37.1 // indirect
zombiezen.com/go/sqlite v1.4.2 // indirect
) )

28
go.sum
View File

@ -1,11 +1,11 @@
git.sharkk.net/Sharkk/Nigiri v1.0.0 h1:N0MvWOoX54iXjR8D1LqGIFrtMAPdaoj/32n13Ou/p90=
git.sharkk.net/Sharkk/Nigiri v1.0.0/go.mod h1:HWpMtXaodPXE7dZXQ6tbZNL0DRV9PT65D0DOV0NAwsM=
git.sharkk.net/Sharkk/Sushi v1.1.1 h1:ynU16l6vAhY/JUwHlI4zMQiPuL9lcs88W/mAGZsL4Rw= git.sharkk.net/Sharkk/Sushi v1.1.1 h1:ynU16l6vAhY/JUwHlI4zMQiPuL9lcs88W/mAGZsL4Rw=
git.sharkk.net/Sharkk/Sushi v1.1.1/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o= git.sharkk.net/Sharkk/Sushi v1.1.1/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@ -26,16 +26,40 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo= zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc= zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=

View File

@ -1,6 +1,9 @@
package actions package actions
import ( import (
"dk/internal/database"
"dk/internal/helpers/exp"
"dk/internal/models/fightlogs"
"dk/internal/models/fights" "dk/internal/models/fights"
"dk/internal/models/monsters" "dk/internal/models/monsters"
"dk/internal/models/spells" "dk/internal/models/spells"
@ -8,210 +11,273 @@ import (
"dk/internal/models/users" "dk/internal/models/users"
"math" "math"
"math/rand" "math/rand"
"strconv"
) )
func HandleAttack(fight *fights.Fight, user *users.User) { type FightResult struct {
// Load monster data to get armor FightUpdates map[string]any
monster, err := monsters.Find(fight.MonsterID) UserUpdates map[string]any
if err != nil { LogAction func() error
fight.AddAction("Monster not found!") Ended bool
return Victory bool
Won bool
} }
// Player attack damage calculation with sqrt scaling func HandleAttack(fight *fights.Fight, user *users.User) *FightResult {
monster, err := monsters.Find(fight.MonsterID)
if err != nil {
return &FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "Monster not found!") },
}
}
// Calculate damage
attackPower := float64(user.Attack) attackPower := float64(user.Attack)
minAttack := attackPower * 0.75 minAttack := attackPower * 0.75
maxAttack := attackPower maxAttack := attackPower
rawAttack := math.Ceil(rand.Float64()*(maxAttack-minAttack) + minAttack) rawAttack := math.Ceil(rand.Float64()*(maxAttack-minAttack) + minAttack)
// Progressive scaling using square root for smooth progression
tohit := rawAttack / (1.2 + math.Sqrt(attackPower)*0.05) tohit := rawAttack / (1.2 + math.Sqrt(attackPower)*0.05)
// Critical hit chance based on strength // Critical hit
criticalRoll := rand.Intn(150) + 1 criticalRoll := rand.Intn(150) + 1
if float64(criticalRoll) <= math.Sqrt(float64(user.Strength)) { if float64(criticalRoll) <= math.Sqrt(float64(user.Strength)) {
tohit *= 2 // Critical hit tohit *= 2
} }
// Monster defense calculation with more aggressive scaling // Monster defense
armor := float64(monster.Armor) armor := float64(monster.Armor)
minBlock := armor * 0.75 minBlock := armor * 0.75
maxBlock := armor maxBlock := armor
rawBlock := math.Ceil(rand.Float64()*(maxBlock-minBlock) + minBlock) rawBlock := math.Ceil(rand.Float64()*(maxBlock-minBlock) + minBlock)
// Armor uses higher divisor to balance against player attack
toblock := rawBlock / (1.8 + math.Sqrt(armor)*0.08) toblock := rawBlock / (1.8 + math.Sqrt(armor)*0.08)
// Calculate final damage
damage := tohit - toblock damage := tohit - toblock
if damage < 1 { if damage < 1 {
damage = 1 // Minimum damage damage = 1
} }
// Apply uber damage bonus
if fight.UberDamage > 0 { if fight.UberDamage > 0 {
bonus := math.Ceil(damage * float64(fight.UberDamage) / 100) bonus := math.Ceil(damage * float64(fight.UberDamage) / 100)
damage += bonus damage += bonus
} }
finalDamage := int(damage) finalDamage := int(damage)
newMonsterHP := fight.MonsterHP - finalDamage
if newMonsterHP < 0 {
newMonsterHP = 0
}
// Apply damage and add action result := &FightResult{
fight.DamageMonster(finalDamage) FightUpdates: map[string]any{"monster_hp": newMonsterHP},
fight.AddActionAttackHit(finalDamage) LogAction: func() error { return fightlogs.AddAttackHit(fight.ID, finalDamage) },
}
// Check if monster is defeated // Check if monster defeated
if fight.MonsterHP <= 0 { if newMonsterHP <= 0 {
fight.AddActionMonsterDeath(monster.Name)
rewardGold, rewardExp := calculateRewards(monster, user) rewardGold, rewardExp := calculateRewards(monster, user)
fight.WinFight(rewardGold, rewardExp)
HandleFightWin(fight, user) result.FightUpdates["victory"] = true
} result.FightUpdates["won"] = true
result.FightUpdates["reward_gold"] = rewardGold
result.FightUpdates["reward_exp"] = rewardExp
result.UserUpdates = map[string]any{
"fight_id": 0,
"currently": "Exploring",
"gold": user.Gold + rewardGold,
"exp": user.Exp + rewardExp,
} }
func HandleSpell(fight *fights.Fight, user *users.User, spellID int) { // Handle level up
newLevel, newStats := calculateLevelUp(user.Level, user.Exp+rewardExp, user.Strength, user.Dexterity)
if newLevel > user.Level {
result.UserUpdates["level"] = newLevel
result.UserUpdates["strength"] = newStats.Strength
result.UserUpdates["dexterity"] = newStats.Dexterity
}
result.LogAction = func() error {
if err := fightlogs.AddAttackHit(fight.ID, finalDamage); err != nil {
return err
}
return fightlogs.AddMonsterDeath(fight.ID, monster.Name)
}
result.Ended = true
result.Victory = true
result.Won = true
}
return result
}
func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResult {
spell, err := spells.Find(spellID) spell, err := spells.Find(spellID)
if err != nil { if err != nil {
fight.AddAction("Spell not found!") return &FightResult{
return LogAction: func() error { return fightlogs.AddAction(fight.ID, "Spell not found!") },
}
} }
// Check if user has enough MP
if user.MP < spell.MP { if user.MP < spell.MP {
fight.AddAction("Not enough MP to cast " + spell.Name + "!") return &FightResult{
return LogAction: func() error { return fightlogs.AddAction(fight.ID, "Not enough MP to cast "+spell.Name+"!") },
}
} }
// Check if user knows this spell
if !user.HasSpell(spellID) { if !user.HasSpell(spellID) {
fight.AddAction("You don't know that spell!") return &FightResult{
return LogAction: func() error { return fightlogs.AddAction(fight.ID, "You don't know that spell!") },
}
} }
// Deduct MP result := &FightResult{
user.MP -= spell.MP UserUpdates: map[string]any{"mp": user.MP - spell.MP},
}
switch spell.Type { switch spell.Type {
case spells.TypeHealing: case spells.TypeHealing:
// Heal user newHP := user.HP + spell.Attribute
healAmount := spell.Attribute if newHP > user.MaxHP {
user.HP += healAmount newHP = user.MaxHP
if user.HP > user.MaxHP {
user.HP = user.MaxHP
} }
fight.AddAction("You cast " + spell.Name + " and healed " + strconv.Itoa(healAmount) + " HP!") result.UserUpdates["hp"] = newHP
result.LogAction = func() error { return fightlogs.AddSpellHeal(fight.ID, spell.Name, spell.Attribute) }
case spells.TypeHurt: case spells.TypeHurt:
// Damage monster newMonsterHP := fight.MonsterHP - spell.Attribute
damage := spell.Attribute if newMonsterHP < 0 {
fight.DamageMonster(damage) newMonsterHP = 0
fight.AddAction("You cast " + spell.Name + " and dealt " + strconv.Itoa(damage) + " damage!") }
result.FightUpdates = map[string]any{"monster_hp": newMonsterHP}
result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Attribute) }
// Check if monster is defeated if newMonsterHP <= 0 {
if fight.MonsterHP <= 0 { result.FightUpdates["victory"] = true
fight.WinFight(10, 5) // Basic rewards result.FightUpdates["won"] = true
result.FightUpdates["reward_gold"] = 10
result.FightUpdates["reward_exp"] = 5
result.UserUpdates["fight_id"] = 0
result.UserUpdates["currently"] = "Exploring"
result.Ended = true
result.Victory = true
result.Won = true
} }
default: default:
fight.AddAction("You cast " + spell.Name + " but nothing happened!") result.LogAction = func() error { return fightlogs.AddAction(fight.ID, "You cast "+spell.Name+" but nothing happened!") }
}
} }
func HandleRun(fight *fights.Fight, user *users.User) { return result
// 20% chance to successfully run away }
func HandleRun(fight *fights.Fight, user *users.User) *FightResult {
result := &FightResult{}
if rand.Float32() < 0.2 { if rand.Float32() < 0.2 {
fight.RunAway() result.FightUpdates = map[string]any{"ran_away": true}
user.FightID = 0 result.UserUpdates = map[string]any{
fight.AddAction("You successfully ran away!") "fight_id": 0,
} else { "currently": "Exploring",
fight.AddAction("You failed to run away!")
} }
result.LogAction = func() error { return fightlogs.AddRunSuccess(fight.ID) }
result.Ended = true
} else {
result.LogAction = func() error { return fightlogs.AddRunFail(fight.ID) }
} }
func HandleMonsterAttack(fight *fights.Fight, user *users.User) { return result
// Load monster data }
func HandleMonsterAttack(fight *fights.Fight, user *users.User) *FightResult {
monster, err := monsters.Find(fight.MonsterID) monster, err := monsters.Find(fight.MonsterID)
if err != nil { if err != nil {
return return &FightResult{}
} }
// Monster attack damage calculation // Calculate damage
attackPower := float64(monster.MaxDmg) attackPower := float64(monster.MaxDmg)
minAttack := attackPower * 0.75 minAttack := attackPower * 0.75
maxAttack := attackPower maxAttack := attackPower
tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3 tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3
// User defense calculation
defense := float64(user.Defense) defense := float64(user.Defense)
minBlock := defense * 0.75 minBlock := defense * 0.75
maxBlock := defense maxBlock := defense
toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3 toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3
// Calculate final damage
damage := tohit - toblock damage := tohit - toblock
if damage < 1 { if damage < 1 {
damage = 1 // Minimum damage damage = 1
} }
// Apply uber defense bonus (reduces damage taken)
if fight.UberDefense > 0 { if fight.UberDefense > 0 {
reduction := math.Ceil(damage * float64(fight.UberDefense) / 100) reduction := math.Ceil(damage * float64(fight.UberDefense) / 100)
damage -= reduction damage -= reduction
if damage < 1 { if damage < 1 {
damage = 1 // Still minimum 1 damage damage = 1
} }
} }
finalDamage := int(damage) finalDamage := int(damage)
newHP := user.HP - finalDamage
// Apply damage to user if newHP < 0 {
user.HP -= finalDamage newHP = 0
if user.HP < 0 {
user.HP = 0
} }
// Add monster attack action using memory-optimized format result := &FightResult{
fight.AddActionMonsterAttack(monster.Name, finalDamage) UserUpdates: map[string]any{"hp": newHP},
LogAction: func() error { return fightlogs.AddMonsterAttack(fight.ID, monster.Name, finalDamage) },
// Check if user is defeated
if user.HP <= 0 {
fight.LoseFight()
HandleFightLoss(fight, user)
}
} }
func HandleFightWin(fight *fights.Fight, user *users.User) { if newHP <= 0 {
// Add rewards to user
user.GrantExp(fight.RewardExp)
user.Gold += fight.RewardGold
// Reset fight state
user.FightID = 0
user.Currently = "Exploring"
fight.Save()
user.Save()
}
func HandleFightLoss(fight *fights.Fight, user *users.User) {
// Find closest town to user's position
closestTown := findClosestTown(user.X, user.Y) closestTown := findClosestTown(user.X, user.Y)
townX, townY := 0, 0
if closestTown != nil { if closestTown != nil {
user.X = closestTown.X townX, townY = closestTown.X, closestTown.Y
user.Y = closestTown.Y
} }
// Apply death penalties result.FightUpdates = map[string]any{
user.HP = user.MaxHP / 4 // 25% of max health "victory": true,
user.Gold = (user.Gold * 3) / 4 // 75% of gold "won": false,
}
result.UserUpdates = map[string]any{
"fight_id": 0,
"currently": "In Town",
"hp": user.MaxHP / 4,
"gold": (user.Gold * 3) / 4,
"x": townX,
"y": townY,
}
result.Ended = true
result.Victory = true
result.Won = false
}
// Reset fight state return result
user.FightID = 0 }
user.Currently = "In Town"
fight.Save() type LevelStats struct {
user.Save() Strength int
Dexterity int
}
func calculateLevelUp(currentLevel, newExp, currentStr, currentDex int) (int, LevelStats) {
level := currentLevel
str := currentStr
dex := currentDex
nexp := newExp
for {
expNeeded := exp.Calc(level + 1)
if nexp < expNeeded {
break
}
level++
str++
dex++
nexp -= expNeeded
}
return level, LevelStats{Strength: str, Dexterity: dex}
} }
func findClosestTown(x, y int) *towns.Town { func findClosestTown(x, y int) *towns.Town {
@ -235,7 +301,6 @@ func findClosestTown(x, y int) *towns.Town {
} }
func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) { func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
// Base rewards (83-100% of max)
minExp := (monster.MaxExp * 5) / 6 minExp := (monster.MaxExp * 5) / 6
maxExp := monster.MaxExp maxExp := monster.MaxExp
exp := rand.Intn(maxExp-minExp+1) + minExp exp := rand.Intn(maxExp-minExp+1) + minExp
@ -244,7 +309,6 @@ func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
maxGold := monster.MaxGold maxGold := monster.MaxGold
gold := rand.Intn(maxGold-minGold+1) + minGold gold := rand.Intn(maxGold-minGold+1) + minGold
// Apply bonus multipliers
expBonus := (user.ExpBonus * exp) / 100 expBonus := (user.ExpBonus * exp) / 100
exp += expBonus exp += expBonus
@ -253,3 +317,32 @@ func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
return gold, exp return gold, exp
} }
func ExecuteFightAction(fightID int, result *FightResult) error {
return database.Transaction(func() error {
// Update fight
if len(result.FightUpdates) > 0 {
if err := database.Update("fights", result.FightUpdates, "id", fightID); err != nil {
return err
}
}
// Update user
if len(result.UserUpdates) > 0 {
fight, err := fights.Find(fightID)
if err != nil {
return err
}
if err := database.Update("users", result.UserUpdates, "id", fight.UserID); err != nil {
return err
}
}
// Add log entry
if result.LogAction != nil {
return result.LogAction()
}
return nil
})
}

View File

@ -1,7 +1,7 @@
package actions package actions
import ( import (
"dk/internal/models/control" "dk/internal/control"
"dk/internal/models/fights" "dk/internal/models/fights"
"dk/internal/models/monsters" "dk/internal/models/monsters"
"dk/internal/models/towns" "dk/internal/models/towns"

View File

@ -5,52 +5,53 @@ import (
"dk/internal/models/users" "dk/internal/models/users"
) )
// UserEquipItem equips a given item onto a user. This overwrites any // UserEquipItem calculates equipment updates for a user equipping an item.
// previously equipped item in the slot. Does not save. // Returns map of database field updates without modifying the user struct.
func UserEquipItem(user *users.User, item *items.Item) { func UserEquipItem(user *users.User, item *items.Item) map[string]any {
slotInUse := false updates := make(map[string]any)
if item.Type == items.TypeWeapon && user.WeaponID != 0 {
slotInUse = true
}
if item.Type == items.TypeArmor && user.ArmorID != 0 {
slotInUse = true
}
if item.Type == items.TypeShield && user.ShieldID != 0 {
slotInUse = true
}
var oldItem *items.Item // Calculate stat changes
if slotInUse && item.Type == items.TypeWeapon { newAttack := user.Attack
oldItem, _ = items.Find(user.WeaponID) newDefense := user.Defense
} else if slotInUse && item.Type == items.TypeArmor {
oldItem, _ = items.Find(user.ArmorID)
} else if slotInUse && item.Type == items.TypeShield {
oldItem, _ = items.Find(user.ShieldID)
}
if oldItem != nil {
switch oldItem.Type {
case items.TypeWeapon:
user.Attack -= oldItem.Att
case items.TypeArmor:
user.Defense -= oldItem.Att
case items.TypeShield:
user.Defense -= oldItem.Att
}
}
// Remove old item stats if slot occupied
switch item.Type { switch item.Type {
case items.TypeWeapon: case items.TypeWeapon:
user.Attack += item.Att if user.WeaponID != 0 {
user.WeaponID = item.ID if oldItem, err := items.Find(user.WeaponID); err == nil {
user.WeaponName = item.Name newAttack -= oldItem.Att
}
}
// Add new item
newAttack += item.Att
updates["weapon_id"] = item.ID
updates["weapon_name"] = item.Name
case items.TypeArmor: case items.TypeArmor:
user.Defense += item.Att if user.ArmorID != 0 {
user.ArmorID = item.ID if oldItem, err := items.Find(user.ArmorID); err == nil {
user.ArmorName = item.Name newDefense -= oldItem.Att
}
}
// Add new item
newDefense += item.Att
updates["armor_id"] = item.ID
updates["armor_name"] = item.Name
case items.TypeShield: case items.TypeShield:
user.Defense += item.Att if user.ShieldID != 0 {
user.ShieldID = item.ID if oldItem, err := items.Find(user.ShieldID); err == nil {
user.ShieldName = item.Name newDefense -= oldItem.Att
} }
} }
// Add new item
newDefense += item.Att
updates["shield_id"] = item.ID
updates["shield_name"] = item.Name
}
updates["attack"] = newAttack
updates["defense"] = newDefense
return updates
}

View File

@ -320,6 +320,28 @@ func Insert(tableName string, obj any, excludeFields ...string) (int64, error) {
return DB().LastInsertRowID(), nil return DB().LastInsertRowID(), nil
} }
// Transaction executes multiple operations atomically
func Transaction(fn func() error) error {
conn := DB()
// Begin transaction
if err := sqlitex.Execute(conn, "BEGIN", nil); err != nil {
return err
}
// Execute operations
err := fn()
if err != nil {
// Rollback on error
sqlitex.Execute(conn, "ROLLBACK", nil)
return err
}
// Commit on success
return sqlitex.Execute(conn, "COMMIT", nil)
}
func convertPlaceholders(query string) (string, []string) { func convertPlaceholders(query string) (string, []string) {
var paramTypes []string var paramTypes []string

View File

@ -61,6 +61,9 @@ func processLogin(ctx sushi.Ctx) {
return return
} }
// Update last online time when logging in
user.UpdateLastOnline()
ctx.Login(user.ID, user) ctx.Login(user.ID, user)
// Set success message // Set success message
@ -92,10 +95,10 @@ func showRegister(ctx sushi.Ctx) {
// processRegister handles registration form submission // processRegister handles registration form submission
func processRegister(ctx sushi.Ctx) { func processRegister(ctx sushi.Ctx) {
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username"))) username := strings.TrimSpace(ctx.Form("username").String())
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email"))) email := strings.TrimSpace(ctx.Form("email").String())
userPassword := string(ctx.PostArgs().Peek("password")) userPassword := ctx.Form("password").String()
confirmPassword := string(ctx.PostArgs().Peek("confirm_password")) confirmPassword := ctx.Form("confirm_password").String()
formData := map[string]string{ formData := map[string]string{
"username": username, "username": username,
@ -108,18 +111,21 @@ func processRegister(ctx sushi.Ctx) {
return return
} }
// Check if username already exists
if _, err := users.ByUsername(username); err == nil { if _, err := users.ByUsername(username); err == nil {
setFlashAndFormData(ctx, "Username already exists", formData) setFlashAndFormData(ctx, "Username already exists", formData)
ctx.Redirect("/register") ctx.Redirect("/register")
return return
} }
// Check if email already exists
if _, err := users.ByEmail(email); err == nil { if _, err := users.ByEmail(email); err == nil {
setFlashAndFormData(ctx, "Email already registered", formData) setFlashAndFormData(ctx, "Email already registered", formData)
ctx.Redirect("/register") ctx.Redirect("/register")
return return
} }
// Create new user
user := users.New() user := users.New()
user.Username = username user.Username = username
user.Email = email user.Email = email
@ -127,6 +133,13 @@ func processRegister(ctx sushi.Ctx) {
user.ClassID = 1 user.ClassID = 1
user.Auth = 1 user.Auth = 1
// Validate before inserting
if err := user.Validate(); err != nil {
setFlashAndFormData(ctx, fmt.Sprintf("Invalid user data: %s", err.Error()), formData)
ctx.Redirect("/register")
return
}
if err := user.Insert(); err != nil { if err := user.Insert(); err != nil {
setFlashAndFormData(ctx, "Failed to create account", formData) setFlashAndFormData(ctx, "Failed to create account", formData)
ctx.Redirect("/register") ctx.Redirect("/register")
@ -186,22 +199,22 @@ func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
var user *users.User var user *users.User
var err error var err error
// Try username first
user, err = users.ByUsername(usernameOrEmail) user, err = users.ByUsername(usernameOrEmail)
if err != nil { if err != nil {
fmt.Println(err.Error()) // If username not found, try email
user, err = users.ByEmail(usernameOrEmail) user, err = users.ByEmail(usernameOrEmail)
if err != nil { if err != nil {
fmt.Println(err.Error()) return nil, fmt.Errorf("user not found")
return nil, err
} }
} }
isValid, err := password.VerifyPassword(plainPassword, user.Password) isValid, err := password.VerifyPassword(plainPassword, user.Password)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("password verification error: %w", err)
} }
if !isValid { if !isValid {
return nil, fmt.Errorf("invalid username/email or password") return nil, fmt.Errorf("invalid password")
} }
return user, nil return user, nil

View File

@ -3,7 +3,9 @@ package routes
import ( import (
"dk/internal/actions" "dk/internal/actions"
"dk/internal/components" "dk/internal/components"
"dk/internal/database"
"dk/internal/helpers" "dk/internal/helpers"
"dk/internal/models/fightlogs"
"dk/internal/models/fights" "dk/internal/models/fights"
"dk/internal/models/monsters" "dk/internal/models/monsters"
"dk/internal/models/spells" "dk/internal/models/spells"
@ -25,7 +27,6 @@ func RegisterFightRoutes(app *sushi.App) {
group.Post("/", handleFightAction) group.Post("/", handleFightAction)
} }
// requireFighting middleware ensures the user is in a fight
func requireFighting() sushi.Middleware { func requireFighting() sushi.Middleware {
return func(ctx sushi.Ctx, next func()) { return func(ctx sushi.Ctx, next func()) {
user := ctx.GetCurrentUser() user := ctx.GetCurrentUser()
@ -60,16 +61,23 @@ func showFight(ctx sushi.Ctx) {
return return
} }
// If turn 0, determine first strike and advance to turn 1 // Initialize fight on first view
if fight.Turn == 0 { if fight.Turn == 0 {
// 50% chance user goes first err := database.Transaction(func() error {
return database.Update("fights", map[string]any{
"first_strike": rand.Float32() < 0.5,
"turn": 1,
}, "id", fight.ID)
})
if err != nil {
ctx.SendError(500, "Failed to initialize fight")
return
}
fight.FirstStrike = rand.Float32() < 0.5 fight.FirstStrike = rand.Float32() < 0.5
fight.Turn = 1 fight.Turn = 1
fight.Save()
} }
monHpPct := helpers.ClampPct(float64(fight.MonsterHP), float64(fight.MonsterMaxHP), 0, 100) monHpPct := helpers.ClampPct(float64(fight.MonsterHP), float64(fight.MonsterMaxHP), 0, 100)
monHpColor := "" monHpColor := ""
if monHpPct < 35 { if monHpPct < 35 {
monHpColor = "danger" monHpColor = "danger"
@ -86,6 +94,9 @@ func showFight(ctx sushi.Ctx) {
} }
} }
// Get recent fight actions
lastAction, _ := fightlogs.GetLastAction(fight.ID)
components.RenderPage(ctx, "Fighting", "fight/fight.html", map[string]any{ components.RenderPage(ctx, "Fighting", "fight/fight.html", map[string]any{
"fight": fight, "fight": fight,
"user": user, "user": user,
@ -95,6 +106,7 @@ func showFight(ctx sushi.Ctx) {
"spells": spellMap.ToSlice(), "spells": spellMap.ToSlice(),
"action": sess.GetFlashMessage("action"), "action": sess.GetFlashMessage("action"),
"mon_action": sess.GetFlashMessage("mon_action"), "mon_action": sess.GetFlashMessage("mon_action"),
"last_action": lastAction,
}) })
} }
@ -110,84 +122,85 @@ func handleFightAction(ctx sushi.Ctx) {
} }
action := string(ctx.FormValue("action")) action := string(ctx.FormValue("action"))
var userAction string var result *actions.FightResult
switch action { switch action {
case "attack": case "attack":
actions.HandleAttack(fight, user) result = actions.HandleAttack(fight, user)
userAction = fight.GetLastAction()
case "spell": case "spell":
spellIDStr := string(ctx.FormValue("spell_id")) spellIDStr := string(ctx.FormValue("spell_id"))
if spellID, err := strconv.Atoi(spellIDStr); err == nil { if spellID, err := strconv.Atoi(spellIDStr); err == nil {
actions.HandleSpell(fight, user, spellID) result = actions.HandleSpell(fight, user, spellID)
userAction = fight.GetLastAction() } else {
result = &actions.FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "Invalid spell!") },
}
} }
case "run": case "run":
actions.HandleRun(fight, user) result = actions.HandleRun(fight, user)
userAction = fight.GetLastAction() default:
result = &actions.FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "Invalid action!") },
}
}
// If successfully ran away, redirect to explore // Execute the action
if fight.RanAway { err = actions.ExecuteFightAction(fight.ID, result)
user.Currently = "Exploring" if err != nil {
user.Save() ctx.SendError(500, "Failed to execute fight action")
return
}
// Handle fight end states
if result.Ended {
if result.Won {
sess.SetFlash("success", fmt.Sprintf("Victory! You gained rewards!"))
ctx.Redirect("/explore", 302)
} else if result.Victory {
sess.SetFlash("error", "You have been defeated! You lost some gold and were sent to the nearest town.")
ctx.Redirect("/town", 302)
} else {
// Ran away
sess.SetFlash("success", "You successfully escaped!") sess.SetFlash("success", "You successfully escaped!")
ctx.Redirect("/explore", 302) ctx.Redirect("/explore", 302)
return
}
default:
fight.AddAction("Invalid action!")
userAction = "Invalid action!"
}
// Flash user action
sess.SetFlash("action", userAction)
// Check if fight ended due to user action
if fight.Victory {
if fight.Won {
// Player won
sess.SetFlash("success", fmt.Sprintf("Victory! You gained %d gold and %d experience!", fight.RewardGold, fight.RewardExp))
sess.DeleteFlash("action")
sess.DeleteFlash("mon_action")
ctx.Redirect("/explore", 302)
} else {
// Player lost
sess.SetFlash("error", "You have been defeated! You lost some gold and were sent to the nearest town.")
sess.DeleteFlash("action")
sess.DeleteFlash("mon_action")
ctx.Redirect("/town", 302)
} }
return return
} }
// Monster attacks back if fight is still active // Monster attacks back if fight continues
if fight.IsActive() && user.HP > 0 { if fight.IsActive() && user.HP > 0 {
actions.HandleMonsterAttack(fight, user) monsterResult := actions.HandleMonsterAttack(fight, user)
// Check if fight ended due to monster attack // Execute monster action
if fight.Victory { err = actions.ExecuteFightAction(fight.ID, monsterResult)
if fight.Won { if err != nil {
sess.SetFlash("success", fmt.Sprintf("Victory! You gained %d gold and %d experience!", fight.RewardGold, fight.RewardExp)) ctx.SendError(500, "Failed to execute monster action")
sess.DeleteFlash("action") return
sess.DeleteFlash("mon_action") }
// Check if monster action ended fight
if monsterResult.Ended {
if monsterResult.Won {
sess.SetFlash("success", "Victory!")
ctx.Redirect("/explore", 302) ctx.Redirect("/explore", 302)
} else { } else {
sess.SetFlash("error", "You have been defeated! You lost some gold and were sent to the nearest town.") sess.SetFlash("error", "You have been defeated!")
sess.DeleteFlash("action")
sess.DeleteFlash("mon_action")
ctx.Redirect("/town", 302) ctx.Redirect("/town", 302)
} }
return return
} }
monsterAction := fight.GetLastAction()
sess.SetFlash("mon_action", monsterAction)
} }
fight.IncrementTurn() // Increment turn
fight.Save() err = database.Transaction(func() error {
user.Save() return database.Update("fights", map[string]any{
"turn": fight.Turn + 1,
}, "id", fight.ID)
})
if err != nil {
ctx.SendError(500, "Failed to increment turn")
return
}
// Redirect back to fight page
ctx.Redirect("/fight", 302) ctx.Redirect("/fight", 302)
} }

View File

@ -3,6 +3,7 @@ package routes
import ( import (
"dk/internal/actions" "dk/internal/actions"
"dk/internal/components" "dk/internal/components"
"dk/internal/database"
"dk/internal/models/towns" "dk/internal/models/towns"
"dk/internal/models/users" "dk/internal/models/users"
"slices" "slices"
@ -53,9 +54,18 @@ func Move(ctx sushi.Ctx) {
return return
} }
user.Currently = currently err = database.Transaction(func() error {
user.SetPosition(newX, newY) return database.Update("users", map[string]any{
user.Save() "currently": currently,
"x": newX,
"y": newY,
}, "id", user.ID)
})
if err != nil {
ctx.SendError(500, "failed to update user position")
return
}
switch currently { switch currently {
case "In Town": case "In Town":
@ -101,10 +111,20 @@ func Teleport(ctx sushi.Ctx) {
return return
} }
user.TP -= town.TPCost err = database.Transaction(func() error {
user.SetPosition(town.X, town.Y) return database.Update("users", map[string]any{
user.Currently = "In Town" "tp": user.TP - town.TPCost,
user.Save() "x": town.X,
"y": town.Y,
"currently": "In Town",
}, "id", user.ID)
})
if err != nil {
sess.SetFlash("error", "Failed to complete teleport.")
ctx.Redirect("/")
return
}
sess.SetFlash("success", "You teleported to "+town.Name+" successfully!") sess.SetFlash("success", "You teleported to "+town.Name+" successfully!")
ctx.Redirect("/town") ctx.Redirect("/town")

View File

@ -3,6 +3,7 @@ package routes
import ( import (
"dk/internal/actions" "dk/internal/actions"
"dk/internal/components" "dk/internal/components"
"dk/internal/database"
"dk/internal/helpers" "dk/internal/helpers"
"dk/internal/models/items" "dk/internal/models/items"
"dk/internal/models/towns" "dk/internal/models/towns"
@ -93,9 +94,20 @@ func rest(ctx sushi.Ctx) {
return return
} }
user.Gold -= town.InnCost err := database.Transaction(func() error {
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP return database.Update("users", map[string]any{
user.Save() "gold": user.Gold - town.InnCost,
"hp": user.MaxHP,
"mp": user.MaxMP,
"tp": user.MaxTP,
}, "id", user.ID)
})
if err != nil {
sess.SetFlash("error", "Failed to rest at inn.")
ctx.Redirect("/town/inn")
return
}
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{ components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
"town": town, "town": town,
@ -158,9 +170,28 @@ func buyItem(ctx sushi.Ctx) {
return return
} }
user.Gold -= item.Value // Get equipment updates from actions
actions.UserEquipItem(user, item) equipUpdates := actions.UserEquipItem(user, item)
user.Save()
err = database.Transaction(func() error {
// Start with gold deduction
updates := map[string]any{
"gold": user.Gold - item.Value,
}
// Add equipment updates
for field, value := range equipUpdates {
updates[field] = value
}
return database.Update("users", updates, "id", user.ID)
})
if err != nil {
sess.SetFlash("error", "Failed to purchase item.")
ctx.Redirect("/town/shop")
return
}
ctx.Redirect("/town/shop") ctx.Redirect("/town/shop")
} }
@ -228,11 +259,22 @@ func buyMap(ctx sushi.Ctx) {
return return
} }
user.Gold -= mapped.MapCost
townIDs := user.GetTownIDs() townIDs := user.GetTownIDs()
townIDs = append(townIDs, id) townIDs = append(townIDs, id)
user.SetTownIDs(townIDs) newTownsString := helpers.IntsToString(townIDs)
user.Save()
err = database.Transaction(func() error {
return database.Update("users", map[string]any{
"gold": user.Gold - mapped.MapCost,
"towns": newTownsString,
}, "id", user.ID)
})
if err != nil {
sess.SetFlash("error", "Failed to purchase map.")
ctx.Redirect("/town/maps")
return
}
ctx.Redirect("/town/maps") ctx.Redirect("/town/maps")
} }

View File

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"dk/internal/database"
"dk/internal/models/users" "dk/internal/models/users"
"dk/internal/routes" "dk/internal/routes"
"dk/internal/template" "dk/internal/template"
@ -55,6 +56,12 @@ func start(port string) error {
return fmt.Errorf("failed to get current working directory: %w", err) return fmt.Errorf("failed to get current working directory: %w", err)
} }
err = database.Init(filepath.Join(cwd, "data/dk.db"))
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer database.DB().Close()
template.InitializeCache(cwd) template.InitializeCache(cwd)
app := sushi.New() app := sushi.New()