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.

316 lines
8.1 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package model
import (
"math"
"testing"
"time"
)
func TestDerivedCombatStatsFromBaseAttributes(t *testing.T) {
now := time.Now()
hero := &Hero{
Attack: 10,
Defense: 5,
Speed: 1.0,
Strength: 10,
Constitution: 8,
Agility: 6,
Gear: map[EquipmentSlot]*GearItem{
SlotMainHand: {
PrimaryStat: 5,
SpeedModifier: 1.3,
},
SlotChest: {
PrimaryStat: 4,
SpeedModifier: 0.7,
AgilityBonus: -3,
},
},
}
gotAttack := hero.EffectiveAttackAt(now)
// atk = 10 + 10*2 + 3/4 + 8/8 = 31 + weapon.PrimaryStat(5) = 36
if gotAttack != 36 {
t.Fatalf("expected attack 36, got %d", gotAttack)
}
gotDefense := hero.EffectiveDefenseAt(now)
// def = 5 + 8/4 + 3/8 = 7 + chest.PrimaryStat(4) = 11
if gotDefense != 11 {
t.Fatalf("expected defense 11, got %d", gotDefense)
}
gotSpeed := hero.EffectiveSpeedAt(now)
wantSpeed := (1.0 + 3*AgilityCoef) * 1.3 * 0.7
if math.Abs(gotSpeed-wantSpeed) > 0.001 {
t.Fatalf("expected speed %.3f, got %.3f", wantSpeed, gotSpeed)
}
}
func TestBuffsProvideTemporaryStatEffects(t *testing.T) {
now := time.Now()
hero := &Hero{
Attack: 10,
Defense: 5,
Speed: 1.0,
Strength: 10,
Constitution: 8,
Agility: 6,
Buffs: []ActiveBuff{
{
Buff: DefaultBuffs[BuffRage],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(5 * time.Second),
},
{
Buff: DefaultBuffs[BuffWarCry],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(5 * time.Second),
},
{
Buff: DefaultBuffs[BuffShield],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(5 * time.Second),
},
},
}
if hero.EffectiveAttackAt(now) <= 30 {
t.Fatalf("expected buffed attack to increase above baseline")
}
if hero.EffectiveDefenseAt(now) <= 5 {
t.Fatalf("expected shield constitution bonus to increase defense")
}
if hero.EffectiveSpeedAt(now) <= 1.0 {
t.Fatalf("expected war cry to increase attack speed")
}
}
func TestEffectiveSpeedIsCapped(t *testing.T) {
now := time.Now()
hero := &Hero{
Speed: 2.5,
Agility: 200,
Gear: map[EquipmentSlot]*GearItem{
SlotMainHand: {SpeedModifier: 1.5},
},
Buffs: []ActiveBuff{
{
Buff: DefaultBuffs[BuffWarCry],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(10 * time.Second),
},
},
}
got := hero.EffectiveSpeedAt(now)
if got != MaxAttackSpeed {
t.Fatalf("expected speed cap %.1f, got %.3f", MaxAttackSpeed, got)
}
}
func TestRushDoesNotAffectAttackSpeed(t *testing.T) {
now := time.Now()
hero := &Hero{Speed: 1.0, Agility: 5}
baseSpeed := hero.EffectiveSpeedAt(now)
hero.Buffs = []ActiveBuff{{
Buff: DefaultBuffs[BuffRush],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(5 * time.Second),
}}
buffedSpeed := hero.EffectiveSpeedAt(now)
if math.Abs(buffedSpeed-baseSpeed) > 0.001 {
t.Fatalf("Rush should not affect attack speed: base=%.3f, buffed=%.3f", baseSpeed, buffedSpeed)
}
}
func TestRushAffectsMovementSpeed(t *testing.T) {
now := time.Now()
hero := &Hero{Speed: 1.0, Agility: 5}
baseMoveSpeed := hero.MovementSpeedMultiplier(now)
if baseMoveSpeed != 1.0 {
t.Fatalf("expected base movement multiplier 1.0, got %.3f", baseMoveSpeed)
}
hero.Buffs = []ActiveBuff{{
Buff: DefaultBuffs[BuffRush],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(5 * time.Second),
}}
got := hero.MovementSpeedMultiplier(now)
want := 1.5
if math.Abs(got-want) > 0.001 {
t.Fatalf("expected Rush to give movement multiplier %.1f, got %.3f", want, got)
}
}
func TestSlowDoesNotAffectAttackSpeed(t *testing.T) {
now := time.Now()
hero := &Hero{Speed: 1.0, Agility: 5}
baseSpeed := hero.EffectiveSpeedAt(now)
hero.Debuffs = []ActiveDebuff{{
Debuff: DefaultDebuffs[DebuffSlow],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(3 * time.Second),
}}
debuffedSpeed := hero.EffectiveSpeedAt(now)
if math.Abs(debuffedSpeed-baseSpeed) > 0.001 {
t.Fatalf("Slow should not affect attack speed: base=%.3f, debuffed=%.3f", baseSpeed, debuffedSpeed)
}
}
func TestSlowAffectsMovementSpeed(t *testing.T) {
now := time.Now()
hero := &Hero{Speed: 1.0, Agility: 5}
hero.Debuffs = []ActiveDebuff{{
Debuff: DefaultDebuffs[DebuffSlow],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(3 * time.Second),
}}
got := hero.MovementSpeedMultiplier(now)
want := 0.6 // 1.0 * (1 - 0.4)
if math.Abs(got-want) > 0.001 {
t.Fatalf("expected Slow to give movement multiplier %.1f, got %.3f", want, got)
}
}
func TestIceSlowReducesAttackSpeed(t *testing.T) {
now := time.Now()
hero := &Hero{Speed: 1.0, Agility: 5}
baseSpeed := hero.EffectiveSpeedAt(now)
hero.Debuffs = []ActiveDebuff{{
Debuff: DefaultDebuffs[DebuffIceSlow],
AppliedAt: now.Add(-time.Second),
ExpiresAt: now.Add(3 * time.Second),
}}
debuffedSpeed := hero.EffectiveSpeedAt(now)
want := baseSpeed * 0.8
if math.Abs(debuffedSpeed-want) > 0.01 {
t.Fatalf("IceSlow should reduce attack speed by 20%%: expected %.3f, got %.3f", want, debuffedSpeed)
}
}
func TestLevelUpParity(t *testing.T) {
hero1 := &Hero{
Level: 1, XP: 500,
MaxHP: 100, HP: 100,
Attack: 10, Defense: 5, Speed: 1.0,
Strength: 5, Constitution: 5, Agility: 5, Luck: 5,
}
hero2 := *hero1
// Level up hero1 via LevelUp()
levels1 := 0
for hero1.LevelUp() {
levels1++
}
// Level up hero2 via LevelUp() too (should be identical since both use same method)
levels2 := 0
for hero2.LevelUp() {
levels2++
}
if levels1 != levels2 {
t.Fatalf("expected same levels gained: %d vs %d", levels1, levels2)
}
if hero1.Level != hero2.Level || hero1.MaxHP != hero2.MaxHP ||
hero1.Attack != hero2.Attack || hero1.Defense != hero2.Defense {
t.Fatalf("heroes diverged after identical LevelUp calls")
}
}
// v3 progression: XP bases ×10 vs v2; secondary-stat cadences ×10 (3→30, 4→40, …).
func TestProgressionV3CanonicalSnapshots(t *testing.T) {
now := time.Now()
snap := func(targetLevel int) *Hero {
h := &Hero{
Level: 1, XP: 0,
MaxHP: 100, HP: 100,
Attack: 10, Defense: 5, Speed: 1.0,
Strength: 1, Constitution: 1, Agility: 1, Luck: 1,
}
for h.Level < targetLevel {
h.XP += XPToNextLevel(h.Level)
if !h.LevelUp() {
t.Fatalf("expected level-up before reaching target level %d", targetLevel)
}
}
h.RefreshDerivedCombatStats(now)
return h
}
t.Run("L30", func(t *testing.T) {
h := snap(30)
if h.MaxHP != 103 || h.Attack != 11 || h.Defense != 6 || h.Strength != 1 {
t.Fatalf("L30 snapshot: maxHp=%d atk=%d def=%d str=%d", h.MaxHP, h.Attack, h.Defense, h.Strength)
}
if h.EffectiveAttackAt(now) != 13 || h.EffectiveDefenseAt(now) != 6 {
t.Fatalf("L30 derived: atkPow=%d defPow=%d", h.EffectiveAttackAt(now), h.EffectiveDefenseAt(now))
}
})
t.Run("L45", func(t *testing.T) {
h := snap(45)
if h.MaxHP != 104 || h.Attack != 11 || h.Defense != 6 || h.Strength != 2 {
t.Fatalf("L45 snapshot: maxHp=%d atk=%d def=%d str=%d", h.MaxHP, h.Attack, h.Defense, h.Strength)
}
if h.EffectiveAttackAt(now) != 15 || h.EffectiveDefenseAt(now) != 6 {
t.Fatalf("L45 derived: atkPow=%d defPow=%d", h.EffectiveAttackAt(now), h.EffectiveDefenseAt(now))
}
})
}
func TestXPToNextLevelFormula(t *testing.T) {
if got := XPToNextLevel(1); got != 180 {
t.Fatalf("XPToNextLevel(1) = %d, want 180", got)
}
if got := XPToNextLevel(2); got != 230 {
t.Fatalf("XPToNextLevel(2) = %d, want 230", got)
}
if got := XPToNextLevel(10); got != 1450 {
t.Fatalf("XPToNextLevel(10) = %d, want 1450", got)
}
if got := XPToNextLevel(30); got != 23000 {
t.Fatalf("XPToNextLevel(30) = %d, want 23000", got)
}
}
func TestLevelUpDoesNotRestoreHP(t *testing.T) {
hero := &Hero{
Level: 1,
XP: 200,
HP: 40,
MaxHP: 100,
Attack: 10,
Defense: 5,
Speed: 1.0,
Strength: 5,
Constitution: 5,
Agility: 5,
Luck: 5,
}
if !hero.LevelUp() {
t.Fatal("expected hero to level up")
}
if hero.HP == hero.MaxHP {
t.Fatalf("level-up should NOT restore HP (spec §3.3): hp=%d max=%d", hero.HP, hero.MaxHP)
}
if hero.HP != 40 {
t.Fatalf("HP should be unchanged after level-up: got %d, want 40", hero.HP)
}
}