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 @onready var objective_panel: PanelContainer = $ObjectivePanel @onready var objective_text: RichTextLabel = $ObjectivePanel/MarginContainer/VBoxContainer/ObjectiveText # --- 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 func set_objective(text: String) -> void: if objective_panel and objective_text: objective_text.text = text objective_panel.visible = true func hide_objective() -> void: if objective_panel: objective_panel.visible = false # --------------------------------------------------------------------------- # 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() var vp = get_viewport() if vp: vp.set_input_as_handled() # --------------------------------------------------------------------------- # Power-Up Showcase — one card per powerup with icon + name + description # --------------------------------------------------------------------------- var _current_showcase: Control func show_powerup_card(pid: int) -> void: var powerups = { 11: { "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." }, 12: { "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]." }, 13: { "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." }, 14: { "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." } } if not powerups.has(pid): return var pu = powerups[pid] # 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 var tex = load(pu["icon"]) if tex: icon_rect.texture = tex 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) name_label.text = pu["name"] 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) desc_label.text = pu["desc"] vbox.add_child(desc_label) # Position the showcase above the dialogue panel # We rely on call deferred or await frame to ensure layout sizing is processed await get_tree().process_frame if not is_inside_tree() or not is_instance_valid(showcase): return 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 ) _current_showcase = showcase func hide_powerup_card() -> void: if is_instance_valid(_current_showcase): _current_showcase.queue_free() _current_showcase = null