# Item Schema

## World Item (server-authoritative)

```json
{
  "id": "string",
  "type": "radio_station | dice | wheel | clock | widget | piano",
  "title": "string",
  "x": 0,
  "y": 0,
  "createdBy": "user-id",
  "createdByName": "username",
  "updatedBy": "user-id",
  "updatedByName": "username",
  "createdAt": 1735689600000,
  "updatedAt": 1735689600000,
  "version": 1,
  "capabilities": ["editable", "carryable", "deletable", "usable"],
  "useSound": "sounds/roll.ogg",
  "emitSound": "sounds/clock.ogg",
  "params": {},
  "carrierId": null
}
```

- `useSound`: optional client-played one-shot sound when item `use` succeeds; global item field and not user-editable in V1.
- `emitSound`: optional continuously-looping spatial sound emitted from the item on the grid; global item field and not user-editable in V1.
- `capabilities`, `useSound`, and `emitSound` are derived from global item-type definitions at runtime (not stored per-instance in persisted state).
- `createdBy` / `updatedBy` are stable user IDs.
- `createdByName` / `updatedByName` are display-name snapshots used for inspect/readout text.
- `useCooldownMs`: global per item type (`radio_station=1000`, `dice=1000`, `wheel=4000`, `clock=1000`, `widget=1000`, `piano=1000`), not per-instance editable.
- `emitRange`: global spatial range default per item type (`radio_station=10`, `dice=15`, `wheel=15`, `clock=10`, `widget=15`, `piano=15`).
  - `radio_station` can override this per instance via `params.emitRange` (`5..20`).
- `directional`: global directional attenuation flag per item type (`radio_station=true`, others `false`); `widget` can override per instance via `params.directional`.

## Persisted Item State (`server/runtime/items.json`)

```json
{
  "id": "string",
  "type": "radio_station | dice | wheel | clock | widget | piano",
  "title": "string",
  "x": 0,
  "y": 0,
  "createdBy": "user-id",
  "createdByName": "username",
  "updatedBy": "user-id",
  "updatedByName": "username",
  "createdAt": 1735689600000,
  "updatedAt": 1735689600000,
  "version": 1,
  "params": {},
  "carrierId": null
}
```

- Persisted state stores only instance data.
- Global/type-level properties are loaded from server registry in `server/app/item_catalog.py`.
- Per-type use/update validation and message behavior are implemented in per-item modules under `server/app/items/types/*/definition.py`, `validator.py`, and `actions.py`, discovered via plugins in `server/app/items/types/*/plugin.py`.
- Client-side add/edit metadata is consumed from `welcome.uiDefinitions` via `client/src/items/itemRegistry.ts` (no local fallback definitions).
- End-to-end add-item template: `docs/item-type-template.md`.

## Type Params

### `radio_station`

```json
{
  "streamUrl": "",
  "enabled": true,
  "mediaChannel": "stereo",
  "mediaVolume": 50,
  "mediaEffect": "off",
  "mediaEffectValue": 50,
  "stationName": "",
  "nowPlaying": "",
  "facing": 0,
  "emitRange": 10
}
```

- `streamUrl`: string, empty allowed until configured.
- `enabled`: boolean on/off flag.
  - UI behavior: in property menu, `Enter` toggles on/off directly.
- `mediaVolume`: integer, range `0-100`, default `50`.
- `mediaChannel`: one of `stereo | mono | left | right`, default `stereo`.
- `mediaEffect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
- `mediaEffectValue`: number, range `0-100`, precision `0.1`.
- UI visibility: `mediaEffectValue` is shown only when `mediaEffect != off` (`visibleWhen: {"mediaEffect": "!off"}`).
- `stationName`: server-managed station label derived from ICY metadata when available.
- `nowPlaying`: server-managed stream title derived from ICY metadata when available.
- `facing`: number, range `0-360`, step `1` (used when `directional=true`).
- UI visibility: `facing` is shown only when `directional=true` (`visibleWhen` metadata).
- `emitRange`: integer, range `5-20`, default `10`.

### `dice`

```json
{
  "sides": 6,
  "number": 2
}
```

- `sides`: integer, range `1-100`.
- `number`: integer, range `1-100`.

### `wheel`

```json
{
  "spaces": "yes, no"
}
```

- `spaces`: comma-delimited string of values.
- Server validation:
  - must include at least 1 value
  - max 100 values
  - each value max 80 chars

### `clock`

```json
{
  "timeZone": "America/Detroit",
  "use24Hour": false,
  "topOfHourAnnounce": true,
  "alarmEnabled": false,
  "alarmTime": "12:00 AM"
}
```

- `timeZone`: one representative IANA zone per world UTC offset. Includes:
  `America/Anchorage`, `America/Argentina/Buenos_Aires`, `America/Chicago`, `America/Detroit`,
  `America/Halifax`, `America/Indiana/Indianapolis`, `America/Kentucky/Louisville`,
  `America/Los_Angeles`, `America/St_Johns`, `Asia/Bangkok`, `Asia/Dhaka`, `Asia/Dubai`,
  `Asia/Hong_Kong`, `Asia/Kabul`, `Asia/Karachi`, `Asia/Kathmandu`, `Asia/Kolkata`,
  `Asia/Seoul`, `Asia/Singapore`, `Asia/Tehran`, `Asia/Tokyo`, `Asia/Yangon`,
  `Atlantic/Azores`, `Atlantic/South_Georgia`, `Australia/Brisbane`, `Australia/Darwin`,
  `Australia/Eucla`, `Australia/Lord_Howe`, `Europe/Berlin`, `Europe/Helsinki`,
  `Europe/London`, `Europe/Moscow`, `Pacific/Apia`, `Pacific/Auckland`, `Pacific/Chatham`,
  `Pacific/Honolulu`, `Pacific/Kiritimati`, `Pacific/Noumea`, `Pacific/Pago_Pago`, `UTC`.
- `use24Hour`: boolean (or `on/off` in updates), default `false`.
- `topOfHourAnnounce`: boolean (or `on/off` in updates), default `true`.
- `alarmEnabled`: boolean (or `on/off` in updates), default `false`.
- `alarmTime`: default `12:00 AM`; accepts `HH:MM` (24-hour mode) or `H:MM AM/PM` (12-hour mode).
- UI visibility: `alarmTime` is shown only when `alarmEnabled=true` (`visibleWhen` metadata).
- Global defaults: `useSound=none`, `emitSound=sounds/clock.ogg`.
- Clock speech announcement audio is emitted via `item_clock_announce` packets using `/sounds/clock/el640/*.ogg`.

### `widget`

```json
{
  "enabled": true,
  "directional": false,
  "facing": 0,
  "emitRange": 15,
  "emitVolume": 100,
  "emitSoundSpeed": 50,
  "emitSoundTempo": 50,
  "emitInitialDelay": 0,
  "emitLoopDelay": 0,
  "emitEffect": "off",
  "emitEffectValue": 50,
  "useSound": "",
  "emitSound": ""
}
```

- `enabled`: boolean (or `on/off` in updates), default `true`.
- `directional`: boolean (or `on/off` in updates), default `false`.
- `facing`: number, range `0-360`, step `1`.
- UI visibility: `facing` is shown only when `directional=true` (`visibleWhen` metadata).
- `emitRange`: integer, range `1-20`, default `15`.
- `emitVolume`: integer, range `0-100`, default `100`.
- `emitSoundSpeed`: integer, range `0-100`, default `50`; controls emitted sound speed/pitch (`0=0.5x`, `50=1.0x`, `100=2.0x`).
- `emitSoundTempo`: integer, range `0-100`, default `50`; controls emitted sound tempo (`0=0.5x`, `50=1.0x`, `100=2.0x`).
- `emitInitialDelay`: number, range `0-300`, precision `0.1`, default `0`; delay in seconds before emitted audio starts after enable.
- `emitLoopDelay`: number, range `0-300`, precision `0.1`, default `0`; delay in seconds between each emitted playback.
- `emitEffect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
- `emitEffectValue`: number, range `0-100`, precision `0.1`, default `50`.
- `useSound`: empty, filename (assumed under `sounds/`), or full URL.
- `emitSound`: empty, filename (assumed under `sounds/`), or full URL.

### `piano`

```json
{
  "instrument": "piano",
  "voiceMode": "poly",
  "octave": 0,
  "attack": 15,
  "decay": 45,
  "release": 35,
  "brightness": 55,
  "emitRange": 15
}
```

- `instrument`: one of
  `piano | electric_piano | guitar | organ | bass | violin | synth_lead | brass | nintendo | drum_kit`.
- `voiceMode`: one of `poly | mono`.
- `octave`: integer, range `-2..2` (default `0`; bass defaults to `-1`).
- Selecting a new instrument resets `voiceMode`/`octave`/`attack`/`decay`/`release`/`brightness` to that instrument's defaults.
- `attack`: integer, range `0-100`, default `15`.
- `decay`: integer, range `0-100`, default `45`.
- `release`: integer, range `0-100`, default `35`.
- `brightness`: integer, range `0-100`, default `55`.
- `emitRange`: integer, range `5-20`, default `15`.
- `songId`: server-managed song reference used for piano demo/playback content.
- Recorded/demo song payload is stored in server song registry (`runtime/piano_songs.json`) using compact format:
  - `meta`: shared synth parameters
  - `keys`: keyId dictionary
  - `states`: parameter-state dictionary (for mid-song instrument/param changes)
  - `events`: `[t, keyIndex, midi, on, stateIndex]`

## Packet Shapes

- `item_upsert`:

```json
{
  "type": "item_upsert",
  "item": { "..." : "World Item" }
}
```

- `item_remove`:

```json
{
  "type": "item_remove",
  "itemId": "item-id"
}
```

- `item_action_result`:

```json
{
  "type": "item_action_result",
  "ok": true,
  "action": "add | pickup | drop | delete | transfer | use | secondary_use | update",
  "message": "human-readable status",
  "itemId": "optional-item-id"
}
```

- `item_use_sound`:

```json
{
  "type": "item_use_sound",
  "itemId": "item-id",
  "sound": "sounds/roll.ogg",
  "x": 12,
  "y": 8
}
```

- `item_clock_announce`:

```json
{
  "type": "item_clock_announce",
  "itemId": "item-id",
  "sounds": ["/sounds/clock/el640/its.ogg", "/sounds/clock/el640/2.ogg", "/sounds/clock/el640/PM.ogg"],
  "x": 12,
  "y": 8
}
```

- `item_piano_note`:

```json
{
  "type": "item_piano_note",
  "itemId": "item-id",
  "senderId": "user-id",
  "keyId": "KeyA",
  "midi": 60,
  "on": true,
  "instrument": "piano",
  "voiceMode": "poly",
  "octave": 0,
  "attack": 15,
  "decay": 45,
  "release": 35,
  "brightness": 55,
  "x": 12,
  "y": 8,
  "emitRange": 15
}
```
