Talos docs

EntityHandle

Since 0.6Changed in 0.7

The entity a behavior is attached to, or any entity returned by a query.

Plain text

Cheap; safe to keep in state.

Reached in a behaviour as ctx.entity.

Signature

export interface EntityHandle {
  /** Scene id, unique within the scene. */
  readonly id: string
  /** Display name from the scene. */
  readonly name: string
  readonly tags: readonly string[]
  /**
   * False once the entity is despawned; every other member then throws, with a
   * `DespawnedEntityError`.
   *
   * Worth checking on any handle kept in `state` across ticks, because {@link despawn} and
   * {@link WorldQuery.spawn} make stale handles reachable in ordinary play.
   */
  readonly alive: boolean
  /** Local position (relative to `parent`). Assignable. */
  position: Vec3
  /** Position after applying the parent chain. */
  readonly worldPosition: Vec3
  /** Local rotation as a quaternion. Assignable. */
  rotation: Quat
  /** Local scale. */
  scale: Vec3
  /** Adds `delta` to the local position. */
  translate(delta: Vec3): void
  /**
   * Moves at most `maxStep` metres toward `target` (all three axes), measured in the world and written
   * back as the local position under the parent. Returns true once there.
   */
  moveToward(target: Target, maxStep: number): boolean
  /** Straight-line distance from this entity's world position to `target`. */
  distanceTo(target: Target): number
  /**
   * Puts the entity somewhere else outright, and makes it stay there.
   *
   * Assigning {@link position} moves the transform and the physics body under it, which is all a
   * crate needs. It is not enough for a character: the kinematic controller keeps its own foot
   * position and writes it back into the entity at the top of every tick, so `entity.position =
   * spawn` is undone before the next frame. A teleport is also a *jump* to the renderer: the frame
   * after it draws the entity where it landed, where a `position =` write is smoothed between
   * ticks — so a prop that wraps, respawns or is recycled from a pool teleports. This is the three writes that make a real move — the
   * transform, the body, and the controller with its velocity cleared — and it is what a respawn,
   * a checkpoint or a teleporter should call.
   *
   * `yaw` is radians about Y, the same angle {@link Controller} steers by, with forward at -Z;
   * omitted, the facing is left alone, and so is the heading of the camera filming the entity. The position is this entity's own, local to its `parent`
   * if it has one, exactly as {@link position} is.
   *
   * Returns false, having changed nothing, when the destination is refused — a coordinate that
   * is not finite, or a point outside the scene's `meta.bounds`. A handle whose entity has been
   * despawned throws, like every other member; see {@link alive}.
   */
  teleport(position: Vec3, yaw?: number): boolean
  /**
   * Rotates so the entity's forward (-Z) faces `target`, keeping Y up. `{ flat: true }` turns about Y
   * only — a character or a turret base facing something on the ground plane. Aimed in world space,
   * from the entity's world position to the target's (a handle's `worldPosition`), so a child of a moved,
   * turned or scaled parent faces the target too. (the usual look-at meaning; three.js's
   * `Object3D.lookAt` aims an object's +Z instead.)
   */
  lookAt(target: Target, opts?: { flat?: boolean }): void
  /**
   * Heading in radians about +Y: where the local forward (-Z) points on the ground plane. 0 faces -Z and
   * **positive turns left**, as three.js's `rotation.y` (a degrees-based angle that turns right is common in other engines). Setting it writes a rotation about Y alone, clearing pitch and roll; a NaN throws
   * `entity.not-finite`. `entity.yaw += turnRate * dt` turns at a rate.
   */
  yaw: number
  /** Where the entity faces, a world unit vector: its -Z through the parent chain (the usual forward vector). */
  readonly forward: Vec3
  /** The entity's right (+X), a world unit vector. */
  readonly right: Vec3
  /** The entity's up (+Y), a world unit vector. */
  readonly up: Vec3
  /**
   * Whether this entity is drawn: its `mesh` and its `sprite`, whichever it has (a visibility switch, like a renderer's enabled flag). Drawing only: colliders, triggers and behaviours carry on. It cascades down the
   * hierarchy: an entity draws only if it and every ancestor are visible, so hiding a parent hides its
   * children and grandchildren. Each entity keeps its own flag (a parent's `false` never rewrites a
   * child's), so showing the parent again brings back exactly the children that were visible; reading
   * `visible` reports the entity's own flag, not the inherited result. The same writes as `set("mesh", { visible })` and `ctx.sprite.visible`. An
   * entity with neither (an empty parent, a group) keeps the flag and hides or shows its subtree; on one with
   * no children either the call does nothing and warns `script-call-inapplicable` once.
   */
  visible: boolean
  /** The entity this one is under, or null at the top of the scene. Changed with {@link setParent}. */
  readonly parent: EntityHandle | null
  /**
   * Puts this entity under `parent` (a handle or a scene id), or at the top of the scene with `null`. Its children come with it.
   *
   * `keepWorld` (default true) keeps where it is in the world and rewrites
   * its local `position`, `rotation` and `scale` to say the same thing under the new parent; `false` keeps
   * the local numbers, so it moves into the new parent's frame.
   *
   * Refused, changing nothing: a parent that is this entity or under it (`RangeError`,
   * `entity.parent-cycle`), a despawned handle (`entity.parent-despawned`) and an id no live entity has
   * (`entity.parent-unknown`). A snapshot records the new parent and a restore puts it back. Since a
   * despawn takes an entity's children with it, move a child out first to keep it.
   */
  setParent(parent: EntityHandle | string | null, opts?: { keepWorld?: boolean }): void
  readonly children: EntityHandle[]
  /**
   * This entity's physics body, the handle its own behaviour has as `ctx.body` but for `wheels`: forces,
   * impulses and velocity on someone else's body. Null while it has none (a static prop, a character). Read live.
   *
   * ```ts
   * // A launch pad: whatever dynamic body drives over it is thrown up and forward.
   * onTriggerEnter(ctx, other) {
   *   other.body?.applyImpulse([0, 900, -600])
   * }
   * ```
   *
   * **Who applies the push.** The pad's own behaviour hook (`onTriggerEnter`, `update`); a screen's `onClick`
   * only emits an event. Co-op today is local — every player is in one world on one machine — so the hook
   * fires once and pushes once; there is no networked co-op yet. A push applied again where it is reported
   * (an event the kart's own script also answers) lands twice. Replays and snapshots replay it exactly on
   * the same engine build (docs/API.md, "Who applies the push").
   */
  readonly body: RigidBodyView | null
  /**
   * This entity's vehicle while it is running, null otherwise: what its `ctx.vehicle` reads, and
   * {@link Vehicle.boost}. Not `drive`, `reset` or `wheels` — those stay with the vehicle's own behaviour.
   * The same rule as {@link EntityHandle.body} says who applies a boost.
   */
  readonly vehicle: VehicleView | null
  hasTag(tag: string): boolean
  /** Whether the scene gives this entity a component of that type (`"mesh"`, `"script"`, `"rigidbody"`, …). */
  hasComponent(type: string): boolean
  /**
   * The component's properties **as the scene authored them**, or null when the entity has no
   * component of that type.
   *
   * What {@link set} has written over them since is a separate question with a separate answer:
   * {@link overrides}. This one never changes at run time.
   */
  component(type: string): Record<string, any> | null
  /**
   * What {@link set} has written over this entity's component, or null when nothing has.
   *
   * Separate from {@link component} on purpose. `component` is the scene as authored and is
   * contract-typed: every field in it is a field the schema has. Two of the three writable
   * component types have fields the schema does *not* have (`light.on`, `mesh.visible`), and
   * `material` is not a scene component at all. Folding them together would make
   * `component("mesh")` report something no scene could ever author.
   *
   * Only what was written is in the object — a light switched off and never recoloured answers
   * `{ on: false }`, not a whole light with defaults filled in. `GET /entity/{id}` reports the
   * same thing in an `overrides` map beside `components`, and `GET /snapshot` carries it, so a
   * restore puts back the light bar as it was.
   *
   * ```ts
   * bar.set("light", { on: true })
   * bar.overrides("light")          // { on: true }
   * bar.component("light").intensity // still the scene's number
   * ```
   */
  overrides<T extends keyof WritableComponents>(component: T): Partial<WritableComponents[T]> | null
  /**
   * Change a component's live values. The renderer shows it on the same tick.
   *
   * Three component types can be written, and between them they are what a scene needs in order
   * to *react* rather than merely exist:
   *
   * - `"material"` — `baseColor`, `emissive`: brake lamps, a cone that turns red when it is hit.
   * - `"light"` — `intensity`, `color`, `on`: a light bar, a torch, a warning beacon.
   * - `"mesh"` — `visible`: hiding a prop without despawning it.
   *
   * A patch names only what it changes; a field it leaves out keeps what it had, which is the
   * scene's value until something writes it. Colours are `"#rrggbb"` — the spelling the scene
   * itself uses — or `[r, g, b]` / `[r, g, b, a]` in 0..1 for a colour the behavior computed.
   *
   * **An unknown component type or field throws**, as does a field of the wrong type and a
   * component this entity does not have. The error's `name` is `ComponentWriteError` and its
   * message starts with the code: `component-not-writable`, `component-field-unknown`,
   * `component-field-type`, `component-absent`. Accepting the call and doing nothing would be
   * the silent failure this method exists to replace, and a misspelled field name is the
   * likeliest mistake there is here.
   *
   * Read it back with {@link overrides}, not with {@link component}: the scene's own values and
   * what a behavior wrote over them are kept apart, and only the second one changes.
   *
   * `"material"` is not a scene component: the contract's `material` is an asset kind and
   * `mesh.material` names a file. This writes a runtime tint over whatever material the mesh
   * resolved to, so it needs a `mesh` component and changes no asset. `emissive` is a gain, not
   * a light — this renderer has no emissive channel, so it brightens the surface and illuminates
   * nothing else. Use a `light` for that.
   *
   * ```ts
   * bar.set("light", { on: true, color: "#2255ff", intensity: 8 })
   * cone.set("material", { baseColor: [1, 0.2, 0.1, 1] })
   * prop.set("mesh", { visible: false })
   * ```
   */
  set<T extends keyof WritableComponents>(component: T, patch: WritableComponents[T]): void
  /**
   * Remove this entity, and every entity under it, from the world. The inverse of
   * {@link WorldQuery.spawn}: despawning a prefab copy's root removes the whole copy.
   *
   * Each descendant goes exactly as if it had been despawned itself (its `dispose`, body, lights,
   * audio, sprite and agent), depth first: descendants before their parent, siblings in id order,
   * this entity last. To keep a child, call `child.setParent(null)` (or a new parent) first.
   *
   * Not only for entities that were spawned — an entity the scene authored can be despawned too.
   * Its physics body, draw binding, behavior, controller, animator, emitter and held input all go
   * with it, and its slot is freed for the next spawn to reuse.
   *
   * After this, **every member of every handle to it throws** except {@link alive}, which answers
   * false. Returns false, having done nothing, if the entity was already gone, so despawning
   * twice is a no-op rather than an error.
   *
   * It is not a change to the scene: {@link SessionHandle.restart} rebuilds from the scene as
   * loaded, so an authored entity despawned here comes back and a spawned one does not.
   */
  despawn(): boolean
}

Import: import type { EntityHandle } from "@argus/api"

Members

MemberSignatureDescription
idreadonly id: stringScene id, unique within the scene.
namereadonly name: stringDisplay name from the scene.
tagsreadonly tags: readonly string[]
alivereadonly alive: booleanFalse once the entity is despawned; every other member then throws, with a DespawnedEntityError. Worth checking on any handle kept in state across ticks, because {@link despawn} and {@link WorldQuery.spawn} make stale handles reachable in ordinary play.
positionposition: Vec3Local position (relative to parent). Assignable.
worldPositionreadonly worldPosition: Vec3Position after applying the parent chain.
rotationrotation: QuatLocal rotation as a quaternion. Assignable.
scalescale: Vec3Local scale.
translatetranslate(delta: Vec3): voidAdds delta to the local position.
moveTowardmoveToward(target: Target, maxStep: number): booleanMoves at most maxStep metres toward target (all three axes), measured in the world and written back as the local position under the parent. Returns true once there.
distanceTodistanceTo(target: Target): numberStraight-line distance from this entity's world position to target.
teleportteleport(position: Vec3, yaw?: number): booleanPuts the entity somewhere else outright, and makes it stay there. Assigning {@link position} moves the transform and the physics body under it, which is all a crate needs. It is not enough for a character: the kinematic controller keeps its own foot position and writes it back into the entity at the top of every tick, so entity.position = spawn is undone before the next frame. A teleport is also a jump to the renderer: the frame after it draws the entity where it landed, where a position = write is smoothed between ticks — so a prop that wraps, respawns or is recycled from a pool teleports. This is the three writes that make a real move — the transform, the body, and the controller with its velocity cleared — and it is what a respawn, a checkpoint or a teleporter should call. yaw is radians about Y, the same angle {@link Controller} steers by, with forward at -Z; omitted, the facing is left alone, and so is the heading of the camera filming the entity. The position is this entity's own, local to its parent if it has one, exactly as {@link position} is. Returns false, having changed nothing, when the destination is refused — a coordinate that is not finite, or a point outside the scene's meta.bounds. A handle whose entity has been despawned throws, like every other member; see {@link alive}.
lookAtlookAt(target: Target, opts?: { flat?: boolean }): voidRotates so the entity's forward (-Z) faces target, keeping Y up. { flat: true } turns about Y only — a character or a turret base facing something on the ground plane. Aimed in world space, from the entity's world position to the target's (a handle's worldPosition), so a child of a moved, turned or scaled parent faces the target too. (the usual look-at meaning; three.js's Object3D.lookAt aims an object's +Z instead.)
yawyaw: numberHeading in radians about +Y: where the local forward (-Z) points on the ground plane. 0 faces -Z and positive turns left, as three.js's rotation.y (a degrees-based angle that turns right is common in other engines). Setting it writes a rotation about Y alone, clearing pitch and roll; a NaN throws entity.not-finite. entity.yaw += turnRate * dt turns at a rate.
forwardreadonly forward: Vec3Where the entity faces, a world unit vector: its -Z through the parent chain (the usual forward vector).
rightreadonly right: Vec3The entity's right (+X), a world unit vector.
upreadonly up: Vec3The entity's up (+Y), a world unit vector.
visiblevisible: booleanWhether this entity is drawn: its mesh and its sprite, whichever it has (a visibility switch, like a renderer's enabled flag). Drawing only: colliders, triggers and behaviours carry on. It cascades down the hierarchy: an entity draws only if it and every ancestor are visible, so hiding a parent hides its children and grandchildren. Each entity keeps its own flag (a parent's false never rewrites a child's), so showing the parent again brings back exactly the children that were visible; reading visible reports the entity's own flag, not the inherited result. The same writes as set("mesh", { visible }) and ctx.sprite.visible. An entity with neither (an empty parent, a group) keeps the flag and hides or shows its subtree; on one with no children either the call does nothing and warns script-call-inapplicable once.
parentreadonly parent: EntityHandle | nullThe entity this one is under, or null at the top of the scene. Changed with {@link setParent}.
setParentsetParent(parent: EntityHandle | string | null, opts?: { keepWorld?: boolean }): voidPuts this entity under parent (a handle or a scene id), or at the top of the scene with null. Its children come with it. keepWorld (default true) keeps where it is in the world and rewrites its local position, rotation and scale to say the same thing under the new parent; false keeps the local numbers, so it moves into the new parent's frame. Refused, changing nothing: a parent that is this entity or under it (RangeError, entity.parent-cycle), a despawned handle (entity.parent-despawned) and an id no live entity has (entity.parent-unknown). A snapshot records the new parent and a restore puts it back. Since a despawn takes an entity's children with it, move a child out first to keep it.
childrenreadonly children: EntityHandle[]
bodyreadonly body: RigidBodyView | nullThis entity's physics body, the handle its own behaviour has as ctx.body but for wheels: forces, impulses and velocity on someone else's body. Null while it has none (a static prop, a character). Read live. ts // A launch pad: whatever dynamic body drives over it is thrown up and forward. onTriggerEnter(ctx, other) { other.body?.applyImpulse([0, 900, -600]) } Who applies the push. The pad's own behaviour hook (onTriggerEnter, update); a screen's onClick only emits an event. Co-op today is local — every player is in one world on one machine — so the hook fires once and pushes once; there is no networked co-op yet. A push applied again where it is reported (an event the kart's own script also answers) lands twice. Replays and snapshots replay it exactly on the same engine build (docs/API.md, "Who applies the push").
vehiclereadonly vehicle: VehicleView | nullThis entity's vehicle while it is running, null otherwise: what its ctx.vehicle reads, and {@link Vehicle.boost}. Not drive, reset or wheels — those stay with the vehicle's own behaviour. The same rule as {@link EntityHandle.body} says who applies a boost.
hasTaghasTag(tag: string): boolean
hasComponenthasComponent(type: string): booleanWhether the scene gives this entity a component of that type ("mesh", "script", "rigidbody", …).
componentcomponent(type: string): Record<string, any> | nullThe component's properties as the scene authored them, or null when the entity has no component of that type. What {@link set} has written over them since is a separate question with a separate answer: {@link overrides}. This one never changes at run time.
overridesoverrides<T extends keyof WritableComponents>(component: T): Partial<WritableComponents[T]> | nullWhat {@link set} has written over this entity's component, or null when nothing has. Separate from {@link component} on purpose. component is the scene as authored and is contract-typed: every field in it is a field the schema has. Two of the three writable component types have fields the schema does not have (light.on, mesh.visible), and material is not a scene component at all. Folding them together would make component("mesh") report something no scene could ever author. Only what was written is in the object — a light switched off and never recoloured answers { on: false }, not a whole light with defaults filled in. GET /entity/{id} reports the same thing in an overrides map beside components, and GET /snapshot carries it, so a restore puts back the light bar as it was. ts bar.set("light", { on: true }) bar.overrides("light") // { on: true } bar.component("light").intensity // still the scene's number
setset<T extends keyof WritableComponents>(component: T, patch: WritableComponents[T]): voidChange a component's live values. The renderer shows it on the same tick. Three component types can be written, and between them they are what a scene needs in order to react rather than merely exist: - "material" — baseColor, emissive: brake lamps, a cone that turns red when it is hit. - "light" — intensity, color, on: a light bar, a torch, a warning beacon. - "mesh" — visible: hiding a prop without despawning it. A patch names only what it changes; a field it leaves out keeps what it had, which is the scene's value until something writes it. Colours are "#rrggbb" — the spelling the scene itself uses — or [r, g, b] / [r, g, b, a] in 0..1 for a colour the behavior computed. An unknown component type or field throws, as does a field of the wrong type and a component this entity does not have. The error's name is ComponentWriteError and its message starts with the code: component-not-writable, component-field-unknown, component-field-type, component-absent. Accepting the call and doing nothing would be the silent failure this method exists to replace, and a misspelled field name is the likeliest mistake there is here. Read it back with {@link overrides}, not with {@link component}: the scene's own values and what a behavior wrote over them are kept apart, and only the second one changes. "material" is not a scene component: the contract's material is an asset kind and mesh.material names a file. This writes a runtime tint over whatever material the mesh resolved to, so it needs a mesh component and changes no asset. emissive is a gain, not a light — this renderer has no emissive channel, so it brightens the surface and illuminates nothing else. Use a light for that. ts bar.set("light", { on: true, color: "#2255ff", intensity: 8 }) cone.set("material", { baseColor: [1, 0.2, 0.1, 1] }) prop.set("mesh", { visible: false })
despawndespawn(): booleanRemove this entity, and every entity under it, from the world. The inverse of {@link WorldQuery.spawn}: despawning a prefab copy's root removes the whole copy. Each descendant goes exactly as if it had been despawned itself (its dispose, body, lights, audio, sprite and agent), depth first: descendants before their parent, siblings in id order, this entity last. To keep a child, call child.setParent(null) (or a new parent) first. Not only for entities that were spawned — an entity the scene authored can be despawned too. Its physics body, draw binding, behavior, controller, animator, emitter and held input all go with it, and its slot is freed for the next spawn to reuse. After this, every member of every handle to it throws except {@link alive}, which answers false. Returns false, having done nothing, if the entity was already gone, so despawning twice is a no-op rather than an error. It is not a change to the scene: {@link SessionHandle.restart} rebuilds from the scene as loaded, so an authored entity despawned here comes back and a spawned one does not.

Example

The tested fixture fixtures/app/agent.ts.

import type { Behavior } from "@argus/api"

// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
  init(ctx) {
    ctx.state.arrived = false
    ctx.nav?.goTo(ctx.props.target as [number, number, number])
  },
  update(ctx) {
    if (ctx.nav?.arrived) ctx.state.arrived = true
  },
} satisfies Behavior