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) Narrator.say("hint_" + String(h.id), String(h.text)) 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() "worldlet": # (where you are on the journey: "worldlet:cove") return not dead and world.has_method("worldlet_id") and String(world.worldlet_id()) in vals "seam": # (an antler pickaxe in your bag, and the cliff tunnel's seam still to dig) return not dead and world.get("tunnel") != null and not world.tunnel.opened \ and player.inventory.count("antler_pickaxe") > 0 return false