diff --git a/assets/dk.css b/assets/dk.css index 157e05d..d8f2143 100644 --- a/assets/dk.css +++ b/assets/dk.css @@ -212,6 +212,11 @@ button.btn { background-color: #00c6ff; background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #00c6ff, #0072ff); border-color: #0072ff; + + &:hover { + background-color: #49d6fd; + background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #49d6fd, #2987fa); + } } } @@ -411,7 +416,7 @@ img#move-compass-disabled { img#monster-image { display: block; - max-height: 360px; + max-width: 80%; margin: 1rem auto; } @@ -453,7 +458,7 @@ select.styled-select { color: white; box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2); text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); - height: 28px; + height: 29px; &:hover { background-color: #909090; @@ -468,4 +473,4 @@ div#battle-window { h2, h3 { color: white; } -} \ No newline at end of file +} diff --git a/assets/images/monsters/Critter.png b/assets/images/monsters/Critter.png new file mode 100644 index 0000000..821ddd4 Binary files /dev/null and b/assets/images/monsters/Critter.png differ diff --git a/assets/images/monsters/Drake.png b/assets/images/monsters/Drake.png new file mode 100644 index 0000000..dd20737 Binary files /dev/null and b/assets/images/monsters/Drake.png differ diff --git a/data/fights.json b/data/fights.json deleted file mode 100644 index 9532abc..0000000 --- a/data/fights.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "id": 1, - "user_id": 1, - "monster_id": 10, - "monster_hp": 16, - "monster_max_hp": 16, - "monster_sleep": 0, - "monster_immune": 0, - "uber_damage": 0, - "uber_defense": 0, - "first_strike": false, - "turn": 1, - "ran_away": false, - "victory": false, - "won": false, - "reward_gold": 0, - "reward_exp": 0, - "actions": [], - "created": 1755215734, - "updated": 1755215734 - }, - { - "id": 2, - "user_id": 1, - "monster_id": 7, - "monster_hp": 12, - "monster_max_hp": 12, - "monster_sleep": 0, - "monster_immune": 1, - "uber_damage": 0, - "uber_defense": 0, - "first_strike": true, - "turn": 1, - "ran_away": false, - "victory": false, - "won": false, - "reward_gold": 0, - "reward_exp": 0, - "actions": [], - "created": 1755215776, - "updated": 1755215776 - }, - { - "id": 3, - "user_id": 1, - "monster_id": 8, - "monster_hp": 14, - "monster_max_hp": 14, - "monster_sleep": 0, - "monster_immune": 0, - "uber_damage": 0, - "uber_defense": 0, - "first_strike": true, - "turn": 1, - "ran_away": false, - "victory": false, - "won": false, - "reward_gold": 0, - "reward_exp": 0, - "actions": [], - "created": 1755215777, - "updated": 1755215777 - }, - { - "id": 4, - "user_id": 1, - "monster_id": 4, - "monster_hp": 10, - "monster_max_hp": 10, - "monster_sleep": 0, - "monster_immune": 0, - "uber_damage": 0, - "uber_defense": 0, - "first_strike": false, - "turn": 1, - "ran_away": false, - "victory": false, - "won": false, - "reward_gold": 0, - "reward_exp": 0, - "actions": [], - "created": 1755222893, - "updated": 1755222893 - } -] \ No newline at end of file diff --git a/internal/actions/fight.go b/internal/actions/fight.go new file mode 100644 index 0000000..b17f402 --- /dev/null +++ b/internal/actions/fight.go @@ -0,0 +1,83 @@ +package actions + +import ( + "dk/internal/models/fights" + "dk/internal/models/spells" + "dk/internal/models/users" + "math/rand" + "strconv" +) + +func HandleAttack(fight *fights.Fight, user *users.User) { + // 20% chance to miss + if rand.Float32() < 0.2 { + fight.AddActionAttackMiss() + return + } + + fight.DamageMonster(1) + fight.AddActionAttackHit(1) + + if fight.MonsterHP <= 0 { + fight.WinFight(10, 5) + } +} + +func HandleSpell(fight *fights.Fight, user *users.User, spellID int) { + spell, err := spells.Find(spellID) + if err != nil { + fight.AddAction("Spell not found!") + return + } + + // Check if user has enough MP + if user.MP < spell.MP { + fight.AddAction("Not enough MP to cast " + spell.Name + "!") + return + } + + // Check if user knows this spell + if !user.HasSpell(spellID) { + fight.AddAction("You don't know that spell!") + return + } + + // Deduct 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 + } + fight.AddAction("You cast " + spell.Name + " and healed " + strconv.Itoa(healAmount) + " HP!") + + case spells.TypeHurt: + // Damage monster + damage := spell.Attribute + fight.DamageMonster(damage) + fight.AddAction("You cast " + spell.Name + " and dealt " + strconv.Itoa(damage) + " damage!") + + // Check if monster is defeated + if fight.MonsterHP <= 0 { + fight.WinFight(10, 5) // Basic rewards + } + + default: + fight.AddAction("You cast " + spell.Name + " but nothing happened!") + } +} + +func HandleRun(fight *fights.Fight, user *users.User) { + // 20% chance to successfully run away + if rand.Float32() < 0.2 { + fight.RunAway() + user.FightID = 0 + fight.AddAction("You successfully ran away!") + } else { + fight.AddAction("You failed to run away!") + } +} diff --git a/internal/csrf/csrf.go b/internal/csrf/csrf.go index 28955d5..df21267 100644 --- a/internal/csrf/csrf.go +++ b/internal/csrf/csrf.go @@ -28,8 +28,6 @@ import ( "dk/internal/router" "dk/internal/session" - - "github.com/valyala/fasthttp" ) const ( @@ -182,8 +180,10 @@ func Middleware() router.Middleware { // Only validate CSRF for state-changing methods if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" { if !ValidateFormToken(ctx) { - ctx.SetStatusCode(fasthttp.StatusForbidden) - ctx.WriteString("CSRF validation failed") + fmt.Println("Failed CSRF validation.") + RotateToken(ctx) + currentPath := string(ctx.Path()) + ctx.Redirect(currentPath, 302) return } } diff --git a/internal/models/fights/fights.go b/internal/models/fights/fights.go index 67a7284..a37b6c5 100644 --- a/internal/models/fights/fights.go +++ b/internal/models/fights/fights.go @@ -6,27 +6,45 @@ import ( "time" ) +// ActionEntry represents a compact fight action +type ActionEntry struct { + Type int `json:"t"` + Data int `json:"d,omitempty"` + Name string `json:"n,omitempty"` // For spell names +} + +// Action type constants +const ( + ActionAttackHit = 1 + ActionAttackMiss = 2 + ActionSpellHeal = 3 + ActionSpellHurt = 4 + ActionRunSuccess = 5 + ActionRunFail = 6 + ActionGeneric = 7 +) + // Fight represents a fight, past or present type Fight struct { - ID int `json:"id"` - UserID int `json:"user_id"` - MonsterID int `json:"monster_id"` - MonsterHP int `json:"monster_hp"` - MonsterMaxHP int `json:"monster_max_hp"` - MonsterSleep int `json:"monster_sleep"` - MonsterImmune int `json:"monster_immune"` - UberDamage int `json:"uber_damage"` - UberDefense int `json:"uber_defense"` - FirstStrike bool `json:"first_strike"` - Turn int `json:"turn"` - RanAway bool `json:"ran_away"` - Victory bool `json:"victory"` - Won bool `json:"won"` - RewardGold int `json:"reward_gold"` - RewardExp int `json:"reward_exp"` - Actions []string `json:"actions"` - Created int64 `json:"created"` - Updated int64 `json:"updated"` + ID int `json:"id"` + UserID int `json:"user_id"` + MonsterID int `json:"monster_id"` + MonsterHP int `json:"monster_hp"` + MonsterMaxHP int `json:"monster_max_hp"` + MonsterSleep int `json:"monster_sleep"` + MonsterImmune int `json:"monster_immune"` + UberDamage int `json:"uber_damage"` + UberDefense int `json:"uber_defense"` + FirstStrike bool `json:"first_strike"` + Turn int `json:"turn"` + RanAway bool `json:"ran_away"` + Victory bool `json:"victory"` + Won bool `json:"won"` + RewardGold int `json:"reward_gold"` + RewardExp int `json:"reward_exp"` + Actions []ActionEntry `json:"actions"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` } func (f *Fight) Save() error { @@ -57,7 +75,7 @@ func New(userID, monsterID int) *Fight { Won: false, RewardGold: 0, RewardExp: 0, - Actions: make([]string, 0), + Actions: make([]ActionEntry, 0), Created: now, Updated: now, } @@ -86,6 +104,84 @@ func (f *Fight) Validate() error { return nil } +// Action methods for backward compatibility +func (f *Fight) AddAction(action string) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionGeneric, Name: action}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionAttackHit(damage int) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionAttackHit, Data: damage}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionAttackMiss() { + f.Actions = append(f.Actions, ActionEntry{Type: ActionAttackMiss}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionSpellHeal(spellName string, healAmount int) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionSpellHeal, Data: healAmount, Name: spellName}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionSpellHurt(spellName string, damage int) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionSpellHurt, Data: damage, Name: spellName}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionRunSuccess() { + f.Actions = append(f.Actions, ActionEntry{Type: ActionRunSuccess}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionRunFail() { + f.Actions = append(f.Actions, ActionEntry{Type: ActionRunFail}) + f.Updated = time.Now().Unix() +} + +// Convert actions to human-readable strings +func (f *Fight) GetActions() []string { + result := make([]string, len(f.Actions)) + for i, action := range f.Actions { + result[i] = f.actionToString(action) + } + return result +} + +func (f *Fight) actionToString(action ActionEntry) string { + switch action.Type { + case ActionAttackHit: + return fmt.Sprintf("You attacked for %d damage!", action.Data) + case ActionAttackMiss: + return "You missed your attack!" + case ActionSpellHeal: + return fmt.Sprintf("You cast %s and healed %d HP!", action.Name, action.Data) + case ActionSpellHurt: + return fmt.Sprintf("You cast %s and dealt %d damage!", action.Name, action.Data) + case ActionRunSuccess: + return "You successfully ran away!" + case ActionRunFail: + return "You failed to run away!" + case ActionGeneric: + return action.Name + default: + return "Unknown action" + } +} + +func (f *Fight) GetLastAction() string { + if len(f.Actions) == 0 { + return "" + } + return f.actionToString(f.Actions[len(f.Actions)-1]) +} + +func (f *Fight) ClearActions() { + f.Actions = make([]ActionEntry, 0) + f.Updated = time.Now().Unix() +} + // FightStore with enhanced BaseStore type FightStore struct { *store.BaseStore[Fight] @@ -311,24 +407,3 @@ func (f *Fight) DamageMonster(damage int) { } f.Updated = time.Now().Unix() } - -func (f *Fight) AddAction(action string) { - f.Actions = append(f.Actions, action) - f.Updated = time.Now().Unix() -} - -func (f *Fight) GetActions() []string { - return f.Actions -} - -func (f *Fight) GetLastAction() string { - if len(f.Actions) == 0 { - return "" - } - return f.Actions[len(f.Actions)-1] -} - -func (f *Fight) ClearActions() { - f.Actions = make([]string, 0) - f.Updated = time.Now().Unix() -} diff --git a/internal/routes/fight.go b/internal/routes/fight.go index 28a1c84..22cde02 100644 --- a/internal/routes/fight.go +++ b/internal/routes/fight.go @@ -1,6 +1,7 @@ package routes import ( + "dk/internal/actions" "dk/internal/auth" "dk/internal/components" "dk/internal/helpers" @@ -11,6 +12,7 @@ import ( "dk/internal/models/users" "dk/internal/router" "math/rand" + "strconv" ) func RegisterFightRoutes(r *router.Router) { @@ -19,6 +21,7 @@ func RegisterFightRoutes(r *router.Router) { group.Use(middleware.RequireFighting()) group.Get("/", showFight) + group.Post("/", handleFightAction) } func showFight(ctx router.Ctx, _ []string) { @@ -46,13 +49,13 @@ func showFight(ctx router.Ctx, _ []string) { fight.Save() } - hpPct := helpers.ClampPct(float64(user.HP), float64(user.MaxHP), 0, 100) + monHpPct := helpers.ClampPct(float64(fight.MonsterHP), float64(fight.MonsterMaxHP), 0, 100) - hpColor := "" - if hpPct < 35 { - hpColor = "danger" - } else if hpPct < 75 { - hpColor = "warning" + monHpColor := "" + if monHpPct < 35 { + monHpColor = "danger" + } else if monHpPct < 75 { + monHpColor = "warning" } spellMap := helpers.NewOrderedMap[int, *spells.Spell]() @@ -68,8 +71,42 @@ func showFight(ctx router.Ctx, _ []string) { "fight": fight, "user": user, "monster": monster, - "mon_hppct": hpPct, - "mon_hpcol": hpColor, + "mon_hppct": monHpPct, + "mon_hpcol": monHpColor, "spells": spellMap.ToSlice(), }) } + +func handleFightAction(ctx router.Ctx, _ []string) { + user := ctx.UserValue("user").(*users.User) + + fight, err := fights.Find(user.FightID) + if err != nil { + ctx.SetContentType("text/plain") + ctx.SetBodyString("Fight not found") + return + } + + action := string(ctx.FormValue("action")) + + switch action { + case "attack": + 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) + } + case "run": + actions.HandleRun(fight, user) + default: + fight.AddAction("Invalid action!") + } + + fight.IncrementTurn() + fight.Save() + user.Save() + + // Redirect back to fight page + ctx.Redirect("/fight", 302) +} diff --git a/templates/fight/fight.html b/templates/fight/fight.html index de51b8d..36e241d 100644 --- a/templates/fight/fight.html +++ b/templates/fight/fight.html @@ -2,7 +2,7 @@ {block "content"}
-

Fighting {monster.Name}

+

{monster.Name}

Level {monster.Level}

{monster.Name} @@ -14,17 +14,24 @@
+ {csrf} +
- +
{if user.Spells != ""} - +
+ - + +
{/if} +
+ +
{/block}