package ai import ( "sync/atomic" "time" "a301_game_server/pkg/logger" "a301_game_server/pkg/mathutil" ) // SpawnPoint defines where and what to spawn. type SpawnPoint struct { MobDef *MobDef Position mathutil.Vec3 RespawnDelay time.Duration MaxCount int // max alive at this point } // spawnEntry tracks a single spawned mob. type spawnEntry struct { mob *Mob alive bool diedAt time.Time } // Spawner manages mob spawning and respawning for a zone. type Spawner struct { points []SpawnPoint mobs map[uint64]*spawnEntry // mobID -> entry pointMobs map[int][]*spawnEntry // spawnPointIndex -> entries nextID *atomic.Uint64 // Callbacks onSpawn func(m *Mob) onRemove func(mobID uint64) } // NewSpawner creates a mob spawner. func NewSpawner(nextID *atomic.Uint64, onSpawn func(*Mob), onRemove func(uint64)) *Spawner { return &Spawner{ mobs: make(map[uint64]*spawnEntry), pointMobs: make(map[int][]*spawnEntry), nextID: nextID, onSpawn: onSpawn, onRemove: onRemove, } } // AddSpawnPoint registers a spawn point. func (s *Spawner) AddSpawnPoint(sp SpawnPoint) { s.points = append(s.points, sp) } // InitialSpawn spawns all mobs at startup. func (s *Spawner) InitialSpawn() { for i, sp := range s.points { for j := 0; j < sp.MaxCount; j++ { s.spawnMob(i, &sp) } } logger.Info("initial spawn complete", "totalMobs", len(s.mobs)) } // Update checks for mobs that need respawning. func (s *Spawner) Update(now time.Time) { for i, sp := range s.points { entries := s.pointMobs[i] aliveCount := 0 for _, e := range entries { if e.alive { aliveCount++ continue } // Check if it's time to respawn. if !e.diedAt.IsZero() && now.Sub(e.diedAt) >= sp.RespawnDelay { s.respawnMob(e) aliveCount++ } } // Spawn new if below max count. for aliveCount < sp.MaxCount { s.spawnMob(i, &sp) aliveCount++ } } } // NotifyDeath marks a mob as dead and starts the respawn timer. func (s *Spawner) NotifyDeath(mobID uint64) { entry, ok := s.mobs[mobID] if !ok { return } entry.alive = false entry.diedAt = time.Now() // Remove from zone (despawn). if s.onRemove != nil { s.onRemove(mobID) } } // GetMob returns a mob by ID. func (s *Spawner) GetMob(id uint64) *Mob { if e, ok := s.mobs[id]; ok { return e.mob } return nil } // AllMobs returns all tracked mobs. func (s *Spawner) AllMobs() []*Mob { result := make([]*Mob, 0, len(s.mobs)) for _, e := range s.mobs { result = append(result, e.mob) } return result } // AliveMobs returns all alive mobs. func (s *Spawner) AliveMobs() []*Mob { var result []*Mob for _, e := range s.mobs { if e.alive { result = append(result, e.mob) } } return result } func (s *Spawner) spawnMob(pointIdx int, sp *SpawnPoint) { id := s.nextID.Add(1) + 100000 // offset to avoid collision with player IDs mob := NewMob(id, sp.MobDef, sp.Position) entry := &spawnEntry{mob: mob, alive: true} s.mobs[id] = entry s.pointMobs[pointIdx] = append(s.pointMobs[pointIdx], entry) if s.onSpawn != nil { s.onSpawn(mob) } } func (s *Spawner) respawnMob(entry *spawnEntry) { entry.mob.Reset() entry.alive = true entry.diedAt = time.Time{} if s.onSpawn != nil { s.onSpawn(entry.mob) } }