Files
chat_grid/server/app/items/radio.py

156 lines
5.7 KiB
Python
Raw Normal View History

"""Radio item schema metadata and behavior."""
from __future__ import annotations
from typing import Callable
from ..item_types import ItemUseResult
from ..models import WorldItem
from .helpers import toggle_bool_param
LABEL = "radio"
TOOLTIP = "Can play stations from the Internet. Tune multiple to the same station and they will sync up."
EDITABLE_PROPERTIES: tuple[str, ...] = (
"title",
"streamUrl",
"enabled",
"channel",
"mediaVolume",
"effect",
"effectValue",
"facing",
"emitRange",
)
CAPABILITIES: tuple[str, ...] = ("editable", "carryable", "deletable", "usable")
USE_SOUND: str | None = None
EMIT_SOUND: str | None = None
USE_COOLDOWN_MS = 1000
EMIT_RANGE = 20
DIRECTIONAL = True
DEFAULT_TITLE = "radio"
DEFAULT_PARAMS: dict = {
"streamUrl": "",
"enabled": True,
"channel": "stereo",
"mediaVolume": 50,
"effect": "off",
"effectValue": 50,
"facing": 0,
"emitRange": 20,
}
CHANNEL_OPTIONS: tuple[str, ...] = ("stereo", "mono", "left", "right")
EFFECT_OPTIONS: tuple[str, ...] = ("reverb", "echo", "flanger", "high_pass", "low_pass", "off")
PROPERTY_METADATA: dict[str, dict[str, object]] = {
"title": {"valueType": "text", "tooltip": "Display name spoken and shown for this item."},
"streamUrl": {"valueType": "text", "tooltip": "Audio stream URL used by this radio."},
"enabled": {"valueType": "boolean", "tooltip": "Turns playback on or off for this radio."},
"channel": {"valueType": "list", "tooltip": "Select how the station audio channels are rendered."},
"mediaVolume": {
"valueType": "number",
"tooltip": "Playback media volume percent for this radio.",
"range": {"min": 0, "max": 100, "step": 1},
},
"effect": {"valueType": "list", "tooltip": "Select the active radio effect."},
"effectValue": {
"valueType": "number",
"tooltip": "Amount for the selected effect.",
"range": {"min": 0, "max": 100, "step": 0.1},
},
"facing": {
"valueType": "number",
"tooltip": "Facing direction in degrees used for directional emit.",
"range": {"min": 0, "max": 360, "step": 0.1},
},
"emitRange": {
"valueType": "number",
"tooltip": "Maximum distance in squares for this radio's emitted audio.",
"range": {"min": 5, "max": 20, "step": 1},
},
}
def validate_update(item: WorldItem, next_params: dict) -> dict:
"""Validate and normalize radio params."""
stream_url = str(next_params.get("streamUrl", "")).strip()
previous_stream_url = str(item.params.get("streamUrl", "")).strip()
next_params["streamUrl"] = stream_url
enabled_value = next_params.get("enabled", True)
if isinstance(enabled_value, bool):
enabled = enabled_value
elif isinstance(enabled_value, (int, float)):
enabled = bool(enabled_value)
elif isinstance(enabled_value, str):
token = enabled_value.strip().lower()
if token in {"on", "true", "1", "yes"}:
enabled = True
elif token in {"off", "false", "0", "no"}:
enabled = False
else:
raise ValueError("enabled must be true/false or on/off.")
else:
raise ValueError("enabled must be true/false or on/off.")
if stream_url and stream_url != previous_stream_url:
enabled = True
if not stream_url:
enabled = False
next_params["enabled"] = enabled
try:
media_volume = int(next_params.get("mediaVolume", 50))
except (TypeError, ValueError) as exc:
raise ValueError("mediaVolume must be a number.") from exc
if not (0 <= media_volume <= 100):
raise ValueError("mediaVolume must be between 0 and 100.")
next_params["mediaVolume"] = media_volume
effect = str(next_params.get("effect", "off")).strip().lower()
if effect not in EFFECT_OPTIONS:
raise ValueError("effect must be one of reverb, echo, flanger, high_pass, low_pass, off.")
next_params["effect"] = effect
channel = str(next_params.get("channel", "stereo")).strip().lower()
if channel not in CHANNEL_OPTIONS:
raise ValueError("channel must be one of stereo, mono, left, right.")
next_params["channel"] = channel
try:
effect_value = float(next_params.get("effectValue", 50))
except (TypeError, ValueError) as exc:
raise ValueError("effectValue must be a number.") from exc
if not (0 <= effect_value <= 100):
raise ValueError("effectValue must be between 0 and 100.")
next_params["effectValue"] = round(effect_value, 1)
try:
facing = float(next_params.get("facing", item.params.get("facing", 0)))
except (TypeError, ValueError) as exc:
raise ValueError("facing must be a number between 0 and 360.") from exc
if not (0 <= facing <= 360):
raise ValueError("facing must be between 0 and 360.")
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
def use_item(item: WorldItem, nickname: str, _clock_formatter: Callable[[dict], str]) -> ItemUseResult:
"""Toggle radio on/off when used."""
next_enabled = toggle_bool_param(item.params, "enabled", default=True)
state_text = "on" if next_enabled else "off"
return ItemUseResult(
self_message=f"You turn {state_text} {item.title}.",
others_message=f"{nickname} turns {state_text} {item.title}.",
updated_params={**item.params, "enabled": next_enabled},
)