Wire the first-shift tutorial into gameplay
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped

Add the non-blocking prompt card, safe compliant visitor, action-driven
advancement through the real wheel/inspection/warning/radio panels, persisted
completion/skip, a Settings replay action and localized tutorial strings.
Scripted tests/captures bypass the tutorial unless --tutorial is passed. Add a
windowed smoke covering the full flow. Headless 237/237, Python 94/94, tutorial
smoke passes.
This commit is contained in:
zwitschi committed 2026-11-01 21:05:59 +01:00
1 parent 76eb5e3a79
commit 86332cd401
12 files changed
+392 -15

No files matched your search

+10
View File
@@ -10,6 +10,7 @@ const InputBindings := preload("res://src/core/input_bindings.gd")
const UiArt := preload("res://src/game/ui/art.gd")
signal applied(settings: Dictionary)
signal replay_tutorial_requested()
signal closed()
var _root: Control = null
@@ -86,6 +87,15 @@ func _build() -> void:
_head_bob.toggled.connect(_on_head_bob_toggled)
column.add_child(_head_bob)
var replay_row := HBoxContainer.new()
replay_row.alignment = BoxContainer.ALIGNMENT_CENTER
column.add_child(replay_row)
var replay := Button.new()
replay.name = "ReplayTutorialButton"
replay.text = "Replay tutorial"
replay.pressed.connect(func(): replay_tutorial_requested.emit())
replay_row.add_child(replay)
var controls_title := Label.new()
controls_title.text = "Controls (click a binding, then press a key or pad button)"
controls_title.add_theme_font_size_override("font_size", 13)
+87
View File
@@ -0,0 +1,87 @@
class_name TutorialOverlay
extends CanvasLayer
const Strings := preload("res://src/core/strings.gd")
## Non-blocking first-shift tutorial card. It never takes mouse input; F1 is
## handled by GameManager so the prompt can stay visible while the player moves.
var _panel: PanelContainer = null
var _prompt: Label = null
var _progress: Label = null
func _init() -> void:
layer = 14
_build()
hide()
func _build() -> void:
var root := Control.new()
root.name = "TutorialRoot"
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(root)
_panel = PanelContainer.new()
_panel.name = "TutorialCard"
_panel.set_anchor(SIDE_LEFT, 0.5)
_panel.set_anchor(SIDE_RIGHT, 0.5)
_panel.set_anchor(SIDE_TOP, 0.08)
_panel.set_anchor(SIDE_BOTTOM, 0.08)
_panel.offset_left = -330
_panel.offset_right = 330
_panel.offset_top = 0
_panel.offset_bottom = 112
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
var style := StyleBoxFlat.new()
style.bg_color = Color(0.035, 0.045, 0.06, 0.92)
style.border_color = Color(0.62, 0.69, 0.8, 0.55)
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.corner_radius_top_left = 10
style.corner_radius_top_right = 10
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
style.content_margin_left = 18
style.content_margin_right = 18
style.content_margin_top = 10
style.content_margin_bottom = 10
_panel.add_theme_stylebox_override("panel", style)
root.add_child(_panel)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 3)
_panel.add_child(column)
var header := HBoxContainer.new()
column.add_child(header)
var title := Label.new()
title.text = Strings.lookup("tutorial.header")
title.add_theme_font_size_override("font_size", 12)
title.modulate = Color(0.6, 0.8, 1.0)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
_progress = Label.new()
_progress.add_theme_font_size_override("font_size", 12)
_progress.modulate = Color(0.65, 0.7, 0.78)
header.add_child(_progress)
_prompt = Label.new()
_prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_prompt.add_theme_font_size_override("font_size", 15)
column.add_child(_prompt)
var hint := Label.new()
hint.text = Strings.lookup("tutorial.skip_hint")
hint.add_theme_font_size_override("font_size", 11)
hint.modulate = Color(0.55, 0.6, 0.68)
column.add_child(hint)
func show_beat(text: String, progress: Vector2i) -> void:
_prompt.text = text
_progress.text = "%d/%d" % [progress.x, progress.y]
visible = true
func close() -> void:
visible = false