package world import ( "testing" "a301_game_server/internal/entity" "a301_game_server/pkg/mathutil" pb "a301_game_server/proto/gen/pb" ) // mockEntity is a minimal entity for testing. type mockEntity struct { id uint64 pos mathutil.Vec3 } func (m *mockEntity) EntityID() uint64 { return m.id } func (m *mockEntity) EntityType() entity.Type { return entity.TypePlayer } func (m *mockEntity) Position() mathutil.Vec3 { return m.pos } func (m *mockEntity) SetPosition(p mathutil.Vec3) { m.pos = p } func (m *mockEntity) Rotation() float32 { return 0 } func (m *mockEntity) SetRotation(float32) {} func (m *mockEntity) ToProto() *pb.EntityState { return &pb.EntityState{EntityId: m.id} } func TestBroadcastAllAOI_GetNearby(t *testing.T) { aoi := NewBroadcastAllAOI() e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)} e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(100, 0, 100)} e3 := &mockEntity{id: 3, pos: mathutil.NewVec3(999, 0, 999)} aoi.Add(e1) aoi.Add(e2) aoi.Add(e3) // With broadcast-all, everyone sees everyone. nearby := aoi.GetNearby(e1) if len(nearby) != 2 { t.Errorf("expected 2 nearby, got %d", len(nearby)) } } func TestBroadcastAllAOI_Remove(t *testing.T) { aoi := NewBroadcastAllAOI() e1 := &mockEntity{id: 1} e2 := &mockEntity{id: 2} aoi.Add(e1) aoi.Add(e2) events := aoi.Remove(e1) if len(events) != 1 { t.Errorf("expected 1 leave event, got %d", len(events)) } if events[0].Type != AOILeave { t.Errorf("expected AOILeave event") } nearby := aoi.GetNearby(e2) if len(nearby) != 0 { t.Errorf("expected 0 nearby after removal, got %d", len(nearby)) } } func TestGridAOI_NearbyInSameCell(t *testing.T) { aoi := NewGridAOI(50, 2) e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(10, 0, 10)} e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(20, 0, 20)} aoi.Add(e1) aoi.Add(e2) nearby := aoi.GetNearby(e1) if len(nearby) != 1 { t.Errorf("expected 1 nearby, got %d", len(nearby)) } if nearby[0].EntityID() != 2 { t.Errorf("expected entity 2, got %d", nearby[0].EntityID()) } } func TestGridAOI_FarAwayNotVisible(t *testing.T) { aoi := NewGridAOI(50, 1) // viewRange=1 means 3x3 grid = 150 units visibility e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)} e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(500, 0, 500)} // far away aoi.Add(e1) aoi.Add(e2) nearby := aoi.GetNearby(e1) if len(nearby) != 0 { t.Errorf("expected 0 nearby for far entity, got %d", len(nearby)) } } func TestGridAOI_MoveGeneratesEvents(t *testing.T) { aoi := NewGridAOI(50, 1) e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)} e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(200, 0, 200)} aoi.Add(e1) aoi.Add(e2) // Initially not visible to each other. nearby := aoi.GetNearby(e1) if len(nearby) != 0 { t.Fatalf("expected not visible initially, got %d", len(nearby)) } // Move e2 close to e1. oldPos := e2.pos e2.pos = mathutil.NewVec3(10, 0, 10) events := aoi.UpdatePosition(e2, oldPos, e2.pos) // Should generate enter events (e1 sees e2, e2 sees e1). enterCount := 0 for _, evt := range events { if evt.Type == AOIEnter { enterCount++ } } if enterCount != 2 { t.Errorf("expected 2 enter events, got %d", enterCount) } } func TestGridAOI_ToggleComparison(t *testing.T) { // Demonstrates the difference between BroadcastAll and Grid AOI. e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)} e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(500, 0, 500)} // BroadcastAll: both visible broadcast := NewBroadcastAllAOI() broadcast.Add(e1) broadcast.Add(e2) if len(broadcast.GetNearby(e1)) != 1 { t.Error("broadcast-all should see all entities") } // Grid: e2 not visible from e1 (too far) grid := NewGridAOI(50, 1) grid.Add(e1) grid.Add(e2) if len(grid.GetNearby(e1)) != 0 { t.Error("grid AOI should NOT see distant entities") } }