diff --git a/combat/combat_test.go b/combat/combat_test.go index 388c1af..ee9c47a 100644 --- a/combat/combat_test.go +++ b/combat/combat_test.go @@ -76,3 +76,63 @@ func TestFleeChance(t *testing.T) { t.Errorf("Flee success rate suspicious: %d/100", successes) } } + +func TestMonsterAIBossAoE(t *testing.T) { + boss := &entity.Monster{Name: "Boss", HP: 100, IsBoss: true} + players := []*entity.Player{entity.NewPlayer("P1", entity.ClassWarrior)} + + // Turn 0 should NOT AoE + _, isAoE := MonsterAI(boss, players, 0) + if isAoE { + t.Error("boss should not AoE on turn 0") + } + // Turn 3 should AoE + _, isAoE = MonsterAI(boss, players, 3) + if !isAoE { + t.Error("boss should AoE on turn 3") + } + // Turn 6 should AoE + _, isAoE = MonsterAI(boss, players, 6) + if !isAoE { + t.Error("boss should AoE on turn 6") + } +} + +func TestMonsterAILowestHP(t *testing.T) { + p1 := entity.NewPlayer("Tank", entity.ClassWarrior) // 120 HP + p2 := entity.NewPlayer("Mage", entity.ClassMage) // 70 HP + p2.HP = 10 // very low + + // Run many times — at least some should target p2 (30% chance) + targetedLow := 0 + for i := 0; i < 100; i++ { + m := &entity.Monster{Name: "Orc", HP: 50} + idx, _ := MonsterAI(m, []*entity.Player{p1, p2}, 1) + if idx == 1 { + targetedLow++ + } + } + // Should target low HP player roughly 30% of time + if targetedLow < 10 || targetedLow > 60 { + t.Errorf("lowest HP targeting out of expected range: %d/100", targetedLow) + } +} + +func TestCalcDamageWithMultiplier(t *testing.T) { + // AoE multiplier 0.8: ATK=20, DEF=5, mult=0.8 → base = 20*0.8 - 5 = 11 + // Range: 11 * 0.85 to 11 * 1.15 = ~9.35 to ~12.65 + for i := 0; i < 50; i++ { + dmg := CalcDamage(20, 5, 0.8) + if dmg < 9 || dmg > 13 { + t.Errorf("AoE damage %d out of expected range 9-13", dmg) + } + } +} + +func TestCalcDamageHighDEF(t *testing.T) { + // When DEF > ATK*mult, should deal minimum 1 damage + dmg := CalcDamage(5, 100, 1.0) + if dmg != 1 { + t.Errorf("expected minimum damage 1, got %d", dmg) + } +} diff --git a/entity/monster_test.go b/entity/monster_test.go index c9f6c30..604cd7c 100644 --- a/entity/monster_test.go +++ b/entity/monster_test.go @@ -36,3 +36,23 @@ func TestMonsterDEFScaling(t *testing.T) { t.Errorf("Boss5 DEF should be base 8, got %d", boss.DEF) } } + +func TestTickTaunt(t *testing.T) { + m := &Monster{Name: "Orc", HP: 50, TauntTarget: true, TauntTurns: 2} + m.TickTaunt() + if m.TauntTurns != 1 || !m.TauntTarget { + t.Error("should still be taunted with 1 turn left") + } + m.TickTaunt() + if m.TauntTurns != 0 || m.TauntTarget { + t.Error("taunt should be cleared at 0") + } +} + +func TestMonsterAtMinFloor(t *testing.T) { + // Slime at floor 1 (minFloor=1) should have base stats + m := NewMonster(MonsterSlime, 1) + if m.HP != 20 || m.ATK != 5 || m.DEF != 1 { + t.Errorf("Slime at min floor should be base stats, got HP=%d ATK=%d DEF=%d", m.HP, m.ATK, m.DEF) + } +} diff --git a/entity/player_test.go b/entity/player_test.go index 35e504e..6b55cfe 100644 --- a/entity/player_test.go +++ b/entity/player_test.go @@ -63,3 +63,130 @@ func TestPlayerTakeDamage(t *testing.T) { t.Error("Player should be dead") } } + +func TestIsOut(t *testing.T) { + p := NewPlayer("test", ClassWarrior) + if p.IsOut() { + t.Error("alive player should not be out") + } + p.Dead = true + if !p.IsOut() { + t.Error("dead player should be out") + } + p.Dead = false + p.Fled = true + if !p.IsOut() { + t.Error("fled player should be out") + } +} + +func TestRevive(t *testing.T) { + p := NewPlayer("test", ClassWarrior) // 120 MaxHP + p.TakeDamage(200) + if !p.IsDead() { + t.Error("should be dead") + } + p.Revive(0.30) + if p.IsDead() { + t.Error("should be alive after revive") + } + if p.HP != 36 { // 120 * 0.30 + t.Errorf("HP should be 36, got %d", p.HP) + } +} + +func TestHealCap(t *testing.T) { + p := NewPlayer("test", ClassWarrior) // 120 HP + p.HP = 100 + p.Heal(50) // should cap at 120 + if p.HP != 120 { + t.Errorf("HP should cap at 120, got %d", p.HP) + } +} + +func TestEffectiveATKWithItems(t *testing.T) { + p := NewPlayer("test", ClassWarrior) // base ATK 12 + p.Inventory = append(p.Inventory, Item{Name: "Sword", Type: ItemWeapon, Bonus: 5}) + p.Inventory = append(p.Inventory, Item{Name: "Sword2", Type: ItemWeapon, Bonus: 3}) + if p.EffectiveATK() != 20 { // 12 + 5 + 3 + t.Errorf("ATK should be 20, got %d", p.EffectiveATK()) + } +} + +func TestEffectiveDEFWithItems(t *testing.T) { + p := NewPlayer("test", ClassWarrior) // base DEF 8 + p.Inventory = append(p.Inventory, Item{Name: "Shield", Type: ItemArmor, Bonus: 4}) + if p.EffectiveDEF() != 12 { // 8 + 4 + t.Errorf("DEF should be 12, got %d", p.EffectiveDEF()) + } +} + +func TestStatusEffectPoison(t *testing.T) { + p := NewPlayer("test", ClassWarrior) // 120 HP + p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 2, Value: 10}) + if !p.HasEffect(StatusPoison) { + t.Error("should have poison") + } + msgs := p.TickEffects() + if len(msgs) != 1 { + t.Errorf("expected 1 message, got %d", len(msgs)) + } + if p.HP != 110 { + t.Errorf("HP should be 110 after poison tick, got %d", p.HP) + } + // Poison can't kill + p.HP = 5 + p.TickEffects() // duration expires after this tick + if p.HP != 1 { + t.Errorf("poison should leave at 1 HP, got %d", p.HP) + } + if p.IsDead() { + t.Error("poison should not kill") + } + if p.HasEffect(StatusPoison) { + t.Error("poison should have expired") + } +} + +func TestStatusEffectBurn(t *testing.T) { + p := NewPlayer("test", ClassMage) // 70 HP + p.AddEffect(ActiveEffect{Type: StatusBurn, Duration: 1, Value: 100}) + p.TickEffects() + if !p.IsDead() { + t.Error("burn should be able to kill") + } +} + +func TestRelicPoisonImmunity(t *testing.T) { + p := NewPlayer("test", ClassWarrior) + p.Relics = append(p.Relics, Relic{Name: "Antidote", Effect: RelicPoisonImmunity}) + p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 3, Value: 10}) + if p.HasEffect(StatusPoison) { + t.Error("should be immune to poison") + } +} + +func TestRelicBurnResist(t *testing.T) { + p := NewPlayer("test", ClassWarrior) + p.Relics = append(p.Relics, Relic{Name: "Flame Guard", Effect: RelicBurnResist}) + p.AddEffect(ActiveEffect{Type: StatusBurn, Duration: 2, Value: 10}) + // Burn value should be halved to 5 + if len(p.Effects) == 0 { + t.Fatal("should have burn effect (resisted, not immune)") + } + if p.Effects[0].Value != 5 { + t.Errorf("burn value should be halved to 5, got %d", p.Effects[0].Value) + } +} + +func TestEffectOverwrite(t *testing.T) { + p := NewPlayer("test", ClassWarrior) + p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 1, Value: 5}) + p.AddEffect(ActiveEffect{Type: StatusPoison, Duration: 3, Value: 10}) // should overwrite + if len(p.Effects) != 1 { + t.Errorf("should have 1 effect, got %d", len(p.Effects)) + } + if p.Effects[0].Duration != 3 || p.Effects[0].Value != 10 { + t.Error("should have overwritten with new values") + } +}