240 lines
6.0 KiB
Go
240 lines
6.0 KiB
Go
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")
|
|
}
|
|
}
|