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

153 lines
3.2 KiB
Go

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