Send world/item UI metadata in welcome and consume on client

This commit is contained in:
Jage9
2026-02-21 19:12:58 -05:00
parent 4f04e735da
commit 008de60727
9 changed files with 274 additions and 42 deletions

View File

@@ -6,6 +6,21 @@ from dataclasses import dataclass
from typing import Literal
ItemType = Literal["radio_station", "dice", "wheel", "clock"]
ITEM_TYPE_SEQUENCE: tuple[ItemType, ...] = ("clock", "dice", "radio_station", "wheel")
ITEM_TYPE_LABELS: dict[ItemType, str] = {
"radio_station": "radio",
"dice": "dice",
"wheel": "wheel",
"clock": "clock",
}
RADIO_EFFECT_OPTIONS: tuple[str, ...] = ("reverb", "echo", "flanger", "high_pass", "low_pass", "off")
RADIO_CHANNEL_OPTIONS: tuple[str, ...] = ("stereo", "mono", "left", "right")
ITEM_TYPE_EDITABLE_PROPERTIES: dict[ItemType, tuple[str, ...]] = {
"radio_station": ("title", "streamUrl", "enabled", "channel", "volume", "effect", "effectValue"),
"dice": ("title", "sides", "number"),
"wheel": ("title", "spaces"),
"clock": ("title", "timeZone", "use24Hour"),
}
CLOCK_DEFAULT_TIME_ZONE = "America/Detroit"
CLOCK_TIME_ZONE_OPTIONS: tuple[str, ...] = (
"America/Anchorage",
@@ -95,6 +110,12 @@ ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = {
),
}
ITEM_PROPERTY_OPTIONS: dict[str, tuple[str, ...]] = {
"effect": RADIO_EFFECT_OPTIONS,
"channel": RADIO_CHANNEL_OPTIONS,
"timeZone": CLOCK_TIME_ZONE_OPTIONS,
}
def get_item_definition(item_type: ItemType) -> ItemDefinition:
"""Return catalog definition for a known item type."""
@@ -110,3 +131,14 @@ def get_item_use_cooldown_ms(item_type: ItemType) -> int:
if isinstance(cooldown_ms, int) and cooldown_ms > 0:
return cooldown_ms
return 1000
def get_item_global_properties(item_type: ItemType) -> dict[str, str | int]:
"""Return non-editable global properties exposed in UI metadata."""
definition = get_item_definition(item_type)
return {
"useSound": definition.use_sound or "none",
"emitSound": definition.emit_sound or "none",
"useCooldownMs": get_item_use_cooldown_ms(item_type),
}

View File

@@ -101,6 +101,8 @@ class WelcomePacket(BasePacket):
id: str
users: list[RemoteUser]
items: list[dict] | None = None
worldConfig: dict | None = None
uiDefinitions: dict | None = None
class UserLeftPacket(BasePacket):

View File

@@ -18,7 +18,16 @@ from websockets.asyncio.server import ServerConnection, serve
from .client import ClientConnection
from .config import load_config
from .item_catalog import CLOCK_DEFAULT_TIME_ZONE, CLOCK_TIME_ZONE_OPTIONS, get_item_use_cooldown_ms
from .item_catalog import (
CLOCK_DEFAULT_TIME_ZONE,
CLOCK_TIME_ZONE_OPTIONS,
ITEM_PROPERTY_OPTIONS,
ITEM_TYPE_EDITABLE_PROPERTIES,
ITEM_TYPE_LABELS,
ITEM_TYPE_SEQUENCE,
get_item_global_properties,
get_item_use_cooldown_ms,
)
from .item_type_handlers import get_item_type_handler
from .item_service import ItemService
from .models import (
@@ -236,9 +245,36 @@ class SignalingServer:
id=client.id,
users=users,
items=[item.model_dump(exclude_none=True) for item in self.items.values()],
worldConfig={"gridSize": self.grid_size},
uiDefinitions=self._build_ui_definitions(),
)
await self._send(client.websocket, packet)
def _build_ui_definitions(self) -> dict:
"""Build server-owned UI definitions for item/menu rendering."""
item_types: list[dict] = []
for item_type in ITEM_TYPE_SEQUENCE:
editable = list(ITEM_TYPE_EDITABLE_PROPERTIES.get(item_type, ("title",)))
property_options: dict[str, list[str]] = {}
for key in editable:
options = ITEM_PROPERTY_OPTIONS.get(key)
if options:
property_options[key] = list(options)
item_types.append(
{
"type": item_type,
"label": ITEM_TYPE_LABELS.get(item_type, item_type),
"editableProperties": editable,
"propertyOptions": property_options,
"globalProperties": get_item_global_properties(item_type),
}
)
return {
"itemTypeOrder": list(ITEM_TYPE_SEQUENCE),
"itemTypes": item_types,
}
async def _broadcast_wheel_result_after_delay(
self,
client: ClientConnection,