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/export_presets.cfg b/export_presets.cfg index d0599d8..7cf6cc8 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -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 diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 2f9ee61..9047994 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -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: diff --git a/scenes/main.gd b/scenes/main.gd index 7915d47..29db97b 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -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 diff --git a/scenes/player.gd b/scenes/player.gd index f4c0efe..562b189 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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(): diff --git a/scenes/tutorial/dim_spotlight.gdshader b/scenes/tutorial/dim_spotlight.gdshader new file mode 100644 index 0000000..481490d --- /dev/null +++ b/scenes/tutorial/dim_spotlight.gdshader @@ -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); + } +} diff --git a/scenes/tutorial/dim_spotlight.gdshader.uid b/scenes/tutorial/dim_spotlight.gdshader.uid new file mode 100644 index 0000000..402f286 --- /dev/null +++ b/scenes/tutorial/dim_spotlight.gdshader.uid @@ -0,0 +1 @@ +uid://b74mlkt6hd88r diff --git a/scenes/tutorial/tutorial_overlay.tscn b/scenes/tutorial/tutorial_overlay.tscn new file mode 100644 index 0000000..9492c88 --- /dev/null +++ b/scenes/tutorial/tutorial_overlay.tscn @@ -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 diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 13ce74e..0fda7f7 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -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 diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index e8fe3d9..e65973a 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -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) diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 1107a8d..4744fce 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -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(): diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 7727137..5024198 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -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: diff --git a/scripts/managers/tutorial_manager.gd b/scripts/managers/tutorial_manager.gd new file mode 100644 index 0000000..97998bf --- /dev/null +++ b/scripts/managers/tutorial_manager.gd @@ -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() diff --git a/scripts/managers/tutorial_manager.gd.uid b/scripts/managers/tutorial_manager.gd.uid new file mode 100644 index 0000000..ed67005 --- /dev/null +++ b/scripts/managers/tutorial_manager.gd.uid @@ -0,0 +1 @@ +uid://bifo8j2px1agw diff --git a/scripts/managers/tutorial_overlay.gd b/scripts/managers/tutorial_overlay.gd new file mode 100644 index 0000000..8bba3f7 --- /dev/null +++ b/scripts/managers/tutorial_overlay.gd @@ -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() diff --git a/scripts/managers/tutorial_overlay.gd.uid b/scripts/managers/tutorial_overlay.gd.uid new file mode 100644 index 0000000..ad76540 --- /dev/null +++ b/scripts/managers/tutorial_overlay.gd.uid @@ -0,0 +1 @@ +uid://bnhgqibcee442