add action handlers, update action tracking, add monster images, fix form

This commit is contained in:
Sky Johnson 2025-08-15 08:03:17 -05:00
parent de0381f668
commit 0d3afffb1e
9 changed files with 271 additions and 150 deletions

View File

@ -212,6 +212,11 @@ button.btn {
background-color: #00c6ff; background-color: #00c6ff;
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #00c6ff, #0072ff); background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #00c6ff, #0072ff);
border-color: #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 { img#monster-image {
display: block; display: block;
max-height: 360px; max-width: 80%;
margin: 1rem auto; margin: 1rem auto;
} }
@ -453,7 +458,7 @@ select.styled-select {
color: white; color: white;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2); box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
height: 28px; height: 29px;
&:hover { &:hover {
background-color: #909090; background-color: #909090;

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@ -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
}
]

83
internal/actions/fight.go Normal file
View File

@ -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!")
}
}

View File

@ -28,8 +28,6 @@ import (
"dk/internal/router" "dk/internal/router"
"dk/internal/session" "dk/internal/session"
"github.com/valyala/fasthttp"
) )
const ( const (
@ -182,8 +180,10 @@ func Middleware() router.Middleware {
// Only validate CSRF for state-changing methods // Only validate CSRF for state-changing methods
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" { if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
if !ValidateFormToken(ctx) { if !ValidateFormToken(ctx) {
ctx.SetStatusCode(fasthttp.StatusForbidden) fmt.Println("Failed CSRF validation.")
ctx.WriteString("CSRF validation failed") RotateToken(ctx)
currentPath := string(ctx.Path())
ctx.Redirect(currentPath, 302)
return return
} }
} }

View File

@ -6,27 +6,45 @@ import (
"time" "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 // Fight represents a fight, past or present
type Fight struct { type Fight struct {
ID int `json:"id"` ID int `json:"id"`
UserID int `json:"user_id"` UserID int `json:"user_id"`
MonsterID int `json:"monster_id"` MonsterID int `json:"monster_id"`
MonsterHP int `json:"monster_hp"` MonsterHP int `json:"monster_hp"`
MonsterMaxHP int `json:"monster_max_hp"` MonsterMaxHP int `json:"monster_max_hp"`
MonsterSleep int `json:"monster_sleep"` MonsterSleep int `json:"monster_sleep"`
MonsterImmune int `json:"monster_immune"` MonsterImmune int `json:"monster_immune"`
UberDamage int `json:"uber_damage"` UberDamage int `json:"uber_damage"`
UberDefense int `json:"uber_defense"` UberDefense int `json:"uber_defense"`
FirstStrike bool `json:"first_strike"` FirstStrike bool `json:"first_strike"`
Turn int `json:"turn"` Turn int `json:"turn"`
RanAway bool `json:"ran_away"` RanAway bool `json:"ran_away"`
Victory bool `json:"victory"` Victory bool `json:"victory"`
Won bool `json:"won"` Won bool `json:"won"`
RewardGold int `json:"reward_gold"` RewardGold int `json:"reward_gold"`
RewardExp int `json:"reward_exp"` RewardExp int `json:"reward_exp"`
Actions []string `json:"actions"` Actions []ActionEntry `json:"actions"`
Created int64 `json:"created"` Created int64 `json:"created"`
Updated int64 `json:"updated"` Updated int64 `json:"updated"`
} }
func (f *Fight) Save() error { func (f *Fight) Save() error {
@ -57,7 +75,7 @@ func New(userID, monsterID int) *Fight {
Won: false, Won: false,
RewardGold: 0, RewardGold: 0,
RewardExp: 0, RewardExp: 0,
Actions: make([]string, 0), Actions: make([]ActionEntry, 0),
Created: now, Created: now,
Updated: now, Updated: now,
} }
@ -86,6 +104,84 @@ func (f *Fight) Validate() error {
return nil 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 // FightStore with enhanced BaseStore
type FightStore struct { type FightStore struct {
*store.BaseStore[Fight] *store.BaseStore[Fight]
@ -311,24 +407,3 @@ func (f *Fight) DamageMonster(damage int) {
} }
f.Updated = time.Now().Unix() 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()
}

View File

@ -1,6 +1,7 @@
package routes package routes
import ( import (
"dk/internal/actions"
"dk/internal/auth" "dk/internal/auth"
"dk/internal/components" "dk/internal/components"
"dk/internal/helpers" "dk/internal/helpers"
@ -11,6 +12,7 @@ import (
"dk/internal/models/users" "dk/internal/models/users"
"dk/internal/router" "dk/internal/router"
"math/rand" "math/rand"
"strconv"
) )
func RegisterFightRoutes(r *router.Router) { func RegisterFightRoutes(r *router.Router) {
@ -19,6 +21,7 @@ func RegisterFightRoutes(r *router.Router) {
group.Use(middleware.RequireFighting()) group.Use(middleware.RequireFighting())
group.Get("/", showFight) group.Get("/", showFight)
group.Post("/", handleFightAction)
} }
func showFight(ctx router.Ctx, _ []string) { func showFight(ctx router.Ctx, _ []string) {
@ -46,13 +49,13 @@ func showFight(ctx router.Ctx, _ []string) {
fight.Save() 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 := "" monHpColor := ""
if hpPct < 35 { if monHpPct < 35 {
hpColor = "danger" monHpColor = "danger"
} else if hpPct < 75 { } else if monHpPct < 75 {
hpColor = "warning" monHpColor = "warning"
} }
spellMap := helpers.NewOrderedMap[int, *spells.Spell]() spellMap := helpers.NewOrderedMap[int, *spells.Spell]()
@ -68,8 +71,42 @@ func showFight(ctx router.Ctx, _ []string) {
"fight": fight, "fight": fight,
"user": user, "user": user,
"monster": monster, "monster": monster,
"mon_hppct": hpPct, "mon_hppct": monHpPct,
"mon_hpcol": hpColor, "mon_hpcol": monHpColor,
"spells": spellMap.ToSlice(), "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)
}

View File

@ -2,7 +2,7 @@
{block "content"} {block "content"}
<div id="battle-window"> <div id="battle-window">
<h2>Fighting {monster.Name}</h2> <h2>{monster.Name}</h2>
<h3>Level {monster.Level}</h3> <h3>Level {monster.Level}</h3>
<img id="monster-image" src="/assets/images/monsters/{monster.Name}.png" alt="{monster.Name}" title="{monster.Name}"> <img id="monster-image" src="/assets/images/monsters/{monster.Name}.png" alt="{monster.Name}" title="{monster.Name}">
@ -14,17 +14,24 @@
</div> </div>
<form action="/fight" method="post"> <form action="/fight" method="post">
{csrf}
<div class="mb-05"> <div class="mb-05">
<button class="btn btn-primary">Attack</button> <button type="submit" name="action" value="attack" class="btn btn-primary">Attack</button>
</div> </div>
{if user.Spells != ""} {if user.Spells != ""}
<select id="spell-select" class="styled-select"> <div class="mb-05">
{for spell in spells} <select id="spell-select" class="styled-select" name="spell_id">
<option value="{spell.ID}">({spell.MP} MP) {spell.Name}</option> {for spell in spells}
{/for} <option value="{spell.ID}">({spell.MP} MP) {spell.Name}</option>
</select> {/for}
</select>
<button class="btn btn-blue">Spell</button> <button type="submit" name="action" value="spell" class="btn btn-blue">Spell</button>
</div>
{/if} {/if}
<div>
<button type="submit" name="action" value="run" class="btn">Run</button>
</div>
</form> </form>
{/block} {/block}