---
name: author-tutorial
description: >
  Author an interactive tutorial, guided lesson, onboarding flow, coach-mark
  tour, or objectives checklist in LunCoSim. Use for work under
  `assets/tutorials/` and for requests involving `mission`, `objective`,
  `coach_step`, `hint`, or `spotlight`.
---

# Author an authored lesson

A lesson is a file-backed Rhai scenario with optional standard USD scene
content. The application Rhai policy reads generic JSON asset snapshots,
selects the tutorial catalog, and builds the workbench menu. The shared Rust
asset and UI layers know only text assets, JSON values, and generic menu trees.
The lesson does not need a Rust type, registry, lifecycle owner, or custom USD
schema.

Read [`author-scenario`](../author-scenario/SKILL.md) first, then use
[`assets/tutorials/README.md`](../../assets/tutorials/README.md) and the
examples under `assets/tutorials/`.

## Add a lesson

1. Add `assets/tutorials/<track>/<name>.rhai`.
2. If it needs a world, reuse or add an authored scene under `assets/`.
3. Add an entry to `assets/tutorials/catalog.json`:

```json
{
  "track": "Sandbox",
  "title": "First Drive",
  "blurb": "Take control of a rover and drive it to a lunar flag.",
  "difficulty": "beginner",
  "source_asset": "lunco://tutorials/sandbox/first_drive.rhai",
  "scene_asset": "lunco://tutorials/sandbox/first_drive.usda"
}
```

The `track` value determines the submenu containing the lesson. Reuse an
existing track when the lesson belongs to that learning path. The Rhai policy
at `assets/scripting/policy/application_asset_lifecycle.rhai` discovers the unique
marked catalog from generic asset-scope events and uses the shared `parse_json`
function; do not add tutorial-specific Rust loading or menu code.

The menu submits the generic `RunScenarioAsset` command. It uses
`ScenarioReloadPolicy::Restart` for a predictable fresh start and lets the
command resolve an omitted host target to the active `WorldRoot`. Other apps
can reuse the command without importing tutorial code.

## Rhai contract

Use the shared prelude:

- `hint(...)`, `spotlight(anchor, caption)`, and `notify_kind(...)` for
  presentation;
- `coach_step(steps, index)` with an `on_event` cursor for a guided tour;
- `mission(me, ctx)` and `objective(...)` for an objective-driven exercise;
- `input_binding(...)`/`input_hint(...)` for the controller-owned semantic
  labels.

In LunCoSim, mission objectives are shown in View. Tutorial hints and actions
remain visible in every perspective, as do spotlight and coach-step overlays;
use those overlays to guide learners toward Builder or Editor panel anchors.

Use `panel.center` for the viewport or central work area, a generic
`panel.side_browser`, `panel.right_inspector`, or `panel.bottom` anchor for a
whole dock, and `panel.<id>` for a specific panel. Choose an anchor published
by the active layout; `coach_step` focuses a named panel before drawing its
spotlight.

Progression must observe semantic commands or authoritative state. Never gate a
lesson on a physical key name or a timer. A lesson must not open a USD layer
directly; if it needs a world, the catalog's `scene_asset` is the request.
Never branch on the Rust build profile (`is_debug()` or `debug_assertions`). The
authored `lint.rhai` policy rejects that coupling; use `is_unattended()` when
attended and automated execution need different behavior.

For physical waypoint arrival, author an Avian sensor in USD and map its generic
`SENSOR_ENTER` event to a lesson event in Rhai. Match `evt.source` to the exact
sensor with `sensor_entered(evt, sensor_id)`, then validate that `evt.value`
identifies the intended subject or one of its colliders before emitting the
lesson event. Use `SensorOccupants` in `on_start` to handle an existing contact.
Objectives and task waits consume the lesson event; distance thresholds do not
define arrival. Do not add a tutorial-specific `TriggerZone` label to a shared
sensor asset.

Example objective:

```rhai
fn mission(me, ctx) {
    [
        objective("possess", #{
            text: "Select the rover to take control",
            requires_event: "cmd:AcquireControl",
        }),
        objective("reach_flag", #{
            text: "Drive to the glowing flag",
            requires: ["possess"],
            requires_event: "flag_reached",
        }),
    ]
}
```

## USD scene contract

Scene ownership stays in the USD scene command layer. `RunScenarioAsset`
submits a `SceneTransitionIntent`; the USD owner resolves and composes it, and
the generic scenario driver waits for the completion/readiness edge.

Use standard USD composition and schemas: `subLayers`, `references`,
`payloads`, `UsdPhysics`, and `UsdLux`. For a repeatable celestial lesson,
author a non-zero epoch on the scene root with the celestial payload. Otherwise
the startup-installed scene-time policy selects current computer UTC converted
to TDB after the scene and its queued projections settle. Time-dependent
consumers wait for that result. Missing time with celestial sources is reported
by runtime warning and lint. For a basic or UI lesson, author a fixed
`DistantLight` or omit the world. Do not add tutorial-specific API schemas.

## Test without a Rust rebuild

Put behavior assertions in `assets/scenarios/tests/<name>.rhai` and use the
production scene-test binary. Keep Rust tests limited to generic scripting,
asset, USD, and lifecycle seams.

```bash
"$LUNCOSIM_BIN" test \
  --scene scenes/tests/tutorial_first_drive.usda --max-ticks 6000
```

`--validate` proves preflight only. Inspect the authored verdict and process
exit code for runtime evidence. Rhai, catalog, and authored USD edits should
be replayed through the already-built production binary whenever possible.
