feat: add tutorial section
This commit is contained in:
@@ -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"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="build/tekton_armageddon_v2.1.exe"
|
||||
export_path="build/tekton_armageddon_v2.1.2.exe"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
|
||||
@@ -121,6 +121,18 @@ func _ready():
|
||||
# Initial UI update
|
||||
_on_profile_updated()
|
||||
|
||||
# Inject Tutorial button
|
||||
var tutorial_btn = Button.new()
|
||||
tutorial_btn.name = "TutorialBtn"
|
||||
tutorial_btn.text = "PLAY TUTORIAL"
|
||||
tutorial_btn.theme_type_variation = "FlatButton"
|
||||
tutorial_btn.add_theme_font_size_override("font_size", 24)
|
||||
tutorial_btn.pressed.connect(_on_tutorial_pressed)
|
||||
var btn_section = main_menu_panel.get_node_or_null("VBoxContainer/ButtonSection")
|
||||
if btn_section:
|
||||
btn_section.add_child(tutorial_btn)
|
||||
btn_section.move_child(tutorial_btn, 0)
|
||||
|
||||
# Connect button signals - Main Menu
|
||||
create_room_btn.pressed.connect(_on_create_room_pressed)
|
||||
browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed)
|
||||
@@ -333,6 +345,21 @@ func _show_panel(panel_name: String) -> void:
|
||||
# Main Menu Button Handlers
|
||||
# =============================================================================
|
||||
|
||||
func _on_tutorial_pressed() -> void:
|
||||
if AuthManager.is_guest:
|
||||
if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player":
|
||||
LobbyManager.local_player_name = NameGenerator.generate_guest_name()
|
||||
else:
|
||||
LobbyManager.local_player_name = UserProfileManager.get_display_name()
|
||||
connection_status.text = "Starting Tutorial Scenario..."
|
||||
_apply_loadout_character()
|
||||
LobbyManager.start_tutorial("Freemode")
|
||||
# Give the engine two frames for room_joined signal to settle,
|
||||
# then immediately force-start the game — bypassing the lobby wait screen.
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
LobbyManager.start_game(true)
|
||||
|
||||
func _on_create_room_pressed() -> void:
|
||||
# Use profile name for logged-in users, or guest for others
|
||||
if AuthManager.is_guest:
|
||||
|
||||
@@ -101,6 +101,15 @@ func _ready():
|
||||
if top_settings_btn:
|
||||
if not top_settings_btn.pressed.is_connected(_toggle_pause_menu):
|
||||
top_settings_btn.pressed.connect(_toggle_pause_menu)
|
||||
|
||||
# Tutorial Override
|
||||
if LobbyManager.get("is_tutorial_mode"):
|
||||
var tutorial_script = load("res://scripts/managers/tutorial_manager.gd")
|
||||
if tutorial_script:
|
||||
var tutorial_node = Node.new()
|
||||
tutorial_node.set_script(tutorial_script)
|
||||
tutorial_node.name = "TutorialManager"
|
||||
add_child(tutorial_node)
|
||||
|
||||
func _setup_multiplayer_spawners():
|
||||
# Setup MultiplayerSpawner for Static Tekton Stands
|
||||
|
||||
@@ -1644,6 +1644,9 @@ func start_movement_along_path(path: Array, clear_visual: bool = true, force: bo
|
||||
if TurnManager.turn_based_mode:
|
||||
end_turn()
|
||||
_after_action_completed()
|
||||
|
||||
if movement_manager and movement_manager.has_method("_on_movement_finished"):
|
||||
movement_manager.call_deferred("_on_movement_finished")
|
||||
)
|
||||
|
||||
#func trigger_finish_line():
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
// Spotlight rect in viewport pixel coordinates
|
||||
// vec4(x, y, width, height) — set to vec4(0,0,0,0) to dim everything
|
||||
uniform vec4 spotlight = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
uniform vec2 viewport_size = vec2(1366.0, 720.0);
|
||||
uniform float dim_alpha : hint_range(0.0, 1.0) = 0.72;
|
||||
uniform float padding : hint_range(0.0, 32.0) = 8.0;
|
||||
|
||||
void fragment() {
|
||||
bool has_spotlight = spotlight.z > 1.0 && spotlight.w > 1.0;
|
||||
|
||||
if (has_spotlight) {
|
||||
// Convert spotlight rect to UV space
|
||||
float left = (spotlight.x - padding) / viewport_size.x;
|
||||
float top = (spotlight.y - padding) / viewport_size.y;
|
||||
float right = (spotlight.x + spotlight.z + padding) / viewport_size.x;
|
||||
float bottom = (spotlight.y + spotlight.w + padding) / viewport_size.y;
|
||||
|
||||
bool in_hole = (UV.x >= left) && (UV.x <= right)
|
||||
&& (UV.y >= top) && (UV.y <= bottom);
|
||||
|
||||
if (in_hole) {
|
||||
COLOR = vec4(0.0, 0.0, 0.0, 0.0); // transparent — element shows through
|
||||
} else {
|
||||
COLOR = vec4(0.0, 0.0, 0.0, dim_alpha);
|
||||
}
|
||||
} else {
|
||||
COLOR = vec4(0.0, 0.0, 0.0, dim_alpha);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b74mlkt6hd88r
|
||||
@@ -0,0 +1,159 @@
|
||||
[gd_scene format=3 uid="uid://b457o3pea81nm"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bnhgqibcee442" path="res://scripts/managers/tutorial_overlay.gd" id="1_script"]
|
||||
[ext_resource type="Shader" uid="uid://b74mlkt6hd88r" path="res://scenes/tutorial/dim_spotlight.gdshader" id="2_shader"]
|
||||
[ext_resource type="Texture2D" uid="uid://brhn1dhp1gm13" path="res://assets/graphics/character_selection/sc_characters/sc_copper.png" id="3_portrait"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_dim"]
|
||||
shader = ExtResource("2_shader")
|
||||
shader_parameter/spotlight = Vector4(0, 0, 0, 0)
|
||||
shader_parameter/viewport_size = Vector2(1366, 720)
|
||||
shader_parameter/dim_alpha = 0.72
|
||||
shader_parameter/padding = 10.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_border"]
|
||||
bg_color = Color(0, 0, 0, 0)
|
||||
border_width_left = 4
|
||||
border_width_top = 4
|
||||
border_width_right = 4
|
||||
border_width_bottom = 4
|
||||
border_color = Color(1, 0.85, 0.1, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
||||
bg_color = Color(0.08, 0.08, 0.13, 0.97)
|
||||
border_width_left = 4
|
||||
border_width_top = 4
|
||||
border_width_right = 4
|
||||
border_width_bottom = 4
|
||||
border_color = Color(0.4, 0.7, 1, 1)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
|
||||
[node name="TutorialOverlay" type="CanvasLayer" unique_id=1522233495]
|
||||
layer = 100
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="InputMask" type="Control" parent="." unique_id=441807788]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="DimRect" type="ColorRect" parent="." unique_id=1364863078]
|
||||
material = SubResource("ShaderMaterial_dim")
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0, 0, 0, 1)
|
||||
|
||||
[node name="HighlightBorder" type="Panel" parent="." unique_id=25061739]
|
||||
visible = false
|
||||
offset_right = 100.0
|
||||
offset_bottom = 100.0
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_border")
|
||||
|
||||
[node name="DialoguePanel" type="PanelContainer" parent="." unique_id=2001383324]
|
||||
offset_left = 283.0
|
||||
offset_top = 510.0
|
||||
offset_right = 1083.0
|
||||
offset_bottom = 690.0
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="DialoguePanel" unique_id=1771861001]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="DialoguePanel/MarginContainer" unique_id=357088930]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="Portrait" type="TextureRect" parent="DialoguePanel/MarginContainer/HBoxContainer" unique_id=1911601986]
|
||||
custom_minimum_size = Vector2(120, 120)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
texture = ExtResource("3_portrait")
|
||||
expand_mode = 2
|
||||
stretch_mode = 6
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="DialoguePanel/MarginContainer/HBoxContainer" unique_id=1395184995]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SpeakerName" type="Label" parent="DialoguePanel/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1999858404]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.5, 0.8, 1, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "COMMANDER COPPER"
|
||||
|
||||
[node name="DialogueText" type="RichTextLabel" parent="DialoguePanel/MarginContainer/HBoxContainer/VBoxContainer" unique_id=476321723]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/normal_font_size = 19
|
||||
bbcode_enabled = true
|
||||
|
||||
[node name="NextIndicator" type="Label" parent="DialoguePanel/MarginContainer/HBoxContainer/VBoxContainer" unique_id=554389315]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 0.5, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Click anywhere to continue ▼"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="HighlightZones" type="Node" parent="." unique_id=2013793015]
|
||||
metadata/_comment = "Visual-editor zones: open tutorial_overlay.tscn in Godot, drag each Control to match the game UI element you want highlighted. Positions are in 1366x720 viewport space."
|
||||
|
||||
[node name="PlayerBoard" type="Control" parent="HighlightZones" unique_id=461308610]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_left = 36.0
|
||||
offset_top = 87.0
|
||||
offset_right = 301.0
|
||||
offset_bottom = 374.0
|
||||
|
||||
[node name="PowerBar" type="Control" parent="HighlightZones" unique_id=421147912]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_left = 30.0
|
||||
offset_top = 339.0
|
||||
offset_right = 317.0
|
||||
offset_bottom = 415.0
|
||||
|
||||
[node name="GoalsTimer" type="Control" parent="HighlightZones" unique_id=1065585227]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_left = 499.0
|
||||
offset_top = 1.0
|
||||
offset_right = 875.0
|
||||
offset_bottom = 161.0
|
||||
|
||||
[node name="PowerUpItem" type="Control" parent="HighlightZones" unique_id=474081571]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_left = 130.0
|
||||
offset_top = 501.0
|
||||
offset_right = 220.0
|
||||
offset_bottom = 591.0
|
||||
|
||||
[node name="Leaderboard" type="Control" parent="HighlightZones" unique_id=1503191193]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_left = 1080.0
|
||||
offset_top = 96.0
|
||||
offset_right = 1356.0
|
||||
offset_bottom = 405.0
|
||||
@@ -41,6 +41,9 @@ const LAN_PORT: int = 7777 # Port for LAN direct connections
|
||||
const LAN_DISCOVERY_PORT: int = 7778 # Port for LAN discovery
|
||||
var local_player_name: String = "Player"
|
||||
|
||||
# Tutorial Mode Flag
|
||||
var is_tutorial_mode: bool = false
|
||||
|
||||
var _udp_peer: PacketPeerUDP
|
||||
var _broadcast_timer: Timer
|
||||
|
||||
@@ -139,6 +142,14 @@ func _update_available_areas(mode: String) -> void:
|
||||
# Room Creation / Joining
|
||||
# =============================================================================
|
||||
|
||||
func start_tutorial(mode: String = "Freemode") -> void:
|
||||
is_tutorial_mode = true
|
||||
game_mode = mode
|
||||
match_duration = 300
|
||||
GameStateManager.enable_bots = true
|
||||
GameStateManager.max_players = 2 # 1 Player + 1 Bot
|
||||
create_room_lan("Tutorial")
|
||||
|
||||
func create_room(room_name: String) -> void:
|
||||
"""Host creates a new room with the given name (Nakama)."""
|
||||
is_host = true
|
||||
|
||||
@@ -50,7 +50,10 @@ func _process(delta):
|
||||
player.grab_item(player.current_position)
|
||||
|
||||
if move_vec != Vector2i.ZERO:
|
||||
var target_position = player.current_position + move_vec
|
||||
var base_pos = player.current_position
|
||||
if movement_manager.is_moving and player.target_position != Vector2i(-1, -1):
|
||||
base_pos = player.target_position
|
||||
var target_position = base_pos + move_vec
|
||||
movement_manager.simple_move_to(target_position)
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ func _can_rpc() -> bool:
|
||||
|
||||
func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
if is_moving:
|
||||
# Queue logic for smooth continuous movement
|
||||
if grid_position != player.target_position and grid_position != player.current_position:
|
||||
if movement_queue.is_empty():
|
||||
movement_queue.append(grid_position)
|
||||
else:
|
||||
movement_queue[0] = grid_position
|
||||
return false
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
|
||||
@@ -123,14 +123,18 @@ func grab_item(grid_position: Vector2i) -> bool:
|
||||
if multiplayer.is_server():
|
||||
# HOST/SERVER: Broadcast to all clients
|
||||
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
||||
# Use main's RPC which properly looks up player by ID on each client
|
||||
var peer_id = player.name.to_int()
|
||||
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||
player.has_performed_action = true
|
||||
player.consume_action_points(1)
|
||||
player.rpc("force_action_state_none")
|
||||
else:
|
||||
# CLIENT: Send RPC request to server for validation
|
||||
# CLIENT: Optimistic Local Update for lag compensation
|
||||
player.has_performed_action = true
|
||||
player.consume_action_points(1)
|
||||
player.force_action_state_none()
|
||||
|
||||
# Send RPC request to server for validation
|
||||
player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item)
|
||||
|
||||
return true # Action applied locally
|
||||
@@ -277,6 +281,10 @@ func _force_sync_to_client(cell: Vector3i, server_item: int):
|
||||
# Sync their Playerboard (which they thought they updated)
|
||||
main.rpc_id(peer_id, "sync_playerboard", peer_id, player.playerboard)
|
||||
|
||||
# Restore Action Points locally for the client (sync action points back)
|
||||
# player is the caller peer since this is the server instance of the player's manager
|
||||
player.rpc_id(peer_id, "sync_action_points", player.action_points)
|
||||
|
||||
print("Server: Forced sync to client %d due to action failure." % peer_id)
|
||||
|
||||
func bot_try_grab_item() -> bool:
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bifo8j2px1agw
|
||||
@@ -0,0 +1,246 @@
|
||||
extends CanvasLayer
|
||||
class_name TutorialOverlay
|
||||
|
||||
# --- Scene node references ---
|
||||
@onready var dim_rect: ColorRect = $DimRect
|
||||
@onready var highlight_border: Panel = $HighlightBorder
|
||||
@onready var dialogue_panel: PanelContainer = $DialoguePanel
|
||||
@onready var text_label: RichTextLabel = $DialoguePanel/MarginContainer/HBoxContainer/VBoxContainer/DialogueText
|
||||
@onready var next_indicator: Label = $DialoguePanel/MarginContainer/HBoxContainer/VBoxContainer/NextIndicator
|
||||
@onready var highlight_zones: Node = $HighlightZones
|
||||
|
||||
# --- State ---
|
||||
var is_waiting_for_input: bool = false
|
||||
signal next_pressed
|
||||
|
||||
var _pulse_tween: Tween
|
||||
var _dim_material: ShaderMaterial
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
get_viewport().size_changed.connect(_update_panel_position)
|
||||
_dim_material = dim_rect.material as ShaderMaterial
|
||||
_update_panel_position()
|
||||
clear_highlight()
|
||||
|
||||
func _update_panel_position() -> void:
|
||||
if not is_instance_valid(dialogue_panel) or not get_viewport():
|
||||
return
|
||||
var vp = get_viewport().get_visible_rect().size
|
||||
var panel_w = dialogue_panel.size.x if dialogue_panel.size.x > 0 else 800.0
|
||||
var panel_h = dialogue_panel.size.y if dialogue_panel.size.y > 0 else 180.0
|
||||
dialogue_panel.position = Vector2(
|
||||
(vp.x - panel_w) / 2.0,
|
||||
vp.y - panel_h - 24.0
|
||||
)
|
||||
# Keep shader viewport size in sync
|
||||
if _dim_material:
|
||||
_dim_material.set_shader_parameter("viewport_size", vp)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Highlight — named zone lookup (preferred) OR raw Rect2
|
||||
# ---------------------------------------------------------------------------
|
||||
func highlight_zone(zone_name: String) -> void:
|
||||
"""Highlight a named zone defined in the HighlightZones node in the scene."""
|
||||
if not highlight_zones:
|
||||
return
|
||||
var zone = highlight_zones.get_node_or_null(zone_name)
|
||||
if zone and zone is Control:
|
||||
# Use the zone's rect directly — it's already in the CanvasLayer's coord space
|
||||
highlight_rect(Rect2(zone.position, zone.size))
|
||||
else:
|
||||
push_warning("TutorialOverlay: highlight zone '%s' not found." % zone_name)
|
||||
|
||||
func highlight_rect(rect: Rect2) -> void:
|
||||
if not is_inside_tree() or not _dim_material:
|
||||
return
|
||||
var vp = get_viewport().get_visible_rect().size
|
||||
|
||||
# Pass the rect to the shader (pixel coordinates) for the cutout hole
|
||||
_dim_material.set_shader_parameter("viewport_size", vp)
|
||||
_dim_material.set_shader_parameter("spotlight",
|
||||
Vector4(rect.position.x, rect.position.y, rect.size.x, rect.size.y))
|
||||
|
||||
# Position the glowing border panel on top
|
||||
highlight_border.position = rect.position - Vector2(6, 6)
|
||||
highlight_border.size = rect.size + Vector2(12, 12)
|
||||
highlight_border.visible = true
|
||||
|
||||
# Pulsing glow animation
|
||||
if _pulse_tween:
|
||||
_pulse_tween.kill()
|
||||
_pulse_tween = create_tween().set_loops()
|
||||
_pulse_tween.tween_property(highlight_border, "self_modulate:a", 0.25, 0.75)
|
||||
_pulse_tween.tween_property(highlight_border, "self_modulate:a", 1.0, 0.75)
|
||||
|
||||
func clear_highlight() -> void:
|
||||
if _dim_material:
|
||||
_dim_material.set_shader_parameter("spotlight", Vector4(0, 0, 0, 0))
|
||||
if _pulse_tween:
|
||||
_pulse_tween.kill()
|
||||
_pulse_tween = null
|
||||
if is_instance_valid(highlight_border):
|
||||
highlight_border.visible = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Show / Hide whole overlay
|
||||
# ---------------------------------------------------------------------------
|
||||
func hide_overlay() -> void:
|
||||
dim_rect.visible = false
|
||||
highlight_border.visible = false
|
||||
dialogue_panel.visible = false
|
||||
|
||||
func show_overlay() -> void:
|
||||
dim_rect.visible = true
|
||||
dialogue_panel.visible = true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dialogue
|
||||
# ---------------------------------------------------------------------------
|
||||
func display_text(bbcode_text: String, wait_for_click: bool = true) -> void:
|
||||
show_overlay()
|
||||
text_label.text = bbcode_text
|
||||
# Wait one frame so the panel has laid itself out before calling position update
|
||||
await get_tree().process_frame
|
||||
_update_panel_position()
|
||||
|
||||
if wait_for_click:
|
||||
next_indicator.visible = true
|
||||
is_waiting_for_input = true
|
||||
await next_pressed
|
||||
else:
|
||||
next_indicator.visible = false
|
||||
is_waiting_for_input = false
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not is_waiting_for_input:
|
||||
return
|
||||
var consumed = false
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
consumed = true
|
||||
elif event is InputEventScreenTouch and event.pressed:
|
||||
consumed = true
|
||||
if consumed:
|
||||
is_waiting_for_input = false
|
||||
next_indicator.visible = false
|
||||
next_pressed.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Power-Up Showcase — one card per powerup with icon + name + description
|
||||
# ---------------------------------------------------------------------------
|
||||
func show_powerup_showcase() -> void:
|
||||
"""Display each powerup as an interactive card. Awaits click between each."""
|
||||
var powerups = [
|
||||
{
|
||||
"icon": "res://assets/graphics/touch_control/speed.png",
|
||||
"name": "[color=gold]⚡ Speed Boost[/color]",
|
||||
"desc": "Temporarily increases your movement speed for [color=yellow]5 seconds[/color]. Great for racing across the board to grab tiles before opponents."
|
||||
},
|
||||
{
|
||||
"icon": "res://assets/graphics/touch_control/freeze_area.png",
|
||||
"name": "[color=aqua]❄ Area Freeze[/color]",
|
||||
"desc": "Freezes all opponents within a radius of [color=aqua]5 tiles[/color] around you, slowing them to a crawl for [color=yellow]3 seconds[/color]."
|
||||
},
|
||||
{
|
||||
"icon": "res://assets/graphics/touch_control/wall.png",
|
||||
"name": "[color=gray]🧱 Iron Wall[/color]",
|
||||
"desc": "Projects a full row or column of wall tiles in front of you, blocking opponent movement and protecting your tiles."
|
||||
},
|
||||
{
|
||||
"icon": "res://assets/graphics/touch_control/ghost.png",
|
||||
"name": "[color=white]👻 Ghost Mode[/color]",
|
||||
"desc": "Makes you [color=white]invisible[/color] for [color=yellow]6 seconds[/color]. You cannot be rammed while invisible — perfect for escaping danger or sneaking around."
|
||||
},
|
||||
]
|
||||
|
||||
# Build a showcase panel sitting above the dialogue panel
|
||||
var showcase = PanelContainer.new()
|
||||
showcase.custom_minimum_size = Vector2(800, 220)
|
||||
showcase.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var showcase_style = StyleBoxFlat.new()
|
||||
showcase_style.bg_color = Color(0.05, 0.05, 0.12, 0.97)
|
||||
showcase_style.border_width_left = 3
|
||||
showcase_style.border_width_top = 3
|
||||
showcase_style.border_width_right = 3
|
||||
showcase_style.border_width_bottom = 3
|
||||
showcase_style.border_color = Color(0.6, 0.4, 1.0, 1.0)
|
||||
showcase_style.corner_radius_top_left = 12
|
||||
showcase_style.corner_radius_top_right = 12
|
||||
showcase_style.corner_radius_bottom_left = 12
|
||||
showcase_style.corner_radius_bottom_right = 12
|
||||
showcase.add_theme_stylebox_override("panel", showcase_style)
|
||||
add_child(showcase)
|
||||
|
||||
var margin = MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 24)
|
||||
margin.add_theme_constant_override("margin_top", 16)
|
||||
margin.add_theme_constant_override("margin_right", 24)
|
||||
margin.add_theme_constant_override("margin_bottom", 16)
|
||||
showcase.add_child(margin)
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 20)
|
||||
margin.add_child(hbox)
|
||||
|
||||
# Icon
|
||||
var icon_rect = TextureRect.new()
|
||||
icon_rect.custom_minimum_size = Vector2(96, 96)
|
||||
icon_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_rect.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
hbox.add_child(icon_rect)
|
||||
|
||||
# Text column
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
hbox.add_child(vbox)
|
||||
|
||||
var name_label = RichTextLabel.new()
|
||||
name_label.bbcode_enabled = true
|
||||
name_label.fit_content = true
|
||||
name_label.scroll_active = false
|
||||
name_label.add_theme_font_size_override("normal_font_size", 24)
|
||||
vbox.add_child(name_label)
|
||||
|
||||
var desc_label = RichTextLabel.new()
|
||||
desc_label.bbcode_enabled = true
|
||||
desc_label.fit_content = true
|
||||
desc_label.scroll_active = false
|
||||
desc_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
desc_label.add_theme_font_size_override("normal_font_size", 18)
|
||||
vbox.add_child(desc_label)
|
||||
|
||||
# Counter label (e.g. "1 / 4")
|
||||
var counter_label = Label.new()
|
||||
counter_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
counter_label.add_theme_font_size_override("font_size", 14)
|
||||
counter_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.8))
|
||||
vbox.add_child(counter_label)
|
||||
|
||||
# Position the showcase above the dialogue panel
|
||||
await get_tree().process_frame
|
||||
var vp = get_viewport().get_visible_rect().size
|
||||
showcase.position = Vector2(
|
||||
(vp.x - showcase.custom_minimum_size.x) / 2.0,
|
||||
dialogue_panel.position.y - showcase.custom_minimum_size.y - 12
|
||||
)
|
||||
|
||||
# Cycle through each powerup
|
||||
for i in powerups.size():
|
||||
var pu = powerups[i]
|
||||
var tex = load(pu["icon"])
|
||||
if tex:
|
||||
icon_rect.texture = tex
|
||||
name_label.text = pu["name"]
|
||||
desc_label.text = pu["desc"]
|
||||
counter_label.text = "Power-Up %d / %d — Click to continue ▼" % [i + 1, powerups.size()]
|
||||
|
||||
# Reuse the existing input wait mechanism
|
||||
is_waiting_for_input = true
|
||||
await next_pressed
|
||||
|
||||
# Clean up showcase
|
||||
showcase.queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnhgqibcee442
|
||||
Reference in New Issue
Block a user