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() mm.addMovementSpawnInternal(spawnID) } // addMovementSpawnInternal adds a spawn to movement tracking without acquiring lock func (mm *MobMovementManager) addMovementSpawnInternal(spawnID int32) { if _, exists := mm.movementSpawns[spawnID]; exists { return // Already tracking } spawn := mm.zone.GetSpawn(spawnID) if spawn == nil { return } x := spawn.GetX() y := spawn.GetY() z := spawn.GetZ() heading := spawn.GetHeading() 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 := spawn.GetX() y := spawn.GetY() z := spawn.GetZ() heading := spawn.GetHeading() 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.addMovementSpawnInternal(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 := spawn.GetX() currentY := spawn.GetY() currentZ := spawn.GetZ() // 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.SetX(newX) spawn.SetY(newY, false) spawn.SetZ(newZ) spawn.SetHeadingFromFloat(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.SetX(command.TargetX) spawn.SetY(command.TargetY, false) spawn.SetZ(command.TargetZ) spawn.SetHeadingFromFloat(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) { currentHeading := spawn.GetHeading() // 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.SetHeadingFromFloat(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.SetHeadingFromFloat(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 := spawn.GetX() currentY := spawn.GetY() currentZ := spawn.GetZ() // 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 := spawn.GetX() currentY := spawn.GetY() currentZ := spawn.GetZ() currentHeading := spawn.GetHeading() 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 // 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.SetX(command.TargetX) spawn.SetY(command.TargetY, false) spawn.SetZ(command.TargetZ) spawn.SetHeadingFromFloat(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.SetX(stuckInfo.Position.X) spawn.SetY(stuckInfo.Position.Y, false) spawn.SetZ(stuckInfo.Position.Z) spawn.SetHeadingFromFloat(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 }