Skip to content
quest'n'click

CreateHow-toFor authorsApplies from 4.3.0

Moving characters: patrols and walks

Cette page en françaisThis page on the GitHub wiki

Make a character walk on its own: a guard who patrols a room, a nurse who goes from room to room and catches the hero, Grandpa who goes home once the key is found. Everything here is the DSL as it is: a script, walk, moveActor, an event, and conditions.

What you get

A character that moves between two spots of a room, or between rooms, on a loop or when something happens; rules that know where it is; and a game the solver still proves, because the patrol is part of the state it explores.

Context and versions

  • A created project (game/) or the engine repository (games/<id>/); the same files and commands, see the layout page.
  • From 4.3.0 for this page. Scripts, events and moveActor exist since 1.3 ("World"); schema 3 (stable ids, stepIds) since 3.0.

Prerequisites

  • The character declared in game.ts with a walk pose in its sprites (without one, it slides in its current pose) (characters).
  • The rooms placed in the editor: the character's actor placed in each room it can be in, with a walk area (geometry).
  • Scripts and events, for the fields used below.

Where to run the commands

Write the content in the game's files (rooms/<room>.ts, game.ts, cast.ts). Check from the root of the project: npm run validate and npm run solve in the engine repository, npx quest-n-click validate and npx quest-n-click solve in a created project.

Steps: a guard who patrols a room

  1. A looping script in the room. The guard walks to the vault, waits, walks to the window, waits, forever: { walk: [x, y], who: 'guard' } and { wait } in a script with loop: true. A loop must contain a wait, waitUntil or waitEvent.
  1. Say where he is with a flag. Conditions cannot read a character's position inside a room; let the script set a flag at each stop ({ set: ['guard_at', 'vault'] }), and let the rules test it ({ flag: 'guard_at', eq: 'vault' }).
  1. Stop the patrol when it no longer matters: while: '!vault_open' rewinds the script and stops it once the vault is open (or { stopScript: 'guard_patrol' } from a rule).
  1. The rules. The special case first: opening the vault while he is there gets caught; otherwise it works.
{
  "room": {
    "id": "vault_hall",
    "name": "The vault hall",
    "decor": "decor/vault_hall",
    "actors": { "guard": { "char": "guard" } },
    "hotspots": { "vault": { "name": "vault door" } },
    "look": { "guard": "A guard. He walks. He turns. He walks.", "vault": "The vault door." },
    "on": [
      { "id": "hall.vault-watched", "verb": "open", "a": "vault", "if": { "flag": "guard_at", "eq": "vault" },
        "do": [{ "say": ["guard", "Hey! Step away from that door."] }] },
      { "id": "hall.vault-open", "verb": "open", "a": "vault", "if": "!vault_open",
        "do": ["Quick, while he is at the window.", { "set": "vault_open" }] }
    ],
    "scripts": [
      { "id": "guard_patrol", "loop": true, "while": "!vault_open",
        "stepIds": ["to-vault", "at-vault", "wait-vault", "to-window", "at-window", "wait-window"],
        "do": [
          { "walk": [520, 340], "who": "guard" },
          { "set": ["guard_at", "vault"] },
          { "wait": 4000 },
          { "walk": [140, 340], "who": "guard" },
          { "set": ["guard_at", "window"] },
          { "wait": 4000 }
        ] }
    ],
    "hints": [{ "id": "hall.hint", "until": "vault_open", "lines": ["Wait until the guard is at the window."] }]
  },
  "layout": {
    "walk": { "area": [[20, 300], [620, 300], [630, 395], [10, 395]] },
    "entries": { "default": [320, 360] },
    "hotspots": { "vault": { "rect": [480, 140, 90, 170], "approach": [470, 350] } },
    "actors": { "guard": { "x": 140, "y": 340 } }
  },
  "characters": {
    "guard": { "name": "Guard", "color": "#8fd3ff", "kind": ["person"], "sprites": { "idle": ["guard/idle"], "walk": ["guard/walk1", "guard/walk2"] } }
  }
}

The walk … who coordinates are the one place where a position is typed: a scripted move. Where the guard stops is kept in the save.

Steps: a patrol between rooms (the nurse)

  1. Give the character a starting room. A character declared as an actor in several rooms normally shows in all of them (Grandma in the kitchen and the garden). Give it a room, and only its actor in that room shows; moveActor sends it elsewhere:

    // cast.ts
    edna: { name: 'Edna', color: '#f0f', room: 'kitchen', sprites: { idle: ['edna/idle'], walk: […] } },
    // hall.ts and kitchen.ts both declare   actors: { edna: { char: 'edna' } }   and place her in their layout.
    // anywhere:  { moveActor: ['edna', 'hall'] }             at her layout position in the hall
    //            { moveActor: ['edna', 'hall'], at: 'door' } at that entry point (or a point) of the hall
    // conditions: { actorIn: ['edna', 'hall'] }

    The character is removed from the room it leaves and appears in the one it reaches, on screen if the player is there. The validator refuses moveActor toward a room where the character is not an actor, or for a character with no room.

  1. A game script moves her and says so. After each move it emits an event; a game listener catches the hero when they meet, and each room's onEnter catches him when he walks in on her:

    // game.ts
    scripts: [{ id: 'edna_patrols', loop: true, stepIds: ['w1', 'to-hall', 'moved-1', 'w2', 'to-kitchen', 'moved-2'], do: [
      { wait: 6000 }, { moveActor: ['edna', 'hall'] }, { emit: 'edna_moved' },
      { wait: 6000 }, { moveActor: ['edna', 'kitchen'] }, { emit: 'edna_moved' },
    ] }],
    events: [{ id: 'edna.catches', on: 'edna_moved',
      if: { any: [{ all: [{ room: 'hall' }, { actorIn: ['edna', 'hall'] }] }, { all: [{ room: 'kitchen' }, { actorIn: ['edna', 'kitchen'] }] }] },
      do: [{ say: ['edna', 'Got you!'] }, { goto: 'dungeon' }] }],
    // hall.ts (and the same in kitchen.ts):
    onEnter: [{ if: { actorIn: ['edna', 'hall'] }, then: [{ say: ['edna', 'Got you!'] }, { goto: 'dungeon' }] }],

    The dungeon has a loose brick back to the hall. The whole game is a test fixture, tests/fixtures/classics.ts (mansion), and the mechanic is described on classic mechanics.

  1. Checkpoints. A checkpoint starts with every moving character in its starting room; where: { edna: 'hall' } puts one elsewhere. A checkpoint with goals must be a state the game can really reach when those goals hold: npm run solve -- --prove --chapters proves each chapter from every reachable boundary state and reports a checkpoint that matches none, with the dimensions that differ (room: house vs market). used, seen and players[].used let a checkpoint name what a real playthrough leaves behind (an item already used, a once-listener already fired) (chapters).

Expected result, and how to check it

  • On the dev server, the guard walks back and forth while you play; opening the vault while he stands by it gets the warning, at the window it opens.
  • npm run validate (or npx quest-n-click validate) prints no error.
  • npm run solve finishes, and its path shows the script's turns as steps (Script edna_patrols): the solver waits for the patrol to move, then acts.

Common errors

SymptomCauseFix
loop script "…" never waits (add a wait, waitUntil or waitEvent)A loop without a pauseAdd a { wait }
schema v3 script needs one stable step id per command (n expected)stepIds missing or the wrong lengthOne id per top-level command
character "…" has no starting room ("room" in its definition), needed to move it between roomsmoveActor on a character without roomAdd room in its definition
"…" is not an actor in <room>: declare it there (actors) and place it in the layoutmoveActor toward a room that does not declare itDeclare the actor there and place it
The character shows in every room at onceNo room on the characterGive it a starting room
The character slides instead of walkingNo walk poseAdd walk to its sprites
The patrol stops for good after an item changes handsA while checked before every commandGain the new item before losing the old one

Files or data changed

rooms/<room>.ts (the room's script, rules, onEnter), game.ts (game scripts, listeners, checkpoints), cast.ts (the starting room), layout/<room>.json (the actor placed in each room). In a save: the script's position, where each moving character is (where), each actor's position and the flags the script sets.

Recovery or going back

Removing a script or renaming its steps changes what an old save holds: add a migration step (renameScript, renameScriptStep, dropScript) when players have saves (state and saves).

References