Use this pattern when a gameplay system should respond to the clock's authoritative time-of-day rather than wall-clock time, per-action timers, or a coarse half-day heuristic. It turns "the clock exposes time" into "the clock drives observable simulation behavior."
Typical case: an AI planner that only plans a night action when the world is actually dark. Or an event system that only rolls night encounters after dusk.
If the only consumer is a visual tint or a once-per-day rollover, prefer the
lighting rig or signal_bus.date_changed guides instead. This pattern is for
game logic that needs a fact, not a signal.
When to use this pattern
- Your system already reasons in facts/flags, and "it is night" is one of them.
- You want that fact to come from one authoritative clock, not from each consumer re-deriving "night" independently (which drifts).
- You need the fact during planning/decisions, not just for a UI tween.
When NOT to use this pattern
- The consumer is presentation-only (use Godot
delta; see Clock-Driven Animation Pattern). - The consumer can subscribe to a clock signal directly (
clock.signal_busTOD /date_changedevents) — signals are preferred over polling when an event exists.
Authority rule
When Calendar Time is present and healthy, the clock owns temporal truth. Game-local fallback inference must never override it.
Concretely: read the authoritative time-of-day first. Only fall back to a
game-local heuristic when the authoritative stage is unavailable (plugin absent,
clock unassigned, or no DayNightCycleService bound). The fallback must be
strictly weaker/secondary so the clock always wins once it can speak.
Who owns what
- The plugin owns the stage. The clock reports the current TOD stage. It is objective and save/load-stable.
- The game owns the fact. Whether a
Duskinstant counts as "night" is game policy. It can differ per system (sleep, encounters, visibility). The fact source below is a game-owned mapping. It is never authored by the plugin. In Thistletide it lives insrc/bridges/calendar_time/calendar_time_goap_facts.gd. That file belongs to the game, not to the addon.
Recipe
A game-local fact source derives facts only through the plugin's public surface — it never reads private clock state. It is then injected additively into whatever system consumes facts (a GOAP bridge, an AI blackboard, an event gate). The consumer appends the source's facts to its own; existing facts are never removed, so omitting the source at runtime is a no-op.
The canonical reference is Thistletide's CalendarTimeGoapFacts
(src/bridges/calendar_time/), adapted below.
1. A minimal, copyable fact source
class_name GameTimeFactSource
extends RefCounted
# Game-local fact keys. `is_night` already exists on the consumer's action
# table; the finer stage facts are additive, observation-level facts.
const FACT_IS_NIGHT := "is_night"
const FACT_IS_DAWN := "is_dawn"
const FACT_IS_DAY := "is_day"
# Authoritative source: a GameClock whose DayNightCycleService reports TOD.
var _clock :GameClock
# Optional game-local fallback used only when the clock reports no TOD stage
# (e.g. plugin absent in a headless run). A Callable keeps this snippet free
# of any game-specific heuristic type — each game supplies its own.
var _fallback_is_night :Callable = Callable()
# `clock` is the game clock. `fallback_is_night` is an OPTIONAL zero-arg
# predicate returning true when your game's own heuristic says "night".
func configure(clock :GameClock, fallback_is_night :Callable = Callable())-> void:
_clock= clock
_fallback_is_night= fallback_is_night
func is_night()-> bool:
return get_facts().has(FACT_IS_NIGHT)
# Deterministic facts for the current authoritative instant. Called each
# planning/evaluation tick; cheap and side-effect free.
func get_facts()-> PackedStringArray:
var facts :PackedStringArray = []
# Authoritative path: the plugin's DayNightCycleService classifies the
# current instant by the calendar's dawn/day/dusk/night schema. Reads the
# public TOD name only (duck-typed: works with a raw GameClock or an
# adapter that exposes get_time_of_day()).
if _clock!= null and _clock.has_method("get_time_of_day"):
var tod :String = str(_clock.get_time_of_day())
match tod:
"Night","Dusk":
facts.append(FACT_IS_NIGHT)
"Dawn":
facts.append(FACT_IS_DAWN)
"Daytime":
facts.append(FACT_IS_DAY)
# Authority rule: if the clock spoke, trust it.
return facts
# Fallback: plugin TOD unavailable (e.g. plugin absent in a headless run)
# -> the game-local heuristic. Never overrides the authoritative clock.
if not _fallback_is_night.is_null()and _fallback_is_night.call():
facts.append(FACT_IS_NIGHT)
return facts2. Inject the facts additively
In the consumer (e.g. the system that builds the start-fact list for your planner), append the source's facts and let existing facts win:
func _build_facts():
var facts :PackedStringArray = []
# ... your game's own survival/perception facts ...
# Authoritative calendar_time facts. Additive only: a night/dawn/dusk/day
# fact is appended only if the game hasn't already built it, so existing
# facts are untouched and omitting the source at runtime is a no-op.
if _time_fact_source!= null and _time_fact_source.has_method("get_facts"):
for fin _time_fact_source.get_facts():
if not facts.has(f):
facts.append(f)
return facts3. Let a single fact gate a night-only behavior
Register a behavior whose precondition is the night fact, so the action is simply not planable during day:
# In your action/goal table:
# id = "sleep_at_night"
# pre = [FACT_TIRED, FACT_IS_NIGHT]
# effect = [FACT_RESTED]
# in_place = trueThe fact is additive and only present when the clock reports night (or the
fallback heuristic fires). So the action is inert by day and reachable at
night — no if is_night() branches scattered through the behavior. The
planner's goal selection can still return FACT_RESTED from a tired fact; only
the path to that goal changes with the clock.
In a BFS planner that selects the first applicable action, register the night action before the generic fallback so it wins at night. Its
FACT_IS_NIGHTprecondition never holds by day, so day planning is unchanged.
Save/load and determinism
- Do not serialize the fact. Facts are derived from the clock every tick,
so a saved
is_nightflag would drift. Restore the clock (see Examples) and re-derive. - For save/load-safe, frame-exact snapshots, prefer
CalendarTimeSemanticState.from_clock(clock)— a stable, schema-versioned read that is the canonical consumer surface for "what time is it right now."
See also
- Examples — save/load the clock that drives this source.
- TimeScale & DriveMode — host ownership of the clock you are reading from.
- Clock-Driven Animation Pattern — when to use the clock vs Godot delta.