163 lines
4.3 KiB
GDScript
163 lines
4.3 KiB
GDScript
extends Area3D
|
|
|
|
enum State {
|
|
ROAM,
|
|
CHARGE,
|
|
COOLDOWN
|
|
}
|
|
|
|
var current_state: State = State.ROAM
|
|
var gridmap: Node
|
|
var arena_manager: Node
|
|
|
|
var move_speed: float = 3.0
|
|
var charge_speed: float = 12.0
|
|
var target_pos: Vector3
|
|
var state_timer: float = 0.0
|
|
var slow_timer: float = 0.0
|
|
|
|
func _ready():
|
|
add_to_group("MektonBulls")
|
|
body_entered.connect(_on_body_entered)
|
|
|
|
func initialize(manager: Node, grid: Node, start_pos: Vector3):
|
|
arena_manager = manager
|
|
gridmap = grid
|
|
position = start_pos
|
|
_pick_roam_target()
|
|
|
|
func apply_slow(duration: float) -> void:
|
|
slow_timer = duration
|
|
|
|
func _physics_process(delta: float):
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
state_timer -= delta
|
|
if slow_timer > 0:
|
|
slow_timer -= delta
|
|
|
|
match current_state:
|
|
State.ROAM:
|
|
_process_roam(delta)
|
|
if state_timer <= 0:
|
|
_try_start_charge()
|
|
State.CHARGE:
|
|
_process_charge(delta)
|
|
State.COOLDOWN:
|
|
if state_timer <= 0:
|
|
current_state = State.ROAM
|
|
_pick_roam_target()
|
|
|
|
func _process_roam(delta: float):
|
|
var dist = position.distance_to(target_pos)
|
|
if dist < 0.1:
|
|
_pick_roam_target()
|
|
else:
|
|
var dir = (target_pos - position).normalized()
|
|
dir.y = 0
|
|
if dir.length_squared() > 0:
|
|
var actual_speed = move_speed
|
|
if slow_timer > 0: actual_speed *= 0.5
|
|
position += dir * actual_speed * delta
|
|
# Face direction implicitly
|
|
var look_target = position + dir
|
|
look_target.y = position.y
|
|
if position.distance_squared_to(look_target) > 0.01:
|
|
look_at(look_target, Vector3.UP)
|
|
|
|
func _pick_roam_target():
|
|
if not arena_manager: return
|
|
|
|
var bounds = arena_manager.arena_size_for_phase(arena_manager.current_phase)
|
|
# Random walkable position within bounds
|
|
# We know 0,0 is boundary, bounds.x-1 is boundary
|
|
var min_x = 1
|
|
var max_x = bounds.x - 2
|
|
var min_z = 1
|
|
var max_z = bounds.y - 2
|
|
|
|
if min_x > max_x: max_x = min_x
|
|
if min_z > max_z: max_z = min_z
|
|
|
|
# Avoid exact center (where the static delivery target presumably sits)
|
|
var cx = int(bounds.x / 2.0)
|
|
var cz = int(bounds.y / 2.0)
|
|
|
|
var rx = cx
|
|
var rz = cz
|
|
while rx == cx and rz == cz:
|
|
rx = randi_range(min_x, max_x)
|
|
rz = randi_range(min_z, max_z)
|
|
|
|
var world_x = rx * gridmap.cell_size.x + gridmap.cell_size.x / 2.0
|
|
var world_z = rz * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
|
|
|
|
target_pos = Vector3(world_x, position.y, world_z)
|
|
state_timer = randf_range(2.0, 4.0)
|
|
|
|
func _try_start_charge():
|
|
# Find closest player
|
|
var players = get_tree().get_nodes_in_group("Players")
|
|
var closest_player = null
|
|
var min_dist = INF
|
|
|
|
for p in players:
|
|
if p.is_in_group("Players") and p.has_method("is_eliminated") and not p.is_eliminated():
|
|
var d = position.distance_to(p.position)
|
|
if d < min_dist and d < 15.0: # Range check
|
|
min_dist = d
|
|
closest_player = p
|
|
|
|
if closest_player:
|
|
current_state = State.CHARGE
|
|
target_pos = closest_player.position
|
|
target_pos.y = position.y
|
|
state_timer = 2.0 # Max charge duration
|
|
if has_node("/root/SfxManager"):
|
|
get_node("/root/SfxManager").play_rpc("bull_charge")
|
|
else:
|
|
_pick_roam_target()
|
|
|
|
func _process_charge(delta: float):
|
|
var dir = (target_pos - position).normalized()
|
|
dir.y = 0
|
|
var actual_speed = charge_speed
|
|
if slow_timer > 0: actual_speed *= 0.5
|
|
position += dir * actual_speed * delta
|
|
|
|
var look_target = position + dir
|
|
look_target.y = position.y
|
|
if position.distance_squared_to(look_target) > 0.01:
|
|
look_at(look_target, Vector3.UP)
|
|
|
|
var dist = position.distance_to(target_pos)
|
|
if dist < 0.5 or state_timer <= 0:
|
|
# Hit destination or timeout
|
|
current_state = State.COOLDOWN
|
|
state_timer = 1.5
|
|
|
|
func _on_body_entered(body: Node3D):
|
|
if body.is_in_group("Players") and multiplayer.is_server():
|
|
# Knock them out
|
|
if body.has_method("eliminate"):
|
|
body.eliminate()
|
|
else:
|
|
print("[MektonBull] Knocked out player", body.name)
|
|
# Dispatch via manager
|
|
if arena_manager and arena_manager.has_signal("player_eliminated"):
|
|
arena_manager.player_eliminated.emit(body.name.to_int())
|
|
|
|
# Polish: SFX + Camera Shake
|
|
rpc("sync_bull_impact")
|
|
|
|
@rpc("authority", "call_local", "unreliable")
|
|
func sync_bull_impact() -> void:
|
|
if has_node("/root/SfxManager"):
|
|
get_node("/root/SfxManager").play("bull_impact")
|
|
|
|
var root = get_tree().root
|
|
var main = root.get_node_or_null("Main")
|
|
if main and main.get("screen_shake_manager"):
|
|
main.screen_shake_manager.shake(0.2, 0.5)
|