Contents
Where things live Scene definition Text blocks Choices Auto navigation Conditions Hour gates Numeric gates Localization vars GotchasWhere things live
| What | Where |
|---|---|
| Scene runtime (resolver + renderer) | src/classes/game/util/sceneManager.js |
| Scene packs (data) |
src/data/scenes/* (examples:
src/data/scenes/sample/general.js,
src/data/scenes/system/traversal.js)
|
| Scene registry | src/data/scenes/index.js (exports SCENE_DEFS) |
| Localization dictionaries | src/data/i18n/en/*.js and src/data/i18n/pl/*.js |
| Common scene authoring helpers |
src/data/scenes/util/common.js (ex:
TIME_OF_DAY, BREAK)
|
priority wins),
then renders its text + choices.
Scene definition
A scene is a plain object. The only required field is id.
Anything not listed in the tables below is ignored by SceneManager.
// src/data/scenes/yourPack/whatever.js
import { TIME_OF_DAY, BREAK } from "../util/common.js";
export const SCENES = [
{
id: "home.default", // required, globally unique
priority: 10, // higher wins if multiple scenes match
// You can write `when`, `if`, `conditions`, or `condition` at scene root.
when: {
placeKeys: ["player_home"],
notPlayerFlags: ["injured"],
},
// Opt-in auto navigation (default is OFF):
// - traversal: inject travel choices when outside
// - exit: inject an Exit choice when inside a place
autoChoices: { exit: true },
// Text can be a string key OR an array of blocks.
text: [
"scene.home.default.text",
{ when: { hour: TIME_OF_DAY.evening }, key: "scene.home.default.evening" },
BREAK,
{ when: { playerFlags: ["injured"] }, keys: ["scene.injured.0", "scene.injured.1"] },
{ keys: ["scene.flavour.0", "scene.flavour.1"], pick: "random" },
{ raw: "(You can also include raw literal text.)" },
],
choices: [
{
id: "home.tidyUp",
text: "choice.home.tidyUp",
minutes: 10,
setFlag: "homeIsTidy",
},
],
},
];
Scene fields (supported)
| Field | Meaning | Notes |
|---|---|---|
id required |
Unique scene id. | Used for debugging and nextSceneId jumps. |
priority optional |
Resolver tie-breaker (higher wins). | Defaults to 0. |
when / if / conditions / condition
optional
|
Conditions under which the scene can appear. | Same condition language as text blocks & conditional choices. |
text optional |
String key or array of blocks to build the final scene text. | See Text blocks. |
textJoiner optional |
String inserted between adjacent text blocks. |
Implementation note
In the current implementation, blocks are effectively concatenated
directly (no automatic newlines). To insert line breaks, include
\n/\n\n in your localized strings (or in raw).
|
choices optional |
Array of choice objects (buttons). | See Choices. |
autoChoices optional |
Opt in to auto-injected travel/exit buttons. |
Supported forms:
|
Text blocks
text can be a single string, or an array of blocks.
SceneManager processes blocks top-to-bottom.
Supported block types
- String: treated as an i18n key (looked up via localizer).
-
Conditional block object:
{ when/if/conditions/condition: {...}, key: "..." } -
Multi-key block:
{ when/if/...?: {...}, keys: ["...","..."] } -
Random pick:
{ keys: ["...","..."], pick: "random" }(picks one key; stable while the scene is active) - Literal block:
{ raw: "Some unlocalized text" } -
BREAK token:
BREAKimported fromsrc/data/scenes/util/common.jsNote: in the current SceneManager implementation, BREAK does not insert any characters. Treat it as a structural marker (reserved for future). If you want visible spacing, add\n/\n\ninside your i18n strings (or userawcontaining newlines).
Example
import { TIME_OF_DAY, BREAK } from "../util/common.js";
text: [
"scene.home.default.text",
{ when: { hour: TIME_OF_DAY.morning }, key: "scene.home.default.morning" },
BREAK,
{ when: { playerFlags: ["injured"] }, keys: [
"scene.injured.0",
"scene.injured.1",
]},
{ keys: ["scene.flavour.0", "scene.flavour.1"], pick: "random" },
{ raw: "(Raw text here.)" },
]
Choices
Choices are buttons shown under the scene. A choice can: advance time, move the player (location/place), set/clear flags, enqueue an urgent scene, and/or hard-jump to a specific next scene.
choices: [
{
id: "travel.toMarket", // required (unique within the scene)
text: "choice.travel.toLocation", // recommended (i18n key). If omitted, label falls back to id.
minutes: 7, // optional (defaults to 0)
hideMinutes: false, // optional (default false). When true, hides the "(N minutes)" suffix.
// Optional condition gate for showing the choice:
when: { notPlayerFlags: ["injured"] },
// If conditions fail, the choice is normally hidden.
// showAnyway shows it as disabled (unclickable):
// showAnyway: true,
// Optional extra interpolation vars for the label:
vars: { destName: "Market Square" },
// Actions (pick what you need):
moveToLocationId: "market_district", // move to a different location
// setPlaceId: "shop_01", // enter a place (place.key becomes currentPlaceKey)
// setPlaceId: null, // exit to outside
// exitToOutside: true, // explicit exit to outside
// moveToHome: true, // go home (uses game.homeLocationId/homePlaceId)
// Flags:
// setFlag: "waitingForPackage", // or setFlags: ["a","b"]
// clearFlag: "waitingForPackage", // or clearFlags: ["a","b"]
// Scene control:
// nextSceneId: "special.cutscene", // hard jump (even if it wouldn't match)
// queueSceneId: "ambulance.arrives",
// queuePriority: 999,
}
]
Choice fields (supported)
| Field | Meaning | Notes |
|---|---|---|
id required |
Unique within a scene. | Used to dispatch the click. |
text optional |
Localization key for the button label. |
If omitted, label falls back to id. The localizer receives
scene vars + {minutes} + anything from vars.
|
minutes optional |
How much time passes when clicking. | Defaults to 0. Also used for the “(N minutes)” suffix. |
hideMinutes optional |
Hide the “(N minutes)” suffix. | Default is false. |
vars optional |
Extra interpolation variables for the choice label. | { vars: { destName: "Market" } } enables {destName} in
text.
|
when / if / conditions / condition
optional
|
Condition gate to show/enable the choice. |
If the condition fails, the choice is hidden by default.
Combine with showAnyway to show disabled.
|
showAnyway optional |
Show the choice even when its conditions fail. |
When conditions fail and showAnyway: true, SceneManager renders
the choice as disabled and refuses to execute it.
|
setPlaceId optional |
Enter/leave a place within the current location. |
Set to a string Place.id to enter; set to null to exit to outside.
Entering sets game.currentPlaceId and also sets
game.currentPlaceKey
to the Place.key for that id.
|
exitToOutside optional |
Exit any place and return to “outside”. | Equivalent to setPlaceId: null. Outside = no placeId and no placeKey. |
moveToLocationId optional |
Move to another location. | Also clears current place state. |
moveToHome optional |
Shortcut to home location + home place. | Uses game.homeLocationId and game.homePlaceId. |
setFlag / setFlags optional |
Set story flags. | Accepts string or array. |
clearFlag / clearFlags optional |
Clear story flags. | Accepts string or array. |
queueSceneId optional |
Enqueue an urgent scene. | Queue is resolved before normal scene matching. |
queuePriority optional |
Priority for queueSceneId. |
Defaults to 999. |
nextSceneId optional |
Hard-jump to a specific scene. | Overrides normal matching (SceneManager forces that scene id). If the id is unknown, SceneManager routes to its fallback scene. |
setPlaceKey not supported |
Legacy place movement key. |
If present on a choice, SceneManager throws an error.
Use setPlaceId, exitToOutside, or moveToHome.
|
Auto navigation (optional)
SceneManager can auto-inject navigation choices into the currently active scene, but only when that scene opts in.
Definitions
- Outside:
game.currentPlaceId == nullandgame.currentPlaceKey == null. - Inside a place: either
currentPlaceIdorcurrentPlaceKeyis set.
Traversal choices (when outside)
If the active scene enables autoChoices.traversal, and the player is outside,
SceneManager adds choices for:
-
Connected locations: based on
game.location.neighbors(choice id:travel.location.<neighborId>, text key:choice.travel.toLocation, minutes from edge). -
Places in the current location: based on
game.location.places(choice id:travel.place.<placeId>, text key:choice.travel.toPlace, minutes fromplace.props.minutesFromStreet/place.props.minutes/place.props.travelMinutesor default2).
Injected choices include vars: { destName }.
If your scene already defines a choice with the same id, SceneManager does not add a duplicate.
Exit choice (when inside a place)
If the active scene enables autoChoices.exit, and the player is inside a place,
SceneManager injects an Exit choice unless you already defined one.
- Injected id:
place.exit - Injected text key:
choice.place.exit -
Injected minutes:
place.props.minutesFromStreet/place.props.minutes/place.props.travelMinutesor default2 - Injected action:
exitToOutside: true
autoChoices (boolean or { traversal/exit }), or menu/isMenu
(both), with explicit overrides via autoTraversal / autoExit.
Conditions
Conditions decide whether a scene/text/choice is active.
Wherever a condition block is supported, you can use any of:
when, if, conditions, or condition.
Boolean combinators (all supported keys)
// Explicit AND / OR / NOT
when: {
and: [ { ... }, { ... } ], // alias: all
or: [ { ... }, { ... } ], // alias: any
not: { ... },
}
// Arrays are treated as implicit AND:
when: [
{ outside: true },
{ hour: { between: [18, 23] } },
]
Condition keys (matchers)
| Key | Meaning | Example |
|---|---|---|
locationIds / locationId |
Match if current location id is one of these. | { locationIds: ["downtown"] } |
locationTags / locationTag |
Match if current location has any of these tags. | { locationTags: ["parkland", "urban_core"] } |
placeKeys / placeKey |
Match if game.currentPlaceKey is one of these. |
{ placeKeys: ["player_home"] } |
outside |
Match only when the player is outside. | { outside: true } |
inPlace / insidePlace |
Match only when the player is inside any place. | { inPlace: true } |
weatherKinds / notWeatherKinds |
Match against world weather kind. | { weatherKinds: ["rain", "storm"] } |
seasons / notSeasons |
Match against world season. | { seasons: ["winter"] } |
dayKinds / notDayKinds |
Match day kind (from calendar): "workday" or "day off". |
{ dayKinds: ["workday"] } |
daysOfWeek / notDaysOfWeek |
Match day-of-week keys: sun, mon, tue,
wed, thu, fri, sat.
|
{ daysOfWeek: ["sat", "sun"] } |
date / notDate |
Match an exact date spec. Each spec may include any of:
{ year?, month?, day? }. Omitted fields are wildcards.
Accepts a single spec or an array of specs.
|
{ date: { month: 12, day: 31 } } |
dateRange / notDateRange |
Match an inclusive date range:
{ from: {..}, to: {..} }. Bounds may be partial.
Accepts a single range or an array.
|
{ dateRange: { from: { month: 12, day: 20 }, to: { month: 12, day: 31 } } }
|
holidays / notHolidays |
Match if today includes any holiday/special name. Matching is case-insensitive. | { holidays: ["New Year's Day"] } |
npcsPresent / npcPresent |
Require all listed NPC ids to be at the current location. | { npcsPresent: ["taylor"] } |
hour / hours / hourOfDay |
Hour gate (UTC, 0–23). Supports several forms (see below). | { hour: { between: [22, 5] } } |
playerFlags / playerFlag |
Require all flags to be set. A flag is considered set if either:
|
{ playerFlags: ["injured"] } |
notPlayerFlags / notPlayerFlag |
Require all flags to be unset. | { notPlayerFlags: ["injured"] } |
playerStats / stats |
Gate on player stats by name. The value uses player’s computed stat by default. Each stat entry is a numeric gate. | { playerStats: { strength: { gte: 5 } } } |
playerStat |
Single-stat shorthand:
{ name|stat: "...", base?: true, ...gate }.
|
{ playerStat: { name: "strength", gte: 5 } } |
playerSkills / skills |
Gate on numeric skill values (often 0..1 meters). Missing skills do not satisfy any gate. | { playerSkills: { athletics: { gte: 0.6 } } } |
Hour gates (UTC)
The hour condition supports several forms.
Between ranges are start inclusive and end exclusive.
If between: [x, x] the implementation treats it as “all day”.
import { TIME_OF_DAY } from "../util/common.js";
// exact hour
when: { hour: 18 }
// any of these hours
when: { hour: [18, 19, 20] }
// comparator gate (ANDed)
when: { hour: { gte: 18, lt: 23 } }
// between gate (wraps midnight when start > end)
when: { hour: { between: [22, 5] } }
// explicit eq / ne
when: { hour: { ne: 3 } }
// convenience buckets
when: { hour: TIME_OF_DAY.morning }
Numeric gates (stats & skills)
Numeric gates are used by playerStats, playerStat, and
playerSkills.
| Form | Meaning | Example |
|---|---|---|
5 |
Exact match. | { playerStats: { strength: 5 } } |
[3,4,5] |
Any match. | { playerStats: { strength: [3,4,5] } } |
{ gte/gt/lte/lt } |
Comparator(s), AND-ed. | { playerStats: { strength: { gte: 5 } } } |
{ between: [lo, hi] } |
Inclusive range. | { playerStats: { luck: { between: [2, 6] } } } |
{ eq/ne } |
Explicit equality/inequality. | { playerSkills: { stealth: { ne: 0 } } } |
Base stat vs computed stat
// Default: computed/current value
when: {
playerStats: {
strength: { gte: 6 },
}
}
// Use base stat (if the player model supports it)
when: {
playerStats: {
strength: { gte: 6, base: true },
}
}
// Same idea with playerStat shorthand
when: {
playerStat: { name: "strength", base: true, gte: 6 }
}
Localization vars
When text/choices are translated, the localizer receives a vars object.
You can interpolate values in strings using braces:
"It is {time.hhmm}", "You are on {street.name}", etc.
Available vars (scene text)
vars = {
time: {
hour, // 0..23 (UTC)
minute, // 0..59 (UTC)
hhmm, // formatted time, e.g. "18:45"
},
location: {
id, // current location id
name,
tags, // unique tag list
},
place: {
id, // current place id (or null)
key, // current place key (or null)
name, // place.name if inside a real place; otherwise a derived street/virtual name
},
street: {
name, // derived from any neighbor edge streetName (fallback "Street")
},
player: {
stats: { /* precomputed numeric stats */ }
},
npcsHere: [
{ id, name },
...
],
}
Choice label vars
Choice labels receive all scene vars, plus:
minutes (the choice minutes) and anything from choice.vars.
Example i18n string
// i18n dictionary:
"scene.street.default.text": "You are outside on {street.name}. It's {time.hhmm}."
// might render:
You are outside on Maple Street. It's 18:45.
Gotchas
Condition blocks are shared everywhere
Scene selection, conditional text blocks, and conditional choices all use the same condition engine. If it works in one place, it works in all three.
showAnyway renders disabled choices (and clicks do nothing)
If a choice fails its conditions and showAnyway: true, the choice is shown but disabled.
Clicking it does not execute actions.
setPlaceKey is not supported on choices
setPlaceKey on a choice object, SceneManager throws.
Use setPlaceId, exitToOutside, or moveToHome.
Newlines and paragraph spacing
SceneManager does not reliably insert automatic newlines between blocks.
If you need spacing, embed \n or \n\n inside your localized strings.
When nothing matches
If no scene matches, SceneManager falls back to either a configured fallback scene
(constructor fallbackSceneId) or the first registered scene, so the game never
hard-crashes.
If you see an unexpected fallback, your conditions are probably too strict.