From 3a50d2d324def1c09b01b6582e3fd856bc9aa7dc Mon Sep 17 00:00:00 2001 From: adtpdn Date: Fri, 27 Mar 2026 01:52:45 +0800 Subject: [PATCH] feat: update some visual and fixing the bug --- .../enhanced_gridmap/meshlibrary/default.tres | 2 +- .../meshlibrary/tile_safe_zone.tres | 2 +- scenes/main.gd | 8 +- scenes/player.gd | 6 + scripts/managers/goals_cycle_manager.gd | 4 +- scripts/managers/player_input_manager.gd | 7 +- scripts/managers/special_tiles_manager.gd | 3 + scripts/managers/static_tekton_manager.gd | 39 ++- scripts/managers/stop_n_go_manager.gd | 270 +++++++++++++++++- 9 files changed, 310 insertions(+), 31 deletions(-) diff --git a/addons/enhanced_gridmap/meshlibrary/default.tres b/addons/enhanced_gridmap/meshlibrary/default.tres index 5c1d663..cdb133e 100644 --- a/addons/enhanced_gridmap/meshlibrary/default.tres +++ b/addons/enhanced_gridmap/meshlibrary/default.tres @@ -14,7 +14,7 @@ [ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"] [ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"] [ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"] -[ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] +[ext_resource type="BoxMesh" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] [sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"] load_path = "res://.godot/imported/tile_heart.png-deeef50755ca225f028608dfd16900e6.s3tc.ctex" diff --git a/addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres b/addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres index da6a472..6ca61b1 100644 --- a/addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres +++ b/addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres @@ -2,7 +2,7 @@ [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ou2ex"] transparency = 1 -albedo_color = Color(0.38039216, 1, 0.33333334, 1) +albedo_color = Color(0.38039216, 1, 0.33333334, 0.65) [resource] material = SubResource("StandardMaterial3D_ou2ex") diff --git a/scenes/main.gd b/scenes/main.gd index ffd3d31..3f08ecc 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -720,6 +720,10 @@ func _auto_start_from_lobby(): func _start_game(): if multiplayer.is_server(): + # NOW assign spawn positions for EVERYONE (Host, Client, Bots) + # We do this BEFORE the stabilization delay so players are moved away from (0,0) immediately. + _assign_random_spawn_positions() + # Wait for Nakama websocket to actually be open, up to 5 seconds # SKIP THIS FOR LAN MODE if not LobbyManager.is_lan_mode: @@ -735,10 +739,6 @@ func _start_game(): # before the countdown starts. await get_tree().create_timer(1.5).timeout - # NOW assign spawn positions for EVERYONE (Host, Client, Bots) - # This safely sends RPCs over the completed socket connection - _assign_random_spawn_positions() - # PRE-GAME COUNTDOWN (3s) # Spawn static obstacles before countdown starts (Stop n Go only) if obstacle_manager and LobbyManager.game_mode == "Stop n Go": diff --git a/scenes/player.gd b/scenes/player.gd index f39c1cb..0e41c1a 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -1096,6 +1096,9 @@ func _process(delta): if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible: visible = true + if not multiplayer.has_multiplayer_peer(): + return + if is_multiplayer_authority(): # Visual debugging - show display name with connection status $Name.text = display_name if not display_name.is_empty() else str(name) @@ -1184,6 +1187,9 @@ var last_sent_position: Vector3 func _physics_process(delta): # Sync position periodically (Heartbeat / Smoothing) + if not multiplayer.has_multiplayer_peer(): + return + if is_multiplayer_authority(): # OPTIMIZATION: Only send smoothing updates if we ARE NOT currently mid-tween # The start/end of paths are already synced via start_movement_along_path. diff --git a/scripts/managers/goals_cycle_manager.gd b/scripts/managers/goals_cycle_manager.gd index b7ee77c..574e3f3 100644 --- a/scripts/managers/goals_cycle_manager.gd +++ b/scripts/managers/goals_cycle_manager.gd @@ -61,7 +61,7 @@ func _process(delta): emit_signal("global_timer_updated", global_match_timer) # Server broadcasts global timer sync every second - if multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta): + if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta): rpc("sync_global_timer", global_match_timer) # Update cycle timer if cycle is active @@ -87,7 +87,7 @@ func _process(delta): emit_signal("timer_updated", current_cycle_timer) # Server broadcasts timer sync every second - if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): + if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): rpc("sync_timer", current_cycle_timer) # ============================================================================= diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index ceee7cf..051b7b3 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -14,7 +14,7 @@ func initialize(p_player: Node3D, p_movement_manager: Node, p_race_manager: Node func _process(delta): # Early return conditions - if not is_instance_valid(player) or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots") or player.is_frozen or player.is_stop_frozen: + if not is_instance_valid(player) or not multiplayer.has_multiplayer_peer() or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots") or player.is_frozen or player.is_stop_frozen: return if TurnManager.turn_based_mode: @@ -59,8 +59,9 @@ func _process(delta): func handle_unhandled_input(event): # Early return if not authorized human player - if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): - player.set_process_unhandled_input(false) + if not multiplayer.has_multiplayer_peer() or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): + if multiplayer.has_multiplayer_peer(): + player.set_process_unhandled_input(false) return var main = player.get_node("/root/Main") diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 5ade05a..5589033 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -166,6 +166,7 @@ func add_powerup_from_item(item_id: int): for e in inventory: inventory[e] = false powerup_levels[e] = 1 # Reset levels of discarded powerups + _exit_targeting_mode() # Instant Level 8 on pickup (User Request) inventory[effect] = true @@ -183,6 +184,8 @@ func sync_inventory_add(effect: int, level: int): for e in inventory: inventory[e] = false powerup_levels[e] = 1 # Reset levels of discarded powerups + + _exit_targeting_mode() inventory[effect] = true powerup_levels[effect] = level diff --git a/scripts/managers/static_tekton_manager.gd b/scripts/managers/static_tekton_manager.gd index c10a733..827a75a 100644 --- a/scripts/managers/static_tekton_manager.gd +++ b/scripts/managers/static_tekton_manager.gd @@ -49,17 +49,8 @@ func calculate_spawn_points(count: int, gridmap: Node) -> Array[Vector2i]: # Determine Position Type for Bias # 0:TL, 1:TR, 2:BL, 3:BR, 4:Center - var pos_type = -1 - match zone_idx: - 0: pos_type = 0 # TL - 2: pos_type = 1 # TR - 6: pos_type = 2 # BL - 8: pos_type = 3 # BR - 4: pos_type = 4 # Center - - # Use PURE RANDOM spot in zone (User Request #2) - # instead of biased corner/center logic - var pos = _pick_spot_in_zone(zone, gridmap) + # We pass zone_idx to _pick_spot_in_zone to snap corners + var pos = _pick_spot_in_zone(zone, gridmap, zone_idx) if pos != Vector2i(-1, -1): spawn_points.append(pos) @@ -126,11 +117,29 @@ func _is_valid_3x3(center: Vector2i, gridmap: Node) -> bool: return false return true -func _pick_spot_in_zone(zone: Rect2i, gridmap: Node) -> Vector2i: - # Find a valid 3x3 spot in the zone - # The returned position is the CENTER of the 3x3 area +func _pick_spot_in_zone(zone: Rect2i, gridmap: Node, zone_idx: int = -1) -> Vector2i: + """ + Find a valid 3x3 spot in the zone. + The returned position is the CENTER of the 3x3 area. + If zone_idx is a corner (0, 2, 6, 8), we snap to the absolute map corner. + """ + # CORNER SNAPPING: If this is a corner zone, force it to the extreme corner + # to ensure the 3x3 Stand fills the corner completely (no 1-tile gaps). + if zone_idx == 0: # Top-Left + var center = Vector2i(1, 1) + if _is_valid_3x3(center, gridmap): return center + elif zone_idx == 2: # Top-Right + var center = Vector2i(gridmap.columns - 2, 1) + if _is_valid_3x3(center, gridmap): return center + elif zone_idx == 6: # Bottom-Left + var center = Vector2i(1, gridmap.rows - 2) + if _is_valid_3x3(center, gridmap): return center + elif zone_idx == 8: # Bottom-Right + var center = Vector2i(gridmap.columns - 2, gridmap.rows - 2) + if _is_valid_3x3(center, gridmap): return center + + # Fallback/Random logic for non-corner zones or if preferred corner was invalid var attempts = 0 - while attempts < 30: attempts += 1 # Ensure center is at least 1 tile away from edges of the map to fit 3x3 diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 6a4a8c3..8b783ad 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -12,6 +12,8 @@ enum Phase {GO, STOP} # Dynamic Safe Zone var active_safe_zone_rects: Array[Rect2i] = [] var spawned_safe_zones: int = 0 +var _safe_zone_animating: bool = false +var _outline_nodes: Array[Node3D] = [] # Track perimeter outline containers for cleanup # Power-Up Tile Spawning const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode) @@ -97,6 +99,12 @@ func _process(delta): print("[StopNGo] GO phase ending soon. Spawning 3 safe zones...") for i in range(3): _spawn_dynamic_safe_zone() + + # Trigger global VFX and outline drawing on all clients + if can_rpc(): + rpc("sync_all_safe_zones_vfx") + else: + sync_all_safe_zones_vfx() if phase_timer <= 0: if current_phase == Phase.GO: @@ -607,19 +615,200 @@ func _spawn_dynamic_safe_zone(): gridmap.set_cell_item(Vector3i(px, 2, pz), TILE_SAFE) if can_rpc() and main: main.rpc("sync_grid_item", px, 2, pz, TILE_SAFE) - - # Trigger safe zone appear visual effects on all clients - if can_rpc(): - rpc("sync_safe_zone_vfx") @rpc("authority", "call_local", "reliable") -func sync_safe_zone_vfx(): +func sync_all_safe_zones_vfx(): var main = get_node_or_null("/root/Main") if main and main.get("vfx_manager"): if main.vfx_manager.has_method("play_safe_zone_appear"): main.vfx_manager.play_safe_zone_appear() elif main.vfx_manager.get("animation_player"): main.vfx_manager.animation_player.play("safe-zone-appear") + + # Build and animate the CONTINUOUS outer outline for ALL zones combined + var outline = _create_merged_safe_zone_outlines() + if outline: + _outline_nodes.append(outline) + _animate_outline_appear(outline) + + # Animate the safe zone panels appearing (alpha 0 → 0.65) + _animate_safe_zone_appear() + +func _animate_safe_zone_appear(): + """Two-phase appear: sharp bright flash then settle to semi-transparent. + Guarded so only ONE animation runs even when all 3 zones spawn at once.""" + if _safe_zone_animating: + return + _safe_zone_animating = true + + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap or not gridmap.mesh_library: + _safe_zone_animating = false + return + + var original_mesh = gridmap.mesh_library.get_item_mesh(TILE_SAFE) + if not is_instance_valid(original_mesh): + _safe_zone_animating = false + return + + var mat = original_mesh.material + if not is_instance_valid(mat): + _safe_zone_animating = false + return + + # Duplicate mesh+material so we animate without touching the shared .tres on disk. + var anim_mat: StandardMaterial3D = mat.duplicate() + anim_mat.albedo_color = Color(mat.albedo_color.r, mat.albedo_color.g, mat.albedo_color.b, 0.0) + + var anim_mesh = original_mesh.duplicate() + anim_mesh.material = anim_mat + gridmap.mesh_library.set_item_mesh(TILE_SAFE, anim_mesh) + + const TARGET_ALPHA := 0.65 + var tween = create_tween() + + # Phase 1 — Sharp bright flash: alpha 0 → 1.0 in 0.2s (EXPO ease-out = instant pop) + tween.tween_method( + func(a: float): + if is_instance_valid(anim_mat): anim_mat.albedo_color.a = a, + 0.0, 1.0, 0.2 + ).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO) + + # Phase 2 — Settle: alpha 1.0 → 0.65 in 0.35s (CUBIC ease-in-out = soft land) + tween.tween_method( + func(a: float): + if is_instance_valid(anim_mat): anim_mat.albedo_color.a = a, + 1.0, TARGET_ALPHA, 0.35 + ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC) + + # Done — anim_mesh stays in library at alpha=0.65 (no restore needed) + tween.tween_callback(func(): + _safe_zone_animating = false + ) + +func _create_merged_safe_zone_outlines() -> Node3D: + """Create thin edge BoxMesh strips ONLY on the outer perimeter of the merged safe zones. + Inner edges between adjacent safe zones are skipped.""" + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return null + + var cs: Vector3 = gridmap.cell_size + + # Sit slightly above the safe panel (layer 2, panel top ~= 2*cs.y + 0.05) + var y := 2.0 * cs.y + 0.09 + const T := 0.09 # outline thickness (world units) + const H := 0.045 # outline height + + # One shared material (transparent, emissive lime-green) — starts invisible for animation + var mat := StandardMaterial3D.new() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.albedo_color = Color(0.15, 1.0, 0.3, 0.0) # start at alpha=0 + mat.emission_enabled = true + mat.emission = Color(0.1, 0.9, 0.25) + mat.emission_energy_multiplier = 1.8 + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + var container := Node3D.new() + container.add_to_group("SafeZoneOutlines") + gridmap.add_child(container) + + var corner_points = {} + + # Scan layer 2 for boundaries + for x in range(gridmap.columns): + for z in range(gridmap.rows): + if gridmap.get_cell_item(Vector3i(x, 2, z)) != TILE_SAFE: + continue + + var cell_cx = x * cs.x + cs.x * 0.5 + var cell_cz = z * cs.z + cs.z * 0.5 + + # Check North (z - 1) + if gridmap.get_cell_item(Vector3i(x, 2, z - 1)) != TILE_SAFE: + var mi := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(cs.x, H, T) + box.material = mat + mi.mesh = box + mi.position = Vector3(cell_cx, y, z * cs.z) + container.add_child(mi) + corner_points[Vector2i(x, z)] = true + corner_points[Vector2i(x + 1, z)] = true + + # Check South (z + 1) + if gridmap.get_cell_item(Vector3i(x, 2, z + 1)) != TILE_SAFE: + var mi := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(cs.x, H, T) + box.material = mat + mi.mesh = box + mi.position = Vector3(cell_cx, y, z * cs.z + cs.z) + container.add_child(mi) + corner_points[Vector2i(x, z + 1)] = true + corner_points[Vector2i(x + 1, z + 1)] = true + + # Check West (x - 1) + if gridmap.get_cell_item(Vector3i(x - 1, 2, z)) != TILE_SAFE: + var mi := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(T, H, cs.z) + box.material = mat + mi.mesh = box + mi.position = Vector3(x * cs.x, y, cell_cz) + container.add_child(mi) + corner_points[Vector2i(x, z)] = true + corner_points[Vector2i(x, z + 1)] = true + + # Check East (x + 1) + if gridmap.get_cell_item(Vector3i(x + 1, 2, z)) != TILE_SAFE: + var mi := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(T, H, cs.z) + box.material = mat + mi.mesh = box + mi.position = Vector3(x * cs.x + cs.x, y, cell_cz) + container.add_child(mi) + corner_points[Vector2i(x + 1, z)] = true + corner_points[Vector2i(x + 1, z + 1)] = true + + # Add a small cylinder at every exposed corner to connect lines and round the tips + for cp in corner_points: + var mi := MeshInstance3D.new() + var cyl := CylinderMesh.new() + cyl.top_radius = T * 0.5 + cyl.bottom_radius = T * 0.5 + cyl.height = H * 0.98 # Slightly shorter to prevent top-face Z-fighting with lines + cyl.radial_segments = 16 + cyl.material = mat + mi.mesh = cyl + mi.position = Vector3(cp.x * cs.x, y, cp.y * cs.z) + container.add_child(mi) + + return container + +func _animate_outline_appear(container: Node3D): + """Tween the shared outline material from transparent to full opacity, + mirroring the safe zone panel appear animation.""" + if not is_instance_valid(container) or container.get_child_count() == 0: return + var mi := container.get_child(0) as MeshInstance3D + if not mi or not mi.mesh: return + var mat := mi.mesh.material as StandardMaterial3D + if not mat: return + + var tween := create_tween() + # Flash in: alpha 0→1 in 0.2s + tween.tween_method( + func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a, + 0.0, 1.0, 0.2 + ).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO) + # Settle: alpha 1→0.9 in 0.35s + tween.tween_method( + func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a, + 1.0, 0.9, 0.35 + ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC) 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 @@ -642,6 +831,64 @@ func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width: return true +@rpc("authority", "call_local", "reliable") +func sync_safe_zone_disappear_vfx(): + _animate_safe_zone_disappear() + +func _animate_safe_zone_disappear(): + """Two-phase disappear: quick flicker-brighten then fade to invisible. + Plays before cells are cleared so the panel smoothly vanishes.""" + var gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap or not gridmap.mesh_library: return + + var cur_mesh = gridmap.mesh_library.get_item_mesh(TILE_SAFE) + if not is_instance_valid(cur_mesh): return + var cur_mat = cur_mesh.material + if not is_instance_valid(cur_mat): return + + var fade_mat: StandardMaterial3D = cur_mat.duplicate() + var fade_mesh = cur_mesh.duplicate() + fade_mesh.material = fade_mat + gridmap.mesh_library.set_item_mesh(TILE_SAFE, fade_mesh) + + var start_alpha: float = cur_mat.albedo_color.a + var tween = create_tween() + + # Phase 1 — Flicker brighten: alpha → 0.95 in 0.15s (warn the player) + tween.tween_method( + func(a: float): + if is_instance_valid(fade_mat): fade_mat.albedo_color.a = a, + start_alpha, 0.95, 0.15 + ).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE) + + # Phase 2 — Fade out: alpha 0.95 → 0 in 0.4s (EXPO ease-in = fast vanish) + tween.tween_method( + func(a: float): + if is_instance_valid(fade_mat): fade_mat.albedo_color.a = a, + 0.95, 0.0, 0.4 + ).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO) + + # Fade out all outline nodes in sync + for outline in _outline_nodes: + if not is_instance_valid(outline) or outline.get_child_count() == 0: continue + var mi := outline.get_child(0) as MeshInstance3D + if not mi or not mi.mesh: continue + var omat := mi.mesh.material as StandardMaterial3D + if not omat: continue + var fade_start := omat.albedo_color.a + var otween := create_tween() + # Flicker brighten + otween.tween_method( + func(a: float): if is_instance_valid(omat): omat.albedo_color.a = a, + fade_start, 1.0, 0.15 + ).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE) + # Fade to zero + otween.tween_method( + func(a: float): if is_instance_valid(omat): omat.albedo_color.a = a, + 1.0, 0.0, 0.4 + ).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO) + 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") @@ -650,6 +897,13 @@ func _clear_dynamic_safe_zones(): print("[StopNGo] Clearing %d active safe zones." % active_safe_zone_rects.size()) + # Play disappear animation on all peers, then wait for it to finish (0.55s) before clearing. + if can_rpc(): + rpc("sync_safe_zone_disappear_vfx") + else: + sync_safe_zone_disappear_vfx() + await get_tree().create_timer(0.55).timeout + for rect in active_safe_zone_rects: for rx in range(rect.size.x): for rz in range(rect.size.y): @@ -664,6 +918,12 @@ func _clear_dynamic_safe_zones(): active_safe_zone_rects.clear() spawned_safe_zones = 0 + + # Free all outline containers now that the animation has finished + for outline in _outline_nodes: + if is_instance_valid(outline): + outline.queue_free() + _outline_nodes.clear() func _scatter_player_tiles(player_node: Node): """Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells."""