Skip to content

Commit 1756336

Browse files
2002yyclaude
andcommitted
Add Cardfront tutorial overlay (v0.2.5-cardfront-tutorial)
3-step tutorial overlay for first-time Cardfront entry: - Step 1: explains energy/parts resource bar - Step 2: explains hand panel card selection - Step 3: explains battlefield highlight zones - Skip button marks tutorial as completed - Settings toggle "新手提示:开启 / 关闭" to disable - Fix: remove mouse_filter from CanvasLayer _init() Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b462b2d commit 1756336

8 files changed

Lines changed: 312 additions & 0 deletions

File tree

scenes/ui/SettingsPanel.tscn

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ theme_override_colors/font_color = Color(0.88, 0.92, 1, 1)
5050
theme_override_font_sizes/font_size = 14
5151
button_pressed = false
5252

53+
[node name="NewbieHintCheck" type="CheckButton" parent="PanelMargin/MainVBox"]
54+
text = "新手提示:开启 / 关闭"
55+
theme_override_colors/font_color = Color(0.88, 0.92, 1, 1)
56+
theme_override_font_sizes/font_size = 14
57+
button_pressed = true
58+
5359
[node name="HintLabel" type="Label" parent="PanelMargin/MainVBox"]
5460
text = "部分设置重启后生效"
5561
theme_override_colors/font_color = Color(0.62, 0.68, 0.82, 1)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[gd_scene format=3]
2+
3+
[ext_resource type="Script" path="res://scripts/cardfront/ui/CardfrontTutorialOverlay.gd" id="1_tutorial"]
4+
5+
[node name="CardfrontTutorialOverlay" type="CanvasLayer"]
6+
layer = 30
7+
process_mode = 3
8+
script = ExtResource("1_tutorial")

scripts/GameRuntimeContext.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var card_detail_popup = null
3737
var toast_layer = null
3838
var effect_visual_bridge = null
3939
var card_audio_feedback = null
40+
var tutorial_overlay = null
4041
var resource_states: Dictionary = {}
4142
var last_yield_snapshot: Dictionary = {}
4243
var current_config: Dictionary = {}

scripts/Main.gd

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,32 @@ func _create_cardfront_region_info_panel() -> void:
504504
runtime.region_info_panel = panel_setup.get("region_info_panel", null)
505505

506506

507+
func _create_cardfront_tutorial_overlay() -> void:
508+
runtime.tutorial_overlay = null
509+
if not _is_cardfront_mode():
510+
return
511+
var ui_canvas = _hud_ref("ui_canvas")
512+
if ui_canvas == null:
513+
return
514+
var overlay_setup: Dictionary = CardfrontModeScript.create_tutorial_overlay(ui_canvas)
515+
if bool(overlay_setup.get("configured", false)):
516+
runtime.tutorial_overlay = overlay_setup.get("tutorial_overlay", null)
517+
_wire_tutorial_settings_signals(overlay_setup.get("tutorial_overlay", null))
518+
519+
520+
func _wire_tutorial_settings_signals(overlay) -> void:
521+
if overlay == null or not is_instance_valid(overlay):
522+
return
523+
var settings_panel = _hud_ref("settings_panel")
524+
if settings_panel == null or not is_instance_valid(settings_panel):
525+
return
526+
if not settings_panel.has_signal("settings_changed"):
527+
return
528+
var callable := Callable(overlay, "on_settings_changed")
529+
if not settings_panel.settings_changed.is_connected(callable):
530+
settings_panel.settings_changed.connect(callable)
531+
532+
507533
func _create_cardfront_feedback_layers() -> void:
508534
runtime.card_detail_popup = null
509535
runtime.toast_layer = null
@@ -593,6 +619,7 @@ func _create_ui() -> void:
593619
_create_cardfront_feedback_layers()
594620
_create_cardfront_effect_visual_bridge()
595621
_create_cardfront_region_info_panel()
622+
_create_cardfront_tutorial_overlay()
596623

597624
_on_scores_changed(runtime.battlefield.count_cells_by_team())
598625

scripts/PlayerSettingsStore.gd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ static func default_settings() -> Dictionary:
77
return {
88
"show_performance_info": OS.is_debug_build(),
99
"low_effect_mode": false,
10+
"show_newbie_hint": true,
11+
"tutorial_completed": false,
1012
}
1113

1214
static func load_settings() -> Dictionary:
@@ -35,6 +37,8 @@ static func sanitize_settings(settings: Dictionary) -> Dictionary:
3537
return {
3638
"show_performance_info": _coerce_bool(settings.get("show_performance_info", defaults["show_performance_info"]), bool(defaults["show_performance_info"])),
3739
"low_effect_mode": _coerce_bool(settings.get("low_effect_mode", defaults["low_effect_mode"]), bool(defaults["low_effect_mode"])),
40+
"show_newbie_hint": _coerce_bool(settings.get("show_newbie_hint", defaults["show_newbie_hint"]), bool(defaults["show_newbie_hint"])),
41+
"tutorial_completed": _coerce_bool(settings.get("tutorial_completed", defaults["tutorial_completed"]), bool(defaults["tutorial_completed"])),
3842
}
3943

4044
static func _coerce_bool(value, default_value: bool) -> bool:

scripts/SettingsPanel.gd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ signal settings_changed(settings: Dictionary)
77

88
@onready var performance_check: CheckButton = get_node("PanelMargin/MainVBox/PerformanceCheck")
99
@onready var low_effect_check: CheckButton = get_node("PanelMargin/MainVBox/LowEffectCheck")
10+
@onready var newbie_hint_check: CheckButton = get_node("PanelMargin/MainVBox/NewbieHintCheck")
1011

1112
var current_settings: Dictionary = {}
1213

@@ -17,17 +18,21 @@ func _ready() -> void:
1718

1819
performance_check.toggled.connect(_on_setting_toggled)
1920
low_effect_check.toggled.connect(_on_setting_toggled)
21+
newbie_hint_check.toggled.connect(_on_setting_toggled)
2022
get_node("PanelMargin/MainVBox/CloseButton").pressed.connect(hide_panel)
2123

2224

2325
func _apply_settings_to_controls() -> void:
2426
performance_check.button_pressed = bool(current_settings.get("show_performance_info", true))
2527
low_effect_check.button_pressed = bool(current_settings.get("low_effect_mode", false))
28+
newbie_hint_check.button_pressed = bool(current_settings.get("show_newbie_hint", true))
2629

2730

2831
func _on_setting_toggled(_pressed: bool) -> void:
2932
current_settings["show_performance_info"] = performance_check.button_pressed
3033
current_settings["low_effect_mode"] = low_effect_check.button_pressed
34+
current_settings["show_newbie_hint"] = newbie_hint_check.button_pressed
35+
current_settings["tutorial_completed"] = bool(current_settings.get("tutorial_completed", false))
3136
PlayerSettingsStoreClass.save_settings(current_settings)
3237
settings_changed.emit(current_settings.duplicate())
3338

scripts/cardfront/CardfrontMode.gd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const CardfrontCardDetailPopupScene = preload("res://scenes/ui/cardfront/Cardfro
3333
const CardfrontToastLayerScene = preload("res://scenes/ui/cardfront/CardfrontToastLayer.tscn")
3434
const CardfrontEffectVisualBridgeScript = preload("res://scripts/cardfront/ui/CardfrontEffectVisualBridge.gd")
3535
const CardfrontCardAudioFeedbackScript = preload("res://scripts/cardfront/ui/CardfrontCardAudioFeedback.gd")
36+
const CardfrontTutorialOverlayScene = preload("res://scenes/ui/cardfront/CardfrontTutorialOverlay.tscn")
3637
const LEGACY_SIDE_BUTTON_TOP_AFTER_REGION: float = 292.0
3738
const LEGACY_SIDE_BUTTON_GAP: float = 8.0
3839

@@ -376,6 +377,20 @@ static func create_feedback_layers(ui_layer: Node, feedback_bus, resource_states
376377
}
377378

378379

380+
static func create_tutorial_overlay(ui_layer: Node) -> Dictionary:
381+
if ui_layer == null or not is_instance_valid(ui_layer):
382+
return {"configured": false, "reason": "missing_ui_layer"}
383+
384+
var overlay = CardfrontTutorialOverlayScene.instantiate()
385+
ui_layer.add_child(overlay)
386+
overlay.setup()
387+
388+
return {
389+
"configured": true,
390+
"tutorial_overlay": overlay,
391+
}
392+
393+
379394
static func create_effect_visual_bridge(game_layer: Node, feedback_bus, vfx_layer) -> Dictionary:
380395
if game_layer == null or not is_instance_valid(game_layer):
381396
return {"configured": false, "reason": "missing_game_layer"}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
extends CanvasLayer
2+
class_name CardfrontTutorialOverlay
3+
4+
const PlayerSettingsStoreClass = preload("res://scripts/PlayerSettingsStore.gd")
5+
6+
const VIEW_W: float = 1120.0
7+
const VIEW_H: float = 720.0
8+
9+
const DIM_COLOR := Color(0.03, 0.06, 0.15, 0.75)
10+
const FOCUS_BORDER_COLOR := Color(1.0, 1.0, 1.0, 0.60)
11+
const FOCUS_BORDER_WIDTH: float = 2.0
12+
13+
const TOOLTIP_SIZE := Vector2(360.0, 160.0)
14+
const TOOLTIP_GAP: float = 20.0
15+
16+
const PULSE_SPEED: float = 2.0
17+
const PULSE_ALPHA_MIN: float = 0.30
18+
const PULSE_ALPHA_MAX: float = 0.80
19+
20+
const STEP_DATA: Array[Dictionary] = [
21+
{
22+
"title": "① 资源栏",
23+
"text": "屏幕顶部显示的能量(⚡)和零件(⚙)是您的资源。\n能量和零件会随时间自动增长,用于打出各种卡牌。",
24+
"focus_rect": Rect2(440, 82, 240, 48),
25+
},
26+
{
27+
"title": "② 手牌栏",
28+
"text": "屏幕底部是您的手牌。点击一张卡牌将其选中,\n然后点击战场上的高亮区域来释放卡牌效果。",
29+
"focus_rect": Rect2(276, 562, 568, 158),
30+
},
31+
{
32+
"title": "③ 战场区域",
33+
"text": "选中卡牌后,战场上会高亮显示可以释放的区域。\n点击高亮格子即可释放效果,影响战局走向。",
34+
"focus_rect": Rect2(100, 150, 600, 400),
35+
},
36+
]
37+
38+
var _current_step: int = 0
39+
var _total_steps: int = STEP_DATA.size()
40+
var _ui_time: float = 0.0
41+
var _is_tutorial_active: bool = false
42+
43+
var _dim_top: ColorRect
44+
var _dim_bottom: ColorRect
45+
var _dim_left: ColorRect
46+
var _dim_right: ColorRect
47+
var _focus_highlight: ColorRect
48+
var _tooltip_panel: Panel
49+
var _tooltip_bg: ColorRect
50+
var _accent_line: ColorRect
51+
var _title_label: Label
52+
var _text_label: Label
53+
var _step_label: Label
54+
var _next_button: Button
55+
var _skip_button: Button
56+
57+
58+
func _init() -> void:
59+
name = "CardfrontTutorialOverlay"
60+
layer = 30
61+
process_mode = Node.PROCESS_MODE_ALWAYS
62+
63+
64+
func setup() -> void:
65+
_build_nodes()
66+
var settings := PlayerSettingsStoreClass.load_settings()
67+
var show_hint: bool = bool(settings.get("show_newbie_hint", true))
68+
var tutorial_done: bool = bool(settings.get("tutorial_completed", false))
69+
if show_hint and not tutorial_done:
70+
start_tutorial()
71+
else:
72+
queue_free()
73+
74+
75+
func _build_nodes() -> void:
76+
for name_str in ["DimTop", "DimBottom", "DimLeft", "DimRight"]:
77+
var rect := ColorRect.new()
78+
rect.name = name_str
79+
rect.color = DIM_COLOR
80+
rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
81+
add_child(rect)
82+
_dim_top = $DimTop
83+
_dim_bottom = $DimBottom
84+
_dim_left = $DimLeft
85+
_dim_right = $DimRight
86+
87+
_focus_highlight = ColorRect.new()
88+
_focus_highlight.name = "FocusHighlight"
89+
_focus_highlight.color = FOCUS_BORDER_COLOR
90+
_focus_highlight.mouse_filter = Control.MOUSE_FILTER_IGNORE
91+
add_child(_focus_highlight)
92+
93+
_tooltip_panel = Panel.new()
94+
_tooltip_panel.name = "TooltipPanel"
95+
_tooltip_panel.size = TOOLTIP_SIZE
96+
_tooltip_panel.mouse_filter = Control.MOUSE_FILTER_STOP
97+
add_child(_tooltip_panel)
98+
99+
_tooltip_bg = ColorRect.new()
100+
_tooltip_bg.name = "PanelBg"
101+
_tooltip_bg.color = Color(0.06, 0.09, 0.15, 0.96)
102+
_tooltip_bg.size = TOOLTIP_SIZE
103+
_tooltip_panel.add_child(_tooltip_bg)
104+
105+
_accent_line = ColorRect.new()
106+
_accent_line.name = "AccentLine"
107+
_accent_line.color = Color(0.98, 0.76, 0.18, 0.55)
108+
_accent_line.position = Vector2(12.0, 4.0)
109+
_accent_line.size = Vector2(TOOLTIP_SIZE.x - 24.0, 2.0)
110+
_tooltip_panel.add_child(_accent_line)
111+
112+
_title_label = Label.new()
113+
_title_label.name = "TitleLabel"
114+
_title_label.position = Vector2(16.0, 12.0)
115+
_title_label.size = Vector2(TOOLTIP_SIZE.x - 32.0, 24.0)
116+
_title_label.add_theme_font_size_override("font_size", 18)
117+
_title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.72, 1.0))
118+
_tooltip_panel.add_child(_title_label)
119+
120+
_text_label = Label.new()
121+
_text_label.name = "TextLabel"
122+
_text_label.position = Vector2(16.0, 40.0)
123+
_text_label.size = Vector2(TOOLTIP_SIZE.x - 32.0, 74.0)
124+
_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
125+
_text_label.add_theme_font_size_override("font_size", 13)
126+
_text_label.add_theme_color_override("font_color", Color(0.82, 0.90, 1.0, 1.0))
127+
_tooltip_panel.add_child(_text_label)
128+
129+
_step_label = Label.new()
130+
_step_label.name = "StepLabel"
131+
_step_label.position = Vector2(16.0, TOOLTIP_SIZE.y - 38.0)
132+
_step_label.size = Vector2(80.0, 22.0)
133+
_step_label.add_theme_font_size_override("font_size", 12)
134+
_step_label.add_theme_color_override("font_color", Color(0.62, 0.68, 0.82, 1.0))
135+
_tooltip_panel.add_child(_step_label)
136+
137+
_next_button = Button.new()
138+
_next_button.name = "NextButton"
139+
_next_button.position = Vector2(TOOLTIP_SIZE.x - 110.0, TOOLTIP_SIZE.y - 40.0)
140+
_next_button.size = Vector2(94.0, 28.0)
141+
_next_button.text = "下一步"
142+
_next_button.add_theme_font_size_override("font_size", 14)
143+
_next_button.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0, 1.0))
144+
_next_button.self_modulate = Color(0.24, 0.52, 0.92, 1.0)
145+
_next_button.pressed.connect(_on_next_pressed)
146+
_tooltip_panel.add_child(_next_button)
147+
148+
_skip_button = Button.new()
149+
_skip_button.name = "SkipButton"
150+
_skip_button.position = Vector2(VIEW_W - 140.0, 16.0)
151+
_skip_button.size = Vector2(124.0, 28.0)
152+
_skip_button.text = "跳过 / 不再提示"
153+
_skip_button.add_theme_font_size_override("font_size", 12)
154+
_skip_button.add_theme_color_override("font_color", Color(0.62, 0.68, 0.82, 1.0))
155+
_skip_button.self_modulate = Color(0.34, 0.34, 0.54, 0.85)
156+
_skip_button.mouse_filter = Control.MOUSE_FILTER_STOP
157+
_skip_button.pressed.connect(_on_skip_pressed)
158+
add_child(_skip_button)
159+
160+
161+
func start_tutorial() -> void:
162+
_current_step = 0
163+
visible = true
164+
_is_tutorial_active = true
165+
_apply_step(_current_step)
166+
set_process(true)
167+
168+
169+
func _apply_step(step_index: int) -> void:
170+
if step_index < 0 or step_index >= _total_steps:
171+
return
172+
var step: Dictionary = STEP_DATA[step_index]
173+
var focus: Rect2 = step["focus_rect"]
174+
175+
_dim_top.position = Vector2(0.0, 0.0)
176+
_dim_top.size = Vector2(VIEW_W, focus.position.y)
177+
178+
_dim_bottom.position = Vector2(0.0, focus.position.y + focus.size.y)
179+
_dim_bottom.size = Vector2(VIEW_W, VIEW_H - focus.position.y - focus.size.y)
180+
181+
_dim_left.position = Vector2(0.0, focus.position.y)
182+
_dim_left.size = Vector2(focus.position.x, focus.size.y)
183+
184+
_dim_right.position = Vector2(focus.position.x + focus.size.x, focus.position.y)
185+
_dim_right.size = Vector2(VIEW_W - focus.position.x - focus.size.x, focus.size.y)
186+
187+
_focus_highlight.position = focus.position - Vector2(FOCUS_BORDER_WIDTH, FOCUS_BORDER_WIDTH)
188+
_focus_highlight.size = focus.size + Vector2(FOCUS_BORDER_WIDTH * 2.0, FOCUS_BORDER_WIDTH * 2.0)
189+
190+
_position_tooltip(focus)
191+
192+
_title_label.text = step["title"]
193+
_text_label.text = step["text"]
194+
_step_label.text = "%d / %d" % [step_index + 1, _total_steps]
195+
_next_button.text = "完成!" if step_index >= _total_steps - 1 else "下一步"
196+
197+
198+
func _position_tooltip(focus: Rect2) -> void:
199+
var tooltip_x: float = (VIEW_W - TOOLTIP_SIZE.x) * 0.5
200+
var tooltip_y: float
201+
match _current_step:
202+
0:
203+
tooltip_y = focus.position.y + focus.size.y + TOOLTIP_GAP
204+
1:
205+
tooltip_y = focus.position.y - TOOLTIP_GAP - TOOLTIP_SIZE.y
206+
_:
207+
tooltip_y = VIEW_H - TOOLTIP_SIZE.y - 240.0
208+
_tooltip_panel.position = Vector2(tooltip_x, tooltip_y)
209+
210+
211+
func _on_next_pressed() -> void:
212+
_current_step += 1
213+
if _current_step >= _total_steps:
214+
_complete_tutorial()
215+
else:
216+
_apply_step(_current_step)
217+
218+
219+
func _on_skip_pressed() -> void:
220+
_complete_tutorial()
221+
222+
223+
func _complete_tutorial() -> void:
224+
var settings := PlayerSettingsStoreClass.load_settings()
225+
settings["tutorial_completed"] = true
226+
PlayerSettingsStoreClass.save_settings(settings)
227+
_is_tutorial_active = false
228+
visible = false
229+
set_process(false)
230+
231+
232+
func on_settings_changed(settings: Dictionary) -> void:
233+
var show_hint: bool = bool(settings.get("show_newbie_hint", true))
234+
if _is_tutorial_active and not show_hint:
235+
_is_tutorial_active = false
236+
visible = false
237+
set_process(false)
238+
var saved := PlayerSettingsStoreClass.load_settings()
239+
saved["tutorial_completed"] = true
240+
PlayerSettingsStoreClass.save_settings(saved)
241+
242+
243+
func _process(delta: float) -> void:
244+
_ui_time += delta
245+
var pulse := PULSE_ALPHA_MIN + (PULSE_ALPHA_MAX - PULSE_ALPHA_MIN) * (0.5 + 0.5 * sin(_ui_time * PULSE_SPEED))
246+
_focus_highlight.modulate = Color(FOCUS_BORDER_COLOR.r, FOCUS_BORDER_COLOR.g, FOCUS_BORDER_COLOR.b, pulse)

0 commit comments

Comments
 (0)