153 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|