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" "dk/internal/models/towns" "dk/internal/models/users" "math" "math/rand" ) type FightResult struct { FightUpdates map[string]any UserUpdates map[string]any LogAction func() error Ended bool Victory bool Won bool RewardGold int RewardExp int } func (r *FightResult) EndFightWithVictory(monster *monsters.Monster, user *users.User) { rewardGold, rewardExp := calculateRewards(monster, user) r.FightUpdates["victory"] = true r.FightUpdates["won"] = true r.FightUpdates["reward_gold"] = rewardGold r.FightUpdates["reward_exp"] = rewardExp newLevel, newStr, newDex, newExp := user.CalculateLevelUp(rewardExp) r.UserUpdates = map[string]any{ "fight_id": 0, "currently": "Exploring", "gold": user.Gold + rewardGold, "exp": newExp, "level": newLevel, "strength": newStr, "dexterity": newDex, } r.Ended = true r.Victory = true r.Won = true r.RewardGold = rewardGold r.RewardExp = rewardExp } func HandleAttack(fight *fights.Fight, user *users.User) *FightResult { monster, err := monsters.Find(fight.MonsterID) if err != nil { return &FightResult{ LogAction: func() error { return fightlogs.AddAction(fight.ID, "Monster not found!") }, } } // Calculate damage attackPower := float64(user.Attack) minAttack := attackPower * 0.75 maxAttack := attackPower rawAttack := math.Ceil(rand.Float64()*(maxAttack-minAttack) + minAttack) tohit := rawAttack / (1.2 + math.Sqrt(attackPower)*0.05) // Critical hit criticalRoll := rand.Intn(150) + 1 if float64(criticalRoll) <= math.Sqrt(float64(user.Strength)) { tohit *= 2 } // Monster defense armor := float64(monster.Armor) minBlock := armor * 0.75 maxBlock := armor rawBlock := math.Ceil(rand.Float64()*(maxBlock-minBlock) + minBlock) toblock := rawBlock / (1.8 + math.Sqrt(armor)*0.08) damage := tohit - toblock if damage < 1 { damage = 1 } if fight.UberDamage > 0 { bonus := math.Ceil(damage * float64(fight.UberDamage) / 100) damage += bonus } finalDamage := int(damage) newMonsterHP := max(fight.MonsterHP-finalDamage, 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 { result.EndFightWithVictory(monster, user) result.LogAction = func() error { if err := fightlogs.AddAttackHit(fight.ID, finalDamage); err != nil { return err } return fightlogs.AddMonsterDeath(fight.ID, monster.Name) } } return result } func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResult { spell, err := spells.Find(spellID) if err != nil { return &FightResult{ LogAction: func() error { return fightlogs.AddAction(fight.ID, "Spell not found!") }, } } if user.MP < spell.MP { return &FightResult{ LogAction: func() error { return fightlogs.AddAction(fight.ID, "Not enough MP to cast "+spell.Name+"!") }, } } if !user.HasSpell(spellID) { return &FightResult{ LogAction: func() error { return fightlogs.AddAction(fight.ID, "You don't know that spell!") }, } } result := &FightResult{ UserUpdates: map[string]any{"mp": user.MP - spell.MP}, } switch spell.Type { case spells.TypeHealing: newHP := min(user.HP+spell.Attribute, user.MaxHP) result.UserUpdates["hp"] = newHP result.LogAction = func() error { return fightlogs.AddSpellHeal(fight.ID, spell.Name, spell.Attribute) } case spells.TypeHurt: newMonsterHP := max(fight.MonsterHP-spell.Attribute, 0) result.FightUpdates = map[string]any{"monster_hp": newMonsterHP} result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Attribute) } if newMonsterHP <= 0 { monster, err := monsters.Find(fight.MonsterID) if err == nil { result.EndFightWithVictory(monster, user) } } default: 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) *FightResult { result := &FightResult{} if rand.Float32() < 0.2 { 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 { result.LogAction = func() error { return fightlogs.AddRunFail(fight.ID) } } return result } func HandleMonsterAttack(fight *fights.Fight, user *users.User) *FightResult { monster, err := monsters.Find(fight.MonsterID) if err != nil { return &FightResult{} } // Calculate damage attackPower := float64(monster.MaxDmg) minAttack := attackPower * 0.75 maxAttack := attackPower tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3 defense := float64(user.Defense) minBlock := defense * 0.75 maxBlock := defense toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3 damage := tohit - toblock if damage < 1 { damage = 1 } if fight.UberDefense > 0 { reduction := math.Ceil(damage * float64(fight.UberDefense) / 100) damage -= reduction if damage < 1 { damage = 1 } } finalDamage := int(damage) newHP := max(user.HP-finalDamage, 0) 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 } type LevelStats struct { Strength int Dexterity int } 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 } return level, LevelStats{Strength: str, Dexterity: dex} } 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 } func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) { // More generous reward range: 90-100% of max instead of 83-100% minExp := (monster.MaxExp * 9) / 10 maxExp := monster.MaxExp exp := max(rand.Intn(maxExp-minExp+1)+minExp, 1) minGold := (monster.MaxGold * 9) / 10 maxGold := monster.MaxGold gold := max(rand.Intn(maxGold-minGold+1)+minGold, 1) // Apply bonuses expBonus := (user.ExpBonus * exp) / 100 exp += expBonus goldBonus := (user.GoldBonus * gold) / 100 gold += goldBonus 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 }) }