diff --git a/data/hints.json b/data/hints.json new file mode 100644 index 0000000..eabb3d7 --- /dev/null +++ b/data/hints.json @@ -0,0 +1,38 @@ +{ + "guide": "Old Tam", + "_doc": "T4 (docs/TUTORIAL_DESIGN.md §5): one-off tips in normal worlds, each shown once per player (user://hints.json), in list order when several are due. 'when' is kind:value, with | for alternatives: status (a survival status, by prefix), weather (a state), has (an item in your bag), event (howl, died), or a plain check (night, stamina_low, wolf_moon, hard_winter). 'hold' is how long it must stay true first (seconds, default 1).", + "hints": [ + {"id": "died", "when": "event:died", "title": "Back on your feet", + "text": "Dying's not the end out here. Whatever you carried is waiting where you fell, so go back for it before anything else. Sleep in a bed once and that's where you'll wake."}, + {"id": "freezing", "when": "status:Freezing", "title": "Freezing", + "text": "You're freezing, and that eats at your health. Get to a fire or under a roof, now. A cloak helps too."}, + {"id": "cold", "when": "status:Cold", "hold": 3, "title": "Getting cold", + "text": "Feel that chill? A fire warms you, a roof and walls keep the wind off, and a hide cloak buys you time."}, + {"id": "sick", "when": "status:Food poisoning", "title": "Bad food", + "text": "Raw meat or something past its best: your belly's turned. It passes. Rest, don't eat for a bit, and cook your meat next time."}, + {"id": "rotten", "when": "has:rotten_food", "title": "Food goes off", + "text": "Food doesn't keep forever. Eat the fresh stuff first, smoke meat and fish on a rack, or salt it. Rotten food's no good to anyone: throw it out."}, + {"id": "wet", "when": "status:Wet", "hold": 2, "title": "Soaked", + "text": "Wet clothes steal your warmth. Stand under a roof or by a fire and you'll dry off."}, + {"id": "hungry", "when": "status:Hungry", "hold": 2, "title": "Hungry", + "text": "Your belly's empty. Berries and mushrooms tide you over, but cooked meat and fish fill you up for longer."}, + {"id": "diet", "when": "status:Poor diet|Malnourished", "hold": 2, "title": "Mix it up", + "text": "Eat the same thing every day and your body notices. Watch the three diet bars: meat, fish and greens. Keep them all topped up and you'll be stronger for it."}, + {"id": "howl", "when": "event:howl", "title": "Wolves", + "text": "Hear that? Wolves have moved into the hills. They'll come hunting soon after dark. Walls, a door and a fire are your friends."}, + {"id": "wolf_moon", "when": "wolf_moon", "title": "The red moon", + "text": "A red moon. The pack's bolder tonight and the big one leads them. Stay behind a good door, or stand by a fire with a weapon ready."}, + {"id": "night", "when": "night", "hold": 5, "title": "Nightfall", + "text": "Dark and cold till morning. Keep near a fire or carry a torch. A bed under a roof lets you sleep through it."}, + {"id": "hard_winter", "when": "hard_winter", "hold": 3, "title": "Hard winter", + "text": "A hard winter's come in: a few days of bitter cold, and blizzards that freeze you even by day. Stock wood and food, and stay close to home."}, + {"id": "snow", "when": "weather:snow|blizzard", "hold": 3, "title": "Snow", + "text": "Snow's falling. It's colder out in it, and heavy snow piles up on roofs. Keep your fire fed."}, + {"id": "storm", "when": "weather:storm", "hold": 3, "title": "Storm", + "text": "A storm. Lightning goes for the trees, so don't shelter under one. Your buildings are safe enough."}, + {"id": "stamina", "when": "stamina_low", "title": "Out of breath", + "text": "You're spent. Stop swinging and running a moment and your stamina comes back. Rested by a fire, it comes back faster."}, + {"id": "sheltered", "when": "status:Sheltered", "hold": 3, "title": "A proper shelter", + "text": "Roof and walls: the weather can't touch you here. Sit by a fire in here a little while and you'll be Rested."} + ] +} diff --git a/docs/TUTORIAL_DESIGN.md b/docs/TUTORIAL_DESIGN.md index 0ef7414..987e08b 100644 --- a/docs/TUTORIAL_DESIGN.md +++ b/docs/TUTORIAL_DESIGN.md @@ -1,8 +1,24 @@ # Tutorial world design -Status: **T1 and T2 built**: the whole tutorial, on the Valley island -(seed 1337), with Old Tam the hermit. T3 (its own island) and T4 (hints in -real worlds) are proposals. +Status: **T1, T2 and T4 built**: the whole tutorial, on the Valley island +(seed 1337), with Old Tam the hermit, and his tips in normal worlds. T3 +(its own island) waits for the worldlets. + +*As built in T4:* +- 16 tips in data/hints.json, in Old Tam's voice: soaked, cold, freezing, + hungry, a poor diet, food poisoning, food gone rotten, nightfall, the + wolves' howl, the red moon, a hard winter, snow, a storm, out of + breath, a first proper shelter, and back on your feet after dying. +- The `Hints` autoload (scripts/core/hints.gd) checks your own survival + statuses, the weather, the clock and your bag, plus two events (a howl, + a death), so tips work the same online (the server mirrors statuses). + Each shows once per player (user://hints.json), in list order, 16 s + apart, never while you're dead, and not in the tutorial world or on a + server. +- A card top right (scripts/ui/hint_card.gd) for 14 s; H puts it away. + Pause menu: **Old Tam's tips** on/off and **Show again**. On by default. +- tests/test_hints.gd. Headless runs never start tips unless a test asks + (so the suites can't use up your real ones). *As built in T2:* - All five chapters, 22 steps (data/tutorial.json), in Old Tam's voice: diff --git a/project.godot b/project.godot index d89de6f..3b7ee10 100644 --- a/project.godot +++ b/project.godot @@ -26,6 +26,7 @@ Boot="*res://scripts/core/boot.gd" ItemDB="*res://scripts/core/item_db.gd" Actions="*res://scripts/core/actions.gd" Guide="*res://scripts/core/guide.gd" +Hints="*res://scripts/core/hints.gd" Saves="*res://scripts/core/saves.gd" Net="*res://scripts/net/net.gd" WorldNet="*res://scripts/net/world_net.gd" diff --git a/scripts/core/hints.gd b/scripts/core/hints.gd new file mode 100644 index 0000000..4d740f0 --- /dev/null +++ b/scripts/core/hints.gd @@ -0,0 +1,137 @@ +extends Node +## T4 (docs/TUTORIAL_DESIGN.md §5): Old Tam's tips in normal worlds. Each tip +## (data/hints.json) shows once per player, the first time it fits: first +## rain, first night, the wolves' howl, a poor diet, a first death... They +## read only your own state (survival statuses, the weather, the clock, your +## bag) plus two events, so they work the same online. Not in the tutorial +## world (the tutorial covers it) or on a server. A setting turns them off. +## The HUD's hint card (scripts/ui/hint_card.gd) shows them. + +signal shown(hint: Dictionary) + +const GAP := 16.0 ## seconds between tips, so they don't pile up +const START_QUIET := 4.0 ## nothing in the first seconds of a world + +var path := "user://hints.json" ## your settings: on or off, and the tips seen +var enabled := true +var seen: Array = [] +var hints: Array = [] +var guide_name := "" +var world: Node3D = null +var active := false +var _held: Dictionary = {} ## hint id -> seconds its condition has held +var _events: Dictionary = {} ## "howl" -> true, until the tip shows +var _cooldown := 0.0 + +func _ready() -> void: + var d: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://data/hints.json")) + if d is Dictionary: + hints = d.get("hints", []) + guide_name = String(d.get("guide", "")) + load_prefs() + +func load_prefs() -> void: + enabled = true + seen = [] + if not FileAccess.file_exists(path): + return + var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) + if d is Dictionary: + enabled = bool(d.get("enabled", true)) + for id in d.get("seen", []): + seen.append(String(id)) + +func save_prefs() -> void: + var f := FileAccess.open(path, FileAccess.WRITE) + if f != null: + f.store_string(JSON.stringify({"enabled": enabled, "seen": seen}, "\t")) + +func set_enabled(on: bool) -> void: + enabled = on + save_prefs() + +## Every tip can show again (the pause menu's "Show tips again"). +func reset_seen() -> void: + seen = [] + save_prefs() + +var allow_headless := false ## tests: run with no screen (and their own `path`) + +func start(w: Node3D) -> void: + if DisplayServer.get_name() == "headless" and not allow_headless: + return # (the other suites' worlds mustn't use up your real tips) + world = w + active = true + _held = {} + _events = {} + _cooldown = START_QUIET + +func stop() -> void: + active = false + world = null + +## Something that a condition can't read happened: "howl", "died". +func event(id: String) -> void: + if active: + _events[id] = true + +func _process(delta: float) -> void: + if active: + step(delta) + +func step(dt: float) -> void: + if not active or not enabled or world == null or not is_instance_valid(world) or Guide.active: + return + _cooldown -= dt + var due: Dictionary = {} + for h in hints: + var id := String(h.id) + if seen.has(id): + continue + if _met(String(h.when)): + _held[id] = float(_held.get(id, 0.0)) + dt + if due.is_empty() and _held[id] >= float(h.get("hold", 1.0)): + due = h + else: + _held[id] = 0.0 + if not due.is_empty() and _cooldown <= 0.0: + _show(due) + +func _show(h: Dictionary) -> void: + seen.append(String(h.id)) + _events.erase(String(h.when).trim_prefix("event:")) + save_prefs() + _cooldown = GAP + shown.emit(h) + +func _met(when: String) -> bool: + var player: Node = world.player + if player == null or not is_instance_valid(player): + return false + var dead: bool = world.dead_waiting + var kind := when.get_slice(":", 0) + var vals := when.get_slice(":", 1).split("|", false) if when.contains(":") else PackedStringArray() + match kind: + "event": + return not dead and _events.has(vals[0]) + "status": + if dead: + return false + for s in player.survival.statuses: + for v in vals: + if String(s).begins_with(v): + return true + return false + "weather": + return not dead and world.weather != null and String(world.weather.state) in vals + "has": + return not dead and player.inventory.count(vals[0]) > 0 + "night": + return not dead and world.day_night.is_night() + "stamina_low": + return not dead and player.survival.stamina < 8.0 + "wolf_moon": + return not dead and world.day_night.is_night() and world.day_night.wolf_moon_now() + "hard_winter": + return not dead and world.weather != null and world.weather.in_hard_winter() + return false diff --git a/scripts/creatures/wolf_pack.gd b/scripts/creatures/wolf_pack.gd index 4cd745c..b3cbb2f 100644 --- a/scripts/creatures/wolf_pack.gd +++ b/scripts/creatures/wolf_pack.gd @@ -396,6 +396,7 @@ func howl() -> void: if world.server_mode: CreatureNet.howl((live[0] as Node3D).global_position + Vector3(0, 1, 0)) # everyone hears it return + Hints.event("howl") if _audio == null: return var from: Node3D = alpha if alpha_alive() else live[0] # (the alpha's howl is deeper) diff --git a/scripts/net/creature_net.gd b/scripts/net/creature_net.gd index 31865f2..48368f2 100644 --- a/scripts/net/creature_net.gd +++ b/scripts/net/creature_net.gd @@ -250,6 +250,7 @@ func _s2c_howl(at: Vector3) -> void: if not _alive_world(): return howls_heard += 1 + Hints.event("howl") if DisplayServer.get_name() == "headless": return if _howl_player == null or not is_instance_valid(_howl_player): diff --git a/scripts/ui/hint_card.gd b/scripts/ui/hint_card.gd new file mode 100644 index 0000000..f08014d --- /dev/null +++ b/scripts/ui/hint_card.gd @@ -0,0 +1,63 @@ +extends Control +## Old Tam's tips in normal worlds (scripts/core/hints.gd): a card top right, +## under the clock, for 14 seconds. H puts it away early. + +const UITheme = preload("res://scripts/ui/theme.gd") +const SHOW_FOR := 14.0 + +var _box: PanelContainer +var _title: Label +var _text: Label +var _tween: Tween + +func setup() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + mouse_filter = Control.MOUSE_FILTER_IGNORE + _box = UITheme.panel() + _box.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) + _box.grow_horizontal = Control.GROW_DIRECTION_BEGIN + _box.offset_right = -22 + _box.offset_top = 48 + _box.custom_minimum_size = Vector2(340, 0) + _box.mouse_filter = Control.MOUSE_FILTER_IGNORE + _box.modulate.a = 0.0 + _box.visible = false + add_child(_box) + var v := VBoxContainer.new() + v.add_theme_constant_override("separation", 4) + v.custom_minimum_size = Vector2(320, 0) + _box.add_child(v) + v.add_child(UITheme.label("%s's tip (H hides · tips can be turned off in the menu)" % Hints.guide_name if not Hints.guide_name.is_empty() else "Tip (H hides)", 13, UITheme.TEXT_DIM)) + _title = UITheme.label("", 19) + v.add_child(_title) + _text = UITheme.label("", 15, UITheme.TEXT_DIM) + _text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _text.custom_minimum_size = Vector2(320, 0) + v.add_child(_text) + Hints.shown.connect(show_hint) + +func show_hint(h: Dictionary) -> void: + _title.text = String(h.title) + _text.text = ("\"%s\"" % String(h.text)) if not Hints.guide_name.is_empty() else String(h.text) + _box.visible = true + if _tween != null: + _tween.kill() + _tween = create_tween() + _tween.tween_property(_box, "modulate:a", 1.0, 0.4) + _tween.tween_interval(SHOW_FOR) + _tween.tween_property(_box, "modulate:a", 0.0, 0.8) + _tween.tween_callback(func(): _box.visible = false) + +func is_showing() -> bool: + return _box.visible + +func hide_hint() -> void: + if _tween != null: + _tween.kill() + _box.visible = false + _box.modulate.a = 0.0 + +func _unhandled_input(event: InputEvent) -> void: + if _box.visible and event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_H: + hide_hint() + get_viewport().set_input_as_handled() diff --git a/scripts/ui/hud.gd b/scripts/ui/hud.gd index 7d4a6bc..09f7eb0 100644 --- a/scripts/ui/hud.gd +++ b/scripts/ui/hud.gd @@ -179,6 +179,11 @@ func _build_stats(root: Control) -> void: guide.name = "Guide" root.add_child(guide) guide.setup(player) + # Old Tam's tips in normal worlds (T4), in the same place. + var tips := preload("res://scripts/ui/hint_card.gd").new() + tips.name = "HintCard" + root.add_child(tips) + tips.setup() func _process(_d: float) -> void: _fade_chat() diff --git a/scripts/ui/pause_menu.gd b/scripts/ui/pause_menu.gd index b431af8..30d681e 100644 --- a/scripts/ui/pause_menu.gd +++ b/scripts/ui/pause_menu.gd @@ -80,6 +80,26 @@ func bind(p: CharacterBody3D) -> void: sens_row.add_child(sens) _main.add_child(sens_row) + # Old Tam's tips in normal worlds (T4): on or off, or all of them again. + if not Guide.active: + var tips_row := HBoxContainer.new() + var tips := CheckButton.new() + tips.text = "Old Tam's tips" + tips.focus_mode = Control.FOCUS_NONE + tips.button_pressed = Hints.enabled + tips.add_theme_font_size_override("font_size", 18) + tips.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tips.toggled.connect(Hints.set_enabled) + tips_row.add_child(tips) + var again := Button.new() + again.text = "Show again" + again.focus_mode = Control.FOCUS_NONE + again.add_theme_font_size_override("font_size", 15) + again.tooltip_text = "Every tip shows once more, when it next fits" + again.pressed.connect(func(): Hints.reset_seen(); again.text = "Done") + tips_row.add_child(again) + _main.add_child(tips_row) + # The tutorial: skip a step, or leave it (saved, to carry on later). if Guide.active: _main.add_child(_button("Skip this tutorial step", func(): Guide.skip(); close())) diff --git a/scripts/world/world.gd b/scripts/world/world.gd index f47561b..e935d49 100644 --- a/scripts/world/world.gd +++ b/scripts/world/world.gd @@ -187,6 +187,7 @@ func heat_at(pos: Vector3, holder: Node = null) -> float: ## Death: everything you carried goes into a grave where you fell, and you ## wake up at the island start. The world carries on. func _on_player_died() -> void: + Hints.event("died") var pos := player.global_position pos.y = terrain.sample_height(pos.x, pos.z) if pos.y < terrain.water_level: @@ -245,6 +246,7 @@ func respawn_after_death() -> void: ## Online: the server says we died. Same screen; Respawn asks the server. func died_online(text: String, has_items: bool, to_bed: bool, penalty: String = "grave") -> void: + Hints.event("died") dead_waiting = true player.velocity = Vector3.ZERO player.set_physics_process(false) @@ -404,6 +406,10 @@ func _on_terrain_ready() -> void: player.set_physics_process(true) set_physics_process(true) creatures.setup(self) + # Old Tam's tips (T4): your own world or a server's, not the tutorial's. + if not server_mode and not menu_mode and not bool(Saves.read_meta(Saves.current_id).get("tutorial", false)) \ + and not "--tutorial" in OS.get_cmdline_user_args(): + Hints.start(self) if is_online(): _go_online() _maybe_capture() @@ -553,6 +559,8 @@ func _exit_tree() -> void: save_now() if Guide.world == self: Guide.stop() + if Hints.world == self: + Hints.stop() func _notification(what: int) -> void: if what != NOTIFICATION_WM_CLOSE_REQUEST or menu_mode: diff --git a/tests/run_all.sh b/tests/run_all.sh index 61ba18e..81ed26b 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then echo "Importing the project first (fresh copy)..." godot --headless --path . --import >/dev/null 2>&1 fi -for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_terrain_edges test_boundary; do +for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_terrain_edges test_boundary; do extra="" [ "$t" = "test_boundary" ] && extra="--fixed-fps 30" # A suite that waits on a world that failed to load would hang forever, diff --git a/tests/test_hints.gd b/tests/test_hints.gd new file mode 100644 index 0000000..7e6b493 --- /dev/null +++ b/tests/test_hints.gd @@ -0,0 +1,168 @@ +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