# Tutorial: first skeletal animation

Cook a skinned glTF into a `.shinmodel` with skeleton + clips, place it with `hi.ComponentRender` + `hi.ComponentAnimation`, and either play a clip by name or bind an animation graph.

Sample: `hikari-games/example/assets/models/Fox.glb`, graph `assets/animations/locomotion-demo.animgraph.json`, scene `scenes/skeletal_animation.json`, archetype `skinned_bar`.

## Prerequisites

- Product build that cooks assets (Kaji) so Shinra emits SRM1 skeleton + clip trailers.
- A skinned glTF/GLB with **exactly one skin**, JOINTS_0 / WEIGHTS_0, and one or more animation clips. Every authored animation is cooked and keeps its name. A USDZ with one `Skeleton`, `SkelBindingAPI` meshes and `SkelAnimation` clips cooks the same way (see [asset formats](/docs/systems/asset-formats.md)).

## 1. Source model

Sample (Fox GLB, clips `Survey` / `Walk` / `Run`):

| Path | Role |
|------|------|
| `assets/models/Fox.glb` | Source skinned GLB |
| `assets/models/Fox.model.json` | Placeable model doc (Shinra companion) |
| `assets/models/Fox.material.json` | Cooked material companion |
| `assets/animations/locomotion-demo.animgraph.json` | Clip states + `speed` parameter |

A two-bone bar (`assets/models/skinned_bar.gltf`, clip `"Idle"`) is still in the shared pack if you want a minimal clip-only actor. Clip-only playback is also on `scenes/brainstem_animation.json` (HUD tour).

## 2. Cook

Kaji product / assets path (preferred):

```sh
bin/kaji/kaji assets --workspace=<repo> --project=../hikari-games/example
# or full game/editor build, which cooks as part of staging
bin/kaji/kaji game --workspace=<repo> --project=../hikari-games/example --type=monolithic --config=debug
```

Direct Shinra (debug / CI-style check):

```sh
cd src/shinra && cargo run --release -- \
  --input ../../../hikari-games/example/assets \
  --output /tmp/shinra-out \
  --target macos-metal
```

Cooked `models/Fox.shinmodel` must report `has_skeletal_animation`, non-zero `joint_count`, and clip names (sample: `Survey`, `Walk`, `Run`).

## 3. Archetype with animation

`ComponentAnimation` is attachable storage; the archetype must list it so scene `"animation"` binds a player:

```zig
const hi = @import("hikari_game");

pub const SkinnedBarEntity = hi.defineActor(.{
    .archetype = "skinned_bar",
    .components = .{ hi.ComponentRender, hi.ComponentAnimation },
});
```

Sample: `hikari-games/example/src/entities/skinned_bar_entity.zig`.

## 4. Scene actor

Scene `model` placeables use a `.model.json` URI. Graph refs are extension-free stems.

```json
{
  "id": "skel_bar",
  "archetype": "skinned_bar",
  "transform": {
    "position": [0, 0.12, 0],
    "rotation_euler": [0, 0, 0],
    "scale": [0.02, 0.02, 0.02]
  },
  "components": {
    "render": {
      "model": "asset://./models/Fox.model.json",
      "is_visible": true,
      "casts_shadow": true
    },
    "animation": {
      "graph": "asset://./animations/locomotion-demo",
      "playing": true,
      "looping": true,
      "speed": 1.0,
      "time": 0.0
    }
  }
}
```

- Clip strings (when you author `"clip"` instead of `"graph"`) are hashed with FNV-1a and matched to the cooked name table.
- The scene loader derives pack dependencies from model and graph references. Configure their membership in `configs/layout.json`; do not author a scene `packs` list.

Full sample: `hikari-games/example/scenes/skeletal_animation.json`.

## 5. Play from code (optional)

Clip control (takes the actor off the graph until you hand it back):

```zig
hi.animation().play(actor, "Survey");
hi.animation().setLooping(actor, true);
hi.animation().setSpeed(actor, 1.0);
// hi.animation().pause(actor);  // retain current time
// hi.animation().resumePlayback(actor); // continue current clip
// hi.animation().stop(actor);
// hi.animation().setTime(actor, 0.0);
// hi.animation().playGraph(actor); // return to the bound state machine
```

Requires the entity to have `ComponentAnimation` (same as the scene block).

Drive the sample locomotion graph with the `speed` parameter (0 = Survey, ~0.1 = Walk, >1 = Run):

```zig
_ = hi.animation().setGraphFloat(actor, "speed", 1.5);
_ = hi.animation().playGraph(actor);
```

For models with several clips, enumerate the cooked names instead of duplicating
an exporter-specific list in gameplay code:

```zig
const animation = hi.animation();
for (0..animation.clipCount(actor)) |i| {
    const clip_name = animation.clipName(actor, @intCast(i)) orelse continue;
    // Populate a developer menu, select an initial state, etc.
    _ = clip_name;
}
if (animation.hasClip(actor, "Run")) animation.play(actor, "Run");
```

Kawa exposes the same controls and clip discovery through `Animation`:

```kawa
let actor = Actors.find("hero");
let count = Animation.clip_count(actor);
let clip = Animation.clip_name(actor, 0);
Animation.play(actor, clip);
Animation.set_graph_float(actor, "speed", 1.5);
Animation.set_graph(actor, "asset://./animations/locomotion-demo");
Animation.pause(actor);
Animation.resume_playback(actor);
Animation.stop(actor); // Pauses at frame zero.
```

`Animation.has_clip`, `set_time`, `set_speed`, `set_looping`, `is_playing`,
`set_graph_bool`, and `trigger_graph` mirror the Zig gameplay API. Invalid actors,
missing animation components, and unknown clips return `false` (or `nil` for
`clip_name`) instead of trapping.

The model asset viewer shows every clip in a scrollable list. Selecting a row
switches the active clip and resets its timeline; play, pause, stop, loop, scrub,
and the joint overlay all operate on the selection.

## 6. Listen for clip events (optional)

Markers placed on a clip in the graph editor (select a single-clip state → *Events*
→ *Add event*) reach gameplay on the frame the animation crosses them. The demo
graph carries a `footstep` marker on Walk and Run. Declare the callback on any logic
component of the archetype:

```zig
pub fn onAnimationEvent(self: *@This(), actor: hi.ActorContext, event: *const hi.AnimationEvent) void {
    const id = actor.id;
    _ = self;
    if (event.is("footstep")) hi.std.log.info("{s} stepped (foot {d})", .{ hi.world().name(id), event.value });
}
```

Or poll from any system that runs after animation:

```zig
var it = hi.animation().events(actor);
while (it.next()) |event| if (event.is("footstep")) playFootstep(event.value);
if (hi.animation().fired(actor, "impact")) applyHit();
```

Kawa gets the same marker as `on_animation_event(name, value, time)`:

```kawa
fn on_animation_event(name, value, time) {
    if (name == "footstep") { Audio.play("footstep"); }
}
```

A marker belongs to the clip, so it fires from every state and blend space that
plays that clip; in a blend only the dominant clip fires, so a Walk/Run blend does
not double its footsteps.

## 7. Let a one-shot state return by itself (optional)

Add an `Attack` state with `looping` off and draw an arrow `Attack → Survey`. In the
arrow's card tick **Wait for the clip** and leave *Exit at* on `1`: the arrow fires the
moment the clip has played out, no parameter needed. Untick **Can be interrupted** on
`Survey → Attack` if a second trigger during the wind-up must not restart it.

For a hit reaction from anywhere, drag from the **Any State** node's pin to a `Hit`
state and give the arrow a `hit` trigger. It fires from every state on the layer, and
a second trigger while already in `Hit` restarts it.

Gameplay can ask where the graph is instead of mirroring it:

```zig
if (hi.animation().state(actor)) |s| {
    if (s.is("Attack") and s.progress > 0.6) self.hitbox_live = true;
}
```

## 8. Keep Walk and Run in step (optional)

Type the same **Sync group** name — say `Locomotion` — on the Walk and Run states. Both now
run on one phase clock: at any moment each clip is at the same fraction of its own cycle,
the blend between them advances at the blended cadence, and Walk → Run carries the phase
across instead of restarting Run from its first frame. Author the clips to start on the
same foot; the engine aligns the phase, not the content.

## 9. Hang a prop on a joint (optional)

Give the Fox a child actor (a sword, a lamp) and, with the child selected, use
**Attach to joint** in the inspector to pick a joint of the Fox's rig. The child follows
that joint as the Fox animates; its own transform is an offset from the joint. From
code:

```zig
_ = hi.world().attachToJoint(sword, fox, "b_Head_05");
if (hi.animation().jointNamed(fox, "b_Head_05", .world)) |head| hi.std.log.info("head at {d:.2}", .{head.position[1]});
```

## Verify

1. Cook succeeds and `Fox.shinmodel` has skeletal flag + joints + animations.
2. Open the editor / game, load scene `skeletal_animation`.
3. The Fox should pose (Survey while `speed` is 0). Use the visible **Speed** slider: above 0.1 walks, above 1 runs. The control sets the graph parameter through `hi.animation().setGraphFloat`.
4. For clip-only, open `scenes/brainstem_animation` (Browse scenes → Tutorials → Clip Playback) or call `hi.animation().play` with an enumerated name.
5. Change `"clip"` or call `hi.animation().play` with a wrong name → soft no-op (clip hash miss).

## See also

- [Skeletal animation](/docs/systems/animation.md) — full reference: graphs, layers, events, foot IK, root motion, readback
- [First mesh and material](/docs/tutorials/first-mesh-material.md) — `asset://` render binds
- [Assets and Shinra](/docs/systems/assets-and-shinra.md) — cook / packs
