first commit
This commit is contained in:
239
internal/combat/combat_test.go
Normal file
239
internal/combat/combat_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package combat
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"a301_game_server/internal/entity"
|
||||
"a301_game_server/pkg/mathutil"
|
||||
pb "a301_game_server/proto/gen/pb"
|
||||
)
|
||||
|
||||
// mockCombatant implements Combatant for testing.
|
||||
type mockCombatant struct {
|
||||
id uint64
|
||||
pos mathutil.Vec3
|
||||
hp, maxHP int32
|
||||
mp, maxMP int32
|
||||
str, dex, intStat int32
|
||||
}
|
||||
|
||||
func (m *mockCombatant) EntityID() uint64 { return m.id }
|
||||
func (m *mockCombatant) EntityType() entity.Type { return entity.TypePlayer }
|
||||
func (m *mockCombatant) Position() mathutil.Vec3 { return m.pos }
|
||||
func (m *mockCombatant) SetPosition(p mathutil.Vec3) { m.pos = p }
|
||||
func (m *mockCombatant) Rotation() float32 { return 0 }
|
||||
func (m *mockCombatant) SetRotation(float32) {}
|
||||
func (m *mockCombatant) ToProto() *pb.EntityState { return &pb.EntityState{EntityId: m.id} }
|
||||
func (m *mockCombatant) HP() int32 { return m.hp }
|
||||
func (m *mockCombatant) SetHP(hp int32) {
|
||||
if hp < 0 { hp = 0 }
|
||||
if hp > m.maxHP { hp = m.maxHP }
|
||||
m.hp = hp
|
||||
}
|
||||
func (m *mockCombatant) MaxHP() int32 { return m.maxHP }
|
||||
func (m *mockCombatant) MP() int32 { return m.mp }
|
||||
func (m *mockCombatant) SetMP(mp int32) {
|
||||
if mp < 0 { mp = 0 }
|
||||
m.mp = mp
|
||||
}
|
||||
func (m *mockCombatant) IsAlive() bool { return m.hp > 0 }
|
||||
func (m *mockCombatant) Stats() CombatStats {
|
||||
return CombatStats{Str: m.str, Dex: m.dex, Int: m.intStat, Level: 1}
|
||||
}
|
||||
|
||||
func newMock(id uint64, x, z float32) *mockCombatant {
|
||||
return &mockCombatant{
|
||||
id: id, pos: mathutil.NewVec3(x, 0, z),
|
||||
hp: 100, maxHP: 100, mp: 100, maxMP: 100,
|
||||
str: 10, dex: 10, intStat: 10,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAttack(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
attacker := newMock(1, 0, 0)
|
||||
target := newMock(2, 2, 0) // within range (3.0)
|
||||
|
||||
entities := map[uint64]*mockCombatant{1: attacker, 2: target}
|
||||
|
||||
ok, errMsg := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{},
|
||||
func(id uint64) entity.Entity {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("expected success, got error: %s", errMsg)
|
||||
}
|
||||
|
||||
if target.HP() >= 100 {
|
||||
t.Error("target should have taken damage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutOfRange(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
attacker := newMock(1, 0, 0)
|
||||
target := newMock(2, 100, 0) // far away
|
||||
|
||||
entities := map[uint64]*mockCombatant{1: attacker, 2: target}
|
||||
|
||||
ok, _ := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{},
|
||||
func(id uint64) entity.Entity {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
if ok {
|
||||
t.Error("expected failure due to range")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCooldown(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
attacker := newMock(1, 0, 0)
|
||||
target := newMock(2, 2, 0)
|
||||
|
||||
entities := map[uint64]*mockCombatant{1: attacker, 2: target}
|
||||
getEnt := func(id uint64) entity.Entity {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
}
|
||||
|
||||
// First use should succeed.
|
||||
ok, _ := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{}, getEnt, nil)
|
||||
if !ok {
|
||||
t.Fatal("first use should succeed")
|
||||
}
|
||||
|
||||
// Immediate second use should fail (cooldown).
|
||||
ok, errMsg := mgr.UseSkill(attacker, 1, 2, mathutil.Vec3{}, getEnt, nil)
|
||||
if ok {
|
||||
t.Error("expected cooldown failure")
|
||||
}
|
||||
if errMsg != "skill on cooldown" {
|
||||
t.Errorf("expected 'skill on cooldown', got '%s'", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManaConsumption(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
caster := newMock(1, 0, 0)
|
||||
target := newMock(2, 5, 0)
|
||||
caster.mp = 10 // not enough for Fireball (20 mana)
|
||||
|
||||
entities := map[uint64]*mockCombatant{1: caster, 2: target}
|
||||
|
||||
ok, errMsg := mgr.UseSkill(caster, 2, 2, mathutil.Vec3{},
|
||||
func(id uint64) entity.Entity {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
if ok {
|
||||
t.Error("expected mana failure")
|
||||
}
|
||||
if errMsg != "not enough mana" {
|
||||
t.Errorf("expected 'not enough mana', got '%s'", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealSelf(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
caster := newMock(1, 0, 0)
|
||||
caster.hp = 30
|
||||
|
||||
ok, _ := mgr.UseSkill(caster, 3, 0, mathutil.Vec3{},
|
||||
func(id uint64) entity.Entity { return nil },
|
||||
nil,
|
||||
)
|
||||
|
||||
if !ok {
|
||||
t.Fatal("heal should succeed")
|
||||
}
|
||||
if caster.HP() <= 30 {
|
||||
t.Error("HP should have increased")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoisonDoT(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
mgr.SetBroadcast(func(entity.Entity, uint16, interface{}) {}, func(uint64, uint16, interface{}) {})
|
||||
|
||||
attacker := newMock(1, 0, 0)
|
||||
target := newMock(2, 5, 0)
|
||||
|
||||
entities := map[uint64]*mockCombatant{1: attacker, 2: target}
|
||||
|
||||
// Apply poison (skill 5).
|
||||
ok, _ := mgr.UseSkill(attacker, 5, 2, mathutil.Vec3{},
|
||||
func(id uint64) entity.Entity {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("poison should succeed")
|
||||
}
|
||||
|
||||
initialHP := target.HP()
|
||||
|
||||
// Simulate ticks until first DoT tick (2s at 50ms/tick = 40 ticks).
|
||||
for i := 0; i < 40; i++ {
|
||||
mgr.UpdateBuffs(50*time.Millisecond, func(id uint64) Combatant {
|
||||
if e, ok := entities[id]; ok { return e }
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if target.HP() >= initialHP {
|
||||
t.Error("poison DoT should have dealt damage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDamageFormula(t *testing.T) {
|
||||
// Base 15, attacker STR 10 (1.1x), defender DEX 10 (0.95x)
|
||||
// Expected ~15.675 (without crit).
|
||||
result := CalcDamage(15, 10, 10)
|
||||
if result.FinalDamage < 1 {
|
||||
t.Error("damage should be at least 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRespawn(t *testing.T) {
|
||||
mgr := NewManager()
|
||||
|
||||
ent := newMock(1, 50, 50)
|
||||
ent.hp = 0
|
||||
|
||||
spawn := mathutil.NewVec3(0, 0, 0)
|
||||
mgr.Respawn(ent, spawn)
|
||||
|
||||
if !ent.IsAlive() {
|
||||
t.Error("should be alive after respawn")
|
||||
}
|
||||
if ent.HP() != ent.MaxHP() {
|
||||
t.Error("should have full HP after respawn")
|
||||
}
|
||||
if ent.Position() != spawn {
|
||||
t.Error("should be at spawn position")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user