318 lines
13 KiB
GDScript
318 lines
13 KiB
GDScript
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()
|
|
|
|
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])
|
|
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.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
|
|
|
|
_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)
|
|
|
|
# --- TEKTONS SECTION ---
|
|
overlay.clear_highlight()
|
|
await overlay.display_text("Now, what about those little creatures running around the map? Those are [color=cyan]Tektons[/color]!", true)
|
|
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("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)
|
|
|
|
# --- INTERACTIVE 2: HOLO TILE ---
|
|
await overlay.display_text("Now, let's talk about [color=cyan]Holo Tiles[/color]. They grant you special Power-Ups!", true)
|
|
|
|
var holo_pos = _find_closest_tile(local_player.current_position, [11, 12, 13, 14])
|
|
if holo_pos != Vector2i(-1, -1):
|
|
_highlight_floor_cell(holo_pos, Color(0.0, 1.0, 1.0)) # Cyan box
|
|
overlay.display_text("I've highlighted a [color=cyan]Holo Tile[/color] with a cyan box. Go ahead and grab it!", false)
|
|
else:
|
|
# Fallback: force spawn one if the RNG map generation didn't place any
|
|
holo_pos = local_player.current_position + Vector2i(0, 1)
|
|
var gridmap = local_player.get("enhanced_gridmap")
|
|
if gridmap:
|
|
gridmap.set_cell_item(Vector3i(holo_pos.x, 1, holo_pos.y), 11) # Spawn speed boost
|
|
_highlight_floor_cell(holo_pos, Color(0.0, 1.0, 1.0))
|
|
overlay.display_text("I just dropped a Holo Tile near you (cyan box). Go ahead and grab it!", false)
|
|
|
|
overlay.hide_overlay()
|
|
get_tree().paused = false
|
|
|
|
initial_items = local_player.playerboard.count(-1)
|
|
while true:
|
|
if holo_pos != Vector2i(-1, -1) and local_player.get("enhanced_gridmap"):
|
|
if local_player.enhanced_gridmap.get_cell_item(Vector3i(holo_pos.x, 1, holo_pos.y)) == -1:
|
|
break
|
|
elif local_player.playerboard.count(-1) < initial_items:
|
|
break
|
|
await get_tree().process_frame
|
|
|
|
_clear_floor_highlights()
|
|
get_tree().paused = true
|
|
overlay.show_overlay()
|
|
|
|
# --- POWER UP SECTION ---
|
|
# Highlight Power Up Icon
|
|
overlay.highlight_zone("PowerUpItem")
|
|
await overlay.display_text("Nice! You grabbed a Holo Tile — that earned you a special [color=cyan]Power-Up[/color] item!", true)
|
|
await overlay.display_text("There are [color=cyan]4 types of Power-Ups[/color] from Holo Tiles. Let me walk you through each one.", true)
|
|
await overlay.show_powerup_showcase()
|
|
await overlay.display_text("To activate your stored Power-Up, press [color=yellow]'F'[/color]. If you don't like this key, you can remap it in [color=gold]Settings[/color].", true)
|
|
|
|
# --- 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
|
|
|
|
# Freeze it so its AI doesn't run away during the tutorial
|
|
var bc = spawned_bot.get_node_or_null("BotController")
|
|
if bc:
|
|
bc.set_process(false)
|
|
bc.set_physics_process(false)
|
|
spawned_bot.set("is_frozen", true)
|
|
|
|
if spawned_bot:
|
|
await overlay.display_text("Let's try it. I'm placing a Bot right next to you, and I'll instantly fill your Battery Gauge to max.", true)
|
|
|
|
# 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()
|
|
|
|
var n_pos = local_player.current_position + Vector2i(1, 0)
|
|
if local_player.get("enhanced_gridmap"):
|
|
var neighbors = local_player.enhanced_gridmap.get_neighbors(local_player.current_position, 0)
|
|
for n in neighbors:
|
|
if n.get("is_walkable"):
|
|
n_pos = n.position
|
|
break
|
|
|
|
# Safely set bot position
|
|
spawned_bot.current_position = n_pos
|
|
if local_player.get("enhanced_gridmap") and local_player.enhanced_gridmap.has_method("grid_to_world"):
|
|
spawned_bot.global_position = local_player.enhanced_gridmap.grid_to_world(n_pos)
|
|
|
|
overlay.display_text("Press 'Q' to enter Attack Mode, then Walk into the Bot!", false)
|
|
|
|
overlay.hide_overlay()
|
|
get_tree().paused = false
|
|
|
|
# Wait for the player to push the bot (bot's position changes)
|
|
var bot_old_pos = spawned_bot.current_position
|
|
while spawned_bot.current_position == bot_old_pos:
|
|
await get_tree().process_frame
|
|
|
|
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)
|
|
|
|
overlay.hide_overlay()
|
|
get_tree().paused = false
|
|
_set_bots_enabled(true)
|
|
|
|
LobbyManager.is_tutorial_mode = false
|
|
queue_free()
|
|
|
|
# ==============================================================================
|
|
# Helper Floor Highlighting and Tile Detection Logic
|
|
# ==============================================================================
|
|
|
|
var _floor_highlights: Array[Node] = []
|
|
|
|
func _find_closest_tile(start_pos: Vector2i, types: Array) -> 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 < 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()
|