Skip to content

Day-Night Cycle

Build an auto-advancing time system — every 3 turns, time moves forward (Morning → Noon → Evening → Night → Morning). Different time periods activate different lore entries and background music, shifting the AI's writing atmosphere to match. The player doesn't have to do anything — time just flows.


What you'll build

A day-night cycle system embedded in the chat:

  • Auto-counting — every dialogue turn, an internal counter increments by 1. On the 3rd turn, time advances one period
  • Four time periods — Morning → Noon → Evening → Night → Morning (and so on)
  • Atmosphere switching — each period has its own lore entry describing that period's lighting, temperature, NPC behavior, etc. When the period changes, the old entry is disabled and the new one is enabled
  • BGM switching — Morning plays birdsong, Night switches to crickets and frogs. Transitions use crossfade so there's no abrupt cut
  • Time badge — a small icon on the last message in chat (☀️🌤️🌅🌙) so the player always knows what time it is

How it works

The whole system boils down to: counter +1 each turn → counter hits 3 → switch period → reset counter → toggle entries and music.

Player sends message → AI replies → turn ends
  → "every turn +1" behavior fires: turn_counter goes from 0 to 1
  → next turn: turn_counter goes from 1 to 2
  → next turn: turn_counter goes from 2 to 3
  → "variable crossed threshold" behavior fires: turn_counter rises above 2
  → actions execute: time_period set to next period, turn_counter reset to 0
  → old period entry disabled, new period entry enabled
  → crossfade to the new period's BGM
  → message renderer updates the time badge

There are two ways to implement this. Same result, different mental models:

ApproachTriggers usedNumber of behaviorsBest for
Approach A: every-turn +1 + variable crossedevery-turn + variable-crossed2People who want to understand the underlying mechanics
Approach B: fire directly every N turnsturn-count (everyNTurns=3)1People who just want it done

This recipe teaches Approach A (more versatile, and helps you understand how behaviors chain together). Approach B is briefly covered at the end.


Step by step

Step 1: Create variables

We need 2 variables — one to track the current time period, one as a counter.

Editor → sidebar → Variables tab → click "Add Variable" for each

Variable 1: Current time period

FieldValueWhy
NameCurrent Time PeriodFor your own reference
IDtime_periodBehaviors and the message renderer read/write this ID
TypeStringBecause the values are text ("Morning", "Noon", "Evening", "Night")
Default ValueMorningNew sessions start in the morning
CategoryCustomDedicated category for the time system
Behavior RulesDo not modify this variable. It is controlled automatically by the day-night cycle system. Its current value represents the in-game time period.Tells the AI not to change the time on its own — only behaviors can

Variable 2: Turn counter

FieldValueWhy
NameTurn CounterFor your own reference
IDturn_counterIncrements each turn, resets at 3
TypeNumberNeeds arithmetic
Default Value0Starts counting from 0
CategoryCustomDedicated category for the time system
Behavior RulesDo not modify this variable. It is controlled automatically by the day-night cycle system.Prevents the AI from tampering

Why use a counter instead of firing every 3 turns directly?

The counter + variable-crossed approach is more flexible. Say you later want "3 turns during the day, 5 turns at night" — you just add a condition check to the behavior. The turn-count trigger is simpler but less adaptable. Both approaches have their strengths; pick whichever fits your needs.


Step 2: Create four time-period lore entries

Each period needs a lore entry describing that period's atmosphere. Only "Morning" is enabled by default; the other three start disabled.

Editor → Lore tab → create entries one by one

Entry 1: Morning atmosphere

FieldValueWhy
NameMorning AtmosphereFor your own reference
SectionPresetsPreset entries are sent to the AI every time
EnabledYes (toggle on)The game starts in the morning, so this one is on by default

Content:

[Current Time Period: Morning]
It is early morning. Reflect the following atmosphere when describing the scene:
- Soft morning light spills in from the east; the air is fresh and cool
- Dewdrops cling to blades of grass and flower petals, refracting tiny glints of light
- Birds sing in the branches; a rooster crows in the distance
- NPCs are just waking up, shops are opening one by one, foot traffic is picking up
- The overall mood is peaceful and full of hope

Entry 2: Noon atmosphere

FieldValueWhy
NameNoon AtmosphereFor your own reference
SectionPresetsPreset section
EnabledNo (toggle off)Behaviors will enable it when the time period switches

Content:

[Current Time Period: Noon]
It is midday. Reflect the following atmosphere when describing the scene:
- The sun beats down directly overhead; the light is searing and bright, the ground reflecting a blinding white glare
- The air is stifling; distant scenery shimmers and warps in the heat haze
- Most people have retreated to the shade to rest; the streets are quieter than morning
- Taverns and eateries are at their busiest, the smell of food drifting through the air
- The overall mood is languid and sweltering

Entry 3: Evening atmosphere

FieldValueWhy
NameEvening AtmosphereFor your own reference
SectionPresetsPreset section
EnabledNo (toggle off)Behaviors will enable it

Content:

[Current Time Period: Evening]
It is dusk. Reflect the following atmosphere when describing the scene:
- The setting sun paints the sky in shades of orange-red and purple, clouds rimmed with gold
- Long shadows stretch from buildings and trees
- Flocks of birds sweep across the sky heading home; wisps of cooking smoke rise from rooftops
- NPCs are wrapping up for the day, heading home; children chase each other through the streets
- The overall mood is warm, nostalgic, tinged with a gentle melancholy

Entry 4: Night atmosphere

FieldValueWhy
NameNight AtmosphereFor your own reference
SectionPresetsPreset section
EnabledNo (toggle off)Behaviors will enable it

Content:

[Current Time Period: Night]
It is deep night. Reflect the following atmosphere when describing the scene:
- Moonlight and starlight are the only natural sources of illumination, casting a silvery glow over everything
- Most buildings have gone dark; the occasional window glows with dim candlelight
- A cool night breeze carries the chorus of crickets and frogs
- The streets are nearly deserted; night-watch guards pace slowly by, torches in hand
- Danger may lurk in the shadows — wild beasts, thieves, or something stranger still
- The overall mood is mysterious, hushed, and laced with hidden peril

Why is only "Morning" enabled by default? Because the game starts in the morning. If all four entries were enabled at once, the AI would receive morning, noon, evening, and night descriptions simultaneously and not know which one to follow. Enabling only one at a time keeps the AI locked onto the right atmosphere.


Step 3: (Optional) Upload time-period BGM

If you want each period to have its own background music, upload audio files first.

Editor → Audio tab → add tracks

Track IDNameTypeLoopFade InFade Out
bgm_morningMorning ThemeBGMYes2s2s
bgm_noonAfternoon ThemeBGMYes2s2s
bgm_eveningDusk ThemeBGMYes2s2s
bgm_nightNight ThemeBGMYes2s2s

Don't have audio files? Skip this step. The core of the day-night cycle is lore-entry switching — BGM is a nice bonus. You can always add it later.

In the BGM playlist, set autoPlay to true and default to bgm_morning. When the period switches later, behaviors will use the crossfade action to smoothly transition tracks.


Step 4: Create behaviors

This is the heart of the system. We need 2 behaviors — well, actually 6. Read on.

Editor → Behaviors tab → add behaviors one by one

Behavior 1: Count each turn

This one is dead simple — after each dialogue turn, the counter goes up by 1.

WHEN (when to check):

FieldValueWhy
Trigger typeEvery turnFires automatically after each player-AI exchange

DO (what to do):

Action typeSettingsEffect
Modify variableturn_counter add 1Counter +1

That's the only action. No conditions, no extra config. It faithfully adds 1 every turn.

Why not check "is it 3 yet?" right here? Because the behavior system's design philosophy is one behavior, one job. Incrementing the counter is one behavior's job; checking whether it hit 3 is another's. The engine automatically chains them — after the counter increments, if the value crosses the threshold, the other behavior fires.


Behavior 2: Advance the time period

This behavior fires when the counter reaches 3 and executes all the switching logic.

WHEN (when to check):

FieldValueWhy
Trigger typeVariable crossed thresholdFires when turn_counter rises above the threshold
Variableturn_counterThe variable we're watching
DirectionRises aboveFires when the value goes from below the threshold to above it
Threshold2Fires when turn_counter goes from 2 to 3 (rises above 2)

Why is the threshold 2, not 3? The "rises above" direction in variable-crossed detects the moment a value goes from <= threshold to > threshold. When turn_counter goes from 2 to 3, it "rises above 2" — i.e., it goes from <=2 to >2. If you set the threshold to 3, you'd need turn_counter to go from 3 to 4 before it fires, and that's not what we want.

DO (what to do):

This behavior needs to do a lot. Add these actions in order:

#Action typeSettingsEffect
1Modify variableturn_counter set to 0Reset the counter for the next 3-turn countdown
2Disable lore entryMorning AtmosphereTurn off all period entries
3Disable lore entryNoon AtmosphereTurn off all
4Disable lore entryEvening AtmosphereTurn off all
5Disable lore entryNight AtmosphereTurn off all

Wait — that turns off all four. How does it know which one to enable?

Good question. This is where ONLY IF conditions come in for branching. But a single behavior can only have one set of actions. So we split "advance the time period" into 5 behaviors: 1 to reset the counter and disable all entries, and 4 to enable the corresponding period.

Let me reorganize:


Full behavior list (6 total):

Behavior 1: Count each turn

(Same as above — not repeated.)

Behavior 2: Advance — Morning → Noon

WHEN:

FieldValue
Trigger typeVariable crossed threshold
Variableturn_counter
DirectionRises above
Threshold2

ONLY IF:

VariableOperatorValue
time_periodequals (eq)Morning

DO:

#Action typeSettingsEffect
1Modify variableturn_counter set to 0Reset counter
2Modify variabletime_period set to NoonAdvance to next period
3Disable lore entryMorning AtmosphereTurn off old period entry
4Enable lore entryNoon AtmosphereTurn on new period entry
5Play musicbgm_noon, action: crossfade, duration 3sCrossfade to Noon BGM
6Tell AIContent: Time has advanced from Morning to Noon. Naturally reflect this time change in your upcoming descriptions.Lets the AI smoothly transition

Behavior 3: Advance — Noon → Evening

WHEN: Same as Behavior 2 (variable crossed threshold, turn_counter rises above 2)

ONLY IF:

VariableOperatorValue
time_periodequals (eq)Noon

DO:

#Action typeSettingsEffect
1Modify variableturn_counter set to 0Reset counter
2Modify variabletime_period set to EveningAdvance to Evening
3Disable lore entryNoon AtmosphereTurn off Noon entry
4Enable lore entryEvening AtmosphereTurn on Evening entry
5Play musicbgm_evening, action: crossfade, duration 3sCrossfade BGM
6Tell AIContent: Time has advanced from Noon to Evening. Naturally reflect this time change in your upcoming descriptions.AI transition

Behavior 4: Advance — Evening → Night

WHEN: Same as above

ONLY IF:

VariableOperatorValue
time_periodequals (eq)Evening

DO:

#Action typeSettingsEffect
1Modify variableturn_counter set to 0Reset counter
2Modify variabletime_period set to NightAdvance to Night
3Disable lore entryEvening AtmosphereTurn off Evening entry
4Enable lore entryNight AtmosphereTurn on Night entry
5Play musicbgm_night, action: crossfade, duration 3sCrossfade BGM
6Tell AIContent: Time has advanced from Evening to Night. Naturally reflect this time change in your upcoming descriptions.AI transition

Behavior 5: Advance — Night → Morning

WHEN: Same as above

ONLY IF:

VariableOperatorValue
time_periodequals (eq)Night

DO:

#Action typeSettingsEffect
1Modify variableturn_counter set to 0Reset counter
2Modify variabletime_period set to MorningCycle back to Morning
3Disable lore entryNight AtmosphereTurn off Night entry
4Enable lore entryMorning AtmosphereTurn on Morning entry
5Play musicbgm_morning, action: crossfade, duration 3sCrossfade BGM
6Tell AIContent: Time has advanced from Night to Morning — a new day begins. Naturally reflect this time change in your upcoming descriptions.AI transition

Why split into 4 behaviors? Because each period transition needs to enable a different entry and play a different BGM. A single behavior can only have one set of conditions and one set of actions — it doesn't support if-else branching. So we use 4 behaviors with different ONLY IF conditions to simulate branching: when the same trigger fires (counter rises above 2), the engine checks all of them, but only the one whose time_period matches will execute.

Behavior 6: Session initialization

This behavior sets the initial state when a session begins, ensuring variables are at the correct starting values for new sessions or re-entries.

WHEN:

FieldValueWhy
Trigger typeSession start (session-start)Fires once automatically when a new session begins

DO:

#Action typeSettingsEffect
1Modify variabletime_period set to MorningEnsure it starts at Morning
2Modify variableturn_counter set to 0Reset the turn counter

Why do we need a session initialization behavior? Variable default values only take effect when they're first created. If a player quits mid-session and starts a new one, the variables might retain their previous values (e.g., time_period stuck on "Night", turn_counter stuck at 2). The session initialization behavior ensures every new session starts from Morning with the counter at 0.

Behavior priority

All 4 advance behaviors can keep the default priority (0). Their ONLY IF conditions are mutually exclusive — the current time period can only match one of them, so there's no conflict.


Step 5: Build the time-badge message renderer

Show the current period's icon on the last message in chat, so the player always knows what time it is at a glance.

Editor → Message Renderer tab → select Custom TSX → paste:

tsx
export default function Renderer({ content, renderMarkdown, messageIndex }) {
  const api = useYumina();

  // ---- Read variable ----
  const timePeriod = String(api.variables.time_period || "Morning");

  // ---- Period icon and color map ----
  const timeConfig = {
    "Morning": { icon: "☀️", label: "Morning", color: "#fbbf24", bg: "rgba(251,191,36,0.15)" },
    "Noon": { icon: "🌤️", label: "Noon", color: "#f59e0b", bg: "rgba(245,158,11,0.15)" },
    "Evening": { icon: "🌅", label: "Evening", color: "#f97316", bg: "rgba(249,115,22,0.15)" },
    "Night": { icon: "🌙", label: "Night", color: "#818cf8", bg: "rgba(129,140,248,0.15)" },
  };

  const current = timeConfig[timePeriod] || timeConfig["Morning"];

  // ---- Check if this is the last message ----
  const msgs = api.messages || [];
  const isLastMsg = messageIndex === msgs.length - 1;

  return (
    <div>
      {/* Render message text normally */}
      <div
        style={{ color: "#e2e8f0", lineHeight: 1.7 }}
        dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
      />

      {/* Time badge — only on the last message */}
      {isLastMsg && (
        <div style={{
          display: "inline-flex",
          alignItems: "center",
          gap: "6px",
          marginTop: "12px",
          padding: "4px 12px",
          background: current.bg,
          border: `1px solid ${current.color}33`,
          borderRadius: "999px",
          fontSize: "13px",
          color: current.color,
          fontWeight: "600",
        }}>
          <span style={{ fontSize: "16px" }}>{current.icon}</span>
          <span>{current.label}</span>
        </div>
      )}
    </div>
  );
}

Line-by-line breakdown:

  • api.variables.time_period — reads the current time period variable
  • timeConfig — a lookup table mapping each period to an icon, text label, and color. Feel free to change the colors to match your world's style
  • isLastMsg — only shows the badge on the last message, not every message
  • The badge uses inline-flex + border-radius: 999px for a pill shape — subtle but immediately visible

Want to show the time on every message?

Remove the {isLastMsg && ...} check and put the badge directly in the return. Every message will then carry a time stamp, like timestamps in a chat log.


Step 6: Save and test

  1. Click Save at the top of the editor
  2. Click Start Game or go back to the home page and open a new session
  3. Chat normally with the AI. For the first 2 turns, the time badge shows "☀️ Morning" and nothing changes
  4. After turn 3 — time advances to "🌤️ Noon", and the AI's next reply naturally reflects the time change
  5. 3 more turns — advances to "🌅 Evening"
  6. 3 more turns — advances to "🌙 Night". If you set up BGM, you should hear the crossfade
  7. 3 more turns — cycles back to "☀️ Morning", a new day begins

If something goes wrong:

SymptomLikely causeFix
Time never changes"Count each turn" behavior isn't firingCheck that Behavior 1's trigger is set to "Every turn" and the behavior is enabled
No switch on turn 3Threshold is wrongConfirm the "variable crossed threshold" threshold is 2 (not 3), direction is "rises above"
Entries don't change after switchingEntry names don't matchCheck that the "Enable lore entry" / "Disable lore entry" actions in your behaviors reference the correct entry names
All 4 behaviors fire at onceONLY IF conditions are missingEach advance behavior must have an ONLY IF condition restricting the current time_period value
Time badge not visibleSyntax error in message rendererCheck the compilation status at the bottom of the message renderer — it should show a green "OK"
BGM doesn't switchTrack ID mismatch or no audio uploadedConfirm the trackId in the behavior matches the track ID in the Audio tab

Approach B comparison: using the turn-count trigger

If Approach A feels like too many behaviors, you can use the simpler Approach B.

Differences:

Approach A (this recipe)Approach B
Triggersevery-turn + variable-crossedturn-count (everyNTurns=3)
Needs turn_counter variableYesNo
Number of behaviors6 (1 counting + 4 advancing + 1 optional initialization)4 (4 advancing)
FlexibilityHigh (interval can be adjusted dynamically)Low (interval fixed at N)
Best forWorlds that need dynamic time-flow speedWorlds with a fixed rhythm

How to do Approach B:

Remove the turn_counter variable and the "Count each turn" behavior. Change the trigger on all 4 advance behaviors to:

FieldValue
Trigger typeEvery N turns
everyNTurns3

Everything else (ONLY IF conditions, DO actions) stays exactly the same as Approach A. The turn-count trigger automatically fires every 3 turns — no manual counting needed.

How the turn-count trigger works: The engine maintains an internal global turn count. When you set everyNTurns: 3, the engine automatically fires the behavior on turns 3, 6, 9, 12, and so on. You don't need to manage a counter variable yourself.


Quick reference

What you wantHow to do it
Do something every turnBehavior trigger: "Every turn" (every-turn)
Do something every N turnsBehavior trigger: "Every N turns" (turn-count), set everyNTurns
Detect a variable crossing a valueBehavior trigger: "Variable crossed threshold" (variable-crossed), set variable, direction, and threshold
Switch lore entriesActions: "Enable lore entry" / "Disable lore entry"
Crossfade musicAction: "Play music", operation: crossfade, set fade duration
Let the AI know something happenedAction: "Tell AI", write a temporary system instruction
Show a status badge on a messageRead a variable in the message renderer, render with JSX
Simulate if-else branchingMultiple behaviors sharing the same trigger + different ONLY IF conditions

Try it yourself — importable demo world

Download this JSON and import it as a new world to see everything in action:

recipe-9-demo.json

How to import:

  1. Go to Yumina → My WorldsCreate New World
  2. In the editor, click More ActionsImport Package
  3. Select the downloaded .json file
  4. A new world is created with all variables, entries, behaviors, and renderer pre-configured
  5. Start a new session and try it out

What's included:

  • 2 variables (time_period tracks the current period, turn_counter as the turn counter)
  • 4 lore entries (Morning / Noon / Evening / Night atmosphere, only Morning enabled by default)
  • 6 behaviors (1 per-turn counting + 4 period advancing + 1 session initialization)
  • A message renderer (time-period icon badge)
  • 4 BGM tracks (you'll need to upload your own audio files to replace the URLs)

This is Recipe #9

This recipe shows the chaining power of the behavior system — with a simple counter + threshold trigger + conditional branching, you can build a fully automatic time system. The same pattern works for weather changes, seasonal cycles, NPC mood swings, or anything else that "changes automatically on a rhythm".