feat: bullrush branch - mekton bulls arena, HUD, NPC managers, godot_ai updates
This commit is contained in:
@@ -134,6 +134,56 @@ func _normalize_tile(tile: int) -> int:
|
||||
return tile - 4 # 11->7, 12->8, etc.
|
||||
return tile
|
||||
|
||||
# =============================================================================
|
||||
# Mekton Bulls mode helpers
|
||||
# =============================================================================
|
||||
|
||||
func is_mekton_bulls_mode() -> bool:
|
||||
return LobbyManager and LobbyManager.is_game_mode(GameMode.Mode.MEKTON_BULLS)
|
||||
|
||||
func _get_mekton_bulls_manager() -> Node:
|
||||
if gauntlet_manager_override and is_instance_valid(gauntlet_manager_override):
|
||||
return gauntlet_manager_override
|
||||
|
||||
var current = actor
|
||||
while current != null:
|
||||
var bm = current.get_node_or_null("MektonBullsManager")
|
||||
if bm: return bm
|
||||
current = current.get_parent()
|
||||
|
||||
var root = actor.get_tree().root
|
||||
var main = root.get_node_or_null("Main")
|
||||
if main:
|
||||
return main.get_node_or_null("MektonBullsManager")
|
||||
return null
|
||||
|
||||
func _get_active_bulls() -> Array:
|
||||
return actor.get_tree().get_nodes_in_group("MektonBulls")
|
||||
|
||||
func _is_cell_unsafe_in_mekton_bulls(pos: Vector2i) -> bool:
|
||||
"""Cell is unsafe if it's WATER, or if it's on the boundary (soon to be flooded)."""
|
||||
if not is_mekton_bulls_mode(): return false
|
||||
var bm = _get_mekton_bulls_manager()
|
||||
if not bm: return false
|
||||
|
||||
# Check if water
|
||||
var tile = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if tile == 24: # TILE_WATER
|
||||
return true
|
||||
|
||||
if bm.has_method("_is_boundary") and bm._is_boundary(pos):
|
||||
return true
|
||||
|
||||
# Bull proximity
|
||||
var bulls = _get_active_bulls()
|
||||
for b in bulls:
|
||||
var b_pos = enhanced_gridmap.local_to_map(b.position)
|
||||
# If cell is adjacent to the bull, it's unsafe.
|
||||
if abs(b_pos.x - pos.x) <= 1 and abs(b_pos.z - pos.y) <= 1:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
# Goal Analysis
|
||||
# =============================================================================
|
||||
@@ -345,14 +395,28 @@ func find_best_tile_to_grab() -> Dictionary:
|
||||
|
||||
func find_nearest_tile_of_type(tile_types: Array) -> Vector2i:
|
||||
"""Find nearest tile matching any type in array using optimized spiral search."""
|
||||
var current_pos = actor.current_position
|
||||
|
||||
if not enhanced_gridmap:
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
|
||||
if is_mekton_bulls_mode():
|
||||
# Return the nearest uncollected tile from our blueprint
|
||||
var bm = _get_mekton_bulls_manager()
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if bm and pid != null and bm.player_blueprints.has(pid):
|
||||
var bp = bm.player_blueprints[pid]
|
||||
var best_tile = Vector2i(-1, -1)
|
||||
var best_dist = INF
|
||||
for c in bp.cells:
|
||||
if enhanced_gridmap.get_cell_item(Vector3i(c.x, 0, c.y)) == bp.color:
|
||||
var dist = actor.current_position.distance_to(c)
|
||||
if dist < best_dist and _is_valid_move_target(c, true):
|
||||
best_dist = dist
|
||||
best_tile = c
|
||||
if best_tile != Vector2i(-1, -1):
|
||||
return best_tile
|
||||
|
||||
var current_pos = actor.current_position
|
||||
# Optimization: Start check at simple radius
|
||||
# If we find something in the spiral, it is guaranteed to be one of the nearest (by Chebyshev distance logic broadly, or just good enough)
|
||||
|
||||
var max_radius = 25 # Limit search range to prevent full map scans on huge maps
|
||||
if OS.has_feature("mobile"):
|
||||
max_radius = 15 # Stricter limit on mobile
|
||||
@@ -438,10 +502,43 @@ func find_nearest_roaming_tekton() -> Node3D:
|
||||
# Movement Strategy
|
||||
# =============================================================================
|
||||
|
||||
func _should_use_freeze() -> bool:
|
||||
if not is_mekton_bulls_mode(): return false
|
||||
var bm = _get_mekton_bulls_manager()
|
||||
if not bm: return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if not bm.player_powers.has(pid) or bm.player_powers[pid]["FREEZE"] <= 0: return false
|
||||
|
||||
var bulls = _get_active_bulls()
|
||||
var bot_pos = enhanced_gridmap.local_to_map(actor.position)
|
||||
for b in bulls:
|
||||
var b_pos = enhanced_gridmap.local_to_map(b.position)
|
||||
if abs(bot_pos.x - b_pos.x) <= 3 and abs(bot_pos.z - b_pos.y) <= 3:
|
||||
return true
|
||||
return false
|
||||
|
||||
func find_optimal_move_target() -> Vector2i:
|
||||
"""Calculate the best position to move towards."""
|
||||
"""Core decision logic. Evaluates sabotaging vs making progress."""
|
||||
var main = actor.get_tree().get_root().get_node_or_null("Main")
|
||||
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
|
||||
if is_mekton_bulls_mode():
|
||||
# In Mekton Bulls, use powers if viable.
|
||||
if _should_use_freeze():
|
||||
var bm = _get_mekton_bulls_manager()
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
bm.try_use_freeze.rpc_id(1) # Try emitting to server
|
||||
|
||||
# Knock another nearby player
|
||||
var mb_pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
var mb_bm = _get_mekton_bulls_manager()
|
||||
if mb_bm and mb_bm.player_powers.has(mb_pid) and mb_bm.player_powers[mb_pid]["KNOCK"] > 0:
|
||||
var opps = _get_opponents()
|
||||
for op in opps:
|
||||
var dist = actor.position.distance_to(op.position)
|
||||
if dist < enhanced_gridmap.cell_size.x * 2.0:
|
||||
mb_bm.try_use_knock.rpc_id(1, op.name.to_int(), actor.position.direction_to(op.position).normalized())
|
||||
break
|
||||
|
||||
var is_sng = LobbyManager and LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
|
||||
var gc_manager = main.get_node_or_null("GoalsCycleManager") if main else null
|
||||
var time_left = gc_manager.get_global_time_remaining() if gc_manager else 999.0
|
||||
var is_match_running = gc_manager.is_match_running() if gc_manager else false
|
||||
@@ -602,6 +699,11 @@ func _is_valid_move_target(pos: Vector2i, ignore_players: bool = false) -> bool:
|
||||
if not enhanced_gridmap or not enhanced_gridmap.is_position_valid(pos):
|
||||
return false
|
||||
|
||||
if is_mekton_bulls_mode():
|
||||
# Do not move into WATER or the boundary
|
||||
if _is_cell_unsafe_in_mekton_bulls(pos):
|
||||
return false
|
||||
|
||||
# Check Floor 0 (Ground/Walls)
|
||||
var floor_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if floor_item == -1 or floor_item in enhanced_gridmap.non_walkable_items:
|
||||
|
||||
@@ -5,7 +5,8 @@ enum Mode {
|
||||
FREEMODE = 0,
|
||||
STOP_N_GO = 1,
|
||||
TEKTON_DOORS = 2,
|
||||
GAUNTLET = 3
|
||||
GAUNTLET = 3,
|
||||
MEKTON_BULLS = 4
|
||||
}
|
||||
|
||||
static func from_string(mode: String) -> Mode:
|
||||
@@ -18,6 +19,8 @@ static func from_string(mode: String) -> Mode:
|
||||
return Mode.TEKTON_DOORS
|
||||
"Candy Pump Survival":
|
||||
return Mode.GAUNTLET
|
||||
"Mekton Bulls":
|
||||
return Mode.MEKTON_BULLS
|
||||
_:
|
||||
return Mode.FREEMODE
|
||||
|
||||
@@ -31,11 +34,13 @@ static func mode_to_string(mode: Mode) -> String:
|
||||
return "Tekton Doors"
|
||||
Mode.GAUNTLET:
|
||||
return "Candy Pump Survival"
|
||||
Mode.MEKTON_BULLS:
|
||||
return "Mekton Bulls"
|
||||
_:
|
||||
return "Freemode"
|
||||
|
||||
static func is_restricted(mode: Mode) -> bool:
|
||||
return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET
|
||||
return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET or mode == Mode.MEKTON_BULLS
|
||||
|
||||
static func get_all_modes() -> Array[String]:
|
||||
return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival"]
|
||||
return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival", "Mekton Bulls"]
|
||||
|
||||
@@ -36,6 +36,11 @@ signal gauntlet_round_duration_changed(duration: int)
|
||||
signal gauntlet_growth_interval_changed(interval: float)
|
||||
signal gauntlet_cells_per_tick_changed(cells: Dictionary)
|
||||
|
||||
# Mekton Bulls settings signals
|
||||
signal mekton_bulls_round_duration_changed(duration: int)
|
||||
signal mekton_bulls_phase_interval_changed(interval: int)
|
||||
signal mekton_bulls_points_changed(min_pts: int, max_pts: int)
|
||||
|
||||
# Room data structure
|
||||
var current_room: Dictionary = {}
|
||||
var players_in_room: Array = [] # [{id, name, is_ready}]
|
||||
@@ -88,13 +93,19 @@ var gauntlet_cells_per_tick: Dictionary = {
|
||||
"phase3": [8, 10],
|
||||
}
|
||||
|
||||
# Mekton Bulls settings
|
||||
var mekton_bulls_round_duration: int = 120
|
||||
var mekton_bulls_phase_interval: int = 30
|
||||
var mekton_bulls_min_points: int = 100
|
||||
var mekton_bulls_max_points: int = 1000
|
||||
|
||||
# Rematch tracking
|
||||
var rematch_votes: Array = [] # [player_id, ...]
|
||||
|
||||
# Character and area selection
|
||||
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
|
||||
var available_areas: Array[String] = []
|
||||
var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Candy Pump Survival"]
|
||||
var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival", "Mekton Bulls"]
|
||||
var selected_area: String = "Freemode Arena" # Host-controlled
|
||||
var game_mode: String = "Freemode" # Host-controlled
|
||||
var local_character_index: int = 0 # Local player's character index
|
||||
@@ -149,8 +160,12 @@ func _update_available_areas(mode: String) -> void:
|
||||
available_areas = ["Freemode Arena", "Classic", "Colloseum"]
|
||||
"Stop n Go":
|
||||
available_areas = ["Stop N Go Arena"]
|
||||
"Tekton Doors":
|
||||
available_areas = ["Tekton Doors Area"]
|
||||
"Candy Pump Survival":
|
||||
available_areas = ["Gauntlet Arena"]
|
||||
"Mekton Bulls":
|
||||
available_areas = ["Mekton Bulls Arena"]
|
||||
_:
|
||||
available_areas = ["Classic"]
|
||||
|
||||
@@ -584,6 +599,39 @@ func sync_gauntlet_cells_per_tick(cells: Dictionary) -> void:
|
||||
gauntlet_cells_per_tick = cells
|
||||
emit_signal("gauntlet_cells_per_tick_changed", cells)
|
||||
|
||||
# =============================================================================
|
||||
# Mekton Bulls Settings
|
||||
# =============================================================================
|
||||
|
||||
func set_mekton_bulls_round_duration(duration: int) -> void:
|
||||
mekton_bulls_round_duration = duration
|
||||
if is_host: rpc("sync_mekton_bulls_round_duration", duration)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_mekton_bulls_round_duration(duration: int) -> void:
|
||||
mekton_bulls_round_duration = duration
|
||||
emit_signal("mekton_bulls_round_duration_changed", duration)
|
||||
|
||||
func set_mekton_bulls_phase_interval(interval: int) -> void:
|
||||
mekton_bulls_phase_interval = interval
|
||||
if is_host: rpc("sync_mekton_bulls_phase_interval", interval)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_mekton_bulls_phase_interval(interval: int) -> void:
|
||||
mekton_bulls_phase_interval = interval
|
||||
emit_signal("mekton_bulls_phase_interval_changed", interval)
|
||||
|
||||
func set_mekton_bulls_points(min_pts: int, max_pts: int) -> void:
|
||||
mekton_bulls_min_points = min_pts
|
||||
mekton_bulls_max_points = max_pts
|
||||
if is_host: rpc("sync_mekton_bulls_points", min_pts, max_pts)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_mekton_bulls_points(min_pts: int, max_pts: int) -> void:
|
||||
mekton_bulls_min_points = min_pts
|
||||
mekton_bulls_max_points = max_pts
|
||||
emit_signal("mekton_bulls_points_changed", min_pts, max_pts)
|
||||
|
||||
# =============================================================================
|
||||
# Character Selection
|
||||
# =============================================================================
|
||||
@@ -792,6 +840,10 @@ func start_game(force: bool = false) -> void:
|
||||
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||
rpc("sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||
rpc("sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick)
|
||||
# Sync mekton bulls
|
||||
rpc("sync_mekton_bulls_round_duration", mekton_bulls_round_duration)
|
||||
rpc("sync_mekton_bulls_phase_interval", mekton_bulls_phase_interval)
|
||||
rpc("sync_mekton_bulls_points", mekton_bulls_min_points, mekton_bulls_max_points)
|
||||
# Sync game mode
|
||||
rpc("sync_game_mode", game_mode)
|
||||
|
||||
@@ -870,6 +922,9 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
|
||||
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||
rpc_id(requester_id, "sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||
rpc_id(requester_id, "sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick)
|
||||
rpc_id(requester_id, "sync_mekton_bulls_round_duration", mekton_bulls_round_duration)
|
||||
rpc_id(requester_id, "sync_mekton_bulls_phase_interval", mekton_bulls_phase_interval)
|
||||
rpc_id(requester_id, "sync_mekton_bulls_points", mekton_bulls_min_points, mekton_bulls_max_points)
|
||||
rpc_id(requester_id, "sync_game_mode", game_mode)
|
||||
rpc_id(requester_id, "sync_area", selected_area)
|
||||
|
||||
@@ -1021,3 +1076,7 @@ func reset() -> void:
|
||||
doors_swap_time = 15
|
||||
doors_refresh_time = 25
|
||||
doors_required_goals = 8
|
||||
mekton_bulls_round_duration = 120
|
||||
mekton_bulls_phase_interval = 30
|
||||
mekton_bulls_min_points = 100
|
||||
mekton_bulls_max_points = 1000
|
||||
|
||||
@@ -0,0 +1,593 @@
|
||||
extends Node
|
||||
class_name MektonBullsManager
|
||||
|
||||
class Blueprint3x3 extends RefCounted:
|
||||
var anchor: Vector2i
|
||||
var color: int
|
||||
var cells: Array[Vector2i] = []
|
||||
var progress: int = 0
|
||||
|
||||
signal phase_changed(phase_index: int)
|
||||
signal player_eliminated(player_id: int)
|
||||
|
||||
# Nodes
|
||||
var main_scene: Node
|
||||
var gridmap: Node
|
||||
|
||||
# Phase State
|
||||
var current_phase: int = 1
|
||||
var arena_size: Vector2i = Vector2i(20, 20)
|
||||
|
||||
var round_duration: float = 120.0
|
||||
var phase_interval: float = 30.0
|
||||
var round_timer: float = 120.0
|
||||
var phase_timer: float = 30.0
|
||||
|
||||
signal time_remaining_changed(remaining: float)
|
||||
|
||||
var bull_node: Node3D = null
|
||||
const BULL_SCENE = preload("res://scenes/npcs/mekton_bull.tscn")
|
||||
|
||||
enum CellState {
|
||||
SAFE,
|
||||
WATER,
|
||||
BLOCKED
|
||||
}
|
||||
var arena_cells: Dictionary = {}
|
||||
|
||||
var is_active: bool = false
|
||||
var flood_cooldown: float = 0.0
|
||||
|
||||
|
||||
var player_blueprints: Dictionary = {} # { player_id: Blueprint3x3 }
|
||||
var player_powers: Dictionary = {} # { player_id: { "FREEZE": 0, "KNOCK": 0 } }
|
||||
|
||||
var player_cooldowns: Dictionary = {} # { player_id: float }
|
||||
enum PowerType { FREEZE, KNOCK }
|
||||
|
||||
# Placement Tracking
|
||||
var player_placement: Dictionary = {} # { pid: placement_rank } 1=first out
|
||||
var elimination_order: Array = [] # List of pids
|
||||
|
||||
var candy_tick_timer: float = 0.0
|
||||
const GOAL_COLORS = [7, 8, 9, 10]
|
||||
|
||||
|
||||
const TILE_WALKABLE: int = 0
|
||||
const TILE_WATER: int = 24 # Water tile
|
||||
const TILE_OBSTACLE: int = 4 # Wall/obstacle
|
||||
|
||||
func initialize(main: Node, grid: Node) -> void:
|
||||
main_scene = main
|
||||
gridmap = grid
|
||||
|
||||
|
||||
func start_game_mode() -> void:
|
||||
print("[MektonBulls] Starting Mekton Bulls game mode...")
|
||||
|
||||
round_duration = float(LobbyManager.mekton_bulls_round_duration)
|
||||
phase_interval = float(LobbyManager.mekton_bulls_phase_interval)
|
||||
round_timer = round_duration
|
||||
phase_timer = phase_interval
|
||||
|
||||
is_active = true
|
||||
_setup_arena()
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
_spawn_bull()
|
||||
_assign_initial_blueprints()
|
||||
|
||||
|
||||
func _ready():
|
||||
player_eliminated.connect(_on_player_eliminated)
|
||||
|
||||
func _setup_arena() -> void:
|
||||
if not gridmap:
|
||||
gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap:
|
||||
push_error("[MektonBulls] No EnhancedGridMap found!")
|
||||
return
|
||||
|
||||
print("[MektonBulls] Setting up Phase 1 Arena...")
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
rpc("sync_arena_setup")
|
||||
_apply_arena_setup()
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func sync_arena_setup() -> void:
|
||||
_apply_arena_setup()
|
||||
|
||||
func _apply_arena_setup() -> void:
|
||||
if not gridmap:
|
||||
gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
current_phase = 1
|
||||
arena_size = arena_size_for_phase(current_phase)
|
||||
|
||||
gridmap.set("columns", 20)
|
||||
gridmap.set("rows", 20)
|
||||
gridmap.clear()
|
||||
|
||||
# Initial build 20x20
|
||||
for x in range(20):
|
||||
for z in range(20):
|
||||
var pos = Vector2i(x, z)
|
||||
|
||||
if _is_boundary(pos):
|
||||
# Perimeter
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
arena_cells[pos] = CellState.SAFE
|
||||
else:
|
||||
# Walkable floor
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
arena_cells[pos] = CellState.SAFE
|
||||
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
|
||||
gridmap.diagonal_movement = true
|
||||
gridmap.update_grid_data()
|
||||
gridmap.initialize_astar()
|
||||
|
||||
_reposition_npc()
|
||||
_validate_blueprint_after_shrink()
|
||||
|
||||
|
||||
func arena_size_for_phase(phase: int) -> Vector2i:
|
||||
match phase:
|
||||
1: return Vector2i(20, 20)
|
||||
2: return Vector2i(19, 19)
|
||||
3: return Vector2i(18, 18)
|
||||
4: return Vector2i(17, 17)
|
||||
_: return Vector2i(17, 17) # Final phase
|
||||
|
||||
func _is_boundary(pos: Vector2i) -> bool:
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
# Grid shrinks symmetrically towards (0,0) by bounds.
|
||||
return pos.x == 0 or pos.x == bounds.x - 1 or pos.y == 0 or pos.y == bounds.y - 1
|
||||
|
||||
func _shrink_arena() -> void:
|
||||
if current_phase >= 4:
|
||||
return
|
||||
|
||||
current_phase += 1
|
||||
var new_bounds = arena_size_for_phase(current_phase)
|
||||
var old_bounds = arena_size_for_phase(current_phase - 1)
|
||||
|
||||
print("[MektonBulls] Shrinking arena to Phase %d (%dx%d)" % [current_phase, new_bounds.x, new_bounds.y])
|
||||
|
||||
# Apply locally first
|
||||
_apply_ring_shrink(old_bounds, new_bounds)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
rpc("sync_shrink_arena", current_phase)
|
||||
|
||||
emit_signal("phase_changed", current_phase)
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func sync_shrink_arena(new_phase: int) -> void:
|
||||
var old_bounds = arena_size_for_phase(current_phase)
|
||||
current_phase = new_phase
|
||||
var new_bounds = arena_size_for_phase(current_phase)
|
||||
_apply_ring_shrink(old_bounds, new_bounds)
|
||||
emit_signal("phase_changed", current_phase)
|
||||
|
||||
func _apply_ring_shrink(old_bounds: Vector2i, new_bounds: Vector2i) -> void:
|
||||
for x in range(20):
|
||||
for z in range(20):
|
||||
var pos = Vector2i(x, z)
|
||||
|
||||
if x >= new_bounds.x or z >= new_bounds.y:
|
||||
# It is now outside the new bounds -> WATER
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WATER)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
arena_cells[pos] = CellState.WATER
|
||||
elif _is_boundary(pos):
|
||||
# New boundary -> No wall, just the edge of SAFE
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
arena_cells[pos] = CellState.SAFE
|
||||
|
||||
gridmap.update_grid_data()
|
||||
gridmap.initialize_astar()
|
||||
|
||||
_reposition_npc()
|
||||
_validate_blueprint_after_shrink()
|
||||
|
||||
|
||||
func _reposition_npc() -> void:
|
||||
# Reposition Mekton Bull to the center of the current bounds
|
||||
if not bull_node:
|
||||
return
|
||||
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
# Center in world units
|
||||
var cx = (bounds.x / 2.0) * gridmap.cell_size.x
|
||||
var cz = (bounds.y / 2.0) * gridmap.cell_size.z
|
||||
|
||||
bull_node.position = Vector3(cx, 0, cz)
|
||||
|
||||
func get_spawn_points(player_count: int) -> Array[Vector2i]:
|
||||
var spawns: Array[Vector2i] = []
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
# 4 players: inner corners
|
||||
spawns.append(Vector2i(1, 1))
|
||||
if bounds.x > 2: spawns.append(Vector2i(bounds.x - 2, 1))
|
||||
if bounds.y > 2: spawns.append(Vector2i(1, bounds.y - 2))
|
||||
if bounds.x > 2 and bounds.y > 2: spawns.append(Vector2i(bounds.x - 2, bounds.y - 2))
|
||||
|
||||
if player_count > 4:
|
||||
if bounds.x > 4: spawns.append(Vector2i(bounds.x / 2, 1))
|
||||
if bounds.x > 4 and bounds.y > 2: spawns.append(Vector2i(bounds.x / 2, bounds.y - 2))
|
||||
if player_count > 6:
|
||||
if bounds.y > 4: spawns.append(Vector2i(1, bounds.y / 2))
|
||||
if bounds.x > 2 and bounds.y > 4: spawns.append(Vector2i(bounds.x - 2, bounds.y / 2))
|
||||
|
||||
return spawns.slice(0, player_count)
|
||||
|
||||
func _spawn_bull() -> void:
|
||||
if bull_node == null:
|
||||
bull_node = BULL_SCENE.instantiate()
|
||||
bull_node.name = "MektonBull"
|
||||
# Use multiplayer spawner if appropriate, else just add child
|
||||
main_scene.add_child(bull_node)
|
||||
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
var cx = (bounds.x / 2.0) * gridmap.cell_size.x
|
||||
var cz = (bounds.y / 2.0) * gridmap.cell_size.z
|
||||
var start_pos = Vector3(cx, 0, cz)
|
||||
|
||||
if bull_node.has_method("initialize"):
|
||||
bull_node.initialize(self, gridmap, start_pos)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer == null: return
|
||||
if not is_active: return
|
||||
|
||||
if multiplayer.is_server():
|
||||
round_timer -= delta
|
||||
time_remaining_changed.emit(round_timer)
|
||||
rpc("sync_time_remaining", round_timer)
|
||||
|
||||
if round_timer <= 0:
|
||||
_on_round_time_expired()
|
||||
|
||||
phase_timer -= delta
|
||||
if phase_timer <= 0:
|
||||
phase_timer = phase_interval
|
||||
_shrink_arena()
|
||||
|
||||
if flood_cooldown > 0:
|
||||
|
||||
flood_cooldown -= delta
|
||||
|
||||
if multiplayer.is_server():
|
||||
if flood_cooldown <= 0 and bull_node:
|
||||
var bull_pos_3d = gridmap.local_to_map(bull_node.position)
|
||||
var bull_pos_2d = Vector2i(bull_pos_3d.x, bull_pos_3d.z)
|
||||
if _is_boundary(bull_pos_2d):
|
||||
_trigger_water_flood()
|
||||
|
||||
|
||||
candy_tick_timer -= delta
|
||||
if candy_tick_timer <= 0:
|
||||
candy_tick_timer = 0.1
|
||||
_process_candy_tick()
|
||||
|
||||
for pid in player_cooldowns.keys():
|
||||
if player_cooldowns[pid] > 0:
|
||||
player_cooldowns[pid] -= delta
|
||||
|
||||
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_time_remaining(time: float) -> void:
|
||||
round_timer = time
|
||||
time_remaining_changed.emit(round_timer)
|
||||
|
||||
func _on_round_time_expired() -> void:
|
||||
is_active = false
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
for p in players:
|
||||
if p.is_in_group("Players"):
|
||||
var pid = p.name.to_int()
|
||||
if not elimination_order.has(pid):
|
||||
elimination_order.append(pid)
|
||||
player_placement[pid] = elimination_order.size()
|
||||
|
||||
_end_round()
|
||||
|
||||
func _trigger_water_flood() -> void:
|
||||
flood_cooldown = 3.0
|
||||
print("[MektonBulls] Bull is on boundary! Flooding outer ring!")
|
||||
|
||||
# Network sync
|
||||
rpc("sync_water_flood")
|
||||
_apply_water_flood()
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_water_flood() -> void:
|
||||
_apply_water_flood()
|
||||
|
||||
func _apply_water_flood() -> void:
|
||||
# 1. Eliminate any player whose cell is currently _is_boundary
|
||||
# (which is the outermost ring of the current arena_size)
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
|
||||
for p in players:
|
||||
if p.is_in_group("Players") and p.has_method("is_eliminated") and not p.is_eliminated():
|
||||
var p_cell_3d = gridmap.local_to_map(p.position)
|
||||
var p_cell_2d = Vector2i(p_cell_3d.x, p_cell_3d.z)
|
||||
if _is_boundary(p_cell_2d):
|
||||
if multiplayer.is_server():
|
||||
if p.has_method("eliminate"):
|
||||
p.eliminate()
|
||||
else:
|
||||
player_eliminated.emit(p.name.to_int())
|
||||
|
||||
# 2. Play VFX / SFX (placeholder print if no actual scene yet)
|
||||
if main_scene and main_scene.get("vfx_manager") and main_scene.vfx_manager.has_method("play_splash"):
|
||||
var cx = (bounds.x / 2.0) * gridmap.cell_size.x
|
||||
var cz = (bounds.y / 2.0) * gridmap.cell_size.z
|
||||
main_scene.vfx_manager.play_splash(Vector3(cx, 0, cz))
|
||||
|
||||
if has_node("/root/SfxManager"):
|
||||
get_node("/root/SfxManager").play_rpc("water_flood")
|
||||
else:
|
||||
print("[MektonBulls] WHOOSH! Water flood VFX played.")
|
||||
|
||||
# 3. Set those cells to TILE_WATER
|
||||
for x in range(20):
|
||||
for z in range(20):
|
||||
var pos = Vector2i(x, z)
|
||||
if _is_boundary(pos):
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WATER)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
arena_cells[pos] = CellState.WATER
|
||||
|
||||
gridmap.update_grid_data()
|
||||
|
||||
func _assign_initial_blueprints() -> void:
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
for p in players:
|
||||
if p.is_in_group("Players"):
|
||||
_reroll_blueprint(p.name.to_int())
|
||||
|
||||
func _reroll_blueprint(player_id: int) -> void:
|
||||
var bp = Blueprint3x3.new()
|
||||
bp.color = GOAL_COLORS[randi() % GOAL_COLORS.size()]
|
||||
bp.anchor = _get_valid_3x3_anchor()
|
||||
|
||||
# Generate 3x3 cells
|
||||
for dx in range(3):
|
||||
for dz in range(3):
|
||||
var cpos = Vector2i(bp.anchor.x + dx, bp.anchor.y + dz)
|
||||
bp.cells.append(cpos)
|
||||
# Paint it
|
||||
gridmap.set_cell_item(Vector3i(cpos.x, 0, cpos.y), bp.color)
|
||||
|
||||
player_blueprints[player_id] = bp
|
||||
gridmap.update_grid_data()
|
||||
rpc("sync_painted_cells", bp.cells, bp.color)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_painted_cells(cells: Array, color: int) -> void:
|
||||
for c in cells:
|
||||
gridmap.set_cell_item(Vector3i(c.x, 0, c.y), color)
|
||||
|
||||
func _get_valid_3x3_anchor() -> Vector2i:
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
# boundary is 0 and bounds-1
|
||||
# inner is 1 to bounds-2
|
||||
# 3x3 needs x to x+2 -> x+2 <= bounds-2 -> x <= bounds-4
|
||||
var max_x = bounds.x - 4
|
||||
var max_y = bounds.y - 4
|
||||
|
||||
if max_x < 1: max_x = 1
|
||||
if max_y < 1: max_y = 1
|
||||
|
||||
return Vector2i(randi_range(1, max_x), randi_range(1, max_y))
|
||||
|
||||
func _process_candy_tick() -> void:
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
for p in players:
|
||||
if not p.is_in_group("Players") or (p.has_method("is_eliminated") and p.is_eliminated()):
|
||||
continue
|
||||
|
||||
var pid = p.name.to_int()
|
||||
if not player_blueprints.has(pid):
|
||||
continue
|
||||
|
||||
var bp: Blueprint3x3 = player_blueprints[pid]
|
||||
var pos_3d = gridmap.local_to_map(p.position)
|
||||
var pos = Vector2i(pos_3d.x, pos_3d.z)
|
||||
|
||||
if pos in bp.cells:
|
||||
var item = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if item == bp.color:
|
||||
# Pickup!
|
||||
bp.progress += 1
|
||||
# Remove color local
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 0, pos.y), TILE_WALKABLE)
|
||||
rpc("sync_painted_cells", [pos], TILE_WALKABLE)
|
||||
|
||||
if bp.progress >= 9:
|
||||
_grant_power(pid)
|
||||
_reroll_blueprint(pid)
|
||||
|
||||
|
||||
func _grant_power(player_id: int) -> void:
|
||||
print("[MektonBulls] Blueprint complete! Prompting power picker for ", player_id)
|
||||
# Safely call rpc_id
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.get_peers().has(player_id) or player_id == multiplayer.get_unique_id():
|
||||
rpc_id(player_id, "prompt_power_picker")
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func prompt_power_picker() -> void:
|
||||
if hud_node and hud_node.has_method("show_power_picker"):
|
||||
hud_node.show_power_picker()
|
||||
|
||||
var hud_node: Node = null
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_score_completed(placements: Dictionary) -> void:
|
||||
if hud_node and hud_node.has_method("show_placement"):
|
||||
hud_node.show_placement(placements)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func try_pick_power(power_type_str: String) -> void:
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if not player_powers.has(sender):
|
||||
player_powers[sender] = { "FREEZE": 0, "KNOCK": 0 }
|
||||
|
||||
if power_type_str == "FREEZE" or power_type_str == "KNOCK":
|
||||
player_powers[sender][power_type_str] += 1
|
||||
rpc("sync_player_powers", sender, player_powers[sender])
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_player_powers(pid: int, powers: Dictionary) -> void:
|
||||
player_powers[pid] = powers
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func try_use_freeze() -> void:
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if player_cooldowns.get(sender, 0.0) > 0: return
|
||||
if not player_powers.has(sender) or player_powers[sender]["FREEZE"] <= 0: return
|
||||
|
||||
player_powers[sender]["FREEZE"] -= 1
|
||||
player_cooldowns[sender] = 1.0
|
||||
rpc("sync_player_powers", sender, player_powers[sender])
|
||||
|
||||
if bull_node and bull_node.has_method("apply_slow"):
|
||||
bull_node.apply_slow(3.0)
|
||||
if has_node("/root/SfxManager"):
|
||||
get_node("/root/SfxManager").play_rpc("freeze_burst")
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func try_use_knock(target_id: int, dir: Vector3) -> void:
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if player_cooldowns.get(sender, 0.0) > 0: return
|
||||
if not player_powers.has(sender) or player_powers[sender]["KNOCK"] <= 0: return
|
||||
|
||||
player_powers[sender]["KNOCK"] -= 1
|
||||
player_cooldowns[sender] = 1.0
|
||||
rpc("sync_player_powers", sender, player_powers[sender])
|
||||
|
||||
rpc("sync_apply_knock", target_id, dir)
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_apply_knock(target_id: int, dir: Vector3) -> void:
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
for p in players:
|
||||
if p.name == str(target_id):
|
||||
# Translate position 1 cell in dir
|
||||
var move_dist = gridmap.cell_size.x
|
||||
# Normalize to 4 directions
|
||||
var dx = 0
|
||||
var dz = 0
|
||||
if abs(dir.x) > abs(dir.z):
|
||||
dx = sign(dir.x)
|
||||
else:
|
||||
dz = sign(dir.z)
|
||||
|
||||
p.position += Vector3(dx * move_dist, 0, dz * move_dist)
|
||||
|
||||
# Play a smack sound if available
|
||||
if has_node("/root/SfxManager"):
|
||||
get_node("/root/SfxManager").play_rpc("knock_burst")
|
||||
elif main_scene and main_scene.get("audio_manager") and main_scene.audio_manager.has_method("play_sfx"):
|
||||
main_scene.audio_manager.play_sfx("smack")
|
||||
|
||||
|
||||
func _validate_blueprint_after_shrink() -> void:
|
||||
if not multiplayer.is_server(): return
|
||||
var bounds = arena_size_for_phase(current_phase)
|
||||
|
||||
for pid in player_blueprints.keys():
|
||||
var bp: Blueprint3x3 = player_blueprints[pid]
|
||||
var outside = false
|
||||
for c in bp.cells:
|
||||
if c.x <= 0 or c.x >= bounds.x - 1 or c.y <= 0 or c.y >= bounds.y - 1:
|
||||
outside = true
|
||||
break
|
||||
|
||||
if outside:
|
||||
# Clear old ones
|
||||
rpc("sync_painted_cells", bp.cells, TILE_WALKABLE)
|
||||
_reroll_blueprint(pid)
|
||||
|
||||
func _on_player_eliminated(player_id: int) -> void:
|
||||
if not elimination_order.has(player_id):
|
||||
elimination_order.append(player_id)
|
||||
print("[MektonBulls] Player %d eliminated. Rank: %d" % [player_id, elimination_order.size()])
|
||||
# Placement rank: 1 is first out
|
||||
player_placement[player_id] = elimination_order.size()
|
||||
|
||||
if multiplayer.is_server():
|
||||
# Check if only 1 player remains
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
var alive_count = 0
|
||||
var last_alive: int = -1
|
||||
for p in players:
|
||||
if p.is_in_group("Players"):
|
||||
var pid = p.name.to_int()
|
||||
if not elimination_order.has(pid):
|
||||
alive_count += 1
|
||||
last_alive = pid
|
||||
|
||||
if alive_count <= 1:
|
||||
if last_alive != -1 and not elimination_order.has(last_alive):
|
||||
elimination_order.append(last_alive)
|
||||
player_placement[last_alive] = elimination_order.size()
|
||||
is_active = false
|
||||
_end_round()
|
||||
|
||||
func _end_round() -> void:
|
||||
if not multiplayer.is_server(): return
|
||||
print("[MektonBulls] Round ended. Computing placement scores...")
|
||||
|
||||
var total_players = elimination_order.size()
|
||||
if total_players == 0: return
|
||||
|
||||
var min_pts = LobbyManager.mekton_bulls_min_points
|
||||
var max_pts = LobbyManager.mekton_bulls_max_points
|
||||
|
||||
var scores = {}
|
||||
for i in range(total_players):
|
||||
var pid = elimination_order[i]
|
||||
var rank = i + 1 # 1 = first out
|
||||
|
||||
var pts = min_pts
|
||||
if total_players > 1:
|
||||
var t = float(rank - 1) / float(total_players - 1)
|
||||
pts = int(lerp(float(min_pts), float(max_pts), t))
|
||||
|
||||
scores[pid] = pts
|
||||
print("[MektonBulls] Player %d finished rank %d -> %d pts" % [pid, rank, pts])
|
||||
|
||||
# In the real game, we'd sync this to the scores manager
|
||||
# main_scene.rpc("sync_score_updated", scores) - wait, is there a direct scoreboard in Mekton Bulls?
|
||||
# Typically GoalsCycleManager tracks scores, or main.gd
|
||||
if main_scene and main_scene.get("goals_cycle_manager"):
|
||||
for pid in scores.keys():
|
||||
main_scene.goals_cycle_manager.add_score(pid, scores[pid])
|
||||
|
||||
rpc("sync_score_completed", player_placement)
|
||||
|
||||
# End the goal cycle match if it hasn't already
|
||||
if main_scene and main_scene.get("goals_cycle_manager") and main_scene.goals_cycle_manager.is_active:
|
||||
main_scene.goals_cycle_manager.end_match()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnsxsqvj2ea7f
|
||||
@@ -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
|
||||
@@ -0,0 +1,57 @@
|
||||
extends Control
|
||||
|
||||
@onready var bull_tracker = $BullTracker
|
||||
@onready var power_picker = $PowerPicker
|
||||
@onready var placement_panel = $PlacementPanel
|
||||
@onready var placement_list = $PlacementPanel/VBoxContainer/List
|
||||
@onready var counters_lbl = $PowerCounters/Label
|
||||
|
||||
var local_pid: int = -1
|
||||
var arena_manager: Node
|
||||
|
||||
func _ready():
|
||||
power_picker.hide()
|
||||
placement_panel.hide()
|
||||
|
||||
func set_local_player(pid: int):
|
||||
local_pid = pid
|
||||
|
||||
func initialize(manager: Node):
|
||||
arena_manager = manager
|
||||
|
||||
func _process(delta: float):
|
||||
# Hide tracker if bull is close/visible, else point to it
|
||||
if not arena_manager: return
|
||||
|
||||
var local_powers = arena_manager.player_powers.get(local_pid, {"FREEZE": 0, "KNOCK": 0})
|
||||
counters_lbl.text = "Freeze: %d | Knock: %d" % [local_powers.get("FREEZE", 0), local_powers.get("KNOCK", 0)]
|
||||
|
||||
func show_power_picker():
|
||||
power_picker.show()
|
||||
|
||||
func _on_freeze_btn_pressed():
|
||||
if arena_manager:
|
||||
arena_manager.rpc_id(1, "try_pick_power", "FREEZE")
|
||||
power_picker.hide()
|
||||
|
||||
func _on_knock_btn_pressed():
|
||||
if arena_manager:
|
||||
arena_manager.rpc_id(1, "try_pick_power", "KNOCK")
|
||||
power_picker.hide()
|
||||
|
||||
func show_placement(scores: Dictionary):
|
||||
placement_panel.show()
|
||||
for child in placement_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var items = []
|
||||
for pid in scores.keys():
|
||||
items.append({"pid": pid, "rank": scores[pid]})
|
||||
|
||||
items.sort_custom(func(a, b): return a.rank < b.rank)
|
||||
|
||||
for item in items:
|
||||
var lbl = Label.new()
|
||||
lbl.text = "Player %s - Rank %s" % [str(item.pid), str(item.rank)]
|
||||
placement_list.add_child(lbl)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://h2uragoekxs1
|
||||
Reference in New Issue
Block a user