Talos docs

Your first scenario: Crossing Drive

Since next

Day 1 of the training path.

Plain text

Day 1 of the training path. By the end of it you have a scenario a trainee can drive. A car sets off on a town route with a 50 km/h limit, a stop line, a pedestrian crossing and a destination. The HUD shows speed, the limit and the next objective, and the scenario emits an event at every moment that matters. Day 2 scores those events.

  • Part 1, the world (about 4 hours): the ground, the road, the car, the stop line, the crossing, the pedestrian, the buildings and the destination.
  • Part 2, the rules (about 3 hours): what the scripts judge and emit, how to change them, and a self-test that drives the route the way a careful driver would.

The finished project is the Crossing Drive sample. Install it from the launcher's Samples tab and keep it open beside yours. The Linux build has no launcher: install the sample on Windows and copy the folder across, as Start here, "On Linux", says. Every block below is copied from it, less two things that only change its look: the sample's parts each name a material (gloss paint, glass, rubber, matte walls, in scenes/materials/), and it carries scenery (windows, roofs, kerbs, street lamps and trees, tagged scenery, none of them solid). Your scene drives the same without them; copy them across if you want the sample's look. The sample also carries day 2's two files, journal.json and training/crossing-drive.scorecard.json. Leave them out for now.

Before you start

  • Talos is installed and you have played a sample (Start here).
  • Two ways to make each step. Every step is written both ways. Use either, or both. step gives a prompt for it. It works through the Talos tools, checks its own work with the critic, and you watch the change arrive in the editor. Scene changed on disk: press Reload. Each step shows the JSON to add. A block with one entity per line pastes into the entities list as it is. The file may mix that style with one value per line.
    • Ask your assistant. If you connected an AI assistant (Start here, "Connect your AI"), each
    • By hand. Edit the project's files in any text editor. A scene is JSON. When you save it, the editor shows
  • Check each step, with or without the editor. From a terminal in the project folder: wrong and where. Each step gives its N. & "$env:LOCALAPPDATA\Argus\engines\<version>\bin\argus.exe" validate scenes\main.scene.json.
    • argus validate scenes/main.scene.json reads the scene as the engine will. It says ok, N entities, or what is
    • argus run . --headless --frames 600 runs ten seconds with no window. It exits 0, or prints the script error.
    • The command is argus on Linux. On Windows it is argus.exe in the engine's bin folder:
  • Expected in every run: no navmesh (nav-undeclared: ...). This scenario needs no navigation. A run also counts one entity more than the file has: the road builds its surface as an entity of its own, route-surface.
  • Positions are in metres. +Y is up, and the car drives toward -Z. The route runs from z = 150 down to z = -140, then turns left.
  • Play is F5, and Shift+Esc stops. Play runs the scene as it is, unsaved edits included.

Part 1: the world

1. Create the project

In the launcher: Projects ▸ New project. Choose 3D at the right of the Template heading, the Empty template, and the name My Crossing Drive, then press Create. The project opens in the editor with three entities: cam (a fixed camera), floor (100 m of grass) and spawn (where the player starts).

Open scenes/main.scene.json in a text editor. That file is the scene, and every step below changes it.

Check: ok, 3 entities.

2. Make room, and a sun

The route is about 400 m long, so the floor grows to 420 m square. It also gets the tag ground. A sun gives the scene shadows and a direction to its light. Without one, the sky alone lights it.

Ask: "Name the scene crossing-drive. Make the floor 420 m square, keep it 0.5 m thick, and add the tag ground to it. Add a warm directional sun, high and from the front left, after the floor."

By hand:

  1. In meta, set "name": "crossing-drive". Every run prints this name.
  2. Replace the floor entity with this one, and add the sun right after it. The order of entities does not change what a scene does, but it is part of a run's fingerprint (the self-test's digest), so keep the order the guide gives.
{"id": "floor", "name": "floor", "tags": ["static", "ground"], "transform": {"position": [0, -0.25, 0], "rotation": [0, 0, 0, 1], "scale": [420, 0.5, 420]}, "components": [{"type": "mesh", "asset": "cube", "color": "#587A42"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}, {"type": "rigidbody", "kind": "static"}]},
{"id": "sun", "name": "sun", "tags": [], "transform": {"position": [0, 40, 0], "rotation": [-0.38, 0.2, 0.08, 0.9], "scale": [1, 1, 1]}, "components": [{"type": "light", "kind": "directional", "color": "#FFF4E0", "intensity": 1}]}

You should see the grass reach the horizon, and shadows under things from now on. Check: ok, 4 entities.

3. The road

A road is a road component that reads its shape from a small file: the points along its centre line, its width and its painted lines. The engine builds the surface, its collider and a centre line that scripts can read.

Ask: "Add a road called route, 8 m wide with edge and centre lines, in roads/route.road.json, along these points: [0,0,170], [0,0,40], [0,0,-50], [-5,0,-85], [-20,0,-112], [-45,0,-130], [-80,0,-139], [-135,0,-141]."

By hand: create the folder roads in the project folder (beside scenes), and in it route.road.json:

{
 "widthM": 8.0,
 "points": [
  [
   0,
   0,
   170
  ],
  [
   0,
   0,
   40
  ],
  [
   0,
   0,
   -50
  ],
  [
   -5,
   0,
   -85
  ],
  [
   -20,
   0,
   -112
  ],
  [
   -45,
   0,
   -130
  ],
  [
   -80,
   0,
   -139
  ],
  [
   -135,
   0,
   -141
  ]
 ],
 "surface": {
  "finish": {
   "preset": "asphalt",
   "grain": 0.1
  }
 },
 "markings": {
  "edge": {},
  "centre": {}
 }
}

Then add this entity to the scene's entities. source is the file's path from the project folder. material.friction is the grip of the road's surface: 1 is dry asphalt.

{"id": "route", "name": "route", "tags": ["ground"], "transform": {"position": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "road", "source": "roads/route.road.json", "material": {"friction": 1.0}}]}

You should see a grey road with white lines running away from the camera. Select route, and Details shows its width and Edit points, which lets you drag the points in the viewport. Check: ok, 5 entities. If the road is missing, the file has a mistake, and argus validate names it: road-source-invalid with the key it did not expect. A run only warns and goes on without the road, so validate is the check to use.

4. The car

The car is a box with the engine's vehicle component and four wheels. The sedan preset sets its mass, engine, brakes, tyres and steering (Vehicles). The engine drives it from the actions throttle, brake and steer, so the scene declares those keys. The car's script, behaviors/drive-rules, films it with a chase camera and runs the rules. You add the script file now and read it in part 2.

Ask: "Replace the player spawn with a car: the sedan vehicle preset on a 1.8 x 1.45 x 4.7 m box body at [0, 0.05, 150], with a blue body, a dark cabin and four cylinder wheels. Tag it player and trainee. Bind W/S or Up/Down to throttle, A/D or Left/Right to steer, Space to handbrake, and the pad's triggers and left stick. Then copy scenes/behaviors/drive-rules.ts from the Crossing Drive sample and attach it to the car with the props limitKmh 50, stopLine stop-line, pedestrian pedestrian-1 and autoHazard true. Copy the sample's meta.strings too."

By hand:

  1. Delete the spawn entity: the car becomes the player.
  2. Make the camera a chase camera: in the cam entity, set the camera's "kind" to "third-person". The car's script points it at the car.
{"id": "cam", "name": "camera", "tags": [], "transform": {"position": [0, 3, 8], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "camera", "kind": "third-person", "fov": 60, "primary": true}]}
  1. In the scene's meta, add input and replace the empty strings with these (the HUD's words):
"input": {
  "contexts": {
    "gameplay": {
      "throttle": {
        "kind": "axis1",
        "keys": ["S", "W", "Down", "Up"],
        "gamepad": "right-trigger"
      },
      "brake": {
        "kind": "button",
        "keys": [],
        "gamepad": "left-trigger"
      },
      "steer": {
        "kind": "axis1",
        "keys": ["A", "D", "Left", "Right"],
        "gamepad": "left-stick",
        "deadzone": 0.15
      },
      "handbrake": {
        "kind": "button",
        "keys": ["Space"],
        "gamepad": "right-bumper"
      }
    }
  },
  "active": "gameplay"
},
"strings": {
  "objective.stop": "Stop fully at the stop line",
  "objective.crossing": "Watch the crossing ahead",
  "objective.destination": "Drive on to the green post",
  "objective.arrived": "Arrived: run complete",
  "hint.start": "W to drive, S to brake, A/D to steer",
  "result.stopped": "Stopped at the line",
  "result.not-stopped": "Did not stop at the line"
}
  • An axis1 action lists its keys in pairs: the key for -1, then the key for +1. So throttle is S (-1) and W (+1) for one pair, and Down and Up for the other.
  • S at -1 brakes, and reverses once the car has stopped. That is why brake binds no key of its own, only the pad's left trigger.
  • In assets, beside cube, add the meshes the car, the pedestrian and the post use. triangles is each mesh's size, which the scene's budget counts.
"cylinder": {"kind": "glb", "path": "builtin:cylinder", "triangles": 96},
"capsule": {"kind": "glb", "path": "builtin:capsule", "triangles": 816},
"sphere": {"kind": "glb", "path": "builtin:sphere", "triangles": 1024}
  1. Add the car and its parts to entities. Each part has "parent": "car", so its position is relative to the car. The vehicle finds its wheels by the wheel component on its children, and tells front from rear by their z. The car's name is the trainee's, which the instructor console shows; the scripts and the scorecard use its id, car.
{"id": "car", "name": "Sam Taylor", "tags": ["player", "trainee"], "transform": {"position": [0, 0.05, 150], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [1.8, 1.45, 4.7], "center": [0, 0.87, 0]}, {"type": "rigidbody", "kind": "dynamic"}, {"type": "vehicle", "preset": "sedan"}, {"type": "script", "module": "behaviors/drive-rules", "props": {"limitKmh": 50, "stopLine": "stop-line", "pedestrian": "pedestrian-1", "autoHazard": true}}]},
{"id": "car-body", "name": "car-body", "tags": [], "transform": {"position": [0, 0.75, 0], "rotation": [0, 0, 0, 1], "scale": [1.8, 0.75, 4.6]}, "components": [{"type": "mesh", "asset": "cube", "color": "#1F4FA8"}], "parent": "car"},
{"id": "car-cabin", "name": "car-cabin", "tags": [], "transform": {"position": [0, 1.35, 0.2], "rotation": [0, 0, 0, 1], "scale": [1.6, 0.55, 2.3]}, "components": [{"type": "mesh", "asset": "cube", "color": "#1A1E24"}], "parent": "car"},
{"id": "car-wheel-fl", "name": "car-wheel-fl", "tags": [], "transform": {"position": [-0.775, 0.32, -1.4], "rotation": [0, 0, 0.7071, 0.7071], "scale": [0.64, 0.22, 0.64]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#151515"}, {"type": "wheel"}], "parent": "car"},
{"id": "car-wheel-fr", "name": "car-wheel-fr", "tags": [], "transform": {"position": [0.775, 0.32, -1.4], "rotation": [0, 0, 0.7071, 0.7071], "scale": [0.64, 0.22, 0.64]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#151515"}, {"type": "wheel"}], "parent": "car"},
{"id": "car-wheel-rl", "name": "car-wheel-rl", "tags": [], "transform": {"position": [-0.775, 0.32, 1.4], "rotation": [0, 0, 0.7071, 0.7071], "scale": [0.64, 0.22, 0.64]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#151515"}, {"type": "wheel"}], "parent": "car"},
{"id": "car-wheel-rr", "name": "car-wheel-rr", "tags": [], "transform": {"position": [0.775, 0.32, 1.4], "rotation": [0, 0, 0.7071, 0.7071], "scale": [0.64, 0.22, 0.64]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#151515"}, {"type": "wheel"}], "parent": "car"}
  1. Copy scenes/behaviors/drive-rules.ts from the sample into your project's scenes/behaviors/ folder (make the folder).

You should see, after F5, the camera behind the car. W drives, S brakes, and A and D steer. The HUD shows Speed, Limit and Time, and "Stop fully at the stop line". Check: ok, 11 entities, and the ten-second run exits 0. Without a display, step 14's self-test is the proof that the car drives. If the car does not move, check the input block: every action needs at least one key or pad binding, and active must name the context.

5. The stop line

Three things: white paint across the road, a sign, and a trigger volume. A trigger volume is a box collider marked isTrigger, so it is not solid, and a trigger component that names the event it raises. When the car's front enters it, the car's script hears stop-line and judges the stop.

Ask: "At z = 40, add a white stop line across the road, a red stop sign on a grey post on the right, and an invisible trigger volume stop-line across the road that raises the trigger event stop-line."

By hand: add to entities:

{"id": "stop-line-paint", "name": "stop-line-paint", "tags": [], "transform": {"position": [0, 0.03, 40.0], "rotation": [0, 0, 0, 1], "scale": [7.4, 0.02, 0.45]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "stop-sign-post", "name": "stop-sign-post", "tags": [], "transform": {"position": [5.2, 1.25, 41.0], "rotation": [0, 0, 0, 1], "scale": [0.08, 2.5, 0.08]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#8A8B8E"}]},
{"id": "stop-sign", "name": "stop-sign", "tags": [], "transform": {"position": [5.2, 2.75, 41.0], "rotation": [0, 0, 0, 1], "scale": [0.8, 0.8, 0.06]}, "components": [{"type": "mesh", "asset": "cube", "color": "#B0141A"}]},
{"id": "stop-line", "name": "stop line", "tags": ["volume"], "transform": {"position": [0, 1.5, 40.0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [10.0, 3, 0.5], "isTrigger": true}, {"type": "trigger", "event": "stop-line"}]}

You should see the line and the sign. Drive over the line and the objective changes to "Watch the crossing ahead". Check: ok, 15 entities.

A mesh that nothing moves collides as its own shape even with no collider (the run counts these as mesh colliders), so the car can bump the post. The paint says "colliderMode": "none": it is painted on, and the car must not bump over it. Only what part 2 tags as an obstacle counts as a collision.

6. The crossing

The crossing is six white stripes and a pavement on each side, at z = -20. Two volumes go with it. The hazard zone, 32 m before the crossing, is where the pedestrian is sent out. The crossing volume tells the rules that the car has reached the crossing.

Ask: "At z = -20, add a pedestrian crossing: six white stripes 0.6 m wide and 3 m long across the road, and a 3 m wide grey pavement on each side, 24 m long. Add trigger volumes crossing at z = -20 and hazard-zone at z = 12, each across the road and raising the event of its own name."

By hand: add to entities:

{"id": "crossing-stripe-1", "name": "crossing-stripe-1", "tags": [], "transform": {"position": [-3.0, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "crossing-stripe-2", "name": "crossing-stripe-2", "tags": [], "transform": {"position": [-1.8, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "crossing-stripe-3", "name": "crossing-stripe-3", "tags": [], "transform": {"position": [-0.6, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "crossing-stripe-4", "name": "crossing-stripe-4", "tags": [], "transform": {"position": [0.6, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "crossing-stripe-5", "name": "crossing-stripe-5", "tags": [], "transform": {"position": [1.8, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "crossing-stripe-6", "name": "crossing-stripe-6", "tags": [], "transform": {"position": [3.0, 0.03, -20.0], "rotation": [0, 0, 0, 1], "scale": [0.6, 0.02, 3.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#E8E6DF", "colliderMode": "none"}]},
{"id": "pavement-left", "name": "pavement-left", "tags": [], "transform": {"position": [-6.0, 0.06, -20.0], "rotation": [0, 0, 0, 1], "scale": [3.0, 0.12, 24.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#B4AFA6"}]},
{"id": "pavement-right", "name": "pavement-right", "tags": [], "transform": {"position": [6.0, 0.06, -20.0], "rotation": [0, 0, 0, 1], "scale": [3.0, 0.12, 24.0]}, "components": [{"type": "mesh", "asset": "cube", "color": "#B4AFA6"}]},
{"id": "crossing", "name": "crossing", "tags": ["volume"], "transform": {"position": [0, 1.5, -20.0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [10.0, 3, 3], "isTrigger": true}, {"type": "trigger", "event": "crossing"}]},
{"id": "hazard-zone", "name": "hazard zone", "tags": ["volume"], "transform": {"position": [0, 1.5, 12.0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [10.0, 3, 2], "isTrigger": true}, {"type": "trigger", "event": "hazard-zone"}]}

Check: ok, 25 entities.

7. The pedestrian

The pedestrian waits on the right pavement. The parent entity carries the collider and the script, and the two children are what you see: a capsule body and a head. Its collider is a trigger too, so the car passes through rather than stopping dead. The touch is reported as a collision in part 2. Its script, behaviors/pedestrian, walks it across when the event send-pedestrian arrives.

Ask: "Copy scenes/behaviors/pedestrian.ts from the Crossing Drive sample. Add a pedestrian pedestrian-1 (tag pedestrian) on the right pavement at [6.2, 0, -20.8], facing across the road: a kinematic body with a 0.6 x 1.8 x 0.6 trigger box that raises pedestrian-contact, an orange capsule body and a head. Attach behaviors/pedestrian with to [-6.2, 0, -20.8], speed 1.4 and watch car."

By hand: copy scenes/behaviors/pedestrian.ts from the sample, then add to entities:

{"id": "pedestrian-1", "name": "pedestrian", "tags": ["pedestrian"], "transform": {"position": [6.2, 0, -20.8], "rotation": [0, 0.7071, 0, 0.7071], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [0.6, 1.8, 0.6], "center": [0, 0.9, 0], "isTrigger": true}, {"type": "trigger", "event": "pedestrian-contact"}, {"type": "rigidbody", "kind": "kinematic"}, {"type": "script", "module": "behaviors/pedestrian", "props": {"to": [-6.2, 0, -20.8], "speed": 1.4, "watch": "car"}}]},
{"id": "pedestrian-1-body", "name": "pedestrian-1-body", "tags": [], "transform": {"position": [0, 0.8, 0], "rotation": [0, 0, 0, 1], "scale": [0.45, 0.6, 0.3]}, "components": [{"type": "mesh", "asset": "capsule", "color": "#F28C0F"}], "parent": "pedestrian-1"},
{"id": "pedestrian-1-head", "name": "pedestrian-1-head", "tags": [], "transform": {"position": [0, 1.6, 0], "rotation": [0, 0, 0, 1], "scale": [0.24, 0.26, 0.24]}, "components": [{"type": "mesh", "asset": "sphere", "color": "#C68E6A"}], "parent": "pedestrian-1"}

The pedestrian moves, so it has a kinematic rigid body: a body the script moves and physics does not push. The volumes and the buildings stand still and need none.

You should see the pedestrian step out as you approach the crossing, and walk to the far pavement. Check: ok, 28 entities.

8. The destination and the street

The destination is a green post and one more volume, near the end of the road. The buildings make the drive read as a street. They are solid, and part 2 counts driving into one as a collision.

Ask: "Near the end of the road, add a 4 m tall green post at [-118, 2, -135] and a trigger volume destination across the road at [-115, 1.5, -140.4] raising destination. Then put ten box buildings along the street, at least 8 m from the road's centre line, solid, tagged static and building."

By hand: add to entities:

{"id": "destination-post", "name": "destination-post", "tags": [], "transform": {"position": [-118, 2, -135.0], "rotation": [0, 0, 0, 1], "scale": [0.3, 4, 0.3]}, "components": [{"type": "mesh", "asset": "cylinder", "color": "#2EA043"}]},
{"id": "destination", "name": "destination", "tags": ["volume"], "transform": {"position": [-115, 1.5, -140.4], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1]}, "components": [{"type": "collider", "shape": "box", "size": [4, 3, 12.0], "isTrigger": true}, {"type": "trigger", "event": "destination"}]}

and the buildings:

{"id": "building-1", "name": "building-1", "tags": ["static", "building"], "transform": {"position": [-13, 3.44, 120], "rotation": [0, 0, 0, 1], "scale": [10, 7, 12]}, "components": [{"type": "mesh", "asset": "cube", "color": "#B5654A"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-2", "name": "building-2", "tags": ["static", "building"], "transform": {"position": [13, 2.94, 104], "rotation": [0, 0, 0, 1], "scale": [9, 6, 14]}, "components": [{"type": "mesh", "asset": "cube", "color": "#D8CCB2"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-3", "name": "building-3", "tags": ["static", "building"], "transform": {"position": [-14, 3.94, 78], "rotation": [0, 0, 0, 1], "scale": [11, 8, 10]}, "components": [{"type": "mesh", "asset": "cube", "color": "#8E959E"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-4", "name": "building-4", "tags": ["static", "building"], "transform": {"position": [14, 2.94, 62], "rotation": [0, 0, 0, 1], "scale": [10, 6, 12]}, "components": [{"type": "mesh", "asset": "cube", "color": "#C9A77C"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-5", "name": "building-5", "tags": ["static", "building"], "transform": {"position": [-13, 2.94, 6], "rotation": [0, 0, 0, 1], "scale": [9, 6, 12]}, "components": [{"type": "mesh", "asset": "cube", "color": "#6F7F8C"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-6", "name": "building-6", "tags": ["static", "building"], "transform": {"position": [14, 4.44, -2], "rotation": [0, 0, 0, 1], "scale": [10, 9, 10]}, "components": [{"type": "mesh", "asset": "cube", "color": "#A9876B"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-7", "name": "building-7", "tags": ["static", "building"], "transform": {"position": [-14, 3.44, -44], "rotation": [0, 0, 0, 1], "scale": [10, 7, 14]}, "components": [{"type": "mesh", "asset": "cube", "color": "#C7C2B8"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-8", "name": "building-8", "tags": ["static", "building"], "transform": {"position": [15, 2.94, -50], "rotation": [0, 0, 0, 1], "scale": [9, 6, 12]}, "components": [{"type": "mesh", "asset": "cube", "color": "#7E6A5C"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-9", "name": "building-9", "tags": ["static", "building"], "transform": {"position": [-32, 3.44, -92], "rotation": [0, 0, 0, 1], "scale": [12, 7, 10]}, "components": [{"type": "mesh", "asset": "cube", "color": "#B9B4A6"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]},
{"id": "building-10", "name": "building-10", "tags": ["static", "building"], "transform": {"position": [-62, 3.94, -116], "rotation": [0, 0, 0, 1], "scale": [10, 8, 10]}, "components": [{"type": "mesh", "asset": "cube", "color": "#94A3AE"}, {"type": "collider", "shape": "box", "size": [1, 1, 1]}]}

You should see, at the green post, "Arrived: run complete". Under it are whether you stopped at the line, how many times you went over the limit, and how many collisions you had.

That is the whole world. Check: ok, 40 entities (a run says 41: the road's surface). Save it (Ctrl+S in the editor, if you made changes there).

Part 2: the rules

Part 1 copied two scripts. This part reads them, so you can change them and write your own.

9. How a behaviour works

A behaviour is a TypeScript module whose default export has hooks. The engine calls them:

HookWhen
init(ctx)once, when the entity is set up (again after a hot reload, with ctx.state kept)
update(ctx, dt)every tick, 60 a second; dt is 1/60 s
onTrigger(ctx, t)the entity enters or leaves a trigger volume, as either side: the car hears the volumes it drives into, and a volume's own script (the pedestrian's) hears what enters it
onCollision(ctx, c)the entity's body starts touching another solid body; c.other is that entity
onEvent(ctx, ev)any behaviour emitted an event (ctx.events.emit): ev.name, ev.data

What the two scripts use, and what it is:

MemberWhat
t.event, t.entering, t.volume, t.otherin onTrigger: the volume's event name, entering (or leaving), the volume, and what entered it
ctx.entitythis entity: .id, .tags, .distanceTo(other), .moveToward(point, metres)
ctx.vehicleits vehicle: .speed (m/s), .throttle (-1 to 1), .brake (0 to 1)
ctx.world.find(id)another entity, by id (or null)
ctx.events.emit(name, data)sends an event to every behaviour, and to day 2's journal
ctx.log(name, data)writes a line to the log: a headless run prints it
ctx.hud.objective(key), ctx.hud.prompt(key), ctx.hud.set(field, value, options)the HUD's objective line, its prompt, and a named field with a label and unit
ctx.camera.follow(entity), ctx.camera.set("third-person", {...})the chase camera and its distance, height and lag
ctx.props, ctx.statethe script component's props, and this instance's own state, kept across hot reloads and in recordings

Every member is typed. Your code editor completes them from the types in the engine's sdk folder, and the docs site's Script API pages describe each one. Save a script while Play runs, and the change is live at once.

10. The car's rules: drive-rules.ts

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

/**
 * Crossing Drive: the rules of the run, on the trainee's car (the scene's player entity).
 *
 * The engine drives the car itself (`vehicle` with `preset: "sedan"`, docs/VEHICLES.md): W / S or the triggers,
 * A / D or the left stick. This script only
 * - films it (a chase camera) and shows speed, the limit, the time and the next objective on the HUD,
 * - judges the stop: the slowest the car went over the last `STOP_WINDOW_M` metres before the stop line,
 * - sends the pedestrian out when the car enters the hazard zone (`autoHazard`; an instructor can send it instead
 *   with the bus event `send-pedestrian`),
 * - and emits one bus event at each moment that matters, so a scorecard, a recording or a test can read the run:
 *
 * | Event | Data | When |
 * |---|---|---|
 * | `speed-over` | `{ subject, speed_kmh, limit_kmh }` | the speed goes over the limit (once until it is back under) |
 * | `brake-applied` | `{ subject, speed_kmh }` | the brake goes on (once per press) |
 * | `stop-line` | `{ subject, stopped, min_kmh, speed_kmh }` | the car's front reaches the stop line |
 * | `crossing-entered` | `{ subject }` | the car reaches the pedestrian crossing |
 * | `trainee-collision` | `{ subject, other, speed_kmh }` | the car hits an entity tagged building, pedestrian or traffic |
 * | `destination-reached` | `{ subject, seconds }` | the car reaches the green post |
 *
 * Each is also written to the log, so a headless run prints it. The three places on the route also emit
 * `goal-reached` (`stop-line`, `crossing`, `destination`): the self-test checks them.
 * Change the numbers below, or the props in the scene, and press Play again.
 */

const KMH = 3.6
/** Slower than this counts as stopped, km/h. */
const STOPPED_KMH = 1
/** The stop is judged over this many metres before the line, measured from the car's front. */
const STOP_WINDOW_M = 10
/** Half the car's length, m: its front bumper is this far ahead of its centre (the collider is 4.7 m long). */
const HALF_LENGTH = 2.35
/** After a `speed-over`, the speed must fall this far under the limit before another one counts, km/h. */
const REARM_KMH = 2
/** Hitting an entity with one of these tags is a collision. Tag anything else the trainee must not hit. */
const OBSTACLES = ["building", "pedestrian", "traffic"]

interface State {
  /** The clock starts on the first touch of the throttle, not at Play. */
  started: boolean
  seconds: number
  /** The slowest the car went inside the stop window, km/h. */
  approachMinKmh: number
  over: boolean
  overCount: number
  /** The brake was held last tick. */
  braking: boolean
  collisions: number
  /** The places already reached, so each is reported once. */
  reached: string[]
  stopped: boolean | null
}

interface Props {
  /** The speed limit, km/h. */
  limitKmh?: number
  /** The stop-line volume's entity id. */
  stopLine?: string
  /** The pedestrian sent out at the hazard zone. */
  pedestrian?: string
  /** Send the pedestrian out when the car enters the hazard zone (false: only an instructor does). */
  autoHazard?: boolean
}

type C = Ctx<State, Props>

/** Emits a scenario event, and logs it: a headless run (`argus run . --headless --self-test ...`) prints each one. */
function report(ctx: C, name: string, data: Record<string, unknown>) {
  ctx.events.emit(name, data)
  ctx.log(name, data)
}

/** True the first time `place` is reached. The three places on the route also count as goals. */
function once(ctx: C, place: string, goal = true): boolean {
  if (ctx.state.reached.includes(place)) return false
  ctx.state.reached.push(place)
  if (goal) ctx.events.emit("goal-reached", { goal: place })
  return true
}

/** Braking: the brake held, or the throttle pulled back (S) while still rolling forward. */
function braking(ctx: C): boolean {
  const v = ctx.vehicle
  return v !== null && (v.brake > 0.1 || (v.throttle < -0.1 && v.speed > 0.5))
}

function kmh(ctx: C): number {
  return Math.abs(ctx.vehicle?.speed ?? 0) * KMH
}

function collided(ctx: C, other: string) {
  ctx.state.collisions += 1
  report(ctx, "trainee-collision", { subject: ctx.entity.id, other, speed_kmh: round1(kmh(ctx)) })
}

function round1(x: number): number {
  return Math.round(x * 10) / 10
}

export default {
  init(ctx) {
    const s = ctx.state
    // `init` runs again after a hot reload with `state` kept: fill only what is missing.
    if (typeof s.seconds !== "number") {
      s.started = false
      s.seconds = 0
      s.approachMinKmh = Infinity
      s.over = false
      s.overCount = 0
      s.braking = false
      s.collisions = 0
      s.reached = []
      s.stopped = null
    }
    ctx.camera.follow(ctx.entity)
    ctx.camera.set("third-person", { distance: 8, pitch: -0.22, eyeHeight: 1.6, recenterDelay: 0, lookAhead: 0.3 })
    ctx.hud.objective(s.reached.includes("stop-line") ? "objective.destination" : "objective.stop")
    ctx.hud.set("limit", ctx.props.limitKmh ?? 50, { label: "Limit", unit: "km/h" })
    if (!s.started) ctx.hud.prompt("hint.start")
  },

  update(ctx, dt) {
    const s = ctx.state
    const v = ctx.vehicle
    if (v === null) return
    const limit = ctx.props.limitKmh ?? 50
    const speed = kmh(ctx)
    const done = s.reached.includes("destination")

    if (!s.started && Math.abs(v.throttle) > 0.05) {
      s.started = true
      ctx.hud.prompt(null)
    }
    if (s.started && !done) s.seconds += dt

    // The stop: the slowest speed while the car's front is within STOP_WINDOW_M of the line.
    if (!s.reached.includes("stop-line")) {
      const line = ctx.world.find(ctx.props.stopLine ?? "stop-line")
      if (line !== null && ctx.entity.distanceTo(line) - HALF_LENGTH <= STOP_WINDOW_M) {
        s.approachMinKmh = Math.min(s.approachMinKmh, speed)
      }
    }

    // The brake: one event per press, so a reaction time can be measured from the pedestrian stepping out.
    const brake = braking(ctx)
    if (brake && !s.braking) report(ctx, "brake-applied", { subject: ctx.entity.id, speed_kmh: round1(speed) })
    s.braking = brake

    // Over the limit: once per excursion.
    if (!s.over && speed > limit) {
      s.over = true
      s.overCount += 1
      report(ctx, "speed-over", { subject: ctx.entity.id, speed_kmh: round1(speed), limit_kmh: limit })
    } else if (s.over && speed < limit - REARM_KMH) {
      s.over = false
    }

    ctx.hud.set("speed", Math.round(speed), { label: "Speed", unit: "km/h" })
    ctx.hud.set("time", s.seconds, { label: "Time", unit: "s", decimals: 1 })
  },

  onTrigger(ctx, t) {
    if (!t.entering) return
    const s = ctx.state
    switch (t.event) {
      case "stop-line":
        if (!once(ctx, "stop-line")) return
        s.stopped = s.approachMinKmh < STOPPED_KMH
        // min_kmh is what the stop is judged on (the slowest in the window); speed_kmh is the speed crossing the line.
        report(ctx, "stop-line", {
          subject: ctx.entity.id,
          stopped: s.stopped,
          min_kmh: round1(Number.isFinite(s.approachMinKmh) ? s.approachMinKmh : kmh(ctx)),
          speed_kmh: round1(kmh(ctx)),
        })
        ctx.hud.objective("objective.crossing")
        return
      case "hazard-zone":
        if (ctx.props.autoHazard !== false && once(ctx, "hazard-zone", false)) {
          ctx.events.emit("send-pedestrian", { pedestrian: ctx.props.pedestrian ?? "pedestrian-1" })
        }
        return
      case "crossing":
        if (!once(ctx, "crossing")) return
        report(ctx, "crossing-entered", { subject: ctx.entity.id })
        ctx.hud.objective("objective.destination")
        return
      case "destination":
        if (!once(ctx, "destination")) return
        report(ctx, "destination-reached", { subject: ctx.entity.id, seconds: round1(s.seconds) })
        ctx.hud.objective("objective.arrived")
        // A HUD text is a key into meta.strings, never the text itself: one key per state, numbers as numbers.
        ctx.hud.set("stop", s.stopped ? "result.stopped" : "result.not-stopped")
        ctx.hud.set("over", s.overCount, { label: "Times over the limit" })
        ctx.hud.set("collisions", s.collisions, { label: "Collisions" })
        return
      case "pedestrian-contact":
        collided(ctx, t.volume.id)
        return
    }
  },

  onCollision(ctx, c) {
    // Only what is tagged as an obstacle counts. The car touches the floor and the road's surface (an entity the
    // road builds, `route-surface`) all the time, and that is driving, not a collision.
    if (OBSTACLES.some((tag) => c.other.tags.includes(tag))) collided(ctx, c.other.id)
  },
} satisfies Behavior<State, Props>

Things to notice:

  • Nothing reads the wall clock. Time is the sum of dt. That is why a run plays back the same.
  • Each place reports once (once), and the three places on the route also emit goal-reached, which the self-test counts.
  • The stop is judged over a window: the slowest speed while the car's front is within 10 m of the line.
  • Only tagged obstacles are collisions. The car touches the floor and the road's surface all the time, which is driving.
  • Every event is also logged (report), so a run in a terminal prints it.
  • The HUD takes keys, not text. ctx.hud.objective("objective.stop") shows the string objective.stop from meta.strings, and ctx.hud.set("speed", 42, { label: "Speed", unit: "km/h" }) shows a number with its words. A text you build at run time would show as [[the text]]. The critic warns about it (missing-string).

11. The pedestrian: pedestrian.ts

import type { Behavior, Ctx, Vec3 } from "@argus/api"

/**
 * Crossing Drive: a pedestrian who steps out across the road when told to.
 *
 * It waits on the pavement until the bus event `send-pedestrian` arrives. The trainee's car sends it on entering
 * the hazard zone; an instructor or a test can send it at any time with `{ pedestrian: "<this entity's id>" }`, or
 * with no data for every pedestrian. Then it walks to `to` at `speed` m/s and emits
 * `pedestrian-stepped-out` `{ pedestrian, distance_m }`, the distance being from the car it `watch`es.
 * The event is also written to the log, so a headless run prints it. It goes once, and stops where it is if the car
 * touches it (the car reports that as `trainee-collision`).
 *
 * As it walks, its limbs swing: children whose ids end in `-leg-l`, `-leg-r`, `-arm-l` and `-arm-r` turn about
 * the hip and the shoulder, a stride every `STRIDE_M` metres. A pedestrian without them glides.
 */

/** Metres walked per stride, left foot to left foot. */
const STRIDE_M = 1.5
/** How far the legs and the arms swing each way, radians. */
const LEG_SWING = 0.42
const ARM_SWING = 0.32
/** A limb turns about this share of its half-length above its centre: near the top of the capsule. */
const PIVOT = 0.8
/** Seconds the limbs take to come to rest when it stops. */
const SETTLE_S = 0.3
/** Each limb: its id's ending, its swing, and which way it swings (an arm against the leg on its side). */
const LIMBS: [string, number, number][] = [
  ["-leg-l", LEG_SWING, 1],
  ["-leg-r", LEG_SWING, -1],
  ["-arm-l", ARM_SWING, -1],
  ["-arm-r", ARM_SWING, 1],
]

interface State {
  walking: boolean
  gone: boolean
  /** Metres walked: the stride's phase. */
  walked: number
  /** How much of the swing shows: 1 while walking, easing to 0 when it stops. */
  swing: number
}

interface Props {
  /** Where it walks to: the far pavement. */
  to?: Vec3
  /** Walking speed, m/s. */
  speed?: number
  /** The car the reported distance is measured from. */
  watch?: string
}

type C = Ctx<State, Props>

/** Turns each limb about its top (about X; positive swings it forward, toward -Z), keeping the top where it is. */
function pose(ctx: C, phase: number, amount: number) {
  for (const limb of ctx.entity.children) {
    const kind = LIMBS.find(([end]) => limb.id.endsWith(end))
    if (kind === undefined) continue
    const angle = kind[1] * kind[2] * Math.sin(phase) * amount
    const was = 2 * Math.atan2(limb.rotation[0], limb.rotation[3])
    const d = PIVOT * limb.scale[1]
    const [x, y, z] = limb.position
    const topY = y + d * Math.cos(was)
    const topZ = z + d * Math.sin(was)
    limb.position = [x, topY - d * Math.cos(angle), topZ - d * Math.sin(angle)]
    limb.rotation = [Math.sin(angle / 2), 0, 0, Math.cos(angle / 2)]
  }
}

export default {
  init(ctx) {
    if (typeof ctx.state.walking !== "boolean") {
      ctx.state.walking = false
      ctx.state.gone = false
    }
    if (typeof ctx.state.walked !== "number") {
      ctx.state.walked = 0
      ctx.state.swing = 0
    }
  },

  update(ctx, dt) {
    const s = ctx.state
    if (s.walking) {
      const to = ctx.props.to ?? [-6, 0, 0]
      const step = (ctx.props.speed ?? 1.4) * dt
      if (ctx.entity.moveToward(to, step)) s.walking = false
      s.walked += step
      s.swing = 1
    } else if (s.swing > 0) {
      s.swing = Math.max(0, s.swing - dt / SETTLE_S)
    } else {
      return
    }
    pose(ctx, (s.walked / STRIDE_M) * 2 * Math.PI, s.swing)
  },

  onEvent(ctx, ev) {
    if (ev.name !== "send-pedestrian" || ctx.state.gone) return
    // From a script: `{ pedestrian }`. Injected by an instructor: the session command's `target`, if it names one.
    const data = ev.data as { pedestrian?: string; target?: string | null } | null
    const who = data?.pedestrian ?? (typeof data?.target === "string" && data.target !== "world" ? data.target : undefined)
    if (who !== undefined && who !== ctx.entity.id) return
    ctx.state.gone = true
    ctx.state.walking = true
    const car = ctx.world.find(ctx.props.watch ?? "car")
    const distance = car === null ? -1 : Math.round(car.distanceTo(ctx.entity) * 10) / 10
    const stepped = { pedestrian: ctx.entity.id, distance_m: distance }
    ctx.events.emit("pedestrian-stepped-out", stepped)
    ctx.log("pedestrian-stepped-out", stepped)
  },

  onTrigger(ctx, t) {
    // Touched by the trainee's car: stop where it is.
    if (t.entering && t.event === "pedestrian-contact" && t.other.tags.includes("trainee")) ctx.state.walking = false
  },
} satisfies Behavior<State, Props>

The car sends send-pedestrian when it enters the hazard zone. On day 2 the instructor's console sends the same event, so the hazard can come whenever the instructor chooses. Set the car's autoHazard prop to false and only the instructor sends it.

12. The events

These are the scenario's output. Day 2 declares them, scores them and marks them on the review timeline.

EventDataWhen
speed-oversubject, speed_kmh, limit_kmhthe speed goes over the limit (once until it is back under)
brake-appliedsubject, speed_kmhthe brake goes on (once per press)
stop-linesubject, stopped, min_kmh, speed_kmhthe car's front reaches the line; min_kmh is the slowest it went in the last 10 m, stopped is true if that was under 1 km/h, and speed_kmh is its speed crossing the line
pedestrian-stepped-outpedestrian, distance_mthe pedestrian starts across; the distance is from the car
crossing-enteredsubjectthe car reaches the crossing
trainee-collisionsubject, other, speed_kmhthe car hits an entity tagged building, pedestrian or traffic
destination-reachedsubject, secondsthe car reaches the green post

subject is the trainee's car. A field named after a role (subject, other, pedestrian) is an entity id: on day 2 the journal links the event to that entity.

To see them: every event is also logged, so step 14's self-test prints the run line by line, each line starting with the entity's id:

[car] stop-line {"min_kmh":0,"speed_kmh":19.2,"stopped":true,"subject":"car"}
[pedestrian-1] pedestrian-stepped-out {"distance_m":36.4,"pedestrian":"pedestrian-1"}

13. Change it

ToChange
Lower the limitthe car's limitKmh prop
Make the stop stricterSTOPPED_KMH or STOP_WINDOW_M at the top of drive-rules.ts
Bring the pedestrian out latermove hazard-zone toward the crossing
Change the routethe points in roads/route.road.json (move the stop line, crossing and destination with it, then re-tune the self-test)
Drive something elsethe car's vehicle preset: suv, pickup or van (Vehicles)
Count more things as collisionsgive them the tag building, pedestrian or traffic, or add a tag to OBSTACLES in drive-rules.ts

Ask: "Move the stop line 20 m further on, and make the pedestrian walk at 2 m/s." Your assistant moves the paint, the sign and the volume together, and changes the pedestrian's speed prop.

14. A self-test

A self-test is a script of inputs the engine plays by itself, headless, in fixed ticks (Self-test). This one drives like a careful trainee:

  • under the limit;
  • a full stop just short of the stop line;
  • a brake as the pedestrian steps out, and a wait while they cross;
  • then on to the green post.

Each drive step follows the road at a throttle for a number of ticks, each axis step holds the throttle (a negative value brakes), and each action step holds a button, brake or handbrake. End a brake just before the car stops and hold the handbrake while it waits: a brake or a negative throttle held at a standstill reverses. The numbers are tuned to this route: change the route or move the volumes, and tune them again.

What it proves: the route can be driven end to end, every rule fires (the lines it prints), and no script fails. It passes when the stop line, the crossing and the green post are reached with no script error. It does not judge the drive: a run that misses the stop still passes. Judging is day 2's scorecard. Every export carries the self-test, so it also checks a training PC.

Ask: "Write a self-test crossing-drive that drives the route carefully, stops at the line, brakes for the pedestrian, and expects the three goals. Run it, and tune it until the stop line says stopped." (Your assistant writes it with behaviour.write and runs it with critic.selftest.)

By hand: create selftest/crossing-drive.json in the project folder (beside scenes/):

{
  "selfTest": 1,
  "description": "A careful drive: under the limit, a full stop short of the stop line, a brake as the pedestrian steps out, a wait while they cross, then on to the green post. Passes when the stop line, the crossing and the green post are reached with no script errors. Its timings are this route's: move the route or the volumes and re-tune them.",
  "maxFrames": 4200,
  "steps": [
    {
      "frames": 30
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.25,
      "lookAheadM": 12,
      "frames": 600
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.0,
      "lookAheadM": 12,
      "frames": 105
    },
    {
      "action": "brake",
      "frames": 82
    },
    {
      "action": "handbrake",
      "frames": 128
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.25,
      "lookAheadM": 12,
      "frames": 360
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.0,
      "lookAheadM": 12,
      "frames": 115
    },
    {
      "axis": "throttle",
      "value": [
        -0.3,
        0
      ],
      "frames": 183
    },
    {
      "action": "handbrake",
      "frames": 297
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.25,
      "lookAheadM": 12,
      "frames": 500
    },
    {
      "axis": "throttle",
      "value": [
        -0.4,
        0
      ],
      "frames": 30
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.15,
      "lookAheadM": 12,
      "frames": 220
    },
    {
      "axis": "throttle",
      "value": [
        -0.4,
        0
      ],
      "frames": 30
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.15,
      "lookAheadM": 12,
      "frames": 220
    },
    {
      "axis": "throttle",
      "value": [
        -0.4,
        0
      ],
      "frames": 30
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.15,
      "lookAheadM": 12,
      "frames": 220
    },
    {
      "axis": "throttle",
      "value": [
        -0.4,
        0
      ],
      "frames": 30
    },
    {
      "drive": "road",
      "road": "route",
      "throttle": 0.15,
      "lookAheadM": 12,
      "frames": 160
    },
    {
      "action": "brake",
      "frames": 80
    },
    {
      "action": "handbrake",
      "frames": 70
    },
    {
      "expect": "goals",
      "min": 3,
      "timeout": 10
    },
    {
      "capture": "arrived"
    }
  ],
  "expect": {
    "scriptErrors": 0
  }
}

and run it from a terminal in the project folder:

argus run . --headless --self-test crossing-drive

You should see the run, then the verdict:

[car] brake-applied {"speed_kmh":47.2,"subject":"car"}
[car] stop-line {"min_kmh":0,"speed_kmh":19.2,"stopped":true,"subject":"car"}
argus: self-test: goal reached: "stop-line" at frame 1257
[pedestrian-1] pedestrian-stepped-out {"distance_m":36.4,"pedestrian":"pedestrian-1"}
[car] brake-applied {"speed_kmh":39.9,"subject":"car"}
[car] crossing-entered {"subject":"car"}
argus: self-test: goal reached: "crossing" at frame 2454
...
[car] destination-reached {"seconds":58.7,"subject":"car"}
argus: self-test: goal reached: "destination" at frame 3552
argus: self-test passed: ...self-test.json

The report, self-test.json, goes to the engine's log folder (%LOCALAPPDATA%\Argus\logs on Windows, ~/.local/state/argus/logs on Linux). Set ARGUS_SELF_TEST_OUT to a file path to put it elsewhere. A failed run names the step that failed and the goals it reached. Each drive step's distanceM and s in the report say how far it got, which is what you tune.

Done

You have a scenario a trainee can drive, with its rules in two short scripts, its events listed, and a self-test. Compare it with the sample: your scenes/main.scene.json should match it entity for entity, in the same order. Then go on to day 2: Events, metrics and scorecards.