init
This commit is contained in:
commit
e45f121fb9
89 changed files with 336069 additions and 0 deletions
174
demo/scripts/arcade/enemy.gd
Normal file
174
demo/scripts/arcade/enemy.gd
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue