Keep origin and destination audio subscriptions during teleport
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.22 R185";
|
window.CHGRID_WEB_VERSION = "2026.02.22 R186";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function resolveEmitRates(item: WorldItem): { playbackRate: number; preservePitc
|
|||||||
export class ItemEmitRuntime {
|
export class ItemEmitRuntime {
|
||||||
private readonly outputs = new Map<string, EmitOutput>();
|
private readonly outputs = new Map<string, EmitOutput>();
|
||||||
private layerEnabled = true;
|
private layerEnabled = true;
|
||||||
private listenerPosition: { x: number; y: number } | null = null;
|
private listenerPositions: Array<{ x: number; y: number }> = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly audio: AudioEngine,
|
private readonly audio: AudioEngine,
|
||||||
@@ -96,25 +96,28 @@ export class ItemEmitRuntime {
|
|||||||
listenerPosition: { x: number; y: number } | null = null,
|
listenerPosition: { x: number; y: number } | null = null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.layerEnabled = enabled;
|
this.layerEnabled = enabled;
|
||||||
if (listenerPosition) {
|
this.listenerPositions = listenerPosition ? [{ ...listenerPosition }] : [];
|
||||||
this.listenerPosition = { ...listenerPosition };
|
|
||||||
}
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.cleanupAll();
|
this.cleanupAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.sync(items, this.listenerPosition);
|
await this.sync(items, this.listenerPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sync(items: Iterable<WorldItem>, listenerPosition: { x: number; y: number } | null = null): Promise<void> {
|
async sync(
|
||||||
|
items: Iterable<WorldItem>,
|
||||||
|
listenerPositions: Array<{ x: number; y: number }> | { x: number; y: number } | null = null,
|
||||||
|
): Promise<void> {
|
||||||
if (!this.layerEnabled) {
|
if (!this.layerEnabled) {
|
||||||
this.cleanupAll();
|
this.cleanupAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (listenerPosition) {
|
if (Array.isArray(listenerPositions)) {
|
||||||
this.listenerPosition = { ...listenerPosition };
|
this.listenerPositions = listenerPositions.map((listener) => ({ ...listener }));
|
||||||
|
} else if (listenerPositions) {
|
||||||
|
this.listenerPositions = [{ ...listenerPositions }];
|
||||||
}
|
}
|
||||||
const listener = this.listenerPosition;
|
const listeners = this.listenerPositions;
|
||||||
const validIds = new Set<string>();
|
const validIds = new Set<string>();
|
||||||
let audioCtx = this.audio.context;
|
let audioCtx = this.audio.context;
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ export class ItemEmitRuntime {
|
|||||||
const emitSound = String(item.params.emitSound ?? item.emitSound ?? '').trim();
|
const emitSound = String(item.params.emitSound ?? item.emitSound ?? '').trim();
|
||||||
const enabled = item.params.enabled !== false;
|
const enabled = item.params.enabled !== false;
|
||||||
const soundUrl = enabled ? this.resolveSoundUrl(emitSound) : '';
|
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);
|
this.cleanup(item.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -230,13 +233,15 @@ export class ItemEmitRuntime {
|
|||||||
|
|
||||||
private shouldKeepRuntime(
|
private shouldKeepRuntime(
|
||||||
item: WorldItem,
|
item: WorldItem,
|
||||||
listenerPosition: { x: number; y: number } | null,
|
listenerPositions: Array<{ x: number; y: number }>,
|
||||||
currentlyActive: boolean,
|
currentlyActive: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!listenerPosition) return false;
|
if (listenerPositions.length === 0) return false;
|
||||||
const spatialConfig = this.getSpatialConfig(item);
|
const spatialConfig = this.getSpatialConfig(item);
|
||||||
const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS);
|
const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS);
|
||||||
const threshold = baseRange + (currentlyActive ? UNSUBSCRIBE_HYSTERESIS_SQUARES : SUBSCRIBE_PRELOAD_SQUARES);
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export class RadioStationRuntime {
|
|||||||
private readonly sharedRadioSources = new Map<string, SharedRadioSource>();
|
private readonly sharedRadioSources = new Map<string, SharedRadioSource>();
|
||||||
private readonly itemRadioOutputs = new Map<string, ItemRadioOutput>();
|
private readonly itemRadioOutputs = new Map<string, ItemRadioOutput>();
|
||||||
private layerEnabled = true;
|
private layerEnabled = true;
|
||||||
private listenerPosition: { x: number; y: number } | null = null;
|
private listenerPositions: Array<{ x: number; y: number }> = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly audio: AudioEngine,
|
private readonly audio: AudioEngine,
|
||||||
@@ -218,30 +218,33 @@ export class RadioStationRuntime {
|
|||||||
listenerPosition: { x: number; y: number } | null = null,
|
listenerPosition: { x: number; y: number } | null = null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.layerEnabled = enabled;
|
this.layerEnabled = enabled;
|
||||||
if (listenerPosition) {
|
this.listenerPositions = listenerPosition ? [{ ...listenerPosition }] : [];
|
||||||
this.listenerPosition = { ...listenerPosition };
|
|
||||||
}
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.cleanupAll();
|
this.cleanupAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.sync(items, this.listenerPosition);
|
await this.sync(items, this.listenerPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sync(items: Iterable<WorldItem>, listenerPosition: { x: number; y: number } | null = null): Promise<void> {
|
async sync(
|
||||||
|
items: Iterable<WorldItem>,
|
||||||
|
listenerPositions: Array<{ x: number; y: number }> | { x: number; y: number } | null = null,
|
||||||
|
): Promise<void> {
|
||||||
if (!this.layerEnabled) {
|
if (!this.layerEnabled) {
|
||||||
this.cleanupAll();
|
this.cleanupAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (listenerPosition) {
|
if (Array.isArray(listenerPositions)) {
|
||||||
this.listenerPosition = { ...listenerPosition };
|
this.listenerPositions = listenerPositions.map((listener) => ({ ...listener }));
|
||||||
|
} else if (listenerPositions) {
|
||||||
|
this.listenerPositions = [{ ...listenerPositions }];
|
||||||
}
|
}
|
||||||
const listener = this.listenerPosition;
|
const listeners = this.listenerPositions;
|
||||||
const validIds = new Set<string>();
|
const validIds = new Set<string>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type !== 'radio_station') continue;
|
if (item.type !== 'radio_station') continue;
|
||||||
validIds.add(item.id);
|
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);
|
this.cleanup(item.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -408,16 +411,18 @@ export class RadioStationRuntime {
|
|||||||
|
|
||||||
private shouldKeepRuntime(
|
private shouldKeepRuntime(
|
||||||
item: WorldItem,
|
item: WorldItem,
|
||||||
listenerPosition: { x: number; y: number } | null,
|
listenerPositions: Array<{ x: number; y: number }>,
|
||||||
currentlyActive: boolean,
|
currentlyActive: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
const streamUrl = String(item.params.streamUrl ?? '').trim();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
const spatialConfig = this.getSpatialConfig(item);
|
const spatialConfig = this.getSpatialConfig(item);
|
||||||
const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS);
|
const baseRange = Math.max(1, spatialConfig.range || HEARING_RADIUS);
|
||||||
const threshold = baseRange + (currentlyActive ? UNSUBSCRIBE_HYSTERESIS_SQUARES : SUBSCRIBE_PRELOAD_SQUARES);
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,10 +562,20 @@ async function applyAudioLayerState(): Promise<void> {
|
|||||||
|
|
||||||
/** Refreshes distance-gated radio/item stream subscriptions for a listener position. */
|
/** Refreshes distance-gated radio/item stream subscriptions for a listener position. */
|
||||||
async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: number }, force = false): Promise<void> {
|
async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: number }, force = false): Promise<void> {
|
||||||
|
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<void> {
|
||||||
if (!state.running) return;
|
if (!state.running) return;
|
||||||
|
if (listenerPositions.length === 0) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tileX = Math.round(listenerPosition.x);
|
const anchorListener = listenerPositions[listenerPositions.length - 1];
|
||||||
const tileY = Math.round(listenerPosition.y);
|
const tileX = Math.round(anchorListener.x);
|
||||||
|
const tileY = Math.round(anchorListener.y);
|
||||||
const moved = tileX !== lastSubscriptionRefreshTileX || tileY !== lastSubscriptionRefreshTileY;
|
const moved = tileX !== lastSubscriptionRefreshTileX || tileY !== lastSubscriptionRefreshTileY;
|
||||||
if (!force && !moved && now - lastSubscriptionRefreshAt < AUDIO_SUBSCRIPTION_REFRESH_MS) {
|
if (!force && !moved && now - lastSubscriptionRefreshAt < AUDIO_SUBSCRIPTION_REFRESH_MS) {
|
||||||
return;
|
return;
|
||||||
@@ -579,8 +589,8 @@ async function refreshAudioSubscriptionsAt(listenerPosition: { x: number; y: num
|
|||||||
lastSubscriptionRefreshTileX = tileX;
|
lastSubscriptionRefreshTileX = tileX;
|
||||||
lastSubscriptionRefreshTileY = tileY;
|
lastSubscriptionRefreshTileY = tileY;
|
||||||
try {
|
try {
|
||||||
await radioRuntime.sync(state.items.values(), listenerPosition);
|
await radioRuntime.sync(state.items.values(), listenerPositions);
|
||||||
await itemEmitRuntime.sync(state.items.values(), listenerPosition);
|
await itemEmitRuntime.sync(state.items.values(), listenerPositions);
|
||||||
} finally {
|
} finally {
|
||||||
subscriptionRefreshInFlight = false;
|
subscriptionRefreshInFlight = false;
|
||||||
if (subscriptionRefreshPending) {
|
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. */
|
/** Refreshes distance-gated radio/item stream subscriptions on movement or timer cadence. */
|
||||||
async function refreshAudioSubscriptions(force = false): Promise<void> {
|
async function refreshAudioSubscriptions(force = false): Promise<void> {
|
||||||
if (activeTeleport) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await refreshAudioSubscriptionsAt({ x: state.player.x, y: state.player.y }, force);
|
await refreshAudioSubscriptionsAt({ x: state.player.x, y: state.player.y }, force);
|
||||||
@@ -1137,7 +1153,13 @@ function startTeleportTo(targetX: number, targetY: number, completionStatus: str
|
|||||||
}
|
}
|
||||||
stopLoop();
|
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.ArrowUp = false;
|
||||||
state.keysPressed.ArrowDown = false;
|
state.keysPressed.ArrowDown = false;
|
||||||
state.keysPressed.ArrowLeft = false;
|
state.keysPressed.ArrowLeft = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user