144 lines
4.6 KiB
GDScript
144 lines
4.6 KiB
GDScript
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
|