Restore useSound and add looping spatial emitSound
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 R99";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R100";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ export class AudioEngine {
|
|||||||
return this.outputMode;
|
return this.outputMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOutputMode(): OutputMode {
|
||||||
|
return this.outputMode;
|
||||||
|
}
|
||||||
|
|
||||||
toggleLoopback(): boolean {
|
toggleLoopback(): boolean {
|
||||||
this.loopbackEnabled = !this.loopbackEnabled;
|
this.loopbackEnabled = !this.loopbackEnabled;
|
||||||
this.rebuildOutboundEffectGraph();
|
this.rebuildOutboundEffectGraph();
|
||||||
|
|||||||
111
client/src/audio/itemEmitRuntime.ts
Normal file
111
client/src/audio/itemEmitRuntime.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { HEARING_RADIUS, type WorldItem } from '../state/gameState';
|
||||||
|
import { AudioEngine } from './audioEngine';
|
||||||
|
|
||||||
|
type EmitOutput = {
|
||||||
|
soundUrl: string;
|
||||||
|
element: HTMLAudioElement;
|
||||||
|
source: MediaElementAudioSourceNode;
|
||||||
|
gain: GainNode;
|
||||||
|
panner: StereoPannerNode | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ItemEmitRuntime {
|
||||||
|
private readonly outputs = new Map<string, EmitOutput>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly audio: AudioEngine,
|
||||||
|
private readonly resolveSoundUrl: (soundPath: string) => string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
cleanup(itemId: string): void {
|
||||||
|
const output = this.outputs.get(itemId);
|
||||||
|
if (!output) return;
|
||||||
|
output.element.pause();
|
||||||
|
output.element.src = '';
|
||||||
|
output.source.disconnect();
|
||||||
|
output.gain.disconnect();
|
||||||
|
output.panner?.disconnect();
|
||||||
|
this.outputs.delete(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupAll(): void {
|
||||||
|
for (const itemId of Array.from(this.outputs.keys())) {
|
||||||
|
this.cleanup(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(items: Iterable<WorldItem>): Promise<void> {
|
||||||
|
const validIds = new Set<string>();
|
||||||
|
await this.audio.ensureContext();
|
||||||
|
const audioCtx = this.audio.context;
|
||||||
|
if (!audioCtx) return;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const soundUrl = this.resolveSoundUrl(String(item.emitSound ?? '').trim());
|
||||||
|
if (!soundUrl || item.carrierId) {
|
||||||
|
this.cleanup(item.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validIds.add(item.id);
|
||||||
|
const existing = this.outputs.get(item.id);
|
||||||
|
if (existing && existing.soundUrl === soundUrl) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existing) {
|
||||||
|
this.cleanup(item.id);
|
||||||
|
}
|
||||||
|
const element = new Audio(soundUrl);
|
||||||
|
element.loop = true;
|
||||||
|
element.preload = 'none';
|
||||||
|
element.crossOrigin = 'anonymous';
|
||||||
|
const source = audioCtx.createMediaElementSource(element);
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
gain.gain.value = 0;
|
||||||
|
let panner: StereoPannerNode | null = null;
|
||||||
|
source.connect(gain);
|
||||||
|
if (this.audio.supportsStereoPanner()) {
|
||||||
|
panner = audioCtx.createStereoPanner();
|
||||||
|
gain.connect(panner).connect(audioCtx.destination);
|
||||||
|
} else {
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
|
}
|
||||||
|
this.outputs.set(item.id, { soundUrl, element, source, gain, panner });
|
||||||
|
void element.play().catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const itemId of Array.from(this.outputs.keys())) {
|
||||||
|
if (!validIds.has(itemId)) {
|
||||||
|
this.cleanup(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSpatialAudio(items: Map<string, WorldItem>, playerPosition: { x: number; y: number }): void {
|
||||||
|
const audioCtx = this.audio.context;
|
||||||
|
if (!audioCtx) return;
|
||||||
|
|
||||||
|
for (const [itemId, output] of this.outputs.entries()) {
|
||||||
|
const item = items.get(itemId);
|
||||||
|
if (!item || item.carrierId) {
|
||||||
|
output.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.05);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const dist = Math.hypot(item.x - playerPosition.x, item.y - playerPosition.y);
|
||||||
|
let gainValue = 0;
|
||||||
|
let panValue = 0;
|
||||||
|
if (dist < HEARING_RADIUS) {
|
||||||
|
gainValue = Math.pow(1 - dist / HEARING_RADIUS, 2);
|
||||||
|
panValue = Math.sin(((item.x - playerPosition.x) / HEARING_RADIUS) * (Math.PI / 2));
|
||||||
|
}
|
||||||
|
if (dist <= 1) {
|
||||||
|
gainValue = 1;
|
||||||
|
panValue = 0;
|
||||||
|
}
|
||||||
|
output.gain.gain.linearRampToValueAtTime(gainValue, audioCtx.currentTime + 0.1);
|
||||||
|
if (output.panner) {
|
||||||
|
const resolvedPan = this.audio.getOutputMode() === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue));
|
||||||
|
output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type EffectId,
|
type EffectId,
|
||||||
} from './audio/effects';
|
} from './audio/effects';
|
||||||
import { RADIO_CHANNEL_OPTIONS, RadioStationRuntime, normalizeRadioChannel, normalizeRadioEffect, normalizeRadioEffectValue } from './audio/radioStationRuntime';
|
import { RADIO_CHANNEL_OPTIONS, RadioStationRuntime, normalizeRadioChannel, normalizeRadioEffect, normalizeRadioEffectValue } from './audio/radioStationRuntime';
|
||||||
|
import { ItemEmitRuntime } from './audio/itemEmitRuntime';
|
||||||
import {
|
import {
|
||||||
applyPastedText,
|
applyPastedText,
|
||||||
applyTextInput,
|
applyTextInput,
|
||||||
@@ -152,10 +153,10 @@ dom.appVersion.textContent = APP_VERSION
|
|||||||
: 'Another AI experiment with Jage. Version unknown';
|
: 'Another AI experiment with Jage. Version unknown';
|
||||||
const ITEM_TYPE_SEQUENCE: ItemType[] = ['clock', 'dice', 'radio_station', 'wheel'];
|
const ITEM_TYPE_SEQUENCE: ItemType[] = ['clock', 'dice', 'radio_station', 'wheel'];
|
||||||
const ITEM_TYPE_GLOBAL_PROPERTIES: Record<ItemType, Record<string, string | number | boolean>> = {
|
const ITEM_TYPE_GLOBAL_PROPERTIES: Record<ItemType, Record<string, string | number | boolean>> = {
|
||||||
radio_station: { emitSound: 'none', useCooldownMs: 1000 },
|
radio_station: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000 },
|
||||||
dice: { emitSound: 'sounds/roll.ogg', useCooldownMs: 1000 },
|
dice: { useSound: 'sounds/roll.ogg', emitSound: 'none', useCooldownMs: 1000 },
|
||||||
wheel: { emitSound: 'sounds/spin.ogg', useCooldownMs: 4000 },
|
wheel: { useSound: 'sounds/spin.ogg', emitSound: 'none', useCooldownMs: 4000 },
|
||||||
clock: { emitSound: 'sounds/clock.ogg', useCooldownMs: 1000 },
|
clock: { useSound: 'none', emitSound: 'sounds/clock.ogg', useCooldownMs: 1000 },
|
||||||
};
|
};
|
||||||
const EDITABLE_ITEM_PROPERTY_KEYS = new Set([
|
const EDITABLE_ITEM_PROPERTY_KEYS = new Set([
|
||||||
'title',
|
'title',
|
||||||
@@ -210,6 +211,7 @@ let connecting = false;
|
|||||||
const messageBuffer: string[] = [];
|
const messageBuffer: string[] = [];
|
||||||
let messageCursor = -1;
|
let messageCursor = -1;
|
||||||
const radioRuntime = new RadioStationRuntime(audio);
|
const radioRuntime = new RadioStationRuntime(audio);
|
||||||
|
const itemEmitRuntime = new ItemEmitRuntime(audio, resolveIncomingSoundUrl);
|
||||||
let internalClipboardText = '';
|
let internalClipboardText = '';
|
||||||
let replaceTextOnNextType = false;
|
let replaceTextOnNextType = false;
|
||||||
let pendingEscapeDisconnect = false;
|
let pendingEscapeDisconnect = false;
|
||||||
@@ -510,7 +512,19 @@ function getInspectItemPropertyKeys(item: WorldItem): string[] {
|
|||||||
const seen = new Set(editableKeys);
|
const seen = new Set(editableKeys);
|
||||||
const allKeys: string[] = [...editableKeys];
|
const allKeys: string[] = [...editableKeys];
|
||||||
|
|
||||||
const baseKeys = ['type', 'x', 'y', 'carrierId', 'version', 'createdBy', 'createdAt', 'updatedAt', 'capabilities', 'emitSound'];
|
const baseKeys = [
|
||||||
|
'type',
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
'carrierId',
|
||||||
|
'version',
|
||||||
|
'createdBy',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'capabilities',
|
||||||
|
'useSound',
|
||||||
|
'emitSound',
|
||||||
|
];
|
||||||
for (const key of baseKeys) {
|
for (const key of baseKeys) {
|
||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) continue;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
@@ -666,6 +680,7 @@ function getItemPropertyValue(item: WorldItem, key: string): string {
|
|||||||
if (key === 'createdAt') return formatTimestampMs(item.createdAt);
|
if (key === 'createdAt') return formatTimestampMs(item.createdAt);
|
||||||
if (key === 'updatedAt') return formatTimestampMs(item.updatedAt);
|
if (key === 'updatedAt') return formatTimestampMs(item.updatedAt);
|
||||||
if (key === 'capabilities') return item.capabilities.join(', ') || 'none';
|
if (key === 'capabilities') return item.capabilities.join(', ') || 'none';
|
||||||
|
if (key === 'useSound') return item.useSound ?? 'none';
|
||||||
if (key === 'emitSound') return item.emitSound ?? 'none';
|
if (key === 'emitSound') return item.emitSound ?? 'none';
|
||||||
if (key === 'enabled') return item.params.enabled === false ? 'off' : 'on';
|
if (key === 'enabled') return item.params.enabled === false ? 'off' : 'on';
|
||||||
if (key === 'timeZone') return String(item.params.timeZone ?? CLOCK_TIME_ZONE_OPTIONS[0]);
|
if (key === 'timeZone') return String(item.params.timeZone ?? CLOCK_TIME_ZONE_OPTIONS[0]);
|
||||||
@@ -709,6 +724,7 @@ function gameLoop(): void {
|
|||||||
handleMovement();
|
handleMovement();
|
||||||
audio.updateSpatialAudio(peerManager.getPeers(), { x: state.player.x, y: state.player.y });
|
audio.updateSpatialAudio(peerManager.getPeers(), { x: state.player.x, y: state.player.y });
|
||||||
radioRuntime.updateSpatialAudio(state.items, { x: state.player.x, y: state.player.y });
|
radioRuntime.updateSpatialAudio(state.items, { x: state.player.x, y: state.player.y });
|
||||||
|
itemEmitRuntime.updateSpatialAudio(state.items, { x: state.player.x, y: state.player.y });
|
||||||
state.cursorVisible = Math.floor(Date.now() / 500) % 2 === 0;
|
state.cursorVisible = Math.floor(Date.now() / 500) % 2 === 0;
|
||||||
renderer.draw(state);
|
renderer.draw(state);
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
@@ -897,6 +913,7 @@ function disconnect(): void {
|
|||||||
|
|
||||||
peerManager.cleanupAll();
|
peerManager.cleanupAll();
|
||||||
radioRuntime.cleanupAll();
|
radioRuntime.cleanupAll();
|
||||||
|
itemEmitRuntime.cleanupAll();
|
||||||
state.running = false;
|
state.running = false;
|
||||||
state.keysPressed = {};
|
state.keysPressed = {};
|
||||||
state.peers.clear();
|
state.peers.clear();
|
||||||
@@ -961,6 +978,7 @@ async function onMessage(message: IncomingMessage): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await radioRuntime.sync(state.items.values());
|
await radioRuntime.sync(state.items.values());
|
||||||
|
await itemEmitRuntime.sync(state.items.values());
|
||||||
|
|
||||||
gameLoop();
|
gameLoop();
|
||||||
break;
|
break;
|
||||||
@@ -1064,6 +1082,7 @@ async function onMessage(message: IncomingMessage): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await radioRuntime.sync(state.items.values());
|
await radioRuntime.sync(state.items.values());
|
||||||
|
await itemEmitRuntime.sync(state.items.values());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,6 +1090,7 @@ async function onMessage(message: IncomingMessage): Promise<void> {
|
|||||||
state.items.delete(message.itemId);
|
state.items.delete(message.itemId);
|
||||||
state.carriedItemId = getCarriedItem()?.id ?? null;
|
state.carriedItemId = getCarriedItem()?.id ?? null;
|
||||||
radioRuntime.cleanup(message.itemId);
|
radioRuntime.cleanup(message.itemId);
|
||||||
|
itemEmitRuntime.cleanup(message.itemId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,7 +1099,7 @@ async function onMessage(message: IncomingMessage): Promise<void> {
|
|||||||
if (message.action === 'use') {
|
if (message.action === 'use') {
|
||||||
pushChatMessage(message.message);
|
pushChatMessage(message.message);
|
||||||
const item = message.itemId ? state.items.get(message.itemId) : null;
|
const item = message.itemId ? state.items.get(message.itemId) : null;
|
||||||
if (!item?.emitSound && item) {
|
if (!item?.useSound && item) {
|
||||||
audio.sfxLocate({ x: item.x - state.player.x, y: item.y - state.player.y });
|
audio.sfxLocate({ x: item.x - state.player.x, y: item.y - state.player.y });
|
||||||
}
|
}
|
||||||
} else if (message.action !== 'update') {
|
} else if (message.action !== 'update') {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const itemSchema = z.object({
|
|||||||
updatedAt: z.number().int(),
|
updatedAt: z.number().int(),
|
||||||
version: z.number().int(),
|
version: z.number().int(),
|
||||||
capabilities: z.array(z.string()),
|
capabilities: z.array(z.string()),
|
||||||
|
useSound: z.string().optional(),
|
||||||
emitSound: z.string().optional(),
|
emitSound: z.string().optional(),
|
||||||
params: z.record(z.string(), z.unknown()),
|
params: z.record(z.string(), z.unknown()),
|
||||||
carrierId: z.string().nullable().optional(),
|
carrierId: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type WorldItem = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
version: number;
|
version: number;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
|
useSound?: string;
|
||||||
emitSound?: string;
|
emitSound?: string;
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
carrierId?: string | null;
|
carrierId?: string | null;
|
||||||
|
|||||||
@@ -14,14 +14,16 @@
|
|||||||
"updatedAt": 1735689600000,
|
"updatedAt": 1735689600000,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"capabilities": ["editable", "carryable", "deletable", "usable"],
|
"capabilities": ["editable", "carryable", "deletable", "usable"],
|
||||||
"emitSound": "sounds/roll.ogg",
|
"useSound": "sounds/roll.ogg",
|
||||||
|
"emitSound": "sounds/clock.ogg",
|
||||||
"params": {},
|
"params": {},
|
||||||
"carrierId": null
|
"carrierId": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `emitSound`: optional client-played sound path when item `use` succeeds; global item field and not user-editable in V1.
|
- `useSound`: optional client-played one-shot sound when item `use` succeeds; global item field and not user-editable in V1.
|
||||||
- `capabilities` and `emitSound` are derived from global item-type definitions at runtime (not stored per-instance in persisted state).
|
- `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).
|
||||||
- `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.
|
||||||
|
|
||||||
## Persisted Item State (`server/runtime/items.json`)
|
## Persisted Item State (`server/runtime/items.json`)
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
`Europe/London`, `Europe/Moscow`, `Pacific/Apia`, `Pacific/Auckland`, `Pacific/Chatham`,
|
`Europe/London`, `Europe/Moscow`, `Pacific/Apia`, `Pacific/Auckland`, `Pacific/Chatham`,
|
||||||
`Pacific/Honolulu`, `Pacific/Kiritimati`, `Pacific/Noumea`, `Pacific/Pago_Pago`, `UTC`.
|
`Pacific/Honolulu`, `Pacific/Kiritimati`, `Pacific/Noumea`, `Pacific/Pago_Pago`, `UTC`.
|
||||||
- `use24Hour`: boolean (or `on/off` in updates), default `false`.
|
- `use24Hour`: boolean (or `on/off` in updates), default `false`.
|
||||||
|
- Global defaults: `useSound=none`, `emitSound=sounds/clock.ogg`.
|
||||||
|
|
||||||
## Packet Shapes
|
## Packet Shapes
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ CLOCK_TIME_ZONE_OPTIONS: tuple[str, ...] = (
|
|||||||
class ItemDefinition:
|
class ItemDefinition:
|
||||||
default_title: str
|
default_title: str
|
||||||
capabilities: tuple[str, ...]
|
capabilities: tuple[str, ...]
|
||||||
|
use_sound: str | None
|
||||||
emit_sound: str | None
|
emit_sound: str | None
|
||||||
default_params: dict
|
default_params: dict
|
||||||
use_cooldown_ms: int = 1000
|
use_cooldown_ms: int = 1000
|
||||||
@@ -62,25 +63,29 @@ ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = {
|
|||||||
"radio_station": ItemDefinition(
|
"radio_station": ItemDefinition(
|
||||||
default_title="radio",
|
default_title="radio",
|
||||||
capabilities=("editable", "carryable", "deletable", "usable"),
|
capabilities=("editable", "carryable", "deletable", "usable"),
|
||||||
|
use_sound=None,
|
||||||
emit_sound=None,
|
emit_sound=None,
|
||||||
default_params={"streamUrl": "", "enabled": True, "channel": "stereo", "volume": 50, "effect": "off", "effectValue": 50},
|
default_params={"streamUrl": "", "enabled": True, "channel": "stereo", "volume": 50, "effect": "off", "effectValue": 50},
|
||||||
),
|
),
|
||||||
"dice": ItemDefinition(
|
"dice": ItemDefinition(
|
||||||
default_title="Dice",
|
default_title="Dice",
|
||||||
capabilities=("editable", "carryable", "deletable", "usable"),
|
capabilities=("editable", "carryable", "deletable", "usable"),
|
||||||
emit_sound="sounds/roll.ogg",
|
use_sound="sounds/roll.ogg",
|
||||||
|
emit_sound=None,
|
||||||
default_params={"sides": 6, "number": 2},
|
default_params={"sides": 6, "number": 2},
|
||||||
),
|
),
|
||||||
"wheel": ItemDefinition(
|
"wheel": ItemDefinition(
|
||||||
default_title="wheel",
|
default_title="wheel",
|
||||||
capabilities=("editable", "carryable", "deletable", "usable"),
|
capabilities=("editable", "carryable", "deletable", "usable"),
|
||||||
emit_sound="sounds/spin.ogg",
|
use_sound="sounds/spin.ogg",
|
||||||
|
emit_sound=None,
|
||||||
default_params={"spaces": "yes, no"},
|
default_params={"spaces": "yes, no"},
|
||||||
use_cooldown_ms=4000,
|
use_cooldown_ms=4000,
|
||||||
),
|
),
|
||||||
"clock": ItemDefinition(
|
"clock": ItemDefinition(
|
||||||
default_title="clock",
|
default_title="clock",
|
||||||
capabilities=("editable", "carryable", "deletable", "usable"),
|
capabilities=("editable", "carryable", "deletable", "usable"),
|
||||||
|
use_sound=None,
|
||||||
emit_sound="sounds/clock.ogg",
|
emit_sound="sounds/clock.ogg",
|
||||||
default_params={"timeZone": CLOCK_DEFAULT_TIME_ZONE, "use24Hour": False},
|
default_params={"timeZone": CLOCK_DEFAULT_TIME_ZONE, "use24Hour": False},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ItemService:
|
|||||||
updatedAt=now,
|
updatedAt=now,
|
||||||
version=1,
|
version=1,
|
||||||
capabilities=list(item_def.capabilities),
|
capabilities=list(item_def.capabilities),
|
||||||
|
useSound=item_def.use_sound,
|
||||||
emitSound=item_def.emit_sound,
|
emitSound=item_def.emit_sound,
|
||||||
params=deepcopy(item_def.default_params),
|
params=deepcopy(item_def.default_params),
|
||||||
carrierId=None,
|
carrierId=None,
|
||||||
@@ -95,6 +96,7 @@ class ItemService:
|
|||||||
updatedAt=persisted.updatedAt,
|
updatedAt=persisted.updatedAt,
|
||||||
version=persisted.version,
|
version=persisted.version,
|
||||||
capabilities=list(item_def.capabilities),
|
capabilities=list(item_def.capabilities),
|
||||||
|
useSound=item_def.use_sound,
|
||||||
emitSound=item_def.emit_sound,
|
emitSound=item_def.emit_sound,
|
||||||
params=persisted.params,
|
params=persisted.params,
|
||||||
carrierId=persisted.carrierId,
|
carrierId=persisted.carrierId,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class WorldItem(BaseModel):
|
|||||||
updatedAt: int
|
updatedAt: int
|
||||||
version: int
|
version: int
|
||||||
capabilities: list[str]
|
capabilities: list[str]
|
||||||
|
useSound: str | None = None
|
||||||
emitSound: str | None = None
|
emitSound: str | None = None
|
||||||
params: dict
|
params: dict
|
||||||
carrierId: str | None = None
|
carrierId: str | None = None
|
||||||
|
|||||||
@@ -548,12 +548,12 @@ class SignalingServer:
|
|||||||
BroadcastChatMessagePacket(type="chat_message", message=others_message, system=True),
|
BroadcastChatMessagePacket(type="chat_message", message=others_message, system=True),
|
||||||
exclude=client.websocket,
|
exclude=client.websocket,
|
||||||
)
|
)
|
||||||
if item.emitSound:
|
if item.useSound:
|
||||||
await self._broadcast(
|
await self._broadcast(
|
||||||
ItemUseSoundPacket(
|
ItemUseSoundPacket(
|
||||||
type="item_use_sound",
|
type="item_use_sound",
|
||||||
itemId=item.id,
|
itemId=item.id,
|
||||||
sound=item.emitSound,
|
sound=item.useSound,
|
||||||
x=item.x,
|
x=item.x,
|
||||||
y=item.y,
|
y=item.y,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ def test_item_persistence_omits_global_type_properties(tmp_path: Path) -> None:
|
|||||||
assert isinstance(saved, list)
|
assert isinstance(saved, list)
|
||||||
assert len(saved) == 1
|
assert len(saved) == 1
|
||||||
assert "capabilities" not in saved[0]
|
assert "capabilities" not in saved[0]
|
||||||
|
assert "useSound" not in saved[0]
|
||||||
assert "emitSound" not in saved[0]
|
assert "emitSound" not in saved[0]
|
||||||
|
|
||||||
reloaded = ItemService(state_file=state_file)
|
reloaded = ItemService(state_file=state_file)
|
||||||
loaded_item = reloaded.items[item.id]
|
loaded_item = reloaded.items[item.id]
|
||||||
assert loaded_item.emitSound == "sounds/roll.ogg"
|
assert loaded_item.useSound == "sounds/roll.ogg"
|
||||||
|
assert loaded_item.emitSound is None
|
||||||
assert "usable" in loaded_item.capabilities
|
assert "usable" in loaded_item.capabilities
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async def test_radio_channel_update_validates(monkeypatch: pytest.MonkeyPatch) -
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_clock_use_reports_time_and_emits_sound(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_clock_use_reports_time_without_use_sound_packet(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
server = SignalingServer("127.0.0.1", 8765, None, None)
|
server = SignalingServer("127.0.0.1", 8765, None, None)
|
||||||
ws = _fake_ws()
|
ws = _fake_ws()
|
||||||
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=6)
|
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=6)
|
||||||
@@ -146,7 +146,7 @@ async def test_clock_use_reports_time_and_emits_sound(monkeypatch: pytest.Monkey
|
|||||||
|
|
||||||
assert send_payloads[-1].ok is True
|
assert send_payloads[-1].ok is True
|
||||||
assert send_payloads[-1].message == f"{item.title} says 2:15 PM."
|
assert send_payloads[-1].message == f"{item.title} says 2:15 PM."
|
||||||
assert any(getattr(packet, "type", "") == "item_use_sound" for packet in broadcast_payloads)
|
assert not any(getattr(packet, "type", "") == "item_use_sound" for packet in broadcast_payloads)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user