bgen/demo/scripts/arcade/enemy.gd
saarsena@gmail.com e45f121fb9 init
2026-04-16 21:04:50 -04:00

174 lines
4.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name ArcadeEnemy
extends CharacterBody3D
# Three-state FSM:
# WANDER: pick a random passable cell in this enemy's room, walk there,
# occasionally idle.
# CHASE: move toward last-known player grid cell via BFS.
# ATTACK: within melee range — swing, tick cooldown.
#
# Transitions:
# WANDER -> CHASE when player has LOS AND distance ≤ sight_radius
# CHASE -> WANDER when player has been out of sight for lose_sight_turns
# CHASE -> ATTACK when close enough to hit
# ATTACK -> CHASE when out of range
signal died
enum State { WANDER, CHASE, ATTACK }
@export var hp := 8
@export var move_speed := 3.0
@export var sight_radius := 10.0
@export var melee_range := 1.4
@export var melee_damage := 2
@export var melee_cooldown := 0.9
@export var lose_sight_seconds := 3.0
# Set by spawner on instantiate.
var grid_ref: Dictionary = {}
var room_cells: Array = [] # Array[Vector2i]
var player_ref: Node3D = null
var rng := RandomNumberGenerator.new()
var _state := State.WANDER
var _path_target: Vector2i = Vector2i(-1, -1)
var _repath_timer := 0.0
var _attack_timer := 0.0
var _lose_sight_timer := 0.0
var _alive := true
func _ready() -> void:
rng.randomize()
_pick_wander_target()
func set_room(cells: Array) -> void:
room_cells = cells
_pick_wander_target()
func _physics_process(delta: float) -> void:
if not _alive:
return
if _attack_timer > 0.0:
_attack_timer -= delta
_update_state(delta)
match _state:
State.WANDER: _do_wander(delta)
State.CHASE: _do_chase(delta)
State.ATTACK: _do_attack(delta)
# Gravity always.
if not is_on_floor():
velocity.y -= 22.0 * delta
else:
velocity.y = max(velocity.y, 0.0)
move_and_slide()
# --- state machine ---
func _update_state(delta: float) -> void:
if player_ref == null:
return
var my_grid := GridUtil.world_to_grid(global_position)
var p_grid := GridUtil.world_to_grid(player_ref.global_position)
var dist := float(my_grid.distance_to(p_grid))
var can_see := dist <= sight_radius and GridUtil.has_los(grid_ref, my_grid, p_grid)
match _state:
State.WANDER:
if can_see:
_state = State.CHASE
_path_target = p_grid
_repath_timer = 0.0
State.CHASE:
if can_see:
_lose_sight_timer = 0.0
_path_target = p_grid
else:
_lose_sight_timer += delta
if _lose_sight_timer >= lose_sight_seconds:
_state = State.WANDER
_pick_wander_target()
return
# Close enough to swing?
var world_dist := global_position.distance_to(player_ref.global_position)
if world_dist <= melee_range:
_state = State.ATTACK
State.ATTACK:
var world_dist2 := global_position.distance_to(player_ref.global_position)
if world_dist2 > melee_range * 1.3:
_state = State.CHASE
if not can_see:
_lose_sight_timer += delta
if _lose_sight_timer >= lose_sight_seconds:
_state = State.WANDER
_pick_wander_target()
# --- behaviors ---
func _do_wander(delta: float) -> void:
_repath_timer -= delta
if _path_target == Vector2i(-1, -1) or _at_target():
if _repath_timer <= 0.0:
_repath_timer = rng.randf_range(0.8, 2.0)
_pick_wander_target()
_step_toward(_path_target, move_speed * 0.5)
func _do_chase(delta: float) -> void:
_repath_timer -= delta
if _repath_timer <= 0.0:
_repath_timer = 0.25 # repath 4×/sec
_step_toward(_path_target, move_speed)
func _do_attack(_delta: float) -> void:
velocity.x = 0.0
velocity.z = 0.0
if _attack_timer <= 0.0:
_attack_timer = melee_cooldown
if player_ref and player_ref.has_method("take_damage"):
player_ref.take_damage(melee_damage, self)
# --- helpers ---
func _pick_wander_target() -> void:
if room_cells.is_empty():
_path_target = GridUtil.world_to_grid(global_position)
return
_path_target = GridUtil.random_passable(grid_ref, room_cells, rng)
func _at_target() -> bool:
var my_grid := GridUtil.world_to_grid(global_position)
return my_grid == _path_target
func _step_toward(target: Vector2i, speed: float) -> void:
if target == Vector2i(-1, -1):
velocity.x = 0.0
velocity.z = 0.0
return
var my_grid := GridUtil.world_to_grid(global_position)
var nxt := GridUtil.next_step_toward(grid_ref, my_grid, target)
if nxt == my_grid:
velocity.x = 0.0
velocity.z = 0.0
return
var target_world := GridUtil.grid_to_world(nxt, global_position.y)
var dir := (target_world - global_position)
dir.y = 0
dir = dir.normalized()
velocity.x = dir.x * speed
velocity.z = dir.z * speed
# Face movement direction so we look right visually.
if dir.length() > 0.1:
look_at(global_position + dir, Vector3.UP)
func take_damage(amount: int, _source: Node) -> void:
if not _alive:
return
hp -= amount
if hp <= 0:
_alive = false
died.emit()
queue_free()