From 9e17ab9bea571fe73ce76079b791ab9b4d211f5e Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 15 Aug 2025 10:11:24 -0500 Subject: [PATCH] implement monster moves, update log tracking to "action codes" for storage, add notices for actions taken --- data/fights.json | 252 +++++++++++++++++++++++++ internal/actions/fight.go | 157 ++++++++++++++- internal/models/fights/action_codes.go | 140 ++++++++++++++ internal/models/fights/fights.go | 96 ---------- internal/routes/auth.go | 8 - internal/routes/fight.go | 77 +++++++- internal/session/session.go | 5 + templates/fight/fight.html | 3 + 8 files changed, 622 insertions(+), 116 deletions(-) create mode 100644 data/fights.json create mode 100644 internal/models/fights/action_codes.go diff --git a/data/fights.json b/data/fights.json new file mode 100644 index 0000000..9bc95f0 --- /dev/null +++ b/data/fights.json @@ -0,0 +1,252 @@ +[ + { + "id": 1, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 7, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 2 + }, + { + "t": 1, + "d": 1 + }, + { + "t": 1, + "d": 1 + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755264713, + "updated": 1755270326 + }, + { + "id": 2, + "user_id": 1, + "monster_id": 5, + "monster_hp": 7, + "monster_max_hp": 10, + "monster_sleep": 0, + "monster_immune": 1, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 3, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 2, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + } + ], + "created": 1755270342, + "updated": 1755270352 + }, + { + "id": 3, + "user_id": 1, + "monster_id": 5, + "monster_hp": 6, + "monster_max_hp": 10, + "monster_sleep": 0, + "monster_immune": 1, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 4, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 2, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + } + ], + "created": 1755270581, + "updated": 1755270585 + }, + { + "id": 4, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": false, + "turn": 6, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755270614, + "updated": 1755270620 + } +] \ No newline at end of file diff --git a/internal/actions/fight.go b/internal/actions/fight.go index b17f402..5b757eb 100644 --- a/internal/actions/fight.go +++ b/internal/actions/fight.go @@ -2,24 +2,64 @@ package actions import ( "dk/internal/models/fights" + "dk/internal/models/monsters" "dk/internal/models/spells" + "dk/internal/models/towns" "dk/internal/models/users" + "math" "math/rand" "strconv" ) func HandleAttack(fight *fights.Fight, user *users.User) { - // 20% chance to miss - if rand.Float32() < 0.2 { - fight.AddActionAttackMiss() + // Load monster data to get armor + monster, err := monsters.Find(fight.MonsterID) + if err != nil { + fight.AddAction("Monster not found!") return } - fight.DamageMonster(1) - fight.AddActionAttackHit(1) + // Player attack damage calculation + attackPower := float64(user.Attack) + minAttack := attackPower * 0.75 + maxAttack := attackPower + tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3 + // Critical hit chance based on strength + criticalRoll := rand.Intn(150) + 1 + if float64(criticalRoll) <= math.Sqrt(float64(user.Strength)) { + tohit *= 2 // Critical hit + } + + // Monster defense calculation + armor := float64(monster.Armor) + minBlock := armor * 0.75 + maxBlock := armor + toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3 + + // Calculate final damage + damage := tohit - toblock + if damage < 1 { + damage = 1 // Minimum damage + } + + // 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.WinFight(10, 5) + fight.AddActionMonsterDeath(monster.Name) + fight.WinFight(fight.RewardGold, fight.RewardExp) + HandleFightWin(fight, user) } } @@ -81,3 +121,108 @@ func HandleRun(fight *fights.Fight, user *users.User) { fight.AddAction("You failed to run away!") } } + +func HandleMonsterAttack(fight *fights.Fight, user *users.User) { + // Load monster data + monster, err := monsters.Find(fight.MonsterID) + if err != nil { + return + } + + // Monster attack damage calculation + 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 + } + + // 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 + } + } + + finalDamage := int(damage) + + // Apply damage to user + user.HP -= finalDamage + if user.HP < 0 { + user.HP = 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) + } +} + +func HandleFightWin(fight *fights.Fight, user *users.User) { + // Add rewards to user + user.Exp += 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) + if closestTown != nil { + user.X = closestTown.X + user.Y = closestTown.Y + } + + // 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() +} + +func findClosestTown(x, y int) *towns.Town { + allTowns, err := towns.All() + if err != nil || len(allTowns) == 0 { + return nil + } + + var closest *towns.Town + var minDistance float64 + + for _, town := range allTowns { + distance := town.DistanceFromSquared(x, y) + if closest == nil || distance < minDistance { + closest = town + minDistance = distance + } + } + + return closest +} diff --git a/internal/models/fights/action_codes.go b/internal/models/fights/action_codes.go new file mode 100644 index 0000000..43ae4df --- /dev/null +++ b/internal/models/fights/action_codes.go @@ -0,0 +1,140 @@ +package fights + +import ( + "fmt" + "strings" + "time" +) + +// ActionEntry represents a compacted fight action log. This allows us to store more logs +// in the same space as a single string. +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 + ActionMonsterAttack = 8 + ActionMonsterMiss = 9 + ActionMonsterSpell = 10 + ActionMonsterDeath = 11 +) + +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) 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() +} + +func (f *Fight) AddActionMonsterAttack(monsterName string, damage int) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionMonsterAttack, Data: damage, Name: monsterName}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionMonsterMiss(monsterName string) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionMonsterMiss, Name: monsterName}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionMonsterSpell(monsterName, spellName string, damage int) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionMonsterSpell, Data: damage, Name: monsterName + "|" + spellName}) + f.Updated = time.Now().Unix() +} + +func (f *Fight) AddActionMonsterDeath(monsterName string) { + f.Actions = append(f.Actions, ActionEntry{Type: ActionMonsterDeath, Name: monsterName}) + f.Updated = time.Now().Unix() +} + +// Update actionToString method - add these cases +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 + case ActionMonsterAttack: + return fmt.Sprintf("%s attacks for %d damage!", action.Name, action.Data) + case ActionMonsterMiss: + return fmt.Sprintf("%s missed its attack!", action.Name) + case ActionMonsterSpell: + parts := strings.Split(action.Name, "|") + if len(parts) == 2 { + return fmt.Sprintf("%s casts %s for %d damage!", parts[0], parts[1], action.Data) + } + return fmt.Sprintf("%s casts a spell for %d damage!", action.Name, action.Data) + case ActionMonsterDeath: + return fmt.Sprintf("%s has been defeated!", action.Name) + default: + return "Unknown action" + } +} diff --git a/internal/models/fights/fights.go b/internal/models/fights/fights.go index a37b6c5..106ee5a 100644 --- a/internal/models/fights/fights.go +++ b/internal/models/fights/fights.go @@ -6,24 +6,6 @@ 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"` @@ -104,84 +86,6 @@ 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] diff --git a/internal/routes/auth.go b/internal/routes/auth.go index f15fa12..e217164 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -76,15 +76,8 @@ func processLogin(ctx router.Ctx, _ []string) { // showRegister displays the registration form func showRegister(ctx router.Ctx, _ []string) { sess := ctx.UserValue("session").(*session.Session) - var errorHTML string var username, email string - if flash, exists := sess.GetFlash("error"); exists { - if msg, ok := flash.(string); ok { - errorHTML = fmt.Sprintf(`
%s
`, msg) - } - } - if formData, exists := sess.Get("form_data"); exists { if data, ok := formData.(map[string]string); ok { username = data["username"] @@ -95,7 +88,6 @@ func showRegister(ctx router.Ctx, _ []string) { session.Store(sess) components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{ - "error_message": errorHTML, "username": username, "email": email, }) diff --git a/internal/routes/fight.go b/internal/routes/fight.go index 22cde02..0d63163 100644 --- a/internal/routes/fight.go +++ b/internal/routes/fight.go @@ -11,6 +11,8 @@ import ( "dk/internal/models/spells" "dk/internal/models/users" "dk/internal/router" + "dk/internal/session" + "fmt" "math/rand" "strconv" ) @@ -25,6 +27,7 @@ func RegisterFightRoutes(r *router.Router) { } func showFight(ctx router.Ctx, _ []string) { + sess := ctx.UserValue("session").(*session.Session) user := ctx.UserValue("user").(*users.User) fight, err := fights.Find(user.FightID) @@ -68,17 +71,20 @@ func showFight(ctx router.Ctx, _ []string) { } 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(), + "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"), }) } func handleFightAction(ctx router.Ctx, _ []string) { user := ctx.UserValue("user").(*users.User) + sess := ctx.UserValue("session").(*session.Session) fight, err := fights.Find(user.FightID) if err != nil { @@ -88,19 +94,78 @@ func handleFightAction(ctx router.Ctx, _ []string) { } action := string(ctx.FormValue("action")) + var userAction string switch action { case "attack": actions.HandleAttack(fight, user) + userAction = fight.GetLastAction() case "spell": spellIDStr := string(ctx.FormValue("spell_id")) if spellID, err := strconv.Atoi(spellIDStr); err == nil { actions.HandleSpell(fight, user, spellID) + userAction = fight.GetLastAction() } 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 + } 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 + } + + // Monster attacks back if fight is still active + if fight.IsActive() && user.HP > 0 { + 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") + 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") + ctx.Redirect("/town", 302) + } + return + } + + monsterAction := fight.GetLastAction() + sess.SetFlash("mon_action", monsterAction) } fight.IncrementTurn() diff --git a/internal/session/session.go b/internal/session/session.go index e6fd57d..9915d2e 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -81,6 +81,11 @@ func (s *Session) GetFlashMessage(key string) string { return "" } +// DeleteFlash removes a flash from the session. +func (s *Session) DeleteFlash(key string) { + s.GetFlash(key) +} + // RegenerateID creates a new session ID and updates storage func (s *Session) RegenerateID() { oldID := s.ID diff --git a/templates/fight/fight.html b/templates/fight/fight.html index 36e241d..26399de 100644 --- a/templates/fight/fight.html +++ b/templates/fight/fight.html @@ -8,6 +8,9 @@ {monster.Name} +
{action}
+
{mon_action}
+ {fight.MonsterHP}/{fight.MonsterMaxHP}