From ce666b2bd6fa7e38963aee660d79e92ab20e3ebf Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 22 Feb 2026 20:43:57 -0500 Subject: [PATCH] Keep origin and destination audio subscriptions during teleport --- client/public/version.js | 2 +- client/src/audio/itemEmitRuntime.ts | 31 ++++++++++++---------- client/src/audio/radioStationRuntime.ts | 31 ++++++++++++---------- client/src/main.ts | 34 ++++++++++++++++++++----- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/client/public/version.js b/client/public/version.js index 3b88f0a..42d0d9e 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.22 R185"; +window.CHGRID_WEB_VERSION = "2026.02.22 R186"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/itemEmitRuntime.ts b/client/src/audio/itemEmitRuntime.ts index 6eed849..89510b1 100644 --- a/client/src/audio/itemEmitRuntime.ts +++ b/client/src/audio/itemEmitRuntime.ts @@ -63,7 +63,7 @@ function resolveEmitRates(item: WorldItem): { playbackRate: number; preservePitc export class ItemEmitRuntime { private readonly outputs = new Map(); private layerEnabled = true; - private listenerPosition: { x: number; y: number } | null = null; + private listenerPositions: Array<{ x: number; y: number }> = []; constructor( private readonly audio: AudioEngine, @@ -96,25 +96,28 @@ export class ItemEmitRuntime { listenerPosition: { x: number; y: number } | null = null, ): Promise { this.layerEnabled = enabled; - if (listenerPosition) { - this.listenerPosition = { ...listenerPosition }; - } + this.listenerPositions = listenerPosition ? [{ ...listenerPosition }] : []; if (!enabled) { this.cleanupAll(); return; } - await this.sync(items, this.listenerPosition); + await this.sync(items, this.listenerPositions); } - async sync(items: Iterable, listenerPosition: { x: number; y: number } | null = null): Promise { + async sync( + items: Iterable, + listenerPositions: Array<{ x: number; y: number }> | { x: number; y: number } | null = null, + ): Promise { if (!this.layerEnabled) { this.cleanupAll(); return; } - if (listenerPosition) { - this.listenerPosition = { ...listenerPosition }; + if (Array.isArray(listenerPositions)) { + this.listenerPositions = listenerPositions.map((listener) => ({ ...listener })); + } else if (listenerPositions) { + this.listenerPositions = [{ ...listenerPositions }]; } - const listener = this.listenerPosition; + const listeners = this.listenerPositions; const validIds = new Set(); let audioCtx = this.audio.context; @@ -122,7 +125,7 @@ export class ItemEmitRuntime { const emitSound = String(item.params.emitSound ?? item.emitSound ?? '').trim(); const enabled = item.params.enabled !== false; const soundUrl = enabled ? this.resolveSoundUrl(emitSound) : ''; - if (!soundUrl || item.carrierId || !this.shouldKeepRuntime(item, listener, this.outputs.has(item.id))) { + if (!soundUrl || item.carrierId || !this.shouldKeepRuntime(item, listeners, this.outputs.has(item.id))) { this.cleanup(item.id); continue; } @@ -230,13 +233,15 @@ export class ItemEmitRuntime { private shouldKeepRuntime( item: WorldItem, - listenerPosition: { x: number; y: number } | null, + listenerPositions: Array<{ x: number; y: number }>, currentlyActive: boolean, ): boolean { - if (!listenerPosition) return false; + if (listenerPositions.length === 0) return false; const spatialConfig = this.getSpatialConfig(item); const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS); const threshold = baseRange + (currentlyActive ? UNSUBSCRIBE_HYSTERESIS_SQUARES : SUBSCRIBE_PRELOAD_SQUARES); - return Math.hypot(item.x - listenerPosition.x, item.y - listenerPosition.y) <= threshold; + return listenerPositions.some((listenerPosition) => + Math.hypot(item.x - listenerPosition.x, item.y - listenerPosition.y) <= threshold, + ); } } diff --git a/client/src/audio/radioStationRuntime.ts b/client/src/audio/radioStationRuntime.ts index 34d2eb6..25d2ca6 100644 --- a/client/src/audio/radioStationRuntime.ts +++ b/client/src/audio/radioStationRuntime.ts @@ -170,7 +170,7 @@ export class RadioStationRuntime { private readonly sharedRadioSources = new Map(); private readonly itemRadioOutputs = new Map(); private layerEnabled = true; - private listenerPosition: { x: number; y: number } | null = null; + private listenerPositions: Array<{ x: number; y: number }> = []; constructor( private readonly audio: AudioEngine, @@ -218,30 +218,33 @@ export class RadioStationRuntime { listenerPosition: { x: number; y: number } | null = null, ): Promise { this.layerEnabled = enabled; - if (listenerPosition) { - this.listenerPosition = { ...listenerPosition }; - } + this.listenerPositions = listenerPosition ? [{ ...listenerPosition }] : []; if (!enabled) { this.cleanupAll(); return; } - await this.sync(items, this.listenerPosition); + await this.sync(items, this.listenerPositions); } - async sync(items: Iterable, listenerPosition: { x: number; y: number } | null = null): Promise { + async sync( + items: Iterable, + listenerPositions: Array<{ x: number; y: number }> | { x: number; y: number } | null = null, + ): Promise { if (!this.layerEnabled) { this.cleanupAll(); return; } - if (listenerPosition) { - this.listenerPosition = { ...listenerPosition }; + if (Array.isArray(listenerPositions)) { + this.listenerPositions = listenerPositions.map((listener) => ({ ...listener })); + } else if (listenerPositions) { + this.listenerPositions = [{ ...listenerPositions }]; } - const listener = this.listenerPosition; + const listeners = this.listenerPositions; const validIds = new Set(); for (const item of items) { if (item.type !== 'radio_station') continue; validIds.add(item.id); - if (!this.shouldKeepRuntime(item, listener, this.itemRadioOutputs.has(item.id))) { + if (!this.shouldKeepRuntime(item, listeners, this.itemRadioOutputs.has(item.id))) { this.cleanup(item.id); continue; } @@ -408,16 +411,18 @@ export class RadioStationRuntime { private shouldKeepRuntime( item: WorldItem, - listenerPosition: { x: number; y: number } | null, + listenerPositions: Array<{ x: number; y: number }>, currentlyActive: boolean, ): boolean { const streamUrl = String(item.params.streamUrl ?? '').trim(); - if (!streamUrl || item.params.enabled === false || !listenerPosition) { + if (!streamUrl || item.params.enabled === false || listenerPositions.length === 0) { return false; } const spatialConfig = this.getSpatialConfig(item); const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS); const threshold = baseRange + (currentlyActive ? UNSUBSCRIBE_HYSTERESIS_SQUARES : SUBSCRIBE_PRELOAD_SQUARES); - return Math.hypot(item.x - listenerPosition.x, item.y - listenerPosition.y) <= threshold; + return listenerPositions.some((listenerPosition) => + Math.hypot(item.x - listenerPosition.x, item.y - listenerPosition.y) <= threshold, + ); } } diff --git a/client/src/main.ts b/client/src/main.ts index b33d637..eab0036 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -562,10 +562,20 @@ async function applyAudioLayerState(): Promise { /** Refreshes distance-gated radio/item stream subscriptions for a listener position. */ async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: number }, force = false): Promise { + await refreshAudioSubscriptionsForListeners([listenerPosition], force); +} + +/** Refreshes distance-gated radio/item stream subscriptions for one or more listener positions. */ +async function refreshAudioSubscriptionsForListeners( + listenerPositions: Array<{ x: number; y: number }>, + force = false, +): Promise { if (!state.running) return; + if (listenerPositions.length === 0) return; const now = Date.now(); - const tileX = Math.round(listenerPosition.x); - const tileY = Math.round(listenerPosition.y); + const anchorListener = listenerPositions[listenerPositions.length - 1]; + const tileX = Math.round(anchorListener.x); + const tileY = Math.round(anchorListener.y); const moved = tileX !== lastSubscriptionRefreshTileX || tileY !== lastSubscriptionRefreshTileY; if (!force && !moved && now - lastSubscriptionRefreshAt < AUDIO_SUBSCRIPTION_REFRESH_MS) { return; @@ -579,8 +589,8 @@ async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: num lastSubscriptionRefreshTileX = tileX; lastSubscriptionRefreshTileY = tileY; try { - await radioRuntime.sync(state.items.values(), listenerPosition); - await itemEmitRuntime.sync(state.items.values(), listenerPosition); + await radioRuntime.sync(state.items.values(), listenerPositions); + await itemEmitRuntime.sync(state.items.values(), listenerPositions); } finally { subscriptionRefreshInFlight = false; if (subscriptionRefreshPending) { @@ -593,7 +603,13 @@ async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: num /** Refreshes distance-gated radio/item stream subscriptions on movement or timer cadence. */ async function refreshAudioSubscriptions(force = false): Promise { if (activeTeleport) { - await refreshAudioSubscriptionsAt({ x: activeTeleport.targetX, y: activeTeleport.targetY }, force); + await refreshAudioSubscriptionsForListeners( + [ + { x: activeTeleport.startX, y: activeTeleport.startY }, + { x: activeTeleport.targetX, y: activeTeleport.targetY }, + ], + force, + ); return; } await refreshAudioSubscriptionsAt({ x: state.player.x, y: state.player.y }, force); @@ -1137,7 +1153,13 @@ function startTeleportTo(targetX: number, targetY: number, completionStatus: str } stopLoop(); }); - void refreshAudioSubscriptionsAt({ x: targetX, y: targetY }, true); + void refreshAudioSubscriptionsForListeners( + [ + { x: startX, y: startY }, + { x: targetX, y: targetY }, + ], + true, + ); state.keysPressed.ArrowUp = false; state.keysPressed.ArrowDown = false; state.keysPressed.ArrowLeft = false;