You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
7.3 KiB
Go
302 lines
7.3 KiB
Go
package game
|
|
|
|
import (
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/denisovdennis/autohero/internal/model"
|
|
"github.com/denisovdennis/autohero/internal/tuning"
|
|
)
|
|
|
|
func TestOrcWarriorBurstEveryThirdAttack(t *testing.T) {
|
|
enemy := &model.Enemy{
|
|
Type: model.EnemyOrc,
|
|
Attack: 12,
|
|
Speed: 1.0,
|
|
SpecialAbilities: []model.SpecialAbility{model.AbilityBurst},
|
|
}
|
|
|
|
// Attacks 1 and 2 should have multiplier 1.0
|
|
m1 := EnemyAttackDamageMultiplier(enemy)
|
|
if m1 != 1.0 {
|
|
t.Fatalf("attack 1: expected multiplier 1.0, got %.2f", m1)
|
|
}
|
|
m2 := EnemyAttackDamageMultiplier(enemy)
|
|
if m2 != 1.0 {
|
|
t.Fatalf("attack 2: expected multiplier 1.0, got %.2f", m2)
|
|
}
|
|
|
|
// Attack 3 should deal 1.5x
|
|
m3 := EnemyAttackDamageMultiplier(enemy)
|
|
if m3 != 1.5 {
|
|
t.Fatalf("attack 3: expected multiplier 1.5, got %.2f", m3)
|
|
}
|
|
|
|
// Attack 4 back to normal
|
|
m4 := EnemyAttackDamageMultiplier(enemy)
|
|
if m4 != 1.0 {
|
|
t.Fatalf("attack 4: expected multiplier 1.0, got %.2f", m4)
|
|
}
|
|
}
|
|
|
|
func TestLightningTitanChainLightning(t *testing.T) {
|
|
enemy := &model.Enemy{
|
|
Type: model.EnemyLightningTitan,
|
|
Attack: 30,
|
|
Speed: 1.5,
|
|
SpecialAbilities: []model.SpecialAbility{model.AbilityStun, model.AbilityChainLightning},
|
|
}
|
|
|
|
// Attacks 1-5 should be normal (no chain lightning)
|
|
for i := 1; i <= 5; i++ {
|
|
m := EnemyAttackDamageMultiplier(enemy)
|
|
if m != 1.0 {
|
|
t.Fatalf("attack %d: expected multiplier 1.0, got %.2f", i, m)
|
|
}
|
|
}
|
|
|
|
// Attack 6 triggers chain lightning (3x)
|
|
m6 := EnemyAttackDamageMultiplier(enemy)
|
|
if m6 != 3.0 {
|
|
t.Fatalf("attack 6: expected multiplier 3.0, got %.2f", m6)
|
|
}
|
|
}
|
|
|
|
func TestIceGuardianAppliesIceSlow(t *testing.T) {
|
|
hero := &model.Hero{
|
|
ID: 1, HP: 100, MaxHP: 100,
|
|
Attack: 10, Defense: 5, Speed: 1.0,
|
|
Strength: 5, Constitution: 5, Agility: 5,
|
|
}
|
|
enemy := &model.Enemy{
|
|
Type: model.EnemyIceGuardian,
|
|
Attack: 14,
|
|
Defense: 15,
|
|
Speed: 0.7,
|
|
SpecialAbilities: []model.SpecialAbility{model.AbilityIceSlow},
|
|
}
|
|
|
|
now := time.Now()
|
|
applied := false
|
|
for i := 0; i < 200; i++ {
|
|
hero.HP = hero.MaxHP
|
|
hero.Debuffs = nil
|
|
ProcessEnemyAttack(hero, enemy, now)
|
|
for _, d := range hero.Debuffs {
|
|
if d.Debuff.Type == model.DebuffIceSlow {
|
|
applied = true
|
|
if d.Debuff.Magnitude != 0.20 {
|
|
t.Fatalf("IceSlow magnitude should be 0.20, got %.2f", d.Debuff.Magnitude)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if applied {
|
|
break
|
|
}
|
|
}
|
|
if !applied {
|
|
t.Fatal("Ice Guardian never applied IceSlow debuff in 200 attacks")
|
|
}
|
|
}
|
|
|
|
func TestSkeletonKingSummonDamage(t *testing.T) {
|
|
hero := &model.Hero{
|
|
ID: 1, HP: 100, MaxHP: 100,
|
|
}
|
|
enemy := &model.Enemy{
|
|
Type: model.EnemySkeletonKing,
|
|
Attack: 18,
|
|
SpecialAbilities: []model.SpecialAbility{model.AbilityRegen, model.AbilitySummon},
|
|
}
|
|
|
|
start := time.Now()
|
|
// Before 15 seconds: no summon damage.
|
|
dmg := ProcessSummonDamage(hero, enemy, start, start, start.Add(10*time.Second))
|
|
if dmg != 0 {
|
|
t.Fatalf("expected no summon damage before 15s, got %d", dmg)
|
|
}
|
|
|
|
// At 15 seconds: summon damage should occur.
|
|
dmg = ProcessSummonDamage(hero, enemy, start, start.Add(14*time.Second), start.Add(16*time.Second))
|
|
if dmg == 0 {
|
|
t.Fatal("expected summon damage after 15s boundary crossed")
|
|
}
|
|
expectedDmg := max(1, enemy.Attack/4)
|
|
if dmg != expectedDmg {
|
|
t.Fatalf("expected summon damage %d, got %d", expectedDmg, dmg)
|
|
}
|
|
}
|
|
|
|
func TestLootGenerationOnEnemyDeath(t *testing.T) {
|
|
drops := model.GenerateLoot(model.EnemyWolf, 1.0)
|
|
if len(drops) == 0 {
|
|
t.Fatal("expected at least one loot drop (gold)")
|
|
}
|
|
|
|
hasGold := false
|
|
for _, d := range drops {
|
|
if d.ItemType == "gold" {
|
|
hasGold = true
|
|
if d.GoldAmount <= 0 {
|
|
t.Fatal("gold drop should have positive amount")
|
|
}
|
|
}
|
|
}
|
|
if !hasGold {
|
|
t.Fatal("expected gold drop from GenerateLoot")
|
|
}
|
|
}
|
|
|
|
func TestLuckMultiplierWithBuff(t *testing.T) {
|
|
now := time.Now()
|
|
hero := &model.Hero{
|
|
Buffs: []model.ActiveBuff{{
|
|
Buff: mustBuffDef(model.BuffLuck),
|
|
AppliedAt: now.Add(-time.Second),
|
|
ExpiresAt: now.Add(10 * time.Second),
|
|
}},
|
|
}
|
|
|
|
mult := LuckMultiplier(hero, now)
|
|
if mult != 1.75 {
|
|
t.Fatalf("expected luck multiplier 1.75, got %.2f", mult)
|
|
}
|
|
}
|
|
|
|
func TestLuckMultiplierWithoutBuff(t *testing.T) {
|
|
hero := &model.Hero{}
|
|
mult := LuckMultiplier(hero, time.Now())
|
|
if mult != 1.0 {
|
|
t.Fatalf("expected luck multiplier 1.0 without buff, got %.1f", mult)
|
|
}
|
|
}
|
|
|
|
func TestProcessDebuffDamageAppliesPoison(t *testing.T) {
|
|
now := time.Now()
|
|
hero := &model.Hero{
|
|
HP: 100, MaxHP: 100,
|
|
Debuffs: []model.ActiveDebuff{{
|
|
Debuff: mustDebuffDef(model.DebuffPoison),
|
|
AppliedAt: now.Add(-time.Second),
|
|
ExpiresAt: now.Add(4 * time.Second),
|
|
}},
|
|
}
|
|
|
|
dmg := ProcessDebuffDamage(hero, time.Second, now)
|
|
if dmg == 0 {
|
|
t.Fatal("expected poison to deal damage over time")
|
|
}
|
|
if hero.HP >= 100 {
|
|
t.Fatal("expected hero HP to decrease from poison")
|
|
}
|
|
}
|
|
|
|
func TestDodgeAbilityCanAvoidDamage(t *testing.T) {
|
|
now := time.Now()
|
|
hero := &model.Hero{
|
|
ID: 1, HP: 100, MaxHP: 100,
|
|
Attack: 50, Defense: 0, Speed: 1.0,
|
|
Strength: 10, Agility: 5,
|
|
}
|
|
enemy := &model.Enemy{
|
|
Type: model.EnemySkeletonArcher,
|
|
MaxHP: 1000,
|
|
HP: 1000,
|
|
Attack: 10,
|
|
Defense: 0,
|
|
SpecialAbilities: []model.SpecialAbility{model.AbilityDodge},
|
|
}
|
|
|
|
dodged := false
|
|
for i := 0; i < 200; i++ {
|
|
enemy.HP = enemy.MaxHP
|
|
evt := ProcessAttack(hero, enemy, now)
|
|
if evt.Damage == 0 {
|
|
dodged = true
|
|
break
|
|
}
|
|
}
|
|
if !dodged {
|
|
t.Fatal("expected at least one dodge in 200 hero attacks against Skeleton Archer")
|
|
}
|
|
}
|
|
|
|
func TestCritChanceCapsApply(t *testing.T) {
|
|
orig := tuning.Get()
|
|
t.Cleanup(func() {
|
|
tuning.Set(orig)
|
|
})
|
|
cfg := tuning.DefaultValues()
|
|
cfg.HeroCritChanceCap = 0.10
|
|
cfg.EnemyCritChanceCap = 0.20
|
|
cfg.EnemyCriticalMinChance = 0.0
|
|
cfg.HeroBlockChancePerDefense = 0.0
|
|
cfg.HeroBlockChanceCap = 0.0
|
|
tuning.Set(cfg)
|
|
|
|
hero := &model.Hero{
|
|
ID: 1, HP: 100, MaxHP: 100,
|
|
Attack: 20, Defense: 0, Speed: 1.0,
|
|
Strength: 5, Agility: 5,
|
|
Gear: map[model.EquipmentSlot]*model.GearItem{
|
|
model.SlotMainHand: {CritChance: 0.9, StatType: "attack"},
|
|
},
|
|
}
|
|
enemy := &model.Enemy{
|
|
MaxHP: 100,
|
|
HP: 100,
|
|
Attack: 10,
|
|
Defense: 0,
|
|
Speed: 1.0,
|
|
}
|
|
|
|
rand.Seed(1)
|
|
heroEvt := ProcessAttack(hero, enemy, time.Now())
|
|
if heroEvt.IsCrit {
|
|
t.Fatalf("expected hero crit to be capped off, got crit")
|
|
}
|
|
|
|
rand.Seed(1)
|
|
enemy.CritChance = 0.9
|
|
enemyEvt := ProcessEnemyAttack(hero, enemy, time.Now())
|
|
if enemyEvt.IsCrit {
|
|
t.Fatalf("expected enemy crit to be capped off, got crit")
|
|
}
|
|
}
|
|
|
|
func TestDamageRollAppliesRange(t *testing.T) {
|
|
orig := tuning.Get()
|
|
t.Cleanup(func() {
|
|
tuning.Set(orig)
|
|
})
|
|
cfg := tuning.DefaultValues()
|
|
cfg.CombatDamageScale = 1.0
|
|
cfg.CombatDamageRollMin = 0.5
|
|
cfg.CombatDamageRollMax = 0.5
|
|
tuning.Set(cfg)
|
|
|
|
rand.Seed(1)
|
|
breakdown := calculateDamageBreakdown(10, 0, 0)
|
|
if breakdown.RawDamage != 5 || breakdown.FinalDamage != 5 {
|
|
t.Fatalf("expected roll to halve damage to 5, got raw=%d final=%d", breakdown.RawDamage, breakdown.FinalDamage)
|
|
}
|
|
}
|
|
|
|
func mustBuffDef(bt model.BuffType) model.Buff {
|
|
b, ok := model.BuffDefinition(bt)
|
|
if !ok {
|
|
panic("missing buff def: " + string(bt))
|
|
}
|
|
return b
|
|
}
|
|
|
|
func mustDebuffDef(dt model.DebuffType) model.Debuff {
|
|
d, ok := model.DebuffDefinition(dt)
|
|
if !ok {
|
|
panic("missing debuff def: " + string(dt))
|
|
}
|
|
return d
|
|
}
|