Skip to content

Step 2 - Build The Stuck Note Incident

You built the pop in Step 1. Now add the first thing that can go wrong.

This step adds the first concrete incident in Kurzz: Stuck Note.

The goal is to create one musical problem that is:

  • legible to non-musicians
  • solvable by player or AI behavior
  • interesting enough to create different play styles

This tutorial is content/scripting-first. No Rust changes are required. Use Lua scripts first; use Rhai only if your scenario runtime is configured for Rhai.


Before You Start

This step builds on Step 1. scenarios/kurzz/pop/follow_the_leader.yaml must exist and be loadable.

If you have not done Step 1, read 01_lets_pop.md first. The incident spec in this step adds fields to that pop file.


What We Are Building

Player-facing premise:

"A note is stuck in the room. Keep playing through it until the room releases."

Core clear rule:

  • after trigger, count matching plays of one stuck pitch
  • clear when total matching plays reaches 7
  • any participant can contribute (player or AI)

This guarantees eventual recovery while still allowing strategy.

The incident also has survival stakes: while it is unresolved, Rally escalation pressure climbs. On clear, trust and stability trend back up. This is not just musical flavor -- it connects to the player's logistics situation in the larger game.


Why This Design Works

Stuck Note supports multiple valid responses:

  1. Fast clear (spam)

Hammer the stuck pitch to clear quickly.

  1. Drone rebuild (coherent ensemble play)

Use the stuck pitch as a drone, rebuild phrase coherence around it, and clear as a side effect.

  1. Intentional hold (creative risk)

Delay clearing on purpose to hold tension, then resolve later.

"Not clearing yet" is not automatic failure. It is a strategic posture until survival pressure forces a change.


Files You Will Touch

Minimum expected files for this step:

  • scenarios/kurzz/pop/follow_the_leader.yaml (from Step 1; you are adding incident fields)
  • scenarios/kurzz/scripts/musician.stuck_note.lua (new)

Optional (if you separate host script concerns):

  • scenarios/kurzz/scripts/musician.tutorial_cue.lua (existing cue script)

You are defining incident policy in spec plus script, not in engine code.


Spec Surface (YAML)

Add incident metadata to the pop rule set so downstream systems can react.

Suggested fields to emit while the incident is active:

  • incident_kind: stuck_note
  • incident_active: true|false
  • incident_stuck_pitch_midi: <int>
  • incident_clear_progress_count: <int>
  • incident_clear_target_count: 7

If your current rule shape cannot hold all of these directly, keep only the minimum and let script emit the rest as events.

Concrete Rally consequence example (for readability):

  • while active and unresolved, escalation pressure trends up
  • on clear, trust or stability trends up and escalation flattens

This gives the player a visible survival consequence, not just musical flavor.


Script Surface (Lua Preferred)

Create scenarios/kurzz/scripts/musician.stuck_note.lua.

Responsibilities:

  1. Start incident (choose stuck pitch, set active state)
  2. Watch note stream contribution events
  3. Increment progress when a matching pitch arrives
  4. Emit progress events
  5. Emit clear event exactly once at count >= 7

Pseudocode shape:

function tick()
  local active = host.get("incident", "stuck_note_active")

  if not active then
    -- choose pitch and start once
    -- host.set("incident", "stuck_note_active", true)
    -- host.set("incident", "stuck_pitch_midi", 60)
    -- host.set("incident", "clear_progress_count", 0)
    -- host.emit("incident.stuck_note.started.v1", {...})
    return
  end

  -- inspect notes since last tick (implementation-specific host access)
  -- for each matching pitch: increment and emit progress

  local progress = host.get("incident", "clear_progress_count")
  if progress >= 7 then
    -- set inactive and emit cleared exactly once
    -- host.emit("incident.stuck_note.cleared.v1", {...})
  end
end

Rhai fallback:

  • keep identical state fields and event names
  • port syntax only; preserve behavior contract

Event Contract To Keep Stable

Use the same event names and payload intent from the workflow plan:

  1. incident.stuck_note.started.v1
  2. incident.stuck_note.progress.v1
  3. incident.stuck_note.cleared.v1

Keep these stable so Walker/UI work can iterate without script churn.


UX Language Guidelines

Use stage language, not telemetry language.

Good:

  • "Stuck note C4: 3/7 cleared. Mira is helping."
  • "You can hammer C4 to clear, or build around it."
  • "Still active. You are holding tension on purpose."

Avoid:

  • "incident_progress = 0.42"
  • "state mutation success"

Test Pass (Content/Scripting)

Before handing this off, verify these runs:

  1. Player-clear run

  2. player repeats stuck pitch

  3. clear occurs exactly at 7

  4. AI/mixed clear run

  5. player does not force clear

  6. AI or mixed contribution reaches 7 and clears

  7. Intentional delayed clear run

  8. incident remains active for a while

  9. no immediate fail state
  10. clear later by chosen strategy

All three should produce legible event traces and human-readable feedback text.


Done When

  • incident can start from spec+script
  • progress updates on matching note plays from any participant
  • clear happens exactly once at 7
  • at least one run each for player clear, AI/mixed clear, delayed clear
  • consequences are visible as Rally pressure/stability change

What Comes Next

After this step, the next tutorial should connect the incident stream to Walker feedback panels and result language so players see cause/effect immediately.


References

  • `plantangenet/music/docs/MUSIC_IN_PLANTANGENET.md -- How "music" works in plantangeent.
  • plantangenet/music/docs/NEW_POP.md -- How to Develop a new pop
  • scenarios/kurzz/docs/A_new_drummer.md -- lore and voice reference for the Kurzz scenario