Wire the first-shift tutorial into gameplay
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:
1 parent
76eb5e3a79
commit
86332cd401
12 files changed
+392
-15
No files matched your search
@@ -0,0 +1,104 @@
|
||||
extends SceneTree
|
||||
## Windowed tutorial smoke: start the Hollywood shift with the tutorial enabled,
|
||||
## complete all eight beats through the real action methods, verify persistence,
|
||||
## then replay and skip it. Use an isolated user-data dir:
|
||||
## XDG_DATA_HOME=/tmp/gigline-tutorial godot --path . --script res://src/test/smoke_tutorial.gd -- --tutorial
|
||||
|
||||
const Rules := preload("res://src/core/rules.gd")
|
||||
const MAX_FRAMES := 900
|
||||
var gm: Node = null
|
||||
var frames := 0
|
||||
var started := false
|
||||
var failures: Array[String] = []
|
||||
var step := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
root.size = Vector2i(960, 540)
|
||||
root.add_child(load("res://scenes/main.tscn").instantiate())
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
frames += 1
|
||||
if gm == null:
|
||||
gm = root.find_child("GameManager", true, false)
|
||||
return false
|
||||
if frames > MAX_FRAMES:
|
||||
_fail("timeout")
|
||||
return true
|
||||
if not started:
|
||||
# Ignore any saved completion from a previous local run.
|
||||
gm.player_settings["tutorial_completed"] = false
|
||||
gm.player_settings["tutorial_skipped"] = false
|
||||
gm.call("_on_venue_selected", "hollywood_building")
|
||||
started = true
|
||||
return false
|
||||
step += 1
|
||||
match step:
|
||||
5:
|
||||
_check(gm.tutorial_active, "tutorial starts")
|
||||
_check(gm.tutorial != null and gm.tutorial.current()["id"] == "arrive",
|
||||
"starts at the arrival beat")
|
||||
_check(gm.tutorial_overlay != null and gm.tutorial_overlay.visible,
|
||||
"prompt card is visible")
|
||||
_check(gm.npcs.size() == 1, "only the safe visitor is present")
|
||||
gm.guard.global_position += Vector3(0, 0, 1.0)
|
||||
gm.guard.rotation.y += 0.2
|
||||
gm.call("_tutorial_tick")
|
||||
_check(gm.tutorial.current()["id"] == "focus", "movement/look advances")
|
||||
10:
|
||||
gm.call("_cycle_nearby_target", 1)
|
||||
_check(gm.tutorial.current()["id"] == "wheel", "target cycling advances")
|
||||
gm.call("_open_radial")
|
||||
_check(gm.tutorial.current()["id"] == "inspect", "wheel opens")
|
||||
gm.call("_cancel_radial")
|
||||
15:
|
||||
gm.call("_toggle_inspection")
|
||||
_check(gm.tutorial.current()["id"] == "direction", "inspection advances")
|
||||
gm.call("_toggle_inspection")
|
||||
gm.call("_apply_interaction", Rules.Level.SIMPLE_DETERRENCE, "polite direction")
|
||||
_check(gm.tutorial.current()["id"] == "warning", "polite direction advances")
|
||||
20:
|
||||
gm.call("_apply_interaction", Rules.Level.FORMAL_WARNING, "formal warning")
|
||||
_check(gm.tutorial.current()["id"] == "radio", "warning/body camera advances")
|
||||
gm.call("_on_radio_submitted", {
|
||||
"npc_id": "tutorial_visitor",
|
||||
"location": "entrance",
|
||||
"description": "Visitor at the door",
|
||||
"behavior": "calm",
|
||||
"weapons_seen": false,
|
||||
"danger_level": 0,
|
||||
}, false)
|
||||
_check(gm.tutorial.current()["id"] == "review", "radio advances")
|
||||
25:
|
||||
gm.call("_on_shift_ended", gm.coordinator.build_review())
|
||||
_check(gm.tutorial != null and gm.tutorial.is_complete(), "review completes tutorial")
|
||||
_check(not gm.tutorial_active, "tutorial deactivates")
|
||||
_check(bool(gm.player_settings.get("tutorial_completed", false)),
|
||||
"completion is persisted in settings")
|
||||
gm.call("_on_replay_tutorial")
|
||||
30:
|
||||
_check(gm.tutorial_active and gm.tutorial.current()["id"] == "arrive",
|
||||
"replay starts a fresh tutorial")
|
||||
gm.call("_skip_tutorial")
|
||||
_check(bool(gm.player_settings.get("tutorial_skipped", false)),
|
||||
"skip is persisted separately")
|
||||
_finish()
|
||||
return false
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
print("ok: " + message)
|
||||
else:
|
||||
_fail(message)
|
||||
|
||||
func _fail(message: String) -> void:
|
||||
failures.append(message)
|
||||
_finish()
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("SMOKE OK: tutorial prompts, visitor, completion, replay and skip")
|
||||
quit(0)
|
||||
else:
|
||||
for message in failures:
|
||||
printerr("SMOKE FAIL: " + message)
|
||||
quit(1)
|
||||
Reference in new issue
Block a user