222 lines
5.8 KiB
Go
222 lines
5.8 KiB
Go
package ai
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"a301_game_server/internal/combat"
|
|
"a301_game_server/internal/entity"
|
|
"a301_game_server/pkg/mathutil"
|
|
pb "a301_game_server/proto/gen/pb"
|
|
)
|
|
|
|
type mockPlayer struct {
|
|
id uint64
|
|
pos mathutil.Vec3
|
|
hp int32
|
|
maxHP int32
|
|
alive bool
|
|
}
|
|
|
|
func (m *mockPlayer) EntityID() uint64 { return m.id }
|
|
func (m *mockPlayer) EntityType() entity.Type { return entity.TypePlayer }
|
|
func (m *mockPlayer) Position() mathutil.Vec3 { return m.pos }
|
|
func (m *mockPlayer) SetPosition(p mathutil.Vec3) { m.pos = p }
|
|
func (m *mockPlayer) Rotation() float32 { return 0 }
|
|
func (m *mockPlayer) SetRotation(float32) {}
|
|
func (m *mockPlayer) ToProto() *pb.EntityState { return &pb.EntityState{EntityId: m.id} }
|
|
func (m *mockPlayer) HP() int32 { return m.hp }
|
|
func (m *mockPlayer) MaxHP() int32 { return m.maxHP }
|
|
func (m *mockPlayer) SetHP(hp int32) { m.hp = hp; m.alive = hp > 0 }
|
|
func (m *mockPlayer) MP() int32 { return 100 }
|
|
func (m *mockPlayer) SetMP(int32) {}
|
|
func (m *mockPlayer) IsAlive() bool { return m.alive }
|
|
func (m *mockPlayer) Stats() combat.CombatStats { return combat.CombatStats{Str: 10, Dex: 10, Int: 10, Level: 1} }
|
|
|
|
type mockProvider struct {
|
|
entities map[uint64]entity.Entity
|
|
}
|
|
|
|
func (p *mockProvider) GetEntity(id uint64) entity.Entity {
|
|
return p.entities[id]
|
|
}
|
|
|
|
func (p *mockProvider) GetPlayersInRange(center mathutil.Vec3, radius float32) []entity.Entity {
|
|
radiusSq := radius * radius
|
|
var result []entity.Entity
|
|
for _, e := range p.entities {
|
|
if e.EntityType() == entity.TypePlayer {
|
|
if e.Position().DistanceSqTo(center) <= radiusSq {
|
|
result = append(result, e)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type mockSkillUser struct {
|
|
called bool
|
|
}
|
|
|
|
func (s *mockSkillUser) UseSkill(casterID uint64, skillID uint32, targetID uint64, targetPos mathutil.Vec3) (bool, string) {
|
|
s.called = true
|
|
return true, ""
|
|
}
|
|
|
|
func newTestMob() *Mob {
|
|
def := &MobDef{
|
|
ID: 1,
|
|
Name: "TestMob",
|
|
Level: 1,
|
|
HP: 100,
|
|
MP: 0,
|
|
Str: 10,
|
|
Dex: 5,
|
|
Int: 3,
|
|
MoveSpeed: 5.0,
|
|
AggroRange: 10.0,
|
|
AttackRange: 2.5,
|
|
AttackSkill: 1,
|
|
LeashRange: 30.0,
|
|
}
|
|
return NewMob(1000, def, mathutil.NewVec3(50, 0, 50))
|
|
}
|
|
|
|
func TestIdleToChase(t *testing.T) {
|
|
mob := newTestMob()
|
|
player := &mockPlayer{id: 1, pos: mathutil.NewVec3(55, 0, 50), hp: 100, maxHP: 100, alive: true}
|
|
|
|
provider := &mockProvider{
|
|
entities: map[uint64]entity.Entity{1: player, 1000: mob},
|
|
}
|
|
skills := &mockSkillUser{}
|
|
|
|
// Player is within aggro range (5 units < 10 aggro range).
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if mob.State() != StateChase {
|
|
t.Errorf("expected StateChase, got %d", mob.State())
|
|
}
|
|
if mob.TargetID() != 1 {
|
|
t.Errorf("expected target 1, got %d", mob.TargetID())
|
|
}
|
|
}
|
|
|
|
func TestChaseToAttack(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetState(StateChase)
|
|
mob.SetTargetID(1)
|
|
|
|
// Place player within attack range.
|
|
player := &mockPlayer{id: 1, pos: mathutil.NewVec3(52, 0, 50), hp: 100, maxHP: 100, alive: true}
|
|
|
|
provider := &mockProvider{
|
|
entities: map[uint64]entity.Entity{1: player, 1000: mob},
|
|
}
|
|
skills := &mockSkillUser{}
|
|
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if mob.State() != StateAttack {
|
|
t.Errorf("expected StateAttack, got %d", mob.State())
|
|
}
|
|
}
|
|
|
|
func TestAttackUsesSkill(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetState(StateAttack)
|
|
mob.SetTargetID(1)
|
|
|
|
player := &mockPlayer{id: 1, pos: mathutil.NewVec3(52, 0, 50), hp: 100, maxHP: 100, alive: true}
|
|
|
|
provider := &mockProvider{
|
|
entities: map[uint64]entity.Entity{1: player, 1000: mob},
|
|
}
|
|
skills := &mockSkillUser{}
|
|
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if !skills.called {
|
|
t.Error("expected skill to be used")
|
|
}
|
|
}
|
|
|
|
func TestLeashReturn(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetState(StateChase)
|
|
mob.SetTargetID(1)
|
|
|
|
// Move mob far from spawn.
|
|
mob.SetPosition(mathutil.NewVec3(100, 0, 100)) // >30 units from spawn(50,0,50)
|
|
|
|
player := &mockPlayer{id: 1, pos: mathutil.NewVec3(110, 0, 110), hp: 100, maxHP: 100, alive: true}
|
|
|
|
provider := &mockProvider{
|
|
entities: map[uint64]entity.Entity{1: player, 1000: mob},
|
|
}
|
|
skills := &mockSkillUser{}
|
|
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if mob.State() != StateReturn {
|
|
t.Errorf("expected StateReturn (leash), got %d", mob.State())
|
|
}
|
|
}
|
|
|
|
func TestReturnToIdle(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetState(StateReturn)
|
|
mob.SetPosition(mathutil.NewVec3(50.1, 0, 50.1)) // very close to spawn
|
|
|
|
skills := &mockSkillUser{}
|
|
provider := &mockProvider{entities: map[uint64]entity.Entity{1000: mob}}
|
|
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if mob.State() != StateIdle {
|
|
t.Errorf("expected StateIdle after return, got %d", mob.State())
|
|
}
|
|
if mob.HP() != mob.MaxHP() {
|
|
t.Error("mob should heal to full on return")
|
|
}
|
|
}
|
|
|
|
func TestTargetDiesReturnToSpawn(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetState(StateChase)
|
|
mob.SetTargetID(1)
|
|
|
|
// Target is dead.
|
|
player := &mockPlayer{id: 1, pos: mathutil.NewVec3(55, 0, 50), hp: 0, maxHP: 100, alive: false}
|
|
|
|
provider := &mockProvider{
|
|
entities: map[uint64]entity.Entity{1: player, 1000: mob},
|
|
}
|
|
skills := &mockSkillUser{}
|
|
|
|
UpdateMob(mob, 50*time.Millisecond, provider, skills)
|
|
|
|
if mob.State() != StateReturn {
|
|
t.Errorf("expected StateReturn when target dies, got %d", mob.State())
|
|
}
|
|
}
|
|
|
|
func TestMobReset(t *testing.T) {
|
|
mob := newTestMob()
|
|
mob.SetHP(0)
|
|
mob.SetPosition(mathutil.NewVec3(100, 0, 100))
|
|
mob.SetState(StateDead)
|
|
|
|
mob.Reset()
|
|
|
|
if mob.HP() != mob.MaxHP() {
|
|
t.Error("HP should be full after reset")
|
|
}
|
|
if mob.Position() != mob.SpawnPos() {
|
|
t.Error("position should be spawn pos after reset")
|
|
}
|
|
if mob.State() != StateIdle {
|
|
t.Error("state should be Idle after reset")
|
|
}
|
|
}
|