diff --git a/scenes/player.gd b/scenes/player.gd index 9203016..c27174f 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -2172,7 +2172,7 @@ func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath): self.is_carrying_tekton = true self.carried_tekton = tekton - tekton.set_carried(true, self) + tekton.set_carried(true, self ) # Reset my carry timer (3s rule starts fresh) tekton_carry_timer = 0.0 @@ -2399,10 +2399,10 @@ func sync_bump(target_pos: Vector2i, is_soft: bool = false): var tween = create_tween() # Ensure the character starts at logical pos if they were drifting - global_position = original_pos + global_position = original_pos - tween.tween_property(self, "global_position", mid_pos, duration).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - tween.tween_property(self, "global_position", original_pos, duration).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + tween.tween_property(self , "global_position", mid_pos, duration).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + tween.tween_property(self , "global_position", original_pos, duration).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) func knock_tekton(): if not is_multiplayer_authority() or is_frozen or is_stop_frozen or is_invisible: @@ -2472,6 +2472,6 @@ func _find_nearby_carrier() -> Node3D: if p.is_carrying_tekton: # Check adjacency or same tile var dist = Vector2(p.current_position.x, p.current_position.y).distance_to(Vector2(current_position.x, current_position.y)) - if dist <= 1.5: + if dist <= 1.5: return p return null diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index c97f942..053abcd 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -1,7 +1,6 @@ extends Node - # SpecialTilesManager - Handles special effects triggered by holo tile pickups # Holo tile indices (11-14) trigger special effects @@ -370,7 +369,7 @@ func _execute_area_freeze(target_pos: Vector2i = Vector2i.ZERO): # Check if it is STILL Freeze Overlay var current_check = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y)) if current_check == 5: - restore_batch.append({"x": pos.x, "y": 2, "z": pos.y, "item": -1}) + restore_batch.append({"x": pos.x, "y": 2, "z": pos.y, "item": - 1}) if not restore_batch.is_empty(): main.rpc("sync_grid_items_batch", restore_batch) diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 1275b2d..e9cc766 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -10,10 +10,8 @@ signal player_penalized(player_id: int) enum Phase {GO, STOP} # Dynamic Safe Zone -const SAFE_ZONE_PRE_TIME: float = 5.0 # Seconds before STOP to spawn safe zone -const SAFE_ZONE_RADIUS: int = 2 # 5x5 area (radius 2 from center) -var safe_zone_centers: Array[Vector2i] = [] -var safe_zone_spawned: bool = false +var active_safe_zone_rects: Array[Rect2i] = [] +var spawned_safe_zones: int = 0 # Power-Up Tile Spawning const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode) @@ -21,8 +19,7 @@ const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn var powerups_spawned: bool = false var stop_phase_occurred: bool = false -var safe_zone_wall_scene = preload("res://scenes/safe_zone_wall.tscn") - +# Safe zone walls removed for fully open dynamic zones const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [ Vector2i(4, 1), # Power up 1 Vector2i(3, 9), # Power up 2 @@ -101,6 +98,15 @@ func _process(delta): phase_timer -= delta if multiplayer.is_server(): + if current_phase == Phase.GO: + var int_timer = int(ceil(phase_timer)) + if int_timer == 3 and spawned_safe_zones == 0 and phase_timer <= 3.0: + _spawn_dynamic_safe_zone() + elif int_timer == 2 and spawned_safe_zones == 1 and phase_timer <= 2.0: + _spawn_dynamic_safe_zone() + elif int_timer == 1 and spawned_safe_zones == 2 and phase_timer <= 1.0: + _spawn_dynamic_safe_zone() + if phase_timer <= 0: if current_phase == Phase.GO: _start_phase(Phase.STOP) @@ -247,8 +253,9 @@ func _start_phase(phase: Phase): # Refresh power-ups every STOP phase _spawn_powerup_tiles() - # If GO phase starts, clear all STOP phase freezes + # If GO phase starts, clear all STOP phase freezes and dynamic safe zones if phase == Phase.GO: + _clear_dynamic_safe_zones() var all_players = get_tree().get_nodes_in_group("Players") for p in all_players: if p.has_method("sync_stop_freeze"): @@ -347,9 +354,7 @@ func _apply_arena_setup(): gridmap.set_cell_item(Vector3i(x, 0, z), tile_id) gridmap.set_cell_item(Vector3i(x, 1, z), -1) - # Paint Static Safe Zones - _paint_static_safe_zone(gridmap, 7, 11, 6, 9, -1, -1, 8, 8) - _paint_static_safe_zone(gridmap, 15, 19, 1, 5, -1, 18, 2, 2) + # Dynamic Safe Zones are procedural and spawn during GO phase # Note: Specific obstacles removed as per user request to replace with random ones. # MISSION TILES: Moved to start_game_mode() to ensure they spawn AFTER walls. @@ -488,40 +493,91 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool: # Static Safe Zone # ============================================================================= -func _paint_static_safe_zone(gridmap: Node, min_x: int, max_x: int, min_z: int, max_z: int, north_door_x: int = -1, south_door_x: int = -1, west_door_z: int = -1, east_door_z: int = -1): - # Paint safe floor - for x in range(min_x, max_x + 1): - for z in range(min_z, max_z + 1): - gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE) - - # Get center opening for horizontal walls - var center_n_x = north_door_x if north_door_x != -1 else int(float(min_x + max_x) / 2.0) - var center_s_x = south_door_x if south_door_x != -1 else int(float(min_x + max_x) / 2.0) - - # Instantiate Top and Bottom horizontal walls - for x in range(min_x, max_x + 1): - if x != center_n_x: - _instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, min_z), 0) # Bottom/North - if x != center_s_x: - _instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, max_z + 1), 0) # Top/South - - # Get center opening for vertical walls - var center_w_z = west_door_z if west_door_z != -1 else int(float(min_z + max_z) / 2.0) - var center_e_z = east_door_z if east_door_z != -1 else int(float(min_z + max_z) / 2.0) - - for z in range(min_z, max_z + 1): - if z != center_w_z: - _instantiate_safe_zone_wall(Vector3(min_x, 0.0, z + 0.5), 90) # Left/West - if z != center_e_z: - _instantiate_safe_zone_wall(Vector3(max_x + 1, 0.0, z + 0.5), 90) # Right/East - func _is_in_safe_zone(pos: Vector2i) -> bool: - """Check if a position is within ANY of the static safe zones.""" - # Safe zone 1: [7,6] - [11,9] - if pos.x >= 7 and pos.x <= 11 and pos.y >= 6 and pos.y <= 9: return true - # Safe zone 2: [15,1] - [19,5] - if pos.x >= 15 and pos.x <= 19 and pos.y >= 1 and pos.y <= 5: return true - return false + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: + gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return false + + var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) + return floor_tile == TILE_SAFE + +func _spawn_dynamic_safe_zone(): + if not multiplayer.is_server(): return + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return + + var main = get_node("/root/Main") + var possible_rects = [] + + # Check all possible 3x2 and 2x3 areas + for x in range(1, gridmap.columns - 3): + for z in range(1, gridmap.rows - 2): + if _is_valid_safe_zone_area(gridmap, x, z, 3, 2): + possible_rects.append(Rect2i(x, z, 3, 2)) + if _is_valid_safe_zone_area(gridmap, x, z, 2, 3): + possible_rects.append(Rect2i(x, z, 2, 3)) + + if possible_rects.size() > 0: + var rect = possible_rects.pick_random() + active_safe_zone_rects.append(rect) + spawned_safe_zones += 1 + + # Paint floor to TILE_SAFE + for rx in range(rect.size.x): + for rz in range(rect.size.y): + var px = rect.position.x + rx + var pz = rect.position.y + rz + gridmap.set_cell_item(Vector3i(px, 0, pz), TILE_SAFE) + if can_rpc() and main: + main.rpc("sync_grid_item", px, 0, pz, TILE_SAFE) + +func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width: int, height: int) -> bool: + # Avoid bounds or start/finish cols + if start_x < 2 or start_x + width > gridmap.columns - 2: return false + if start_z < 1 or start_z + height > gridmap.rows - 1: return false + + var test_rect = Rect2i(start_x, start_z, width, height) + for existing in active_safe_zone_rects: + if test_rect.intersects(existing): + return false + + for x in range(start_x, start_x + width): + for z in range(start_z, start_z + height): + var floor_0 = gridmap.get_cell_item(Vector3i(x, 0, z)) + var floor_1 = gridmap.get_cell_item(Vector3i(x, 1, z)) + + # Floor must be purely TILE_WALKABLE (0) + if floor_0 != TILE_WALKABLE: + return false + + # Floor 1 must be empty (-1) - no items or obstacles + if floor_1 != -1: + return false + + return true + +func _clear_dynamic_safe_zones(): + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return + var main = get_node_or_null("/root/Main") + + for rect in active_safe_zone_rects: + for rx in range(rect.size.x): + for rz in range(rect.size.y): + var px = rect.position.x + rx + var pz = rect.position.y + rz + + # Only clear if it is actually still a safe zone + if gridmap.get_cell_item(Vector3i(px, 0, pz)) == TILE_SAFE: + gridmap.set_cell_item(Vector3i(px, 0, pz), TILE_WALKABLE) + if can_rpc() and main: + main.rpc("sync_grid_item", px, 0, pz, TILE_WALKABLE) + + active_safe_zone_rects.clear() + spawned_safe_zones = 0 func _scatter_player_tiles(player_node: Node): """Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells.""" @@ -595,16 +651,32 @@ func _scatter_player_tiles(player_node: Node): print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id]) -# Removed dynamic sync methods. +# ============================================================================= +# OLD STATIC SAFE ZONE LOGIC (Retained for Reference) +# ============================================================================= +# var safe_zone_columns: Array[int] = [5, 10, 15] -func _instantiate_safe_zone_wall(pos: Vector3, rotation_deg: float): - if not safe_zone_wall_scene: return - - var wall = safe_zone_wall_scene.instantiate() - add_child(wall) - wall.add_to_group("SafeZoneWalls") - wall.position = pos - wall.rotation_degrees.y = rotation_deg +# func _is_in_safe_zone_old(pos: Vector2i) -> bool: +# return pos.x in safe_zone_columns + +# func _paint_static_safe_zones(gridmap): +# for x in safe_zone_columns: +# for z in range(gridmap.rows): +# gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE) +# # Optional: instantiate walls +# # _instantiate_safe_zone_wall(gridmap, x, z) + +# func _instantiate_safe_zone_wall(gridmap, x: int, z: int): +# var wall_scene = load("res://scenes/environment/safe_zone_wall.tscn") +# if wall_scene: +# var wall = wall_scene.instantiate() +# gridmap.add_child(wall) +# wall.global_position = Vector3( +# x * gridmap.cell_size.x + gridmap.cell_size.x/2, +# 0, +# z * gridmap.cell_size.z + gridmap.cell_size.z/2 +# ) +# ============================================================================= # ============================================================================= # Power-Up Tile Spawning (Speed & Ghost) diff --git a/scripts/managers/touch_controls.gd b/scripts/managers/touch_controls.gd index 9951a63..136b1f3 100644 --- a/scripts/managers/touch_controls.gd +++ b/scripts/managers/touch_controls.gd @@ -160,7 +160,7 @@ func _create_touch_ui(): # User Request: "move those button to ActionsBtn children" attack_mode_button = _find_or_create_action_button(actions_container, "AttackMode", "⚡", button_positions.attack_mode) # Renamed - spawn_boost_button = _find_or_create_action_button(actions_container, "SpawnBoost", "🚀", button_positions.spawn_boost) + # spawn_boost_button = _find_or_create_action_button(actions_container, "SpawnBoost", "🚀", button_positions.spawn_boost) grab_button = _find_or_create_action_button(actions_container, "Grab", "👋", button_positions.grab) put_button = _find_or_create_action_button(actions_container, "Put", "📦", button_positions.put) @@ -173,10 +173,10 @@ func _create_touch_ui(): attack_mode_button.icon = load("res://assets/graphics/touch_control/attack_mode.png") attack_mode_button.expand_icon = true - if spawn_boost_button: - actions_container.move_child(spawn_boost_button, 1) - spawn_boost_button.icon = load("res://assets/graphics/touch_control/spawn_tile.png") - spawn_boost_button.expand_icon = true + # if spawn_boost_button: + # actions_container.move_child(spawn_boost_button, 1) + # spawn_boost_button.icon = load("res://assets/graphics/touch_control/spawn_tile.png") + # spawn_boost_button.expand_icon = true if grab_button: actions_container.move_child(grab_button, 2) @@ -308,7 +308,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String): "Grab": existing_lbl.text = SettingsManager.get_control_text("grab") "Put": existing_lbl.text = "" "AttackMode": existing_lbl.text = SettingsManager.get_control_text("attack_mode") - "SpawnBoost": existing_lbl.text = SettingsManager.get_control_text("spawn_boost") + # "SpawnBoost": existing_lbl.text = SettingsManager.get_control_text("spawn_boost") "TektonGrab": existing_lbl.text = SettingsManager.get_control_text("tekton_grab") @@ -341,7 +341,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String): "Grab": shortcut_lbl.text = SettingsManager.get_control_text("grab") if SettingsManager else "Space" "Put": shortcut_lbl.text = "" # Disabled shortcut "AttackMode": shortcut_lbl.text = SettingsManager.get_control_text("attack_mode") if SettingsManager else "Q" - "SpawnBoost": shortcut_lbl.text = SettingsManager.get_control_text("spawn_boost") if SettingsManager else "E" + # "SpawnBoost": shortcut_lbl.text = SettingsManager.get_control_text("spawn_boost") if SettingsManager else "E" "TektonGrab": shortcut_lbl.text = SettingsManager.get_control_text("tekton_grab") if SettingsManager else "G" @@ -394,13 +394,16 @@ func _on_button_pressed(button_name: String): pass else: print("[TouchControls] PowerUpManager missing on player") - "SpawnBoost": - if local_player and local_player.is_carrying_tekton: + # "SpawnBoost": + # if local_player and local_player.is_carrying_tekton: + # if local_player.powerup_manager and local_player.powerup_manager.has_method("spawn_boost_reward"): + # local_player.powerup_manager.spawn_boost_reward() + "TektonGrab": + if local_player.is_carrying_tekton: if local_player.powerup_manager and local_player.powerup_manager.has_method("spawn_boost_reward"): local_player.powerup_manager.spawn_boost_reward() - "TektonGrab": - if not local_player.is_carrying_tekton: - if local_player.has_method("grab_tekton"): + else: + if not local_player.is_carrying_tekton and local_player.has_method("grab_tekton"): local_player.grab_tekton() @@ -523,13 +526,14 @@ func _apply_settings(): attack_mode_button.offset_bottom = button_positions.attack_mode.y + button_size if spawn_boost_button: - spawn_boost_button.visible = true - spawn_boost_button.scale = Vector2(button_scale, button_scale) - spawn_boost_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) - spawn_boost_button.offset_left = button_positions.spawn_boost.x - spawn_boost_button.offset_top = button_positions.spawn_boost.y - spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size - spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size + # spawn_boost_button.visible = true + # spawn_boost_button.scale = Vector2(button_scale, button_scale) + # spawn_boost_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) + # spawn_boost_button.offset_left = button_positions.spawn_boost.x + # spawn_boost_button.offset_top = button_positions.spawn_boost.y + # spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size + # spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size + spawn_boost_button.visible = false if tekton_grab_button: tekton_grab_button.visible = true @@ -598,15 +602,28 @@ func _on_boost_points_changed(current_points: int, max_points: int): _update_boost_button_state(attack_mode_button, can_attack) # SpawnBoost depends on carrying a Tekton, not boost points - var can_spawn = local_player and local_player.is_carrying_tekton - _update_boost_button_state(spawn_boost_button, can_spawn) + # var can_spawn = local_player and local_player.is_carrying_tekton + # _update_boost_button_state(spawn_boost_button, can_spawn) # Tekton Grab (👋) is only enabled if full AND not already carrying one - var can_grab = is_full and not (local_player and local_player.is_carrying_tekton) - _update_boost_button_state(tekton_grab_button, can_grab) + # Now modified: If CARRYING, it is ALWAYS enabled to act as SpawnBoost. If NOT carrying, needs to be full. + var can_grab_or_spawn = (local_player and local_player.is_carrying_tekton) or (is_full) + _update_boost_button_state(tekton_grab_button, can_grab_or_spawn) func _on_tekton_carried_changed(_is_carrying: bool): # Refresh button states when player grabs/throws a tekton + if tekton_grab_button: + if _is_carrying: + # Swapping to Spawn function (Hotkey E) + tekton_grab_button.icon = load("res://assets/graphics/touch_control/spawn_tile.png") + if SettingsManager: + _ensure_shortcut_label(tekton_grab_button, "SpawnBoost") + else: + # Swapping back to Grab function (Hotkey G) + tekton_grab_button.icon = load("res://assets/graphics/touch_control/grab_tekton.png") + if SettingsManager: + _ensure_shortcut_label(tekton_grab_button, "TektonGrab") + var powerup_mgr = local_player.get_node_or_null("PowerUpManager") if powerup_mgr: _on_boost_points_changed(powerup_mgr.current_boost, powerup_mgr.MAX_BOOST) diff --git a/scripts/static_tekton_controller.gd b/scripts/static_tekton_controller.gd index f4362b7..4d169cd 100644 --- a/scripts/static_tekton_controller.gd +++ b/scripts/static_tekton_controller.gd @@ -38,7 +38,7 @@ func _on_timer_timeout(): if not is_multiplayer_authority(): return if not tekton or not enhanced_gridmap: return - if tekton.get("is_carried") or tekton.get("is_thrown"): + if tekton.get("is_carried") or tekton.get("is_thrown"): _start_timer() return @@ -59,9 +59,9 @@ func _attempt_throw(): var target_world_pos = Vector3(target.x + 0.5, 0, target.y + 0.5) if enhanced_gridmap and "cell_size" in enhanced_gridmap: target_world_pos = Vector3( - target.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x/2, - 0, - target.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z/2 + target.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x / 2, + 0, + target.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z / 2 ) tekton.look_at(Vector3(target_world_pos.x, tekton.global_position.y, target_world_pos.z), Vector3.UP) diff --git a/scripts/tekton.gd b/scripts/tekton.gd index 531e1b2..47c27d0 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -191,12 +191,62 @@ func _process(delta): global_position = carrier.global_position + Vector3(0, 1.5, 0) rotation = carrier.rotation + _update_prompt_label() + var mesh_cache: Array[MeshInstance3D] = [] var original_scales: Array[Vector3] = [] +var prompt_label: Label3D +@onready var SettingsManager = get_node_or_null("/root/SettingsManager") + +func _update_prompt_label(): + if not prompt_label: return + + if is_static_turret or is_carried or is_thrown or is_recovering: + prompt_label.visible = false + return + + var authority_player = null + var players = get_tree().get_nodes_in_group("Players") + for p in players: + if p.name == str(multiplayer.get_unique_id()): + authority_player = p + break + + if not authority_player: + prompt_label.visible = false + return + + # Check distance + var player_pos = Vector2(authority_player.current_position.x, authority_player.current_position.y) + var tekton_pos = Vector2(current_position.x, current_position.y) + if player_pos.distance_to(tekton_pos) > 1.5: + prompt_label.visible = false + return + + # Check power bar + var pw_mgr = authority_player.get_node_or_null("PowerUpManager") + if pw_mgr and pw_mgr.current_boost >= (pw_mgr.MAX_BOOST - 1): + prompt_label.visible = true + else: + prompt_label.visible = false func _ready(): # Cache meshes and their initial scales - _cache_meshes(self ) + _cache_meshes(self) + + prompt_label = Label3D.new() + var shortcut_text = "G" + if SettingsManager and SettingsManager.has_method("get_control_text"): + shortcut_text = SettingsManager.get_control_text("tekton_grab") + prompt_label.text = "[ " + str(shortcut_text) + " ]" + prompt_label.font_size = 64 + prompt_label.outline_size = 12 + prompt_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED + prompt_label.no_depth_test = true + prompt_label.position = Vector3(0, 1.8, 0) + prompt_label.modulate = Color(1.0, 0.9, 0.0) # Yellow text + prompt_label.visible = false + add_child(prompt_label) func _cache_meshes(node: Node): if node is MeshInstance3D: