Make radio emit range editable (5-20)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.21 R119";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R120";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const DEFAULT_CLOCK_TIME_ZONE_OPTIONS = [
|
|||||||
const DEFAULT_ITEM_TYPE_SEQUENCE: ItemType[] = ['clock', 'dice', 'radio_station', 'wheel'];
|
const DEFAULT_ITEM_TYPE_SEQUENCE: ItemType[] = ['clock', 'dice', 'radio_station', 'wheel'];
|
||||||
|
|
||||||
const DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES: Record<ItemType, string[]> = {
|
const DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES: Record<ItemType, string[]> = {
|
||||||
radio_station: ['title', 'streamUrl', 'enabled', 'channel', 'volume', 'effect', 'effectValue', 'facing'],
|
radio_station: ['title', 'streamUrl', 'enabled', 'channel', 'volume', 'effect', 'effectValue', 'facing', 'emitRange'],
|
||||||
dice: ['title', 'sides', 'number'],
|
dice: ['title', 'sides', 'number'],
|
||||||
wheel: ['title', 'spaces'],
|
wheel: ['title', 'spaces'],
|
||||||
clock: ['title', 'timeZone', 'use24Hour'],
|
clock: ['title', 'timeZone', 'use24Hour'],
|
||||||
@@ -131,6 +131,7 @@ export function itemTypeLabel(type: ItemType): string {
|
|||||||
|
|
||||||
export function itemPropertyLabel(key: string): string {
|
export function itemPropertyLabel(key: string): string {
|
||||||
if (key === 'use24Hour') return 'use 24 hour format';
|
if (key === 'use24Hour') return 'use 24 hour format';
|
||||||
|
if (key === 'emitRange') return 'emit range';
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -525,7 +525,9 @@ function itemLabel(item: WorldItem): string {
|
|||||||
|
|
||||||
function getItemSpatialConfig(item: WorldItem): { range: number; directional: boolean; facingDeg: number } {
|
function getItemSpatialConfig(item: WorldItem): { range: number; directional: boolean; facingDeg: number } {
|
||||||
const global = getItemTypeGlobalProperties(item.type);
|
const global = getItemTypeGlobalProperties(item.type);
|
||||||
const rawRange = Number(global.emitRange);
|
const rawParamRange = Number(item.params.emitRange);
|
||||||
|
const rawGlobalRange = Number(global.emitRange);
|
||||||
|
const rawRange = Number.isFinite(rawParamRange) && rawParamRange > 0 ? rawParamRange : rawGlobalRange;
|
||||||
const range = Number.isFinite(rawRange) && rawRange > 0 ? rawRange : 15;
|
const range = Number.isFinite(rawRange) && rawRange > 0 ? rawRange : 15;
|
||||||
const directional = global.directional === true;
|
const directional = global.directional === true;
|
||||||
const rawFacing = Number(item.params.facing ?? 0);
|
const rawFacing = Number(item.params.facing ?? 0);
|
||||||
@@ -713,6 +715,11 @@ function getItemPropertyValue(item: WorldItem, key: string): string {
|
|||||||
if (!Number.isFinite(parsed)) return '0';
|
if (!Number.isFinite(parsed)) return '0';
|
||||||
return String(Math.round(normalizeDegrees(parsed) * 10) / 10);
|
return String(Math.round(normalizeDegrees(parsed) * 10) / 10);
|
||||||
}
|
}
|
||||||
|
if (key === 'emitRange') {
|
||||||
|
const parsed = Number(item.params.emitRange ?? getItemTypeGlobalProperties(item.type)?.emitRange ?? 15);
|
||||||
|
if (!Number.isFinite(parsed)) return '15';
|
||||||
|
return String(Math.round(parsed));
|
||||||
|
}
|
||||||
const globalValue = getItemTypeGlobalProperties(item.type)?.[key];
|
const globalValue = getItemTypeGlobalProperties(item.type)?.[key];
|
||||||
if (globalValue !== undefined) return String(globalValue);
|
if (globalValue !== undefined) return String(globalValue);
|
||||||
return String(item.params[key] ?? '');
|
return String(item.params[key] ?? '');
|
||||||
@@ -2008,6 +2015,14 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
signaling.send({ type: 'item_update', itemId, params: { facing: Math.round(parsed * 10) / 10 } });
|
signaling.send({ type: 'item_update', itemId, params: { facing: Math.round(parsed * 10) / 10 } });
|
||||||
|
} else if (propertyKey === 'emitRange') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isInteger(parsed) || parsed < 5 || parsed > 20) {
|
||||||
|
updateStatus('emit range must be an integer between 5 and 20.');
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signaling.send({ type: 'item_update', itemId, params: { emitRange: parsed } });
|
||||||
} else if (propertyKey === 'spaces') {
|
} else if (propertyKey === 'spaces') {
|
||||||
const spaces = value
|
const spaces = value
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
- `emitSound`: optional continuously-looping spatial sound emitted from the item on the grid; 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).
|
- `capabilities`, `useSound`, and `emitSound` are derived from global item-type definitions at runtime (not stored per-instance in persisted state).
|
||||||
- `useCooldownMs`: global per item type (`radio_station=1000`, `dice=1000`, `wheel=4000`, `clock=1000`), not per-instance editable.
|
- `useCooldownMs`: global per item type (`radio_station=1000`, `dice=1000`, `wheel=4000`, `clock=1000`), not per-instance editable.
|
||||||
- `emitRange`: global spatial range per item type (`radio_station=20`, `dice=15`, `wheel=15`, `clock=10`), not per-instance editable.
|
- `emitRange`: global spatial range default per item type (`radio_station=20`, `dice=15`, `wheel=15`, `clock=10`).
|
||||||
|
- `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`), not per-instance editable.
|
- `directional`: global directional attenuation flag per item type (`radio_station=true`, others `false`), not per-instance editable.
|
||||||
|
|
||||||
## Persisted Item State (`server/runtime/items.json`)
|
## Persisted Item State (`server/runtime/items.json`)
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
"volume": 50,
|
"volume": 50,
|
||||||
"effect": "off",
|
"effect": "off",
|
||||||
"effectValue": 50,
|
"effectValue": 50,
|
||||||
"facing": 0
|
"facing": 0,
|
||||||
|
"emitRange": 20
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
- `effect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
|
- `effect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
|
||||||
- `effectValue`: number, range `0-100`, precision `0.1`.
|
- `effectValue`: number, range `0-100`, precision `0.1`.
|
||||||
- `facing`: number, range `0-360`, precision `0.1` (used when `directional=true`).
|
- `facing`: number, range `0-360`, precision `0.1` (used when `directional=true`).
|
||||||
|
- `emitRange`: integer, range `5-20`, default `20`.
|
||||||
|
|
||||||
### `dice`
|
### `dice`
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
- `effect="off"`
|
- `effect="off"`
|
||||||
- `effectValue=50`
|
- `effectValue=50`
|
||||||
- `facing=0`
|
- `facing=0`
|
||||||
|
- `emitRange=20`
|
||||||
- Global:
|
- Global:
|
||||||
- `useSound=none`
|
- `useSound=none`
|
||||||
- `emitSound=none`
|
- `emitSound=none`
|
||||||
@@ -42,6 +43,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
- `effect`: `reverb | echo | flanger | high_pass | low_pass | off`
|
- `effect`: `reverb | echo | flanger | high_pass | low_pass | off`
|
||||||
- `effectValue`: number `0..100` with `0.1` precision
|
- `effectValue`: number `0..100` with `0.1` precision
|
||||||
- `facing`: number `0..360` with `0.1` precision
|
- `facing`: number `0..360` with `0.1` precision
|
||||||
|
- `emitRange`: integer `5..20`
|
||||||
|
|
||||||
## `dice`
|
## `dice`
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ITEM_TYPE_LABELS: dict[ItemType, str] = {
|
|||||||
RADIO_EFFECT_OPTIONS: tuple[str, ...] = ("reverb", "echo", "flanger", "high_pass", "low_pass", "off")
|
RADIO_EFFECT_OPTIONS: tuple[str, ...] = ("reverb", "echo", "flanger", "high_pass", "low_pass", "off")
|
||||||
RADIO_CHANNEL_OPTIONS: tuple[str, ...] = ("stereo", "mono", "left", "right")
|
RADIO_CHANNEL_OPTIONS: tuple[str, ...] = ("stereo", "mono", "left", "right")
|
||||||
ITEM_TYPE_EDITABLE_PROPERTIES: dict[ItemType, tuple[str, ...]] = {
|
ITEM_TYPE_EDITABLE_PROPERTIES: dict[ItemType, tuple[str, ...]] = {
|
||||||
"radio_station": ("title", "streamUrl", "enabled", "channel", "volume", "effect", "effectValue", "facing"),
|
"radio_station": ("title", "streamUrl", "enabled", "channel", "volume", "effect", "effectValue", "facing", "emitRange"),
|
||||||
"dice": ("title", "sides", "number"),
|
"dice": ("title", "sides", "number"),
|
||||||
"wheel": ("title", "spaces"),
|
"wheel": ("title", "spaces"),
|
||||||
"clock": ("title", "timeZone", "use24Hour"),
|
"clock": ("title", "timeZone", "use24Hour"),
|
||||||
@@ -86,7 +86,16 @@ ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = {
|
|||||||
capabilities=("editable", "carryable", "deletable", "usable"),
|
capabilities=("editable", "carryable", "deletable", "usable"),
|
||||||
use_sound=None,
|
use_sound=None,
|
||||||
emit_sound=None,
|
emit_sound=None,
|
||||||
default_params={"streamUrl": "", "enabled": True, "channel": "stereo", "volume": 50, "effect": "off", "effectValue": 50, "facing": 0},
|
default_params={
|
||||||
|
"streamUrl": "",
|
||||||
|
"enabled": True,
|
||||||
|
"channel": "stereo",
|
||||||
|
"volume": 50,
|
||||||
|
"effect": "off",
|
||||||
|
"effectValue": 50,
|
||||||
|
"facing": 0,
|
||||||
|
"emitRange": 20,
|
||||||
|
},
|
||||||
emit_range=20,
|
emit_range=20,
|
||||||
directional=True,
|
directional=True,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ def _validate_radio_update(item: WorldItem, next_params: dict) -> dict:
|
|||||||
if not (0 <= facing <= 360):
|
if not (0 <= facing <= 360):
|
||||||
raise ValueError("facing must be between 0 and 360.")
|
raise ValueError("facing must be between 0 and 360.")
|
||||||
next_params["facing"] = round(facing, 1)
|
next_params["facing"] = round(facing, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
emit_range = int(next_params.get("emitRange", item.params.get("emitRange", 20)))
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError("emitRange must be an integer between 5 and 20.") from exc
|
||||||
|
if not (5 <= emit_range <= 20):
|
||||||
|
raise ValueError("emitRange must be between 5 and 20.")
|
||||||
|
next_params["emitRange"] = emit_range
|
||||||
return next_params
|
return next_params
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,20 @@ async def test_radio_channel_update_validates(monkeypatch: pytest.MonkeyPatch) -
|
|||||||
assert send_payloads[-1].ok is False
|
assert send_payloads[-1].ok is False
|
||||||
assert "facing must be between 0 and 360" in send_payloads[-1].message.lower()
|
assert "facing must be between 0 and 360" in send_payloads[-1].message.lower()
|
||||||
|
|
||||||
|
await server._handle_message(
|
||||||
|
client,
|
||||||
|
json.dumps({"type": "item_update", "itemId": item.id, "params": {"emitRange": 12}}),
|
||||||
|
)
|
||||||
|
assert send_payloads[-1].ok is True
|
||||||
|
assert item.params.get("emitRange") == 12
|
||||||
|
|
||||||
|
await server._handle_message(
|
||||||
|
client,
|
||||||
|
json.dumps({"type": "item_update", "itemId": item.id, "params": {"emitRange": 4}}),
|
||||||
|
)
|
||||||
|
assert send_payloads[-1].ok is False
|
||||||
|
assert "emitrange must be between 5 and 20" in send_payloads[-1].message.lower()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_clock_use_reports_time_without_use_sound_packet(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_clock_use_reports_time_without_use_sound_packet(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user