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-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;
}
}
}

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/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
}
}

View File

@ -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()
}

View File

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

View File

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