extends SceneTree ## T4 (docs/TUTORIAL_DESIGN.md ยง5): Old Tam's tips in a normal world. Each ## fits its moment, shows once, waits its turn, stays put while you're dead, ## is remembered across sessions, can be switched off or shown again, and ## never runs in the tutorial world. ## Run: godot --headless --path . --script res://tests/test_hints.gd var _fails := 0 var _finished := false var H: Node var S: Node var world: Node3D var player: CharacterBody3D var sv: Node var got: Array = [] var card: Control func check(cond: bool, what: String) -> void: print((" ok " if cond else " FAIL ") + what) if not cond: _fails += 1 func _initialize() -> void: _run.call_deferred() func _run() -> void: H = root.get_node("Hints") S = root.get_node("Saves") H.path = "user://test_hints.json" H.allow_headless = true DirAccess.remove_absolute(H.path) H.load_prefs() H.shown.connect(func(h): got.append(String(h.id))) S.root_dir = "user://test_worlds" for w in S.list_worlds(): S.delete_world(w.id) await _body() check(_finished, "test ran to the end (no script errors)") for w in S.list_worlds(): S.delete_world(w.id) DirAccess.remove_absolute(S.root_dir) DirAccess.remove_absolute(H.path) print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails]) quit(1 if _fails > 0 else 0) func _load(tutorial: bool) -> void: if world != null: world.queue_free() await process_frame S.current_id = S.create_world("Tips" if not tutorial else "Tutorial", 1337, {"tutorial": tutorial}) world = load("res://scenes/world.tscn").instantiate() root.add_child(world) for _i in range(5): await physics_frame player = world.player player.set_physics_process(false) sv = player.survival sv.set_process(false) world.day_night.frozen = true world.day_night.time = 10.0 / 24.0 world.day_night.apply(true) world.weather.set_process(false) world.weather.force("clear") if world.wolves != null: world.wolves.set_physics_process(false) world.creatures.enabled = false ## Runs the tips for `secs` (as frames of 0.5 s), with your statuses fresh. func _for(secs: float) -> void: var t := 0.0 while t < secs: sv._update_statuses() H.step(0.5) t += 0.5 func _body() -> void: print("the tips") var kinds := ["event", "status", "weather", "has", "night", "stamina_low", "wolf_moon", "hard_winter"] var bad: Array = [] for h in H.hints: if not String(h.when).get_slice(":", 0) in kinds or String(h.title).is_empty() or String(h.text).is_empty(): bad.append(h.id) check(H.hints.size() >= 12 and bad.is_empty(), "%d tips, each with a title, text and a condition it knows %s" % [H.hints.size(), bad]) check(H.enabled and H.seen.is_empty(), "on by default, none seen yet") print("a normal world") await _load(false) check(H.active and H.world == world, "they run in your own world") card = load("res://scripts/ui/hint_card.gd").new() root.add_child(card) card.setup() await _for(10.0) check(got.is_empty(), "a calm, dry, fed morning: nothing to say") sv.wet = 60.0 await _for(3.0) check(got == ["wet"], "soaked: the wet tip") check(card.is_showing() and card._title.text == "Soaked" and card._text.text.begins_with("\"Wet clothes"), "the card shows it, in Old Tam's words") sv.wet = 0.0 sv.hunger = 10.0 await _for(6.0) check(got.size() == 1, "hungry straight after: it waits its turn") await _for(14.0) check(got.back() == "hungry", "then the hungry tip") sv.hunger = 90.0 sv.wet = 60.0 await _for(20.0) check(got.count("wet") == 1, "soaked again: once is enough") sv.wet = 0.0 print("the setting") H.set_enabled(false) world.day_night.time = 23.0 / 24.0 world.day_night.apply(true) await _for(25.0) check(got.size() == 2, "tips off: nightfall goes by without one") var f: Variant = JSON.parse_string(FileAccess.get_file_as_string(H.path)) check(f is Dictionary and f.enabled == false and f.seen == ["wet", "hungry"], "the setting and the seen tips are saved") H.set_enabled(true) await _for(6.0) check(got.back() == "night", "tips on: the night tip") world.day_night.time = 10.0 / 24.0 world.day_night.apply(true) print("events") H.event("howl") await _for(17.0) check(got.back() == "howl", "the wolves howl: the wolf tip") world.dead_waiting = true H.event("died") await _for(25.0) check(got.back() == "howl", "dead, on the death screen: no tip yet") world.dead_waiting = false await _for(1.0) check(got.back() == "died", "back on your feet: the death tip") player.inventory.add("rotten_food", 1) await _for(17.0) check(got.back() == "rotten", "something rots in your bag: the food tip") player.inventory.remove("rotten_food", 1) print("next time") H.load_prefs() check(H.seen.size() == 6 and H.seen.has("died"), "the seen tips come back from the file (%d)" % H.seen.size()) await _load(false) got.clear() sv.wet = 60.0 sv.hunger = 10.0 await _for(25.0) check(got.is_empty(), "a new session: wet and hungry again, but they've been said") H.reset_seen() await _for(5.0) check(got == ["wet"], "Show again: the wet tip comes back") sv.wet = 0.0 sv.hunger = 90.0 print("not in the tutorial") await _load(true) check(not H.active, "the tutorial world has its own guide: no tips") got.clear() sv.wet = 60.0 await _for(10.0) check(got.is_empty(), "(soaked there: nothing)") card.queue_free() world.queue_free() await process_frame _finished = true