eq2go/internal/zone/movement_manager.go

628 lines
18 KiB
Go

package zone
import (
"fmt"
"log"
"sync"
"time"
"eq2emu/internal/spawn"
)
// MobMovementManager handles movement for all NPCs and entities in the zone
type MobMovementManager struct {
zone *ZoneServer
movementSpawns map[int32]*MovementState
commandQueue map[int32][]*MovementCommand
stuckSpawns map[int32]*StuckInfo
processedSpawns map[int32]bool
lastUpdate time.Time
isProcessing bool
mutex sync.RWMutex
}
// MovementState tracks the current movement state of a spawn
type MovementState struct {
SpawnID int32
CurrentCommand *MovementCommand
CommandQueue []*MovementCommand
LastPosition *Position
LastMoveTime time.Time
IsMoving bool
IsStuck bool
StuckCount int
Speed float32
MovementMode int8
TargetPosition *Position
TargetHeading float32
PathNodes []*PathNode
CurrentNodeIndex int
PauseTime int32
PauseUntil time.Time
}
// MovementCommand represents a movement instruction
type MovementCommand struct {
Type int8
TargetX float32
TargetY float32
TargetZ float32
TargetHeading float32
Speed float32
MovementMode int8
StuckBehavior int8
MaxDistance float32
CompletionFunc func(*spawn.Spawn, bool) // Called when command completes (success bool)
}
// StuckInfo tracks stuck detection for spawns
type StuckInfo struct {
Position *Position
StuckCount int
LastStuckTime time.Time
Behavior int8
AttemptCount int
}
// NewMobMovementManager creates a new movement manager for the zone
func NewMobMovementManager(zone *ZoneServer) *MobMovementManager {
return &MobMovementManager{
zone: zone,
movementSpawns: make(map[int32]*MovementState),
commandQueue: make(map[int32][]*MovementCommand),
stuckSpawns: make(map[int32]*StuckInfo),
processedSpawns: make(map[int32]bool),
lastUpdate: time.Now(),
}
}
// Process handles movement processing for all managed spawns
func (mm *MobMovementManager) Process() error {
mm.mutex.Lock()
defer mm.mutex.Unlock()
if mm.isProcessing {
return nil // Already processing
}
mm.isProcessing = true
defer func() { mm.isProcessing = false }()
now := time.Now()
deltaTime := now.Sub(mm.lastUpdate).Seconds()
mm.lastUpdate = now
// Process each spawn with movement
for spawnID, state := range mm.movementSpawns {
spawn := mm.zone.GetSpawn(spawnID)
if spawn == nil {
// Spawn no longer exists, remove from tracking
delete(mm.movementSpawns, spawnID)
delete(mm.commandQueue, spawnID)
delete(mm.stuckSpawns, spawnID)
continue
}
// Skip if spawn is paused
if !state.PauseUntil.IsZero() && now.Before(state.PauseUntil) {
continue
}
// Process current command
if err := mm.processSpawnMovement(spawn, state, float32(deltaTime)); err != nil {
log.Printf("%s Error processing movement for spawn %d: %v", LogPrefixMovement, spawnID, err)
}
}
return nil
}
// AddMovementSpawn adds a spawn to movement tracking
func (mm *MobMovementManager) AddMovementSpawn(spawnID int32) {
mm.mutex.Lock()
defer mm.mutex.Unlock()
if _, exists := mm.movementSpawns[spawnID]; exists {
return // Already tracking
}
spawn := mm.zone.GetSpawn(spawnID)
if spawn == nil {
return
}
x, y, z, heading := spawn.GetPosition()
mm.movementSpawns[spawnID] = &MovementState{
SpawnID: spawnID,
LastPosition: NewPosition(x, y, z, heading),
LastMoveTime: time.Now(),
Speed: DefaultRunSpeed,
MovementMode: MovementModeRun,
}
mm.commandQueue[spawnID] = make([]*MovementCommand, 0)
log.Printf("%s Added spawn %d to movement tracking", LogPrefixMovement, spawnID)
}
// RemoveMovementSpawn removes a spawn from movement tracking
func (mm *MobMovementManager) RemoveMovementSpawn(spawnID int32) {
mm.mutex.Lock()
defer mm.mutex.Unlock()
delete(mm.movementSpawns, spawnID)
delete(mm.commandQueue, spawnID)
delete(mm.stuckSpawns, spawnID)
delete(mm.processedSpawns, spawnID)
log.Printf("%s Removed spawn %d from movement tracking", LogPrefixMovement, spawnID)
}
// MoveTo commands a spawn to move to the specified position
func (mm *MobMovementManager) MoveTo(spawnID int32, x, y, z float32, speed float32) error {
command := &MovementCommand{
Type: MovementCommandMoveTo,
TargetX: x,
TargetY: y,
TargetZ: z,
Speed: speed,
MovementMode: MovementModeRun,
StuckBehavior: StuckBehaviorEvade,
}
return mm.QueueCommand(spawnID, command)
}
// SwimTo commands a spawn to swim to the specified position
func (mm *MobMovementManager) SwimTo(spawnID int32, x, y, z float32, speed float32) error {
command := &MovementCommand{
Type: MovementCommandSwimTo,
TargetX: x,
TargetY: y,
TargetZ: z,
Speed: speed,
MovementMode: MovementModeSwim,
StuckBehavior: StuckBehaviorWarp,
}
return mm.QueueCommand(spawnID, command)
}
// TeleportTo instantly moves a spawn to the specified position
func (mm *MobMovementManager) TeleportTo(spawnID int32, x, y, z, heading float32) error {
command := &MovementCommand{
Type: MovementCommandTeleportTo,
TargetX: x,
TargetY: y,
TargetZ: z,
TargetHeading: heading,
Speed: 0, // Instant
MovementMode: MovementModeRun,
}
return mm.QueueCommand(spawnID, command)
}
// RotateTo commands a spawn to rotate to the specified heading
func (mm *MobMovementManager) RotateTo(spawnID int32, heading float32, speed float32) error {
command := &MovementCommand{
Type: MovementCommandRotateTo,
TargetHeading: heading,
Speed: speed,
MovementMode: MovementModeRun,
}
return mm.QueueCommand(spawnID, command)
}
// StopMoving commands a spawn to stop all movement
func (mm *MobMovementManager) StopMoving(spawnID int32) error {
command := &MovementCommand{
Type: MovementCommandStop,
}
return mm.QueueCommand(spawnID, command)
}
// EvadeCombat commands a spawn to evade and return to its spawn point
func (mm *MobMovementManager) EvadeCombat(spawnID int32) error {
spawn := mm.zone.GetSpawn(spawnID)
if spawn == nil {
return fmt.Errorf("spawn %d not found", spawnID)
}
// Get spawn's original position (would need to be stored somewhere)
// For now, use current position as placeholder
x, y, z, heading := spawn.GetPosition()
command := &MovementCommand{
Type: MovementCommandEvadeCombat,
TargetX: x,
TargetY: y,
TargetZ: z,
TargetHeading: heading,
Speed: DefaultRunSpeed * 1.5, // Faster when evading
MovementMode: MovementModeRun,
StuckBehavior: StuckBehaviorWarp,
}
return mm.QueueCommand(spawnID, command)
}
// QueueCommand adds a movement command to the spawn's queue
func (mm *MobMovementManager) QueueCommand(spawnID int32, command *MovementCommand) error {
mm.mutex.Lock()
defer mm.mutex.Unlock()
// Ensure spawn is being tracked
if _, exists := mm.movementSpawns[spawnID]; !exists {
mm.AddMovementSpawn(spawnID)
}
// Add command to queue
if _, exists := mm.commandQueue[spawnID]; !exists {
mm.commandQueue[spawnID] = make([]*MovementCommand, 0)
}
mm.commandQueue[spawnID] = append(mm.commandQueue[spawnID], command)
log.Printf("%s Queued movement command %d for spawn %d", LogPrefixMovement, command.Type, spawnID)
return nil
}
// ClearCommands clears all queued commands for a spawn
func (mm *MobMovementManager) ClearCommands(spawnID int32) {
mm.mutex.Lock()
defer mm.mutex.Unlock()
if queue, exists := mm.commandQueue[spawnID]; exists {
mm.commandQueue[spawnID] = queue[:0] // Clear slice but keep capacity
}
if state, exists := mm.movementSpawns[spawnID]; exists {
state.CurrentCommand = nil
state.IsMoving = false
state.PathNodes = nil
state.CurrentNodeIndex = 0
}
}
// IsMoving returns whether a spawn is currently moving
func (mm *MobMovementManager) IsMoving(spawnID int32) bool {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
if state, exists := mm.movementSpawns[spawnID]; exists {
return state.IsMoving
}
return false
}
// GetMovementState returns the current movement state for a spawn
func (mm *MobMovementManager) GetMovementState(spawnID int32) *MovementState {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
if state, exists := mm.movementSpawns[spawnID]; exists {
// Return a copy to avoid race conditions
return &MovementState{
SpawnID: state.SpawnID,
IsMoving: state.IsMoving,
IsStuck: state.IsStuck,
Speed: state.Speed,
MovementMode: state.MovementMode,
CurrentNodeIndex: state.CurrentNodeIndex,
}
}
return nil
}
// Private methods
func (mm *MobMovementManager) processSpawnMovement(spawn *spawn.Spawn, state *MovementState, deltaTime float32) error {
// Get next command if not currently executing one
if state.CurrentCommand == nil {
state.CurrentCommand = mm.getNextCommand(state.SpawnID)
if state.CurrentCommand == nil {
state.IsMoving = false
return nil // No commands to process
}
}
// Process current command
completed, err := mm.processMovementCommand(spawn, state, deltaTime)
if err != nil {
return err
}
// If command completed, call completion function and get next command
if completed {
if state.CurrentCommand.CompletionFunc != nil {
state.CurrentCommand.CompletionFunc(spawn, true)
}
state.CurrentCommand = nil
state.IsMoving = false
}
return nil
}
func (mm *MobMovementManager) processMovementCommand(spawn *spawn.Spawn, state *MovementState, deltaTime float32) (bool, error) {
command := state.CurrentCommand
if command == nil {
return true, nil
}
switch command.Type {
case MovementCommandMoveTo:
return mm.processMoveTo(spawn, state, command, deltaTime)
case MovementCommandSwimTo:
return mm.processSwimTo(spawn, state, command, deltaTime)
case MovementCommandTeleportTo:
return mm.processTeleportTo(spawn, state, command)
case MovementCommandRotateTo:
return mm.processRotateTo(spawn, state, command, deltaTime)
case MovementCommandStop:
return mm.processStop(spawn, state, command)
case MovementCommandEvadeCombat:
return mm.processEvadeCombat(spawn, state, command, deltaTime)
default:
return true, fmt.Errorf("unknown movement command type: %d", command.Type)
}
}
func (mm *MobMovementManager) processMoveTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
// Calculate distance to target
distanceToTarget := Distance3D(currentX, currentY, currentZ, command.TargetX, command.TargetY, command.TargetZ)
// Check if we've reached the target
if distanceToTarget <= 0.5 { // Close enough threshold
return true, nil
}
// Check for stuck condition
if mm.checkStuck(spawn, state) {
return mm.handleStuck(spawn, state, command)
}
// Calculate movement
speed := command.Speed
if speed <= 0 {
speed = state.Speed
}
maxMove := speed * deltaTime
if maxMove > distanceToTarget {
maxMove = distanceToTarget
}
// Calculate direction
dx := command.TargetX - currentX
dy := command.TargetY - currentY
dz := command.TargetZ - currentZ
// Normalize direction
distance := Distance3D(0, 0, 0, dx, dy, dz)
if distance > 0 {
dx /= distance
dy /= distance
dz /= distance
}
// Calculate new position
newX := currentX + dx*maxMove
newY := currentY + dy*maxMove
newZ := currentZ + dz*maxMove
// Calculate heading to target
newHeading := CalculateHeading(currentX, currentY, command.TargetX, command.TargetY)
// Update spawn position
spawn.SetPosition(newX, newY, newZ, newHeading)
// Update state
state.LastPosition.Set(newX, newY, newZ, newHeading)
state.LastMoveTime = time.Now()
state.IsMoving = true
// Mark spawn as changed for client updates
mm.zone.markSpawnChanged(spawn.GetID())
return false, nil // Not completed yet
}
func (mm *MobMovementManager) processSwimTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
// Similar to MoveTo but with different movement mode
return mm.processMoveTo(spawn, state, command, deltaTime)
}
func (mm *MobMovementManager) processTeleportTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand) (bool, error) {
// Instant teleport
spawn.SetPosition(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading)
// Update state
state.LastPosition.Set(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading)
state.LastMoveTime = time.Now()
state.IsMoving = false
// Mark spawn as changed
mm.zone.markSpawnChanged(spawn.GetID())
log.Printf("%s Teleported spawn %d to (%.2f, %.2f, %.2f)",
LogPrefixMovement, spawn.GetID(), command.TargetX, command.TargetY, command.TargetZ)
return true, nil // Completed immediately
}
func (mm *MobMovementManager) processRotateTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
// Calculate heading difference
headingDiff := HeadingDifference(currentHeading, command.TargetHeading)
// Check if we've reached the target heading
if abs(headingDiff) <= 1.0 { // Close enough threshold
spawn.SetPosition(currentX, currentY, currentZ, command.TargetHeading)
mm.zone.markSpawnChanged(spawn.GetID())
return true, nil
}
// Calculate rotation speed
rotationSpeed := command.Speed
if rotationSpeed <= 0 {
rotationSpeed = 90.0 // Default rotation speed in heading units per second
}
maxRotation := rotationSpeed * deltaTime
// Determine rotation direction and amount
var rotation float32
if abs(headingDiff) <= maxRotation {
rotation = headingDiff
} else if headingDiff > 0 {
rotation = maxRotation
} else {
rotation = -maxRotation
}
// Apply rotation
newHeading := NormalizeHeading(currentHeading + rotation)
spawn.SetPosition(currentX, currentY, currentZ, newHeading)
// Update state
state.LastPosition.Heading = newHeading
state.LastMoveTime = time.Now()
// Mark spawn as changed
mm.zone.markSpawnChanged(spawn.GetID())
return false, nil // Not completed yet
}
func (mm *MobMovementManager) processStop(spawn *spawn.Spawn, state *MovementState, command *MovementCommand) (bool, error) {
state.IsMoving = false
state.PathNodes = nil
state.CurrentNodeIndex = 0
log.Printf("%s Stopped movement for spawn %d", LogPrefixMovement, spawn.GetID())
return true, nil
}
func (mm *MobMovementManager) processEvadeCombat(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
// Similar to MoveTo but with evade behavior
return mm.processMoveTo(spawn, state, command, deltaTime)
}
func (mm *MobMovementManager) getNextCommand(spawnID int32) *MovementCommand {
if queue, exists := mm.commandQueue[spawnID]; exists && len(queue) > 0 {
command := queue[0]
mm.commandQueue[spawnID] = queue[1:] // Remove first command
return command
}
return nil
}
func (mm *MobMovementManager) checkStuck(spawn *spawn.Spawn, state *MovementState) bool {
currentX, currentY, currentZ, _ := spawn.GetPosition()
// Check if spawn has moved significantly since last update
if state.LastPosition != nil {
distance := Distance3D(currentX, currentY, currentZ, state.LastPosition.X, state.LastPosition.Y, state.LastPosition.Z)
if distance < 0.1 && time.Since(state.LastMoveTime) > time.Second*2 {
// Spawn hasn't moved much in 2 seconds
return true
}
}
return false
}
func (mm *MobMovementManager) handleStuck(spawn *spawn.Spawn, state *MovementState, command *MovementCommand) (bool, error) {
spawnID := spawn.GetID()
// Get or create stuck info
stuckInfo, exists := mm.stuckSpawns[spawnID]
if !exists {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
stuckInfo = &StuckInfo{
Position: NewPosition(currentX, currentY, currentZ, currentHeading),
StuckCount: 0,
LastStuckTime: time.Now(),
Behavior: command.StuckBehavior,
}
mm.stuckSpawns[spawnID] = stuckInfo
}
stuckInfo.StuckCount++
stuckInfo.AttemptCount++
log.Printf("%s Spawn %d is stuck (count: %d)", LogPrefixMovement, spawnID, stuckInfo.StuckCount)
switch stuckInfo.Behavior {
case StuckBehaviorNone:
return true, nil // Give up
case StuckBehaviorRun:
// Try to move around the obstacle
return mm.handleStuckWithRun(spawn, state, command, stuckInfo)
case StuckBehaviorWarp:
// Teleport to target
return mm.handleStuckWithWarp(spawn, state, command, stuckInfo)
case StuckBehaviorEvade:
// Return to spawn point
return mm.handleStuckWithEvade(spawn, state, command, stuckInfo)
default:
return true, nil // Unknown behavior, give up
}
}
func (mm *MobMovementManager) handleStuckWithRun(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) {
if stuckInfo.AttemptCount > 5 {
return true, nil // Give up after 5 attempts
}
// Try a slightly different path
currentX, currentY, currentZ, _ := spawn.GetPosition()
// Add some randomness to the movement
offsetX := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0
offsetY := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0
newTargetX := command.TargetX + offsetX
newTargetY := command.TargetY + offsetY
// Update command with new target
command.TargetX = newTargetX
command.TargetY = newTargetY
log.Printf("%s Trying alternate path for stuck spawn %d", LogPrefixMovement, spawn.GetID())
return false, nil // Continue with modified command
}
func (mm *MobMovementManager) handleStuckWithWarp(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) {
// Teleport directly to target
spawn.SetPosition(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading)
mm.zone.markSpawnChanged(spawn.GetID())
log.Printf("%s Warped stuck spawn %d to target", LogPrefixMovement, spawn.GetID())
return true, nil // Command completed
}
func (mm *MobMovementManager) handleStuckWithEvade(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) {
// Return to original position (evade)
if stuckInfo.Position != nil {
spawn.SetPosition(stuckInfo.Position.X, stuckInfo.Position.Y, stuckInfo.Position.Z, stuckInfo.Position.Heading)
mm.zone.markSpawnChanged(spawn.GetID())
log.Printf("%s Evaded stuck spawn %d to original position", LogPrefixMovement, spawn.GetID())
}
return true, nil // Command completed
}