feat: bullrush branch - mekton bulls arena, HUD, NPC managers, godot_ai updates
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ciytpot4av5gw
|
||||
Reference in New Issue
Block a user