362 lines
9.1 KiB
Go
362 lines
9.1 KiB
Go
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"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
)
|
|
|
|
type FightResult struct {
|
|
FightUpdates map[string]any
|
|
UserUpdates map[string]any
|
|
LogAction func() error
|
|
ActionText string // Add this field
|
|
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!") },
|
|
ActionText: "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)
|
|
|
|
damageMultiplier := 1 + math.Pow(float64(user.Attack)/400, 0.6)
|
|
tohit *= damageMultiplier
|
|
|
|
// Critical hit
|
|
critChance := 85 * math.Pow(float64(user.Strength)/300, 0.6)
|
|
criticalRoll := rand.Float64() * 100
|
|
if criticalRoll <= critChance {
|
|
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)
|
|
|
|
actionText := fmt.Sprintf("You attacked for %d damage!", finalDamage)
|
|
|
|
result := &FightResult{
|
|
FightUpdates: map[string]any{"monster_hp": newMonsterHP},
|
|
LogAction: func() error { return fightlogs.AddAttackHit(fight.ID, finalDamage) },
|
|
ActionText: actionText,
|
|
}
|
|
|
|
// Check if monster defeated
|
|
if newMonsterHP <= 0 {
|
|
result.EndFightWithVictory(monster, user)
|
|
result.ActionText = actionText + " " + fmt.Sprintf("%s has been defeated!", monster.Name)
|
|
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!") },
|
|
ActionText: "Spell not found!",
|
|
}
|
|
}
|
|
|
|
if user.MP < spell.MP {
|
|
actionText := "Not enough MP to cast " + spell.Name + "!"
|
|
return &FightResult{
|
|
LogAction: func() error { return fightlogs.AddAction(fight.ID, actionText) },
|
|
ActionText: actionText,
|
|
}
|
|
}
|
|
|
|
if !user.HasSpell(spellID) {
|
|
actionText := "You don't know that spell!"
|
|
return &FightResult{
|
|
LogAction: func() error { return fightlogs.AddAction(fight.ID, actionText) },
|
|
ActionText: actionText,
|
|
}
|
|
}
|
|
|
|
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.ActionText = fmt.Sprintf("You cast %s and healed %d HP!", spell.Name, spell.Attribute)
|
|
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.ActionText = fmt.Sprintf("You cast %s and dealt %d damage!", spell.Name, spell.Attribute)
|
|
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)
|
|
result.ActionText += " " + fmt.Sprintf("%s has been defeated!", monster.Name)
|
|
}
|
|
}
|
|
|
|
default:
|
|
result.ActionText = "You cast " + spell.Name + " but nothing happened!"
|
|
result.LogAction = func() error { return fightlogs.AddAction(fight.ID, result.ActionText) }
|
|
}
|
|
|
|
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.ActionText = "You successfully ran away!"
|
|
result.LogAction = func() error { return fightlogs.AddRunSuccess(fight.ID) }
|
|
result.Ended = true
|
|
} else {
|
|
result.ActionText = "You failed to run away!"
|
|
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},
|
|
ActionText: fmt.Sprintf("%s attacks for %d damage!", monster.Name, finalDamage),
|
|
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
|
|
})
|
|
}
|