Skip to content

Behavior runner (sim/core)

Overview

Periodic actions for sim entities (e.g. colonists) are defined as behaviors in sim/core and ticked by a generic BehaviorRunner. The runner is entity-agnostic; the client provides a context implementation that turns behavior requests into jobs or other side effects.

Paths:

  • sim/core/behaviors/BehaviorRunner, type Behavior<TEntity, TContext>
  • sim/core/objects/colonist/ColonistBehaviors.ts – colonist behaviors and ColonistBehaviorContext interface
  • client: src/client/ui/utils/ColonistBehaviorContextImpl.ts – client implementation of the context

BehaviorRunner (generic)

  • Behavior<TEntity, TContext>: id, intervalMs, run(entity, ctx, deltaTimeMs).
  • intervalMs === 0 → run every tick; otherwise run when accumulated time ≥ intervalMs.
  • BehaviorRunner<TEntity, TContext> constructor: behaviors, context, getEntities(), getEntityId(entity).
  • tick(deltaTimeMs): for each entity, runs each behavior (with per-entity per-behavior accumulators for intervals).

The runner lives entirely in sim/core and has no dependency on jobs or the client. Context is injected so the same mechanism can be reused for other entity types (e.g. trees, creatures) with different context interfaces.

Colonist behaviors

ColonistBehaviorContext (interface in sim):

  • addWanderJob(colonistId) – request a one-tile wander move (client picks valid adjacent tile and adds MoveToJob).
  • requestNeedJob(colonistId, needType: 'drinking' | 'sleeping') – request a drink or sleep job.

Default colonist behaviors (createDefaultColonistBehaviors()):

Behavior intervalMs What it does
needCheck 0 Calls colonist.tickNeeds(deltaTime); if drinking/sleeping crossed ≤10%, calls ctx.requestNeedJob(colonistId, 'drinking'|'sleeping').
wander 1000 If colonist.isFree(), calls ctx.addWanderJob(colonist.getUUID()).

So need decay and “want to drink/sleep” are defined at colonist level; job creation is delegated to the context.

Client context implementation

ColonistBehaviorContextImpl (createColonistBehaviorContext(deps)):

  • Deps: objectManager, jobSystem, needJobScheduler, canMoveTo, mapWidth, mapHeight.
  • addWanderJob: if colonist free, pick random adjacent tile with canMoveTo, add MoveToJob(newX, newY, colonistId) via jobSystem.
  • requestNeedJob: call needJobScheduler.createJobForColonist(colonistId, needType).

When DEBUG_HAUL_ONLY is true, both methods no-op (wander and auto drink/sleep disabled).

Wiring in the game loop

IsometricMap builds the colonist runner once:

  • new BehaviorRunner(createDefaultColonistBehaviors(), createColonistBehaviorContext(deps), getColonists, c => c.getUUID()).
  • Each frame: colonistBehaviorRunner.tick(deltaTime) (before the job system tick).

So all colonist periodic logic (needs + wander) is centralized in the behavior runner; the map only ticks the runner and the job system.

Extending to other entities

To add behaviors for another entity type (e.g. trees, animals):

  1. In sim/core: define a context interface and createDefaultXxxBehaviors() returning Behavior<ThatEntity, ThatContext>[].
  2. In client: implement the context (e.g. create jobs, update state) in a separate module.
  3. In the game loop: create another BehaviorRunner(behaviors, contextImpl, getEntities, getEntityId) and call its tick(deltaTime).