Thoughts About Hijinx
Yes — seeing the existing simulation.area and simulation.region shapes helps a lot. The Kubernetes-ish version fits your system very naturally because your areas already expose the exact thing hijinx need: bounded hosts, environmental conditions, affordances, constraints, and region/feature membership.
The area schema is especially important because affordances are already typed, classified, input/output-aware activities with salience. That is basically the scheduler surface for hijinx binding. An area can already say “I support area.activity.music_venue and area.activity.ensemble_session,” with inputs like agent.musician, agent.human, and item.instrument, and outputs like signal.performance and signal.musical_coherence.
So yes: I would not make “incident” primarily Lua. I would make it a declarative resource.
The shape I see is not one kind, but three related kinds:
kind: rally.incident
Defines the pressure primitive.
kind: rally.hijinx
Binds one or more incidents to an area/environment/social frame.
kind: rally.hijinx.instance
Runtime instance: this specific party started this specific nonsense here, with these witnesses, pouch slots, and filled context.
That gives you a clean split between “definition,” “template,” and “running thing.”
The most Kubernetes-like shape is:
apiVersion: net.plantange/v1
kind: rally.incident
meta:
title: Stuck Note
name:
common:
en: stuck_note
uuid: 02bd9100-a417-7f00-000000000100
description:
common:
en: "A note is stuck in the room. Keep playing through it until the room releases."
spec:
class: musical_pressure
scope: activity
tags:
- kurzz
- music
- pressure
- recovery
requires:
affordances:
- type: area.activity.ensemble_session
min_salience: 0.50
- type: area.activity.music_venue
min_salience: 0.25
signals:
- signal.jazz_session
- signal.musical_coherence
parameters:
stuck_pitch_midi:
type: integer
default: 60
fill:
strategy: fixed
clear_target_count:
type: integer
default: 7
cooldown_steps:
type: integer
default: 8
state:
fields:
active:
type: boolean
default: false
clear_progress_count:
type: integer
default: 0
ai_contribution_count:
type: integer
default: 0
participant_contribution_count:
type: integer
default: 0
triggered_at_step:
type: integer
nullable: true
cleared_at_step:
type: integer
nullable: true
inputs:
- name: ai_matching_hits_this_step
source: incident.ai_matching_hits_this_step
type: integer
default: 0
- name: participant_matching_hits_this_step
source: incident.participant_matching_hits_this_step
type: integer
default: 0
- name: step
source: pop.step
type: integer
default: 0
lifecycle:
start:
when:
all:
- cooldown_ready: true
- no_active_incident_in_lane: true
set:
active: true
clear_progress_count: 0
ai_contribution_count: 0
participant_contribution_count: 0
triggered_at_step: "${inputs.step}"
emit:
- event: incident.stuck_note.started.v1
progress:
when:
expression: "inputs.ai_matching_hits_this_step + inputs.participant_matching_hits_this_step > 0"
increment:
clear_progress_count: "inputs.ai_matching_hits_this_step + inputs.participant_matching_hits_this_step"
ai_contribution_count: "inputs.ai_matching_hits_this_step"
participant_contribution_count: "inputs.participant_matching_hits_this_step"
emit:
- event: incident.stuck_note.progress.v1
clear:
when:
expression: "state.clear_progress_count >= parameters.clear_target_count"
set:
active: false
cleared_at_step: "${inputs.step}"
emit:
- event: incident.stuck_note.cleared.v1
outcomes:
cleared_by:
type: enum
values:
- participant
- ai
- mixed
derive:
participant: "state.participant_contribution_count > 0 and state.ai_contribution_count == 0"
ai: "state.ai_contribution_count > 0 and state.participant_contribution_count == 0"
mixed: "state.participant_contribution_count > 0 and state.ai_contribution_count > 0"
presentation:
pitch_label:
from_midi: "${parameters.stuck_pitch_midi}"
started:
display_text: "Stuck note ${pitch_label}: a note is stuck in the room."
strategy_hint_text: "Hammer ${pitch_label} to clear, or build around it."
barks:
- speaker: jax
text: "Answer me, drummer."
progress:
display_text: "Stuck note ${pitch_label}: ${state.clear_progress_count}/${parameters.clear_target_count} cleared."
strategy_hint_text: "Hammer ${pitch_label} to clear, or build around it."
cleared:
variants:
- when: "outcomes.cleared_by == 'participant'"
display_text: "You forced ${pitch_label} back into the room's pocket."
strategy_hint_text: "Result: strong recovery. Next: leave one bar of space, then re-enter clean."
barks:
- speaker: mira
text: "Ugly save. Useful save."
- when: "outcomes.cleared_by == 'mixed'"
display_text: "You and the band folded ${pitch_label} back into the groove."
strategy_hint_text: "Result: shared recovery. Next: keep pocket steady and avoid overplaying."
barks:
- speaker: jax
text: "There. Keep that pocket."
- default: true
display_text: "The band absorbed ${pitch_label} while you held position."
strategy_hint_text: "Result: passive save. Next: answer sooner on the next incident window."
barks:
- speaker: mira
text: "Leave it. Let it breathe."
rally:
deltas:
started:
escalation: 0.06
trust: 0.0
survivability: -0.02
reason: stuck_note_started
progress:
escalation: 0.0
trust: 0.0
survivability: 0.0
reason: stuck_note_progress
cleared:
escalation: -0.07
trust: 0.08
survivability: 0.05
reason: stuck_note_cleared
That spec replaces most of the Lua you showed. The Lua host becomes an interpreter/executor for a small incident DSL instead of a bespoke incident script.
The only things that still need code are the generic operations:
cooldown_ready
no_active_incident_in_lane
increment counters
evaluate expressions
derive cleared_by
emit events
format presentation strings
apply Rally deltas
convert MIDI to pitch label
That is the right direction. Stuck Note should not need custom Lua to say “add matching hits to progress; clear at seven.” That is schema territory.
Now, the more important move is how this gets instanced in a narrative frame.
That is where rally.hijinx comes in.
apiVersion: net.plantange/v1
kind: rally.hijinx
meta:
title: Make A Scene
name:
common:
en: make_a_scene
uuid: 02bd9100-a417-7f00-000000000200
description:
common:
en: "The party deliberately attracts attention to bend the room, buy time, or turn confusion into local memory."
spec:
activation:
mode: player_or_party
offered_when:
area:
affordances:
any:
- type: area.activity.music_venue
min_salience: 0.50
- class: spectacle
min_salience: 0.50
- class: social
min_salience: 0.50
party:
requires:
- agent.human
- agent.musician
binding:
hosts:
area:
kind: simulation.area
region:
kind: simulation.region
optional: true
environmental_frame:
from:
- area.environment
- region.climate
- current_activity
- rally.route_state
affordances:
stage:
select:
from: area.affordances
prefer:
- class: spectacle
- type: area.activity.music_venue
fallback:
- class: social
witness:
select:
from: social_frame.present_groups
prefer:
- largest
- highest_attention
pressure:
select:
from: rally.pressures
prefer:
- highest
pouch_slots:
reservations:
- scope: party
type: active_scheme
count: 1
- scope: area
type: public_attention
count: 1
questions:
- id: intended_audience
prompt: "Who are you trying to distract?"
choices:
- id: crowd
label: "the crowd"
- id: authority
label: "the nearest authority"
- id: rival
label: "a rival"
- id: nobody
label: "nobody, which is worse"
context_fill:
motive:
strategy: from_highest_rally_pressure
stage:
strategy: from_binding
binding: stage
witness:
strategy: from_binding
binding: witness
authority:
strategy: nearest_actor_with_role
role: authority
fallback: area_owner
tone:
strategy: random_enum
values:
- useful
- suspicious
- accidentally_artistic
- technically_not_illegal
incidents:
mode: pool
select:
count: 1
from:
- ref: rally.incident/stuck_note
weight: 1
- ref: rally.incident/side_door
weight: 1
- ref: rally.incident/cover_groove
weight: 2
- ref: rally.incident/the_mechanic
weight: 1
residue:
on_resolved:
- type: rumor
key: local_rumor
text: "The town remembers the band as helpful maniacs."
- type: rally_delta
trust: 0.04
escalation: 0.02
- type: affordance_memory
target: area
tag: remembers_public_scene
duration: local_arc
on_failed:
- type: warning
key: public_warning
text: "The room decided this was not charming."
- type: rally_delta
trust: -0.03
escalation: 0.08
This is the “instanced in a narrative frame” part.
The area supplies the host. The hijinx binds the affordances. The incident supplies the pressure.
At runtime, you produce an instance:
apiVersion: net.plantange/v1
kind: rally.hijinx.instance
meta:
title: Make A Scene at Kurzz Performance Stage
name:
common:
en: make_a_scene_kurzz_stage_001
uuid: 02bd9100-a417-7f00-000000000300
spec:
template:
ref: rally.hijinx/make_a_scene
status:
phase: active
activation:
mode: party
started_at_step: 144
started_by:
party_id: party.novan
bindings:
area:
ref: simulation.area/performance_stage
uuid: 02bd9100-a417-7f00-000000000001
selected_affordances:
stage:
type: area.activity.music_venue
class: spectacle
session:
type: area.activity.ensemble_session
class: social
environmental_frame:
noise_level: 0.75
light_level: 0.85
visibility: 0.90
narrative:
intended_audience: crowd
tone: technically_not_illegal
motive: make_gas_money
witness: room
authority: venue_owner
pouch_slots:
reserved:
- scope: party
owner: party.novan
type: active_scheme
count: 1
- scope: area
owner: simulation.area/performance_stage
type: public_attention
count: 1
incidents:
active:
- ref: rally.incident/stuck_note
instance_id: incident.stuck_note.144
parameters:
stuck_pitch_midi: 60
clear_target_count: 7
residue:
pending:
- local_rumor
- rally_delta
- affordance_memory
That is the exact Kubernetes mental model:
rally.incident = Deployment template / workload definition
rally.hijinx = higher-level controller spec
rally.hijinx.instance = live object/status
simulation.area = node/namespace/host surface
simulation.region = broader topology/environment provider
The analogy is not perfect, but it is useful.
The simulation.area is not just a location. It is a schedulable host. Its schema says areas have spatial envelope, environmental field conditions, affordances, and constraints.
The simulation.region gives broader topology: features, natives, surface, climate, hydrology, subdivisions, anchors. That makes it useful as a wider binding and context source for hijinx that depend on region-level conditions or features.
So the scheduling flow becomes:
Area declares affordances.
Region supplies broader context.
Hijinx template checks whether it can bind.
Pouch slots decide whether the party can carry it.
Hijinx fills narrative/environment context.
Hijinx deploys one or more incident specs.
Incident instance runs through generic lifecycle.
Hijinx resolves into residue.
For static area incidents, I think your instinct is exactly right. They become auto hijinx:
apiVersion: net.plantange/v1
kind: rally.hijinx
meta:
title: Millstone Remembers
name:
common:
en: millstone_remembers
uuid: 02bd9100-a417-7f00-000000000400
spec:
activation:
mode: auto
schedule:
trigger: area_entered
cooldown_steps: 256
probability: 0.35
binding:
hosts:
area:
kind: simulation.area
match:
meta.name.common.en: millstone_bridge
context_fill:
forbidden_region:
strategy: fixed
value: bridge_sequence
witness:
strategy: random_enum
values:
- old_man
- deputy
- venue_owner
- nobody_visible
incidents:
mode: sequence
sequence:
- ref: rally.incident/forbidden_bridge
residue:
on_resolved:
- type: affordance_memory
target: area
tag: survived_millstone_bridge
on_failed:
- type: rumor
key: millstone_bridge_played
That gives you one unifying model:
Player-started hijinx:
activation.mode = player_or_party
Static area incident:
activation.mode = auto
Both:
bind area + context + incident + residue
That is clean. And it lets you keep the whimsy without writing bespoke scripts for every goat-adjacent misunderstanding.
My suggested kind taxonomy:
simulation.area
A bounded host with environment, affordances, and constraints.
simulation.region
A larger context field containing areas/features/natives/climate/topology.
rally.incident
A declarative pressure unit with lifecycle, inputs, progress, clear, presentation, and immediate deltas.
rally.hijinx
A declarative scheme/context wrapper that binds incidents to area affordances, environmental frame, player/party intent, pouch slots, and residue.
rally.hijinx.instance
A live instantiation of a hijinx with filled context, reserved pouch slots, active incidents, and pending/resolved residue.
The main design warning: do not let rally.incident grow upward into hijinx. Keep it dumb and sharp.
An incident should know:
what pressure is active
what inputs count
how progress happens
when it clears
what immediate feedback to emit
A hijinx should know:
why this is happening here
who started it
what area affordances are being abused
who might remember
what slots are occupied
what residue remains
That keeps Stuck Note from caring whether it is a tutorial problem, a random Kurzz pressure event, or part of “Make A Scene at the funeral home.” It is just the stuck note. The hijinx makes it funny, local, and consequential.