Files
tekton/scripts/managers/tutorial_overlay.gd
T
2026-04-11 06:00:54 +08:00

259 lines
9.2 KiB
GDScript

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