extends Node class_name TutorialManager var overlay var main_scene: Node var local_player: Node func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS # Wait for gridmap, Player, and UI to spawn fully await get_tree().create_timer(1.0).timeout main_scene = get_tree().root.get_node_or_null("Main") if not main_scene: return # Wait for the client to gain character control var max_wait = 5.0 var elapsed = 0.0 while not local_player and elapsed < max_wait: for p in get_tree().get_nodes_in_group("Players"): if p.is_multiplayer_authority(): local_player = p break await get_tree().process_frame elapsed += get_process_delta_time() # Pause match timer during tutorial if main_scene and main_scene.get("goals_cycle_manager"): main_scene.goals_cycle_manager.set_process(false) if not local_player: print("Tutorial Error: Local player not found.") return # Freeze bots while teaching _set_bots_enabled(false) # Make sure the UI overlay exists — instantiate as a full scene var overlay_scene = load("res://scenes/tutorial/tutorial_overlay.tscn") if overlay_scene: overlay = overlay_scene.instantiate() add_child(overlay) _run_tutorial() else: print("Tutorial Error: tutorial_overlay.tscn not found.") func _set_bots_enabled(enabled: bool) -> void: for p in get_tree().get_nodes_in_group("Bots"): var bc = p.get_node_or_null("BotController") if bc: bc.set_process(enabled) bc.set_physics_process(enabled) p.set("is_frozen", not enabled) func _get_ui_rect(path: String) -> Rect2: var node = main_scene.get_node_or_null(path) if node and node is Control: # get_global_rect() returns coords in the node's canvas layer space. # We need to apply the viewport canvas transform so they line up with # the TutorialOverlay CanvasLayer (which renders in viewport space). var rect = node.get_global_rect() return rect return Rect2() func _run_tutorial() -> void: get_tree().paused = true # Intro await overlay.display_text("Welcome to the Arena. I'm Commander Copper. Let me walk you through the basics of Free Mode.", true) # Playerboard Highlight overlay.highlight_zone("PlayerBoard") await overlay.display_text("This is your [color=green]Playerboard[/color]. Your main objective is to fill these empty slots.", true) # Goal / Timer Highlight overlay.highlight_zone("GoalsTimer") await overlay.display_text("These are your [color=gold]Goals[/color]. You must match these exact tiles specifically to score points.", true) # (AP system removed — no longer in use) # Grid Highlight overlay.clear_highlight() await overlay.display_text("Look at the board! You will see [color=gold]Normal Tiles[/color] and shiny [color=cyan]Holo Tiles[/color].", true) # ============================================================================== # --- INTERACTIVE 1: NORMAL TILE --- # ============================================================================== var normal_pos = _find_closest_tile(local_player.current_position, [7, 8, 9, 10], 4) if normal_pos != Vector2i(-1, -1): _highlight_floor_cell(normal_pos, Color(1.0, 1.0, 0.0)) # Yellow box overlay.display_text("I've highlighted a [color=gold]Normal Tile[/color] with a yellow box. Walk over to it and press [color=red]SPACE[/color] to Grab it!", false) else: overlay.display_text("Walk up to any Normal Tile and press [color=red]SPACE[/color] to Grab it!", false) overlay.set_objective("Walk to a [color=gold]Normal Tile[/color] and press [color=red]SPACE[/color] to grab it.") overlay.hide_overlay() get_tree().paused = false # Wait strictly until the specific tile is grabbed, or any tile if no specific one was found var initial_items = local_player.playerboard.count(-1) while true: if normal_pos != Vector2i(-1, -1) and local_player.get("enhanced_gridmap"): if local_player.enhanced_gridmap.get_cell_item(Vector3i(normal_pos.x, 1, normal_pos.y)) == -1: break elif local_player.playerboard.count(-1) < initial_items: break await get_tree().process_frame overlay.hide_objective() _clear_floor_highlights() get_tree().paused = true overlay.show_overlay() await overlay.display_text("Outstanding! You secured a tile.", true) # ============================================================================== # --- SCORE AND GOALS SECTION --- # ============================================================================== overlay.highlight_zone("Leaderboard") await overlay.display_text("Your main objective is to climb the [color=gold]Leaderboard[/color] on the right by scoring points.", true) overlay.highlight_zone("PlayerBoard") await overlay.display_text("You earn points by collecting tiles, but the real score comes from completing your [color=gold]Playerboard Goals[/color]!", true) await overlay.display_text("Fill your entire board shape to complete a goal. This grants massive points, clears the board, and ranks you up!", true) # ============================================================================== # --- POWER UP SECTION --- # ============================================================================== await overlay.display_text("Now, let's talk about [color=cyan]Holo Tiles[/color]. They grant you special Power-Ups!", true) overlay.clear_highlight() await overlay.display_text("There are [color=cyan]4 types of Power-Ups[/color] from Holo Tiles. Let me walk you through each one, one by one.", true) var powerup_ids = [11, 12, 13, 14] var powerup_names = { 11: "[color=gold]Speed Boost[/color] (Make you run fast)", 12: "[color=aqua]Area Freeze[/color] (Slows nearby opponents)", 13: "[color=gray]Iron Wall[/color] (Blocks opponent path)", 14: "[color=white]Ghost Mode[/color] (Invisibility and ram immunity)" } var powerup_enums = {11: 0, 12: 1, 13: 2, 14: 3} var gridmap = local_player.get("enhanced_gridmap") for pid in powerup_ids: overlay.clear_highlight() overlay.show_powerup_card(pid) await overlay.display_text("Next is %s. I will drop this Holo Tile near you." % powerup_names[pid], true) overlay.hide_powerup_card() var ppos = local_player.current_position + Vector2i(0, 1) if gridmap: gridmap.set_cell_item(Vector3i(ppos.x, 1, ppos.y), pid) _highlight_floor_cell(ppos, Color(0.0, 1.0, 1.0)) overlay.display_text("Grab the tile, press [color=yellow]'F'[/color] to use it, and for some skills press it again or Click to confirm!", false) overlay.set_objective("Grab the [color=cyan]Holo Tile[/color] and press [color=yellow]'F'[/color] to use %s." % powerup_names[pid]) overlay.hide_overlay() get_tree().paused = false # Wait for it to be grabbed while gridmap and gridmap.get_cell_item(Vector3i(ppos.x, 1, ppos.y)) == pid: await get_tree().process_frame # Highlight the Power-Up item slot now that they picked it up overlay.highlight_zone("PowerUpItem") # Wait for it to be used var effect_enum = powerup_enums[pid] while local_player.get("special_tiles_manager") and local_player.special_tiles_manager.inventory.get(effect_enum, false) == true: await get_tree().process_frame overlay.clear_highlight() overlay.hide_objective() _clear_floor_highlights() get_tree().paused = true overlay.show_overlay() await overlay.display_text("Excellent! You used %s." % powerup_names[pid], true) # ============================================================================== # --- TEKTONS SECTION --- # ============================================================================== overlay.clear_highlight() # --- DYNAMIC TEKTON FOCUS --- var dynamic_tektons = get_tree().get_nodes_in_group("Tektons") var cam_mgr = main_scene.get("camera_context_manager") if main_scene else null if dynamic_tektons.size() > 0 and cam_mgr: # Find nearest dynamic tekton var nearest = null var nearest_dist = 999999 for tk in dynamic_tektons: if tk.get("is_static_turret") == false: var dist = local_player.global_position.distance_to(tk.global_position) if dist < nearest_dist: nearest_dist = dist nearest = tk if nearest: cam_mgr.process_mode = Node.PROCESS_MODE_ALWAYS cam_mgr.set_player(nearest) await get_tree().create_timer(1.2, true).timeout var cam = main_scene.get_node_or_null("Camera3D200") if cam and cam is Camera3D: var screen_pos = cam.unproject_position(nearest.global_position) overlay.highlight_rect(Rect2(screen_pos.x - 75, screen_pos.y - 75, 150, 150)) await overlay.display_text("Now, what about those little creatures running around the map? Those are [color=cyan]Tektons[/color]!", true) if cam_mgr: cam_mgr.set_player(local_player) cam_mgr.process_mode = Node.PROCESS_MODE_INHERIT overlay.clear_highlight() await get_tree().create_timer(0.8, true).timeout # ---------------------------- await overlay.display_text("Grabbing a dynamic Tekton gives you [color=gold]PASSIVE score points[/color] over time and spawns bonus tiles for you while you carry it.", true) await overlay.display_text("Hold onto them as long as you can... but watch out, opponents will try to RAM you and steal them!", true) await overlay.display_text("Go and catch one of the Tektons running around!", false) overlay.set_objective("Walk into a [color=cyan]Tekton[/color] or press [color=red]G[/color] near one to grab it.") overlay.hide_overlay() get_tree().paused = false while not local_player.is_carrying_tekton: await get_tree().process_frame overlay.hide_objective() get_tree().paused = true overlay.show_overlay() await overlay.display_text("Great job! Notice your score passively increasing.", true) await overlay.display_text("But you won't hold it forever! The Tekton runs away when your carry timer reaches 0, or you can manually throw it by pressing [color=red]G[/color].", false) overlay.set_objective("Wait for the [color=cyan]Tekton[/color] to escape, or press [color=red]G[/color] to throw it!") overlay.hide_overlay() get_tree().paused = false while local_player.is_carrying_tekton: await get_tree().process_frame overlay.hide_objective() get_tree().paused = true overlay.show_overlay() await overlay.display_text("Oops, it got away! But don't worry, you can always catch another one.", true) # ============================================================================== # --- STATIC TEKTON FOCUS --- # ============================================================================== var static_stands = get_tree().get_nodes_in_group("StaticTektonStands") cam_mgr = main_scene.get("camera_context_manager") if main_scene else null if static_stands.size() > 0 and cam_mgr: var target_stand = static_stands[0] cam_mgr.process_mode = Node.PROCESS_MODE_ALWAYS # Allow camera to slide while paused cam_mgr.set_player(target_stand) # Give it time to pan over await get_tree().create_timer(1.2, true).timeout # Dim the screen around it var cam = main_scene.get_node_or_null("Camera3D200") if cam and cam is Camera3D: var screen_pos = cam.unproject_position(target_stand.global_position) overlay.highlight_rect(Rect2(screen_pos.x - 100, screen_pos.y - 120, 200, 200)) await overlay.display_text("Also keep an eye on [color=gray]Static Tektons[/color] standing on podiums. They periodically throw new free tiles onto the board for you to collect!", true) if cam_mgr: cam_mgr.set_player(local_player) cam_mgr.process_mode = Node.PROCESS_MODE_INHERIT overlay.clear_highlight() # Let it pan back briefly await get_tree().create_timer(0.8, true).timeout # ============================================================================== # --- BATTERY GAUGE & RAMMING SECTION --- # ============================================================================== # Switch to highlight Power Bar / Battery Gauge overlay.highlight_zone("PowerBar") await overlay.display_text("Next, look at your [color=cyan]Battery Gauge[/color] — it [color=yellow]fills up automatically over time[/color]. You don't need to do anything to charge it.", true) await overlay.display_text("When the Battery Gauge is [color=red]FULL[/color], you'll see the [color=red]ATTACK MODE READY[/color] notification.", true) overlay.clear_highlight() # Ramming explanation and practice await overlay.display_text("What is Attack Mode? It lets you [color=red]RAM[/color] other players!", true) await overlay.display_text("Press [color=yellow]'Q'[/color] to enter [color=red]ATTACK MODE[/color]. While active, walk directly into an opponent to [color=red]RAM[/color] them!", true) await overlay.display_text("Ramming pushes them back and forces them to drop their tile. Each hit drains your Battery Gauge.", true) var spawned_bot = null for p in get_tree().get_nodes_in_group("Bots"): spawned_bot = p break # Force spawn a dummy bot if the lobby settings disabled them if not spawned_bot and get_node_or_null("/root/PlayerManager"): spawned_bot = get_node("/root/PlayerManager").create_bot(99) get_parent().add_child(spawned_bot) spawned_bot.add_to_group("Players", true) spawned_bot.add_to_group("Bots", true) await get_tree().create_timer(0.2).timeout var bc = spawned_bot.get_node_or_null("BotController") if bc: bc.set_process(true) bc.set_physics_process(true) if spawned_bot: # Always unfreeze in case it was caught in the earlier Area Freeze blast spawned_bot.set("is_frozen", false) var target_bc = spawned_bot.get_node_or_null("BotController") if target_bc: target_bc.set_process(true) target_bc.set_physics_process(true) await overlay.display_text("Let's try it. There is a Bot roaming the map right now. I will fill your Battery Gauge to max.", true) # --- CAMERA PAN TO BOT --- cam_mgr = main_scene.get("camera_context_manager") if main_scene else null if cam_mgr: cam_mgr.process_mode = Node.PROCESS_MODE_ALWAYS cam_mgr.set_player(spawned_bot) await get_tree().create_timer(1.2, true).timeout var cam = main_scene.get_node_or_null("Camera3D200") if cam and cam is Camera3D: var screen_pos = cam.unproject_position(spawned_bot.global_position) overlay.highlight_rect(Rect2(screen_pos.x - 75, screen_pos.y - 100, 150, 150)) await overlay.display_text("Behold, your opponent! Their AI is active so you will have to hunt them down.", true) if cam_mgr: cam_mgr.set_player(local_player) cam_mgr.process_mode = Node.PROCESS_MODE_INHERIT overlay.clear_highlight() await get_tree().create_timer(0.8, true).timeout # ------------------------ # Instantly grant FULL BOOST if local_player.get("powerup_manager"): local_player.powerup_manager.acquire_smash_bonus() local_player.powerup_manager.acquire_smash_bonus() local_player.powerup_manager.acquire_smash_bonus() local_player.powerup_manager.acquire_smash_bonus() overlay.display_text("Press 'Q' to enter Attack Mode, then chase the Bot down to RAM it!", false) overlay.set_objective("Press [color=yellow]'Q'[/color] to enter [color=red]ATTACK MODE[/color], then chase and RAM the bot!") overlay.hide_overlay() get_tree().paused = false # Wait for the player to push the bot while true: if spawned_bot.get("is_knock_mode") == true: break # Fallback: if player's charged strike deactivated early despite having timer left, they likely successfully rammed var t = local_player.get("charged_strike_timer") if local_player.get("is_charged_strike") == false and t != null and t > 0.0 and t < 3.0: break await get_tree().process_frame overlay.hide_objective() get_tree().paused = true overlay.show_overlay() await overlay.display_text("Smashing! You pushed it away. Notice your Battery Gauge was consumed.", true) await overlay.display_text("Beware! Your opponents can RAM you to steal your tiles too. Get ready to defend yourself against the Bots!", true) await overlay.display_text("Tutorial Complete! Returning to the Main Menu.", true) overlay.hide_overlay() get_tree().paused = false if main_scene and main_scene.get("goals_cycle_manager"): main_scene.goals_cycle_manager.set_process(true) LobbyManager.is_tutorial_mode = false LobbyManager.leave_room() get_tree().change_scene_to_file("res://scenes/lobby.tscn") # ============================================================================== # Helper Floor Highlighting and Tile Detection Logic # ============================================================================== var _floor_highlights: Array[Node] = [] func _find_closest_tile(start_pos: Vector2i, types: Array, min_dist: int = 0) -> Vector2i: var gridmap = local_player.get("enhanced_gridmap") if not gridmap: return Vector2i(-1, -1) var best_pos = Vector2i(-1, -1) var best_dist = 999999 for x in range(gridmap.columns): for z in range(gridmap.rows): var item = gridmap.get_cell_item(Vector3i(x, 1, z)) if item in types: var dist = abs(start_pos.x - x) + abs(start_pos.y - z) if dist >= min_dist and dist < best_dist: best_dist = dist best_pos = Vector2i(x, z) return best_pos func _highlight_floor_cell(grid_pos: Vector2i, color: Color): var gridmap = local_player.get("enhanced_gridmap") if not gridmap: return var cs: Vector3 = gridmap.cell_size # Sit slightly above the floor grid (using layer 2 height approximation) var y := 2.0 * cs.y + 0.09 const T := 0.09 # Outline thickness const H := 0.045 # Outline height var mat := StandardMaterial3D.new() mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color = Color(color.r, color.g, color.b, 0.6) mat.emission_enabled = true mat.emission = color mat.emission_energy_multiplier = 2.0 mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_DISABLED var container := Node3D.new() gridmap.add_child(container) _floor_highlights.append(container) var cell_cx = grid_pos.x * cs.x + cs.x * 0.5 var cell_cz = grid_pos.y * cs.z + cs.z * 0.5 var edges = [ {"pos": Vector3(cell_cx, y, grid_pos.y * cs.z), "size": Vector3(cs.x, H, T)}, {"pos": Vector3(cell_cx, y, grid_pos.y * cs.z + cs.z), "size": Vector3(cs.x, H, T)}, {"pos": Vector3(grid_pos.x * cs.x, y, cell_cz), "size": Vector3(T, H, cs.z)}, {"pos": Vector3(grid_pos.x * cs.x + cs.x, y, cell_cz), "size": Vector3(T, H, cs.z)} ] for edge in edges: var mi := MeshInstance3D.new() var box := BoxMesh.new() box.size = edge.size box.material = mat mi.mesh = box mi.position = edge.pos container.add_child(mi) # Subtly pulse the highlight var tween = create_tween().set_loops() tween.tween_method(func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a, 0.7, 0.2, 0.6).set_ease(Tween.EASE_OUT) tween.tween_method(func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a, 0.2, 0.7, 0.6).set_ease(Tween.EASE_IN) func _clear_floor_highlights(): for h in _floor_highlights: if is_instance_valid(h): h.queue_free() _floor_highlights.clear()