Files
a301_game_server/internal/ai/behavior.go
2026-02-26 17:52:48 +09:00

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
}