// kits/dj/kit.ron
(
id: "dj",
version: "1.2.0",
author: "X",
license: "CC-BY-4.0",
needs: [state_runtime, panel, sound, beat_channel], // capabilities, shown to room owners
state_schema: 3,
archetypes: {
"speaker": "speaker.arch.ron",
"turntables": "station.arch.ron#turntables",
"drums": "station.arch.ron#drums",
"bass": "station.arch.ron#bass",
},
group: "deck.ron", // shared state for all archetypes of this kit in a room
sounds: ["kick.snd", "snare.snd", "hat.snd", "ohat.snd", "bass.snd"],
patterns: ["groove.pat"],
panel: "panel.ron",
behaviour: Some("dj.wasm"), // optional L1: bassline + retempo
budgets: (beams: 2400, fills: 600, lights: 6, voices: 12),
)
// kits/dj/speaker.arch.ron
(
footprint: (1, 1),
blocks: true,
ball_height: 2.6,
light: (height: 2.0, reach: 5.0, rgb: [1.4, 0.3, 1.3]),
solid: (height: 3.0, class: furniture),
params: { hue: pick(["#ff33bf", "#26e5ff", "#bf4dff"], seed: id) },
geometry: [
cabinet(along: 1.7, deep: 0.9, z: [0, "min(ceiling - 0.2, 3.2)"], edge: neon(hue, 1.6)),
for z, r in [(0.27, 0.56), (0.60, 0.56), (0.86, 0.20)]: [
face_disc(at: front(0.47), z: "h * z", r: r, fill: "#08080d"),
group(anim: pulse(channel: "beat.thump", base: 0.35, gain: 1.5), lines: [
face_ring(r: r, width: 1.1, ink: neon(hue, 2.2)),
face_ring(r: "r*0.62", width: 0.6, ink: neon(hue, 1.1)),
face_ring(r: "r*0.22", width: 0.8, ink: neon("#fff2ff", 1.6)),
]),
if r > 0.5: emitter(rings, from: front(0.47), z: "h * z", count: 4, r0: 0.35, step: 0.6,
spread: 2.4, standing: true, channel: "beat", width: 1.8, ink: neon(cycle, 2.8)),
],
],
)
// kits/dj/deck.ron
(
persist: runtime,
roles: [
(id: "dj", station: "turntables", label: "Turntables"),
(id: "drums", station: "drums", label: "Drum machine"),
(id: "bass", station: "bass", label: "Bass synth"),
],
one_role_per_player: true,
claim: (walk_beside: true, release_on_leave: true, toggle: true),
clock: (bpm: (key: "bpm", range: (80, 160), default: 122), lead_ticks: 36, retempo: keep_phase),
state: {
"playing": (type: bool, default: false, write: role("dj"), on_set: clock_start),
"bpm": (type: u8, range: (80, 160), default: 122, write: role("dj")),
"xfade": (type: u8, default: 128, write: role("dj")),
"kick.level": (type: u8, default: 200, write: role("drums")),
// 16 more drum knobs, 8 bass knobs, generated by a template like the panel grid
"seed": (type: u8, default: 7, write: role("bass")),
},
rate: (cost: 0.2, coalesce_per_tick: true),
)
# kits/dj/groove.pat (mini-notation, 16 steps per bar)
tempo deck.clock
voice kick kit:dj/kick x . . . x . . . x . . . x . . .
voice clap kit:dj/snare . . . . x . . . . . . . x . . .
voice hat kit:dj/hat 6 3 . 3 6 3 . 3 6 3 . 3 6 3 . 4
voice ohat kit:dj/ohat . . 8 . . . 8 . . . 8 . . . 8 .
every 4 bars, steps 12..15: clap 5 6 8 9
swing odd steps late by "deck.swing / 255 / 3"
bind {kick,clap,hat,ohat}.gain = "level/255 * strength/9 * cos(deck.xfade/255 * pi/2)"
bind {kick,clap,hat,ohat}.tune = "exp2((pitch - 128)/128)"
bind {kick,clap,hat,ohat}.bright = "exp2((tone - 128)/128 * 2)"
bind {kick,clap,hat,ohat}.v = "snap/255"
voice bass kit:dj/bass call dj.wasm::bass_note(step, deck.bass.*)
bind bass.gain = "note.gain * sin(deck.xfade/255 * pi/2)"
// kits/dj/panel.ron: rendered by the host, never kit HTML
panel: (
title: "DJ BOOTH",
sections: [
(role: "dj", rows: [
[button(label: "Play", toggles: "playing"), slider(label: "BPM", knob: "bpm", min: 80, max: 160, show_value: true)],
[crossfader(knob: "xfade", left: "DRUMS", right: "BASS")],
]),
(role: "drums", grid: (columns: ["level", "pitch", "tone", "snap"],
rows: ["kick", "snare", "hat", "ohat"], knob: "{row}.{col}")),
(role: "drums", rows: [[slider(label: "swing", knob: "swing")]]),
(role: "bass", rows: [
[select(label: "Key", knob: "root", options: notes),
select(label: "Scale", knob: "scale", options: ["minor", "dorian", "phrygian", "minor pentatonic", "major"])],
[button(label: "New line", bump: "seed")],
[slider(knob: "density"), slider(knob: "octaves"), slider(knob: "filter"), slider(knob: "squelch"), slider(knob: "level")],
]),
],
holders: true, // the "Turntables: X / Drum machine: free" row
)
package maschinenraum:kit@0.1.0;
interface host {
// Everything is scoped to the calling kit instance in one room. No ambient authority.
get: func(key: string) -> option<list<u8>>;
set: func(key: string, value: list<u8>) -> result<_, string>; // schema + ACL checked
emit: func(ev: list<u8>); // logged if persist: log
broadcast: func(msg: list<u8>); // S2C::Ext to the room
tell: func(player: u32, msg: list<u8>);
tick: func() -> u64; // the world tick
rand: func(seed: u64, n: u64) -> u64; // splitmix64, deterministic
players: func() -> list<player>; // id, nick, tile, role
walk-beside: func(player: u32, obj: u32) -> bool; // core pathing, budgeted
timer: func(after-ticks: u32, tag: u32);
}
interface behaviour {
on-load: func(state: option<list<u8>>, schema: u32) -> list<u8>; // migrate + init
on-interact: func(player: u32, obj: u32);
on-msg: func(player: u32, msg: list<u8>);
on-timer: func(tag: u32);
reduce: func(state: list<u8>, ev: list<u8>) -> list<u8>; // log replay
}
// Pure, deterministic, and callable on the client too: no host imports except `rand`.
interface shared {
bass-note: func(step: u64, knobs: list<u8>) -> option<note>;
}
// kits/dj/waves.wgsl: a kit anim, compiled into a per-kit pipeline variant (L2)
fn kit_anim(p: vec3<f32>, info: vec4<f32>, ch: KitChannels) -> KitAnim {
// ch.beat, ch.tick, ch.knobs (vec4 of the object's bound state)
}
// Gates: naga validation, WebGL2 translation, compile time measured in headless
// Chrome (reject above ~300 ms), no derivatives, no dynamically indexed arrays,
// and a preset fallback the kit must also declare. Signed kits only.