From 1c5c3d47f6f5cbfaa3a8c8d60a27507a62ece651 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Fri, 27 Feb 2026 04:29:02 +0800 Subject: [PATCH] feat: implement Tekton NPC with movement, combat, and interaction mechanics, and add initial portal system components. --- scripts/managers/portal_mode_manager.gd | 62 +++++++++++++++++++++++-- scripts/portal_door.gd | 7 +++ scripts/tekton.gd | 18 ++++--- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/scripts/managers/portal_mode_manager.gd b/scripts/managers/portal_mode_manager.gd index 38757c9..718236d 100644 --- a/scripts/managers/portal_mode_manager.gd +++ b/scripts/managers/portal_mode_manager.gd @@ -19,6 +19,7 @@ var tile_refresh_timer: Timer var finish_spawned: bool = false var missions_required: int = 3 var arena_setup_done: bool = false +var player_portal_cooldowns: Dictionary = {} func initialize(p_main: Node, p_gridmap: Node): main = p_main @@ -346,12 +347,23 @@ func _on_tile_refresh_timer_timeout(): func _refresh_tiles(): # GridMap Floor 0 has the walls (ID 4) and floors (ID 0) # GridMap Floor 1 should have the items (Heart, Star, etc) + # Cache door positions to avoid spawning under them + var door_positions = [] + for door in doors: + if is_instance_valid(door): + var local_pos = gridmap.local_to_map(door.global_position) + door_positions.append(Vector2i(local_pos.x, local_pos.z)) + for x in range(GRID_SIZE): for z in range(GRID_SIZE): # 1. Check if Floor 0 is a wall or empty (non-walkable) var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z)) if floor_0_item == 4 or floor_0_item == -1: continue + + # 1.5. Prevent spawning directly under portal doors + if door_positions.has(Vector2i(x, z)): + continue # 2. Check if Floor 1 is already occupied if gridmap.get_cell_item(Vector3i(x, 1, z)) != -1: @@ -379,6 +391,12 @@ func _pick_weighted_tile(weights: Dictionary) -> int: func handle_portal_interaction(player, door): if not multiplayer.is_server(): return + var current_time = Time.get_ticks_msec() + if player_portal_cooldowns.has(player.name): + if current_time - player_portal_cooldowns[player.name] < 3000: + return + player_portal_cooldowns[player.name] = current_time + var source_id = door.door_id if not connections.has(source_id): return @@ -388,13 +406,51 @@ func handle_portal_interaction(player, door): # Use stored offset to avoid infinite loop (spawn inside the target room) var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0, 0) - # Convert world pos back to grid var target_world = target_door.global_position var target_grid_3d = gridmap.local_to_map(target_world) var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset - print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, target_grid, target_id]) + # Check for overlaps at the target_grid + var final_target = target_grid + var all_players = get_tree().get_nodes_in_group("Players") + var is_occupied = true + var search_radius = 0 + var max_search_radius = 2 + + while is_occupied and search_radius <= max_search_radius: + is_occupied = false + for p in all_players: + if p != player and p.current_position == final_target: + is_occupied = true + break + + if is_occupied: + # Try to find an adjacent cell + search_radius += 1 + var found_empty = false + # Check immediate neighbors first + var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1), + Vector2i(1, 1), Vector2i(-1, 1), Vector2i(1, -1), Vector2i(-1, -1)] + for offset_vec in offsets: + var test_pos = final_target + offset_vec + # Check if it's strictly a floor tile (ID 0) on Floor 0, not a wall + if gridmap.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) == 0: + # Verify no player is on this test_pos + var test_occupied = false + for p in all_players: + if p != player and p.current_position == test_pos: + test_occupied = true + break + if not test_occupied: + final_target = test_pos + found_empty = true + break + + if found_empty: + is_occupied = false + + print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, final_target, target_id]) # Snap player if player.has_method("set_spawn_position"): - player.rpc("set_spawn_position", target_grid) + player.rpc("set_spawn_position", final_target) diff --git a/scripts/portal_door.gd b/scripts/portal_door.gd index 83edd00..b7528f6 100644 --- a/scripts/portal_door.gd +++ b/scripts/portal_door.gd @@ -33,6 +33,13 @@ func _on_body_entered(body: Node3D): if not is_active: return if body.is_in_group("Players") or body.get("is_bot"): + var current_time = Time.get_ticks_msec() + if body.has_meta("last_portal_time"): + if current_time - body.get_meta("last_portal_time") < 3000: + return + + body.set_meta("last_portal_time", current_time) + print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id]) emit_signal("player_entered_portal", body, self ) diff --git a/scripts/tekton.gd b/scripts/tekton.gd index e4d0a72..0ed3b98 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -196,7 +196,7 @@ var original_scales: Array[Vector3] = [] func _ready(): # Cache meshes and their initial scales - _cache_meshes(self) + _cache_meshes(self ) func _cache_meshes(node: Node): if node is MeshInstance3D: @@ -257,7 +257,7 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0): # Spawn tiles (as requested "tekton will spawn a tiles around that floor also") if is_multiplayer_authority(): - spawn_tiles_around(int(8 * intensity)) + spawn_tiles_around(int(8 * intensity)) # Floor Freeze (Visual/Instant - Run on all clients locally) temporarily_change_floor(current_position, 1, 6, 3.0) @@ -281,7 +281,6 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0): controller._start_timer() - func temporarily_change_floor(center: Vector2i, radius: int, new_id: int, duration: float): if not enhanced_gridmap: return @@ -372,6 +371,11 @@ func spawn_tiles_around(count: int = 4): continue if enhanced_gridmap.is_position_valid(pos): + # EXTRA CHECK: Do not spawn tiles on walls (ID 4) or empty void (ID -1) on Floor 0 + var floor_0_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) + if floor_0_item == 4 or floor_0_item == -1: + continue + # 50% chance to spawn something if rng.randf() > 0.5: continue @@ -403,14 +407,14 @@ func play_animation_rpc(anim_name: String): func spawn_projectile_rpc(target_pos: Vector3, duration: float): var projectile = MeshInstance3D.new() var box = BoxMesh.new() - box.size = Vector3(0.4, 0.1, 0.4) + box.size = Vector3(0.4, 0.1, 0.4) projectile.mesh = box var mat = StandardMaterial3D.new() mat.albedo_color = Color(1, 0.5, 0) # Orange projectile.material_override = mat get_parent().add_child(projectile) - projectile.global_position = global_position + Vector3(0, 2.0, 0) + projectile.global_position = global_position + Vector3(0, 2.0, 0) var tween = create_tween() tween.set_parallel(true) @@ -419,8 +423,8 @@ func spawn_projectile_rpc(target_pos: Vector3, duration: float): var mid_y = max(global_position.y, target_pos.y) + 3.0 var tween_y = create_tween() - tween_y.tween_property(projectile, "global_position:y", mid_y, duration/2).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - tween_y.tween_property(projectile, "global_position:y", target_pos.y, duration/2).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(duration/2) + tween_y.tween_property(projectile, "global_position:y", mid_y, duration / 2).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + tween_y.tween_property(projectile, "global_position:y", target_pos.y, duration / 2).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(duration / 2) tween.chain().tween_callback(projectile.queue_free)