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,
`level` 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
(1, 'Life Pebble', 1, 1, 'maxhp,10'),
@ -117,7 +117,7 @@ CREATE TABLE classes (
'hp_rate' INTEGER NOT NULL DEFAULT 2,
'mp_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
(1, 'Adventurer', '', 3, 15, 10, 4, 4, 2, 2, 2, 2),
@ -347,4 +347,83 @@ INSERT INTO towns VALUES
(5, 'Narcissa', -130, -130, 60, 500, 50, '4,7,9,11,13,21,22,23,29,30,31'),
(6, 'Hambry', 170, 170, 90, 1000, 80, '10,11,12,13,14,23,24,30,31'),
(7, 'Gilead', 200, -200, 100, 3000, 110, '12,13,14,15,24,25,26,32'),
(8, 'Endworld', -250, -250, 125, 9000, 160, '16,27,33');
(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
require (
git.sharkk.net/Sharkk/Nigiri v1.0.0
git.sharkk.net/Sharkk/Sushi v1.1.1
github.com/valyala/fasthttp v1.65.0
zombiezen.com/go/sqlite v1.4.2
)
require (
@ -24,5 +24,4 @@ require (
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // 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/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
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/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
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/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/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/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=

View File

@ -1,6 +1,9 @@
package actions
import (
"dk/internal/database"
"dk/internal/helpers/exp"
"dk/internal/models/fightlogs"
"dk/internal/models/fights"
"dk/internal/models/monsters"
"dk/internal/models/spells"
@ -8,210 +11,273 @@ import (
"dk/internal/models/users"
"math"
"math/rand"
"strconv"
)
func HandleAttack(fight *fights.Fight, user *users.User) {
// Load monster data to get armor
type FightResult struct {
FightUpdates map[string]any
UserUpdates map[string]any
LogAction func() error
Ended bool
Victory bool
Won bool
}
func HandleAttack(fight *fights.Fight, user *users.User) *FightResult {
monster, err := monsters.Find(fight.MonsterID)
if err != nil {
fight.AddAction("Monster not found!")
return
return &FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "Monster not found!") },
}
}
// Player attack damage calculation with sqrt scaling
// Calculate damage
attackPower := float64(user.Attack)
minAttack := attackPower * 0.75
maxAttack := attackPower
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)
// Critical hit chance based on strength
// Critical hit
criticalRoll := rand.Intn(150) + 1
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)
minBlock := armor * 0.75
maxBlock := armor
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)
// Calculate final damage
damage := tohit - toblock
if damage < 1 {
damage = 1 // Minimum damage
damage = 1
}
// Apply uber damage bonus
if fight.UberDamage > 0 {
bonus := math.Ceil(damage * float64(fight.UberDamage) / 100)
damage += bonus
}
finalDamage := int(damage)
// Apply damage and add action
fight.DamageMonster(finalDamage)
fight.AddActionAttackHit(finalDamage)
// Check if monster is defeated
if fight.MonsterHP <= 0 {
fight.AddActionMonsterDeath(monster.Name)
rewardGold, rewardExp := calculateRewards(monster, user)
fight.WinFight(rewardGold, rewardExp)
HandleFightWin(fight, user)
newMonsterHP := fight.MonsterHP - finalDamage
if newMonsterHP < 0 {
newMonsterHP = 0
}
result := &FightResult{
FightUpdates: map[string]any{"monster_hp": newMonsterHP},
LogAction: func() error { return fightlogs.AddAttackHit(fight.ID, finalDamage) },
}
// Check if monster defeated
if newMonsterHP <= 0 {
rewardGold, rewardExp := calculateRewards(monster, 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,
}
// 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) {
func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResult {
spell, err := spells.Find(spellID)
if err != nil {
fight.AddAction("Spell not found!")
return
return &FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "Spell not found!") },
}
}
// Check if user has enough MP
if user.MP < spell.MP {
fight.AddAction("Not enough MP to cast " + spell.Name + "!")
return
return &FightResult{
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) {
fight.AddAction("You don't know that spell!")
return
return &FightResult{
LogAction: func() error { return fightlogs.AddAction(fight.ID, "You don't know that spell!") },
}
}
// Deduct MP
user.MP -= spell.MP
result := &FightResult{
UserUpdates: map[string]any{"mp": user.MP - spell.MP},
}
switch spell.Type {
case spells.TypeHealing:
// Heal user
healAmount := spell.Attribute
user.HP += healAmount
if user.HP > user.MaxHP {
user.HP = user.MaxHP
newHP := user.HP + spell.Attribute
if newHP > user.MaxHP {
newHP = 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:
// Damage monster
damage := spell.Attribute
fight.DamageMonster(damage)
fight.AddAction("You cast " + spell.Name + " and dealt " + strconv.Itoa(damage) + " damage!")
newMonsterHP := fight.MonsterHP - spell.Attribute
if newMonsterHP < 0 {
newMonsterHP = 0
}
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 fight.MonsterHP <= 0 {
fight.WinFight(10, 5) // Basic rewards
if newMonsterHP <= 0 {
result.FightUpdates["victory"] = true
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:
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!") }
}
return result
}
func HandleRun(fight *fights.Fight, user *users.User) {
// 20% chance to successfully run away
func HandleRun(fight *fights.Fight, user *users.User) *FightResult {
result := &FightResult{}
if rand.Float32() < 0.2 {
fight.RunAway()
user.FightID = 0
fight.AddAction("You successfully ran away!")
result.FightUpdates = map[string]any{"ran_away": true}
result.UserUpdates = map[string]any{
"fight_id": 0,
"currently": "Exploring",
}
result.LogAction = func() error { return fightlogs.AddRunSuccess(fight.ID) }
result.Ended = true
} else {
fight.AddAction("You failed to run away!")
result.LogAction = func() error { return fightlogs.AddRunFail(fight.ID) }
}
return result
}
func HandleMonsterAttack(fight *fights.Fight, user *users.User) {
// Load monster data
func HandleMonsterAttack(fight *fights.Fight, user *users.User) *FightResult {
monster, err := monsters.Find(fight.MonsterID)
if err != nil {
return
return &FightResult{}
}
// Monster attack damage calculation
// Calculate damage
attackPower := float64(monster.MaxDmg)
minAttack := attackPower * 0.75
maxAttack := attackPower
tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3
// User defense calculation
defense := float64(user.Defense)
minBlock := defense * 0.75
maxBlock := defense
toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3
// Calculate final damage
damage := tohit - toblock
if damage < 1 {
damage = 1 // Minimum damage
damage = 1
}
// Apply uber defense bonus (reduces damage taken)
if fight.UberDefense > 0 {
reduction := math.Ceil(damage * float64(fight.UberDefense) / 100)
damage -= reduction
if damage < 1 {
damage = 1 // Still minimum 1 damage
damage = 1
}
}
finalDamage := int(damage)
// Apply damage to user
user.HP -= finalDamage
if user.HP < 0 {
user.HP = 0
newHP := user.HP - finalDamage
if newHP < 0 {
newHP = 0
}
// Add monster attack action using memory-optimized format
fight.AddActionMonsterAttack(monster.Name, finalDamage)
// Check if user is defeated
if user.HP <= 0 {
fight.LoseFight()
HandleFightLoss(fight, user)
result := &FightResult{
UserUpdates: map[string]any{"hp": newHP},
LogAction: func() error { return fightlogs.AddMonsterAttack(fight.ID, monster.Name, finalDamage) },
}
if newHP <= 0 {
closestTown := findClosestTown(user.X, user.Y)
townX, townY := 0, 0
if closestTown != nil {
townX, townY = closestTown.X, closestTown.Y
}
result.FightUpdates = map[string]any{
"victory": true,
"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
}
return result
}
func HandleFightWin(fight *fights.Fight, user *users.User) {
// 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()
type LevelStats struct {
Strength int
Dexterity int
}
func HandleFightLoss(fight *fights.Fight, user *users.User) {
// Find closest town to user's position
closestTown := findClosestTown(user.X, user.Y)
if closestTown != nil {
user.X = closestTown.X
user.Y = closestTown.Y
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
}
// Apply death penalties
user.HP = user.MaxHP / 4 // 25% of max health
user.Gold = (user.Gold * 3) / 4 // 75% of gold
// Reset fight state
user.FightID = 0
user.Currently = "In Town"
fight.Save()
user.Save()
return level, LevelStats{Strength: str, Dexterity: dex}
}
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) {
// Base rewards (83-100% of max)
minExp := (monster.MaxExp * 5) / 6
maxExp := monster.MaxExp
exp := rand.Intn(maxExp-minExp+1) + minExp
@ -244,7 +309,6 @@ func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
maxGold := monster.MaxGold
gold := rand.Intn(maxGold-minGold+1) + minGold
// Apply bonus multipliers
expBonus := (user.ExpBonus * exp) / 100
exp += expBonus
@ -253,3 +317,32 @@ func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
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
import (
"dk/internal/models/control"
"dk/internal/control"
"dk/internal/models/fights"
"dk/internal/models/monsters"
"dk/internal/models/towns"

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package routes
import (
"dk/internal/actions"
"dk/internal/components"
"dk/internal/database"
"dk/internal/helpers"
"dk/internal/models/items"
"dk/internal/models/towns"
@ -93,9 +94,20 @@ func rest(ctx sushi.Ctx) {
return
}
user.Gold -= town.InnCost
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP
user.Save()
err := database.Transaction(func() error {
return database.Update("users", map[string]any{
"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{
"town": town,
@ -158,9 +170,28 @@ func buyItem(ctx sushi.Ctx) {
return
}
user.Gold -= item.Value
actions.UserEquipItem(user, item)
user.Save()
// Get equipment updates from actions
equipUpdates := actions.UserEquipItem(user, item)
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")
}
@ -228,11 +259,22 @@ func buyMap(ctx sushi.Ctx) {
return
}
user.Gold -= mapped.MapCost
townIDs := user.GetTownIDs()
townIDs = append(townIDs, id)
user.SetTownIDs(townIDs)
user.Save()
newTownsString := helpers.IntsToString(townIDs)
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")
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"syscall"
"dk/internal/database"
"dk/internal/models/users"
"dk/internal/routes"
"dk/internal/template"
@ -55,6 +56,12 @@ func start(port string) error {
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)
app := sushi.New()