package ai import ( "math" "time" "a301_game_server/internal/entity" "a301_game_server/pkg/mathutil" ) // AIState represents the mob's behavioral state. type AIState int const ( StateIdle AIState = iota // Standing at spawn, doing nothing StatePatrol // Wandering near spawn StateChase // Moving toward a target StateAttack // In attack range, using skills StateReturn // Walking back to spawn (leash) StateDead // Dead, waiting for respawn ) // EntityProvider gives the AI access to the game world. type EntityProvider interface { GetEntity(id uint64) entity.Entity GetPlayersInRange(center mathutil.Vec3, radius float32) []entity.Entity } // SkillUser allows the AI to use combat skills. type SkillUser interface { UseSkill(casterID uint64, skillID uint32, targetID uint64, targetPos mathutil.Vec3) (bool, string) } // UpdateMob advances one mob's AI by one tick. func UpdateMob(m *Mob, dt time.Duration, provider EntityProvider, skills SkillUser) { if !m.IsAlive() { m.SetState(StateDead) return } switch m.State() { case StateIdle: updateIdle(m, provider) case StateChase: updateChase(m, dt, provider) case StateAttack: updateAttack(m, provider, skills) case StateReturn: updateReturn(m, dt) case StateDead: // handled by spawner } } func updateIdle(m *Mob, provider EntityProvider) { // Scan for players in aggro range. target := findNearestPlayer(m, provider, m.Def().AggroRange) if target != nil { m.SetTargetID(target.EntityID()) m.SetState(StateChase) } } func updateChase(m *Mob, dt time.Duration, provider EntityProvider) { target := provider.GetEntity(m.TargetID()) if target == nil || !isAlive(target) { m.SetTargetID(0) m.SetState(StateReturn) return } // Leash check: too far from spawn? if m.Position().DistanceXZ(m.SpawnPos()) > m.Def().LeashRange { m.SetTargetID(0) m.SetState(StateReturn) return } dist := m.Position().DistanceXZ(target.Position()) // Close enough to attack? if dist <= m.Def().AttackRange { m.SetState(StateAttack) return } // Move toward target. moveToward(m, target.Position(), dt) } func updateAttack(m *Mob, provider EntityProvider, skills SkillUser) { target := provider.GetEntity(m.TargetID()) if target == nil || !isAlive(target) { m.SetTargetID(0) m.SetState(StateReturn) return } dist := m.Position().DistanceXZ(target.Position()) // Target moved out of attack range? Chase again. if dist > m.Def().AttackRange*1.2 { // 20% buffer to prevent flickering m.SetState(StateChase) return } // Leash check. if m.Position().DistanceXZ(m.SpawnPos()) > m.Def().LeashRange { m.SetTargetID(0) m.SetState(StateReturn) return } // Face target. dir := target.Position().Sub(m.Position()) m.SetRotation(float32(math.Atan2(float64(dir.X), float64(dir.Z)))) // Use attack skill. skills.UseSkill(m.EntityID(), m.Def().AttackSkill, target.EntityID(), mathutil.Vec3{}) } func updateReturn(m *Mob, dt time.Duration) { dist := m.Position().DistanceXZ(m.SpawnPos()) if dist < 0.5 { m.SetPosition(m.SpawnPos()) m.SetState(StateIdle) // Heal to full when returning. m.SetHP(m.MaxHP()) return } moveToward(m, m.SpawnPos(), dt) } // moveToward moves the mob toward a target position at its move speed. func moveToward(m *Mob, target mathutil.Vec3, dt time.Duration) { dir := target.Sub(m.Position()) dir.Y = 0 dist := dir.Length() if dist < 0.01 { return } step := m.Def().MoveSpeed * float32(dt.Seconds()) if step > dist { step = dist } move := dir.Normalize().Scale(step) m.SetPosition(m.Position().Add(move)) // Face movement direction. m.SetRotation(float32(math.Atan2(float64(dir.X), float64(dir.Z)))) } func findNearestPlayer(m *Mob, provider EntityProvider, radius float32) entity.Entity { players := provider.GetPlayersInRange(m.Position(), radius) if len(players) == 0 { return nil } var nearest entity.Entity minDist := float32(math.MaxFloat32) for _, p := range players { if !isAlive(p) { continue } d := m.Position().DistanceXZ(p.Position()) if d < minDist { minDist = d nearest = p } } return nearest } func isAlive(e entity.Entity) bool { type aliveChecker interface { IsAlive() bool } if a, ok := e.(aliveChecker); ok { return a.IsAlive() } return true }