quest'n'click 4.5.0 · r1

Built for quest'n'click 4.5.0, revision r1. Whether it is published, and which one is current, is recorded at publication.json.

Moving characters: patrols and walks

Français · Type: how-to · Audience: author · Context: engine repository, created project · Status: ready · Applies from: 4.3.0

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

Prerequisites

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).

Limit · turn-by-turn A patrol is turn by turn, not real time. Scripts run between the player's actions, never during one: the nurse cannot walk in while the hero is mid-sentence or mid-walk. That is what lets the solver prove the puzzle (it waits for her to leave, then crosses) and what a save restores exactly. A sequence that must not be interrupted goes in a cutscene.

Limit · no-position-condition No condition reads where a character stands inside a room, nor its line of sight. Model "the guard is at the door" with a flag the script sets, and "the guard is in this room" with { actorIn }.

Guarantee · patrol-turn-by-turn · referenced · executed on aff6caab7514 · scope self-checked by the author (AI), not reviewed A patrolling character moved by a script catches the hero turn by turn: when the hero enters her room, or when she enters his.

Expected result, and how to check it

Guarantee · patrol-solved · referenced · executed on aff6caab7514 · scope self-checked by the author (AI), not reviewed The solver proves the patrolling nurse's game: it lets her script run and waits for her to leave.

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


Edit the source of this page · Report an error