first commit
This commit is contained in:
184
internal/ai/behavior.go
Normal file
184
internal/ai/behavior.go
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user