Files
a301_game_server/internal/ai/behavior_test.go
2026-02-26 17:52:48 +09:00

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