NPC Schedule Rules — Scheduler Reference

This page documents how to write scheduleTemplate rules for NPCs in src/data/npc/npcs.js, as interpreted by the scheduler in src/classes/game/util/npcai.js. It focuses on options the scheduler currently recognizes and uses (unknown fields are ignored).

back

Quick mental model

What you write

  • An NPC has a scheduleTemplate.
  • The template contains rules (what the NPC wants to do) and optional movement settings.
  • Rules generate "visit intents" for a week.
  • The scheduler turns intents into a continuous timeline with travel time between locations.

What the scheduler guarantees

  • Schedule is generated as a week: Monday 00:00 → next Monday 00:00 (UTC-based).
  • Slots never overlap; travel is explicit (walk/bus/car).
  • Rules with higher built-in priority win conflicts ( fixed beats random, etc.).
Times are UTC. Rule windows use Date.getUTCHours()/getUTCMinutes() internally. Use 24h strings like "06:00", "22:30", and "24:00".

Top-level structure

{
  // NPC-level schedule config:
  scheduleTemplate: {
    // Optional: season whitelist. If set, NPC has NO schedule when current season is not included.
    season: [Season.SUMMER, Season.AUTUMN],

    // Optional: walking speed modifier. 1 = normal, 1.5 = slower, 0.8 = faster.
    travelModifier: 1,

    // Optional: transport preferences (bus/car).
    transport: {
      bus: { use: true, window: { from: "06:00", to: "22:00" }, fromDuration: 0 },
      car: { use: false, window: { from: "00:00", to: "24:00" }, fromDuration: 10 },
    },

    // Required: the schedule rules.
    rules: [ /* ...rules... */ ],
  }
}
Field Required? Meaning
scheduleTemplate.rules required Array of rule objects. If missing/empty, scheduler produces no schedule.
scheduleTemplate.season optional Season whitelist. If provided and the current season is not included, the scheduler returns an empty schedule.
scheduleTemplate.travelModifier optional Walking speed multiplier (only affects walking segments). Must be a positive number. Default: 1.
scheduleTemplate.transport optional Controls whether bus/car are considered, and when.

Transport config fields

Field Required? Meaning
transport.bus.use, transport.car.use optional Enable/disable that transport mode. Default: false when omitted.
transport.*.window optional Daily UTC time window where this transport mode is allowed. Missing window means "allowed at all times". Supports overnight windows (e.g. 22:00 → 05:00).
transport.*.fromDuration optional Minimum trip length (in minutes) before this transport mode is allowed. If missing or ≤ 0, the mode is allowed regardless of trip length. It compares against the computed walking trip time (including micro-lingers).
The scheduler decides between walk/bus/car dynamically based on environment (night/cold/bad weather), route complexity, and relative speed. Even if use: true, the mode might not be chosen.

Rule types

rule.type must be one of the SCHEDULE_RULES values. Currently handled by the scheduler: home, fixed, random, daily, weekly. follow (and any unknown types) are ignored.

Type What it does Typical use
SCHEDULE_RULES.home Creates hard "be at home" intents for specific timeBlocks on every day. Sleep, long at-home blocks.
SCHEDULE_RULES.fixed Creates a hard intent occupying the entire window. School, work shifts, standing appointments.
SCHEDULE_RULES.random Creates multiple "soft" intents within a window, each picking a target from your targets list. Actual number is estimated from window length and average stay. Errands, wandering, "fill time with activities".
SCHEDULE_RULES.daily Creates at most one "soft" intent per eligible day within the window. "Once per day" routine (gym, café, walk).
SCHEDULE_RULES.weekly Picks one eligible day per week, then creates a single intent within the window. Weekly events (bar night, therapy, market day).
"Hard" vs "soft": home and fixed are treated as hard anchors when resolving conflicts. Soft intents (random/daily/weekly) will be shortened/shifted around to avoid missing hard anchors.

Rule object: fields

Every rule is a plain object. Unknown fields are ignored. Below are the fields the scheduler actually reads.

Field Required? Applies to Meaning
id optional All String ID used for debugging and for linking schedule slots back to the rule. Recommended to keep stable.
type required All One of SCHEDULE_RULES.*.
dayKinds optional All except home Limits rule to specific day kinds (from the calendar): DayKind.WORKDAY, DayKind.DAY_OFF. Can be a single value or an array.
daysOfWeek optional All except home Limits rule to specific weekdays. Use DAY_KEYS values: "sun", "mon", "tue", "wed", "thu", "fri", "sat". Can be a single value or an array.
candidateDays optional alias All except home Alias for daysOfWeek. If both are present, daysOfWeek wins.
probability optional All Chance the rule runs.
  • undefined/null → always
  • ≤ 0 → never
  • ≥ 1 → always
  • Between 0–1 → RNG check
For weekly, this check happens once per week. For other types, once per eligible day.
timeBlocks required (for home) home Array of {from,to} blocks in a day where NPC must be home. cannot cross midnight (if to ≤ from the block is ignored). Split overnight sleep into two blocks instead.
window required (for non-home rule types) fixed, random, daily, weekly A daily time window {from:"HH:MM", to:"HH:MM"}. Overnight windows are allowed (e.g. 22:00 → 03:00).
stayMinutes optional random, daily, weekly (also read by some helpers) Controls how long the NPC stays per visit.
stayMinutes: { min: 20, max: 120, round: 10 }
Defaults if missing: min = 30, max = min. round rounds stay to nearest multiple (minutes), then clamps to fit the window and hard anchors.
targets required (for non-home rule types) fixed, random, daily, weekly Array of target entries (see next section). Must be an array — a single object will be treated as "no targets".
nearest optional fixed, random, daily, weekly Rule-level nearest: if true, the scheduler picks the nearest candidate across all targets combined (instead of randomizing per-target group).
respectOpeningHours optional Any rule using place targets If true, candidate places must be open at the start of the window and still open at (window end − 1 minute). This is a coarse filter: it does not split windows around opening hours.
respectAgeRestriction optional Any rule using place targets Default: true. When true (or absent), places with place.props.ages gates are filtered out if the NPC's age is below ages.min or above ages.max. When set to false, the NPC can target age-gated places anyway.
disallowedTargets optional Any rule using targets Optional "forbidden pool" filter applied when resolving targets. Same shape as target entries, but only supports home, placeKeys, and placeCategory. Any other types are logged to the console and ignored.
If your window is shorter than stayMinutes.min, the rule produces no intents. For fixed, stayMinutes is ignored (it always occupies the full window).

Targets

A rule’s targets is an array of "target entries". Each entry expands into a pool of concrete places across the world. The scheduler then picks from that pool (randomly or by nearest, depending on settings).

Supported target types

Target type Required fields Meaning
TARGET_TYPE.home none Targets the NPC’s home location.
TARGET_TYPE.placeKeys candidates (string or array) Selects any place whose place.key matches one of the candidates.
TARGET_TYPE.placeCategory candidates (tag or array) Selects any place whose place.props.category contains one of the candidates.
The scheduler currently does not resolve TARGET_TYPE.npc, TARGET_TYPE.player, or TARGET_TYPE.unavailable into real locations. If used in targets, they produce no candidates.

Target entry fields

Field Required? Meaning
type required One of the supported target types above.
candidates required (for place targets) For placeKeys / placeCategory: a string/tag or array. Missing/empty means that target entry contributes no candidates.
nearest optional If true, the scheduler chooses the nearest place among the candidates for this entry (based on current location at scheduling time). If omitted/false, a random candidate is chosen from this entry’s pool.
stay optional If true, the visit will try to consume the maximum possible stay time for the intent (instead of picking a random duration between min and max). Works best when stayMinutes.max is large enough to fill the window.

Legacy fields (supported but prefer candidates)

Target entries also accept legacy fields for backwards compatibility:
  • key / keys (as a string or array) for placeKeys
  • categories (tag or array) for placeCategory
If candidates is present, it is used in preference to legacy fields.

disallowedTargets (forbidden pool) details

disallowedTargets is optional and is applied after targets are expanded into concrete candidates, but before the scheduler chooses which place to go to.

disallowedTargets: [
  { type: TARGET_TYPE.placeKeys, candidates: ["mall"] },
  { type: TARGET_TYPE.placeCategory, candidates: [PLACE_TAGS.nightlife, PLACE_TAGS.luxury] },
  { type: TARGET_TYPE.home } // prevents choosing home when home is included as a target
]
Only home, placeKeys, placeCategory are valid in disallowedTargets. Any other type is ignored and will print a console warning.

Examples

1) Sleep at home (home rule)

{
  id: "sleep_at_home",
  type: SCHEDULE_RULES.home,
  // Required for home: timeBlocks (must not cross midnight)
  timeBlocks: [
    { from: "00:00", to: "06:00" },
    { from: "22:00", to: "24:00" },
  ],
}

2) Fixed appointment (fixed rule)

{
  id: "attend_school",
  type: SCHEDULE_RULES.fixed,
  dayKinds: [DayKind.WORKDAY],
  daysOfWeek: ["mon","tue","wed","thu","fri"],
  window: { from: "09:00", to: "15:00" },
  targets: [
    { type: TARGET_TYPE.placeKeys, candidates: ["high_school"], nearest: true },
  ],
  // Optional filters:
  respectOpeningHours: true,
  respectAgeRestriction: true,
}

3) Random errands with forbidden pool

{
  id: "before_school_morning",
  type: SCHEDULE_RULES.random,
  dayKinds: [DayKind.WORKDAY],
  window: { from: "06:00", to: "09:00" },
  stayMinutes: { min: 20, max: 120, round: 10 },
  targets: [
    { type: TARGET_TYPE.placeKeys, candidates: ["library"] },
    { type: TARGET_TYPE.placeKeys, candidates: ["high_school"], nearest: true, stay: true },
    { type: TARGET_TYPE.placeCategory, candidates: [PLACE_TAGS.leisure, PLACE_TAGS.food] },
    { type: TARGET_TYPE.home },
  ],
  disallowedTargets: [
    { type: TARGET_TYPE.placeKeys, candidates: ["mall"] },
    { type: TARGET_TYPE.placeCategory, candidates: [PLACE_TAGS.nightlife, PLACE_TAGS.luxury] },
  ],
  respectOpeningHours: true,
}

4) Age-gated targets: override restriction per rule

{
  id: "juvenile_break_in",
  type: SCHEDULE_RULES.daily,
  window: { from: "18:00", to: "20:00" },
  stayMinutes: { min: 20, max: 60 },
  targets: [
    { type: TARGET_TYPE.placeKeys, candidates: ["jail"] }
  ],
  // Default is true; set to false to ignore place.props.ages.min/max
  respectAgeRestriction: false,
}

Common pitfalls