|
|
|
@ -8,18 +8,38 @@ import (
|
|
|
|
"github.com/denisovdennis/autohero/internal/tuning"
|
|
|
|
"github.com/denisovdennis/autohero/internal/tuning"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
|
|
attackOutcomeHit = "hit"
|
|
|
|
|
|
|
|
attackOutcomeDodge = "dodge"
|
|
|
|
|
|
|
|
attackOutcomeBlock = "block"
|
|
|
|
|
|
|
|
attackOutcomeStun = "stun"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type DamageBreakdown struct {
|
|
|
|
|
|
|
|
RawDamage int
|
|
|
|
|
|
|
|
FinalDamage int
|
|
|
|
|
|
|
|
IsCrit bool
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CalculateDamage computes the final damage dealt from attacker stats to a defender,
|
|
|
|
// CalculateDamage computes the final damage dealt from attacker stats to a defender,
|
|
|
|
// applying defense and critical hits.
|
|
|
|
// applying defense and critical hits.
|
|
|
|
func CalculateDamage(baseAttack int, defense int, critChance float64) (damage int, isCrit bool) {
|
|
|
|
func CalculateDamage(baseAttack int, defense int, critChance float64) (damage int, isCrit bool) {
|
|
|
|
atk := float64(baseAttack)
|
|
|
|
breakdown := calculateDamageBreakdown(baseAttack, defense, critChance)
|
|
|
|
|
|
|
|
return breakdown.FinalDamage, breakdown.IsCrit
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func calculateDamageBreakdown(baseAttack int, defense int, critChance float64) DamageBreakdown {
|
|
|
|
|
|
|
|
atk := float64(baseAttack) * damageRollMultiplier()
|
|
|
|
|
|
|
|
|
|
|
|
// Defense reduces damage (simple formula: damage = atk - def, min 1).
|
|
|
|
// Defense reduces damage (simple formula: damage = atk - def, min 1).
|
|
|
|
dmg := atk - float64(defense)
|
|
|
|
dmg := atk - float64(defense)
|
|
|
|
if dmg < 1 {
|
|
|
|
if dmg < 1 {
|
|
|
|
dmg = 1
|
|
|
|
dmg = 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
raw := int(dmg)
|
|
|
|
|
|
|
|
|
|
|
|
// Critical hit check.
|
|
|
|
// Critical hit check.
|
|
|
|
|
|
|
|
isCrit := false
|
|
|
|
if critChance > 0 && rand.Float64() < critChance {
|
|
|
|
if critChance > 0 && rand.Float64() < critChance {
|
|
|
|
dmg *= 2
|
|
|
|
dmg *= 2
|
|
|
|
isCrit = true
|
|
|
|
isCrit = true
|
|
|
|
@ -30,11 +50,31 @@ func CalculateDamage(baseAttack int, defense int, critChance float64) (damage in
|
|
|
|
dmg = 1
|
|
|
|
dmg = 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return int(dmg), isCrit
|
|
|
|
return DamageBreakdown{
|
|
|
|
|
|
|
|
RawDamage: raw,
|
|
|
|
|
|
|
|
FinalDamage: int(dmg),
|
|
|
|
|
|
|
|
IsCrit: isCrit,
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CalculateIncomingDamage applies shield buff reduction to incoming damage.
|
|
|
|
func damageRollMultiplier() float64 {
|
|
|
|
func CalculateIncomingDamage(rawDamage int, buffs []model.ActiveBuff, now time.Time) int {
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
minRoll := cfg.CombatDamageRollMin
|
|
|
|
|
|
|
|
maxRoll := cfg.CombatDamageRollMax
|
|
|
|
|
|
|
|
if minRoll <= 0 || maxRoll <= 0 {
|
|
|
|
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if maxRoll < minRoll {
|
|
|
|
|
|
|
|
minRoll, maxRoll = maxRoll, minRoll
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if maxRoll == minRoll {
|
|
|
|
|
|
|
|
return maxRoll
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return minRoll + rand.Float64()*(maxRoll-minRoll)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CalculateIncomingDamage applies shield buff and weaken debuff reduction to incoming damage.
|
|
|
|
|
|
|
|
func CalculateIncomingDamage(rawDamage int, buffs []model.ActiveBuff, debuffs []model.ActiveDebuff, now time.Time) int {
|
|
|
|
dmg := float64(rawDamage)
|
|
|
|
dmg := float64(rawDamage)
|
|
|
|
|
|
|
|
|
|
|
|
for _, ab := range buffs {
|
|
|
|
for _, ab := range buffs {
|
|
|
|
@ -45,6 +85,14 @@ func CalculateIncomingDamage(rawDamage int, buffs []model.ActiveBuff, now time.T
|
|
|
|
dmg *= (1 - ab.Buff.Magnitude)
|
|
|
|
dmg *= (1 - ab.Buff.Magnitude)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ad := range debuffs {
|
|
|
|
|
|
|
|
if ad.IsExpired(now) {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if ad.Debuff.Type == model.DebuffWeaken {
|
|
|
|
|
|
|
|
dmg *= (1 - ad.Debuff.Magnitude)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if dmg < 1 {
|
|
|
|
if dmg < 1 {
|
|
|
|
dmg = 1
|
|
|
|
dmg = 1
|
|
|
|
@ -62,16 +110,14 @@ func ProcessAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) model.Co
|
|
|
|
HeroID: hero.ID,
|
|
|
|
HeroID: hero.ID,
|
|
|
|
Damage: 0,
|
|
|
|
Damage: 0,
|
|
|
|
Source: "hero",
|
|
|
|
Source: "hero",
|
|
|
|
|
|
|
|
Outcome: attackOutcomeStun,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
Timestamp: now,
|
|
|
|
Timestamp: now,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
critChance := 0.0
|
|
|
|
critChance := heroCritChance(hero, now)
|
|
|
|
if weapon := hero.Gear[model.SlotMainHand]; weapon != nil {
|
|
|
|
|
|
|
|
critChance = weapon.CritChance
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check enemy dodge ability.
|
|
|
|
// Check enemy dodge ability.
|
|
|
|
if enemy.HasAbility(model.AbilityDodge) {
|
|
|
|
if enemy.HasAbility(model.AbilityDodge) {
|
|
|
|
@ -81,6 +127,7 @@ func ProcessAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) model.Co
|
|
|
|
HeroID: hero.ID,
|
|
|
|
HeroID: hero.ID,
|
|
|
|
Damage: 0,
|
|
|
|
Damage: 0,
|
|
|
|
Source: "hero",
|
|
|
|
Source: "hero",
|
|
|
|
|
|
|
|
Outcome: attackOutcomeDodge,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
Timestamp: now,
|
|
|
|
Timestamp: now,
|
|
|
|
@ -99,6 +146,7 @@ func ProcessAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) model.Co
|
|
|
|
HeroID: hero.ID,
|
|
|
|
HeroID: hero.ID,
|
|
|
|
Damage: dmg,
|
|
|
|
Damage: dmg,
|
|
|
|
Source: "hero",
|
|
|
|
Source: "hero",
|
|
|
|
|
|
|
|
Outcome: attackOutcomeHit,
|
|
|
|
IsCrit: isCrit,
|
|
|
|
IsCrit: isCrit,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
@ -133,6 +181,7 @@ func ProcessEnemyAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) mod
|
|
|
|
if enemy.HasAbility(model.AbilityCritical) && critChance < tuning.Get().EnemyCriticalMinChance {
|
|
|
|
if enemy.HasAbility(model.AbilityCritical) && critChance < tuning.Get().EnemyCriticalMinChance {
|
|
|
|
critChance = tuning.Get().EnemyCriticalMinChance
|
|
|
|
critChance = tuning.Get().EnemyCriticalMinChance
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
critChance = capChance(critChance, tuning.Get().EnemyCritChanceCap)
|
|
|
|
|
|
|
|
|
|
|
|
rawDmg, isCrit := CalculateDamage(enemy.Attack, hero.EffectiveDefenseAt(now), critChance)
|
|
|
|
rawDmg, isCrit := CalculateDamage(enemy.Attack, hero.EffectiveDefenseAt(now), critChance)
|
|
|
|
|
|
|
|
|
|
|
|
@ -142,7 +191,20 @@ func ProcessEnemyAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) mod
|
|
|
|
rawDmg = int(float64(rawDmg) * burstMult)
|
|
|
|
rawDmg = int(float64(rawDmg) * burstMult)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dmg := CalculateIncomingDamage(rawDmg, hero.Buffs, now)
|
|
|
|
if rand.Float64() < hero.EffectiveBlockChance(now) {
|
|
|
|
|
|
|
|
return model.CombatEvent{
|
|
|
|
|
|
|
|
Type: "attack",
|
|
|
|
|
|
|
|
HeroID: hero.ID,
|
|
|
|
|
|
|
|
Damage: 0,
|
|
|
|
|
|
|
|
Source: "enemy",
|
|
|
|
|
|
|
|
Outcome: attackOutcomeBlock,
|
|
|
|
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
|
|
|
|
EnemyHP: enemy.HP,
|
|
|
|
|
|
|
|
Timestamp: now,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dmg := CalculateIncomingDamage(rawDmg, hero.Buffs, hero.Debuffs, now)
|
|
|
|
|
|
|
|
|
|
|
|
hero.HP -= dmg
|
|
|
|
hero.HP -= dmg
|
|
|
|
if hero.HP < 0 {
|
|
|
|
if hero.HP < 0 {
|
|
|
|
@ -156,6 +218,7 @@ func ProcessEnemyAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) mod
|
|
|
|
HeroID: hero.ID,
|
|
|
|
HeroID: hero.ID,
|
|
|
|
Damage: dmg,
|
|
|
|
Damage: dmg,
|
|
|
|
Source: "enemy",
|
|
|
|
Source: "enemy",
|
|
|
|
|
|
|
|
Outcome: attackOutcomeHit,
|
|
|
|
IsCrit: isCrit,
|
|
|
|
IsCrit: isCrit,
|
|
|
|
DebuffApplied: debuffApplied,
|
|
|
|
DebuffApplied: debuffApplied,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
HeroHP: hero.HP,
|
|
|
|
@ -164,6 +227,25 @@ func ProcessEnemyAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) mod
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func heroCritChance(hero *model.Hero, now time.Time) float64 {
|
|
|
|
|
|
|
|
_ = now
|
|
|
|
|
|
|
|
critChance := 0.0
|
|
|
|
|
|
|
|
if weapon := hero.Gear[model.SlotMainHand]; weapon != nil {
|
|
|
|
|
|
|
|
critChance = weapon.CritChance
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return capChance(critChance, tuning.Get().HeroCritChanceCap)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func capChance(chance float64, cap float64) float64 {
|
|
|
|
|
|
|
|
if cap > 0 && chance > cap {
|
|
|
|
|
|
|
|
return cap
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if chance < 0 {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return chance
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// tryApplyDebuff checks enemy abilities and rolls to apply debuffs to the hero.
|
|
|
|
// tryApplyDebuff checks enemy abilities and rolls to apply debuffs to the hero.
|
|
|
|
// Returns the debuff type string if one was applied, empty string otherwise.
|
|
|
|
// Returns the debuff type string if one was applied, empty string otherwise.
|
|
|
|
func tryApplyDebuff(hero *model.Hero, enemy *model.Enemy, now time.Time) string {
|
|
|
|
func tryApplyDebuff(hero *model.Hero, enemy *model.Enemy, now time.Time) string {
|
|
|
|
|