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") } }