import { EFFECT_SEQUENCE } from '../audio/effects'; import { RADIO_CHANNEL_OPTIONS } from '../audio/radioStationRuntime'; import { type ItemType, type WorldItem } from '../state/gameState'; const DEFAULT_CLOCK_TIME_ZONE_OPTIONS = [ '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', ] as const; const DEFAULT_ITEM_TYPE_SEQUENCE: ItemType[] = ['clock', 'dice', 'radio_station', 'wheel']; const DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES: Record = { radio_station: ['title', 'streamUrl', 'enabled', 'channel', 'volume', 'effect', 'effectValue'], dice: ['title', 'sides', 'number'], wheel: ['title', 'spaces'], clock: ['title', 'timeZone', 'use24Hour'], }; const DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES: Record> = { radio_station: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000 }, dice: { useSound: 'sounds/roll.ogg', emitSound: 'none', useCooldownMs: 1000 }, wheel: { useSound: 'sounds/spin.ogg', emitSound: 'none', useCooldownMs: 4000 }, clock: { useSound: 'none', emitSound: 'sounds/clock.ogg', useCooldownMs: 1000 }, }; type UiDefinitionsPayload = { itemTypeOrder?: ItemType[]; itemTypes?: Array<{ type: ItemType; label?: string; editableProperties?: string[]; propertyOptions?: Record; globalProperties?: Record; }>; }; let itemTypeSequence: ItemType[] = [...DEFAULT_ITEM_TYPE_SEQUENCE]; let itemTypeLabels: Record = { radio_station: 'radio', dice: 'dice', wheel: 'wheel', clock: 'clock', }; let itemTypeEditableProperties: Record = { radio_station: [...DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES.radio_station], dice: [...DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES.dice], wheel: [...DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES.wheel], clock: [...DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES.clock], }; let itemTypeGlobalProperties: Record> = { radio_station: { ...DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES.radio_station }, dice: { ...DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES.dice }, wheel: { ...DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES.wheel }, clock: { ...DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES.clock }, }; let optionItemPropertyValues: Partial> = { effect: EFFECT_SEQUENCE.map((effect) => effect.id), channel: [...RADIO_CHANNEL_OPTIONS], timeZone: [...DEFAULT_CLOCK_TIME_ZONE_OPTIONS], }; export let EDITABLE_ITEM_PROPERTY_KEYS = new Set( Object.values(itemTypeEditableProperties).flatMap((keys) => keys), ); function rebuildEditablePropertyKeySet(): void { EDITABLE_ITEM_PROPERTY_KEYS = new Set(Object.values(itemTypeEditableProperties).flatMap((keys) => keys)); } export function getClockTimeZoneOptions(): string[] { return [...(optionItemPropertyValues.timeZone ?? DEFAULT_CLOCK_TIME_ZONE_OPTIONS)]; } export function getDefaultClockTimeZone(): string { return getClockTimeZoneOptions()[0] ?? 'America/Detroit'; } export function getItemTypeSequence(): ItemType[] { return [...itemTypeSequence]; } export function getItemTypeGlobalProperties(itemType: ItemType): Record { return itemTypeGlobalProperties[itemType] ?? {}; } export function getItemPropertyOptionValues(key: string): string[] | undefined { return optionItemPropertyValues[key]; } export function itemTypeLabel(type: ItemType): string { return itemTypeLabels[type] ?? type; } export function itemPropertyLabel(key: string): string { if (key === 'use24Hour') return 'use 24 hour format'; return key; } export function getEditableItemPropertyKeys(item: WorldItem): string[] { return [...(itemTypeEditableProperties[item.type] ?? ['title'])]; } export function getInspectItemPropertyKeys(item: WorldItem): string[] { const editableKeys = getEditableItemPropertyKeys(item); const seen = new Set(editableKeys); const allKeys: string[] = [...editableKeys]; const baseKeys = [ 'type', 'x', 'y', 'carrierId', 'version', 'createdBy', 'createdAt', 'updatedAt', 'capabilities', 'useSound', 'emitSound', ]; for (const key of baseKeys) { if (seen.has(key)) continue; seen.add(key); allKeys.push(key); } const paramKeys = Object.keys(item.params).sort((a, b) => a.localeCompare(b)); for (const key of paramKeys) { if (seen.has(key)) continue; seen.add(key); allKeys.push(key); } const globalKeys = Object.keys(itemTypeGlobalProperties[item.type] ?? {}).sort((a, b) => a.localeCompare(b)); for (const key of globalKeys) { if (seen.has(key)) continue; seen.add(key); allKeys.push(key); } return allKeys; } export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload | undefined): void { if (!uiDefinitions) return; if (Array.isArray(uiDefinitions.itemTypeOrder) && uiDefinitions.itemTypeOrder.length > 0) { itemTypeSequence = uiDefinitions.itemTypeOrder.filter((entry) => typeof entry === 'string') as ItemType[]; } if (!Array.isArray(uiDefinitions.itemTypes) || uiDefinitions.itemTypes.length === 0) { rebuildEditablePropertyKeySet(); return; } const nextLabels = { ...itemTypeLabels }; const nextEditable = { ...itemTypeEditableProperties }; const nextGlobals = { ...itemTypeGlobalProperties }; const nextOptions: Partial> = { ...optionItemPropertyValues }; for (const definition of uiDefinitions.itemTypes) { if (!definition || typeof definition.type !== 'string') continue; const itemType = definition.type as ItemType; if (typeof definition.label === 'string' && definition.label.trim()) { nextLabels[itemType] = definition.label.trim(); } if (Array.isArray(definition.editableProperties) && definition.editableProperties.length > 0) { nextEditable[itemType] = definition.editableProperties.filter((entry) => typeof entry === 'string'); } if (definition.globalProperties && typeof definition.globalProperties === 'object') { const normalized: Record = {}; for (const [key, raw] of Object.entries(definition.globalProperties)) { if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') { normalized[key] = raw; } } nextGlobals[itemType] = normalized; } if (definition.propertyOptions && typeof definition.propertyOptions === 'object') { for (const [propertyKey, values] of Object.entries(definition.propertyOptions)) { if (!Array.isArray(values) || values.length === 0) continue; const normalizedValues = values.filter((entry) => typeof entry === 'string'); if (normalizedValues.length > 0) { nextOptions[propertyKey] = normalizedValues; } } } } itemTypeLabels = nextLabels; itemTypeEditableProperties = nextEditable; itemTypeGlobalProperties = nextGlobals; optionItemPropertyValues = nextOptions; rebuildEditablePropertyKeySet(); }