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)