185 lines
4.3 KiB
Go
185 lines
4.3 KiB
Go
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
|
|
}
|