Add moving teleport start audio loop and end cut-off
This commit is contained in:
BIN
client/public/sounds/teleport_start.ogg
Normal file
BIN
client/public/sounds/teleport_start.ogg
Normal file
Binary file not shown.
@@ -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 R182";
|
window.CHGRID_WEB_VERSION = "2026.02.22 R183";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -394,6 +394,34 @@ export class AudioEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Starts a looping sample and returns a stop callback for explicit teardown. */
|
||||||
|
async startLoopingSample(url: string, gain = 1): Promise<(() => void) | null> {
|
||||||
|
await this.ensureContext();
|
||||||
|
const { audioCtx, sfxGainNode } = this;
|
||||||
|
if (!audioCtx || !sfxGainNode || gain <= 0) return null;
|
||||||
|
try {
|
||||||
|
const buffer = await this.getSampleBuffer(url);
|
||||||
|
const source = audioCtx.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.loop = true;
|
||||||
|
const gainNode = audioCtx.createGain();
|
||||||
|
gainNode.gain.value = gain;
|
||||||
|
source.connect(gainNode).connect(sfxGainNode);
|
||||||
|
source.start();
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
source.stop();
|
||||||
|
} catch {
|
||||||
|
// Ignore already-stopped source.
|
||||||
|
}
|
||||||
|
source.disconnect();
|
||||||
|
gainNode.disconnect();
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanupPeerAudio(peer: SpatialPeerRuntime): void {
|
cleanupPeerAudio(peer: SpatialPeerRuntime): void {
|
||||||
if (peer.audioElement) {
|
if (peer.audioElement) {
|
||||||
peer.audioElement.pause();
|
peer.audioElement.pause();
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ const SYSTEM_SOUND_URLS = {
|
|||||||
} as const;
|
} as const;
|
||||||
const FOOTSTEP_SOUND_URLS = Array.from({ length: 11 }, (_, index) => withBase(`sounds/step-${index + 1}.ogg`));
|
const FOOTSTEP_SOUND_URLS = Array.from({ length: 11 }, (_, index) => withBase(`sounds/step-${index + 1}.ogg`));
|
||||||
const FOOTSTEP_GAIN = 0.7;
|
const FOOTSTEP_GAIN = 0.7;
|
||||||
|
const TELEPORT_START_SOUND_URL = withBase('sounds/teleport_start.ogg');
|
||||||
const TELEPORT_SOUND_URL = withBase('sounds/teleport.ogg');
|
const TELEPORT_SOUND_URL = withBase('sounds/teleport.ogg');
|
||||||
const WALL_SOUND_URL = withBase('sounds/wall.ogg');
|
const WALL_SOUND_URL = withBase('sounds/wall.ogg');
|
||||||
|
|
||||||
@@ -219,6 +220,8 @@ let lastSubscriptionRefreshTileX = Math.round(state.player.x);
|
|||||||
let lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
let lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
||||||
let subscriptionRefreshInFlight = false;
|
let subscriptionRefreshInFlight = false;
|
||||||
let subscriptionRefreshPending = false;
|
let subscriptionRefreshPending = false;
|
||||||
|
let activeTeleportLoopStop: (() => void) | null = null;
|
||||||
|
let activeTeleportLoopToken = 0;
|
||||||
let activeTeleport:
|
let activeTeleport:
|
||||||
| {
|
| {
|
||||||
startX: number;
|
startX: number;
|
||||||
@@ -1096,6 +1099,13 @@ function randomFootstepUrl(): string {
|
|||||||
return FOOTSTEP_SOUND_URLS[Math.floor(Math.random() * FOOTSTEP_SOUND_URLS.length)];
|
return FOOTSTEP_SOUND_URLS[Math.floor(Math.random() * FOOTSTEP_SOUND_URLS.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stops active teleport loop audio, if one is running. */
|
||||||
|
function stopTeleportLoopAudio(): void {
|
||||||
|
if (!activeTeleportLoopStop) return;
|
||||||
|
activeTeleportLoopStop();
|
||||||
|
activeTeleportLoopStop = null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Starts animated teleport movement toward a target tile at fixed squares-per-second pace. */
|
/** Starts animated teleport movement toward a target tile at fixed squares-per-second pace. */
|
||||||
function startTeleportTo(targetX: number, targetY: number, completionStatus: string): void {
|
function startTeleportTo(targetX: number, targetY: number, completionStatus: string): void {
|
||||||
const startX = state.player.x;
|
const startX = state.player.x;
|
||||||
@@ -1115,6 +1125,17 @@ function startTeleportTo(targetX: number, targetY: number, completionStatus: str
|
|||||||
lastSentY: Math.round(startY),
|
lastSentY: Math.round(startY),
|
||||||
completionStatus,
|
completionStatus,
|
||||||
};
|
};
|
||||||
|
stopTeleportLoopAudio();
|
||||||
|
activeTeleportLoopToken += 1;
|
||||||
|
const loopToken = activeTeleportLoopToken;
|
||||||
|
void audio.startLoopingSample(TELEPORT_START_SOUND_URL, FOOTSTEP_GAIN).then((stopLoop) => {
|
||||||
|
if (!stopLoop) return;
|
||||||
|
if (activeTeleport && loopToken === activeTeleportLoopToken) {
|
||||||
|
activeTeleportLoopStop = stopLoop;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopLoop();
|
||||||
|
});
|
||||||
void refreshAudioSubscriptionsAt({ x: targetX, y: targetY }, true);
|
void refreshAudioSubscriptionsAt({ x: targetX, y: targetY }, true);
|
||||||
state.keysPressed.ArrowUp = false;
|
state.keysPressed.ArrowUp = false;
|
||||||
state.keysPressed.ArrowDown = false;
|
state.keysPressed.ArrowDown = false;
|
||||||
@@ -1151,6 +1172,7 @@ function updateTeleport(): void {
|
|||||||
state.player.y = activeTeleport.targetY;
|
state.player.y = activeTeleport.targetY;
|
||||||
signaling.send({ type: 'update_position', x: activeTeleport.targetX, y: activeTeleport.targetY });
|
signaling.send({ type: 'update_position', x: activeTeleport.targetX, y: activeTeleport.targetY });
|
||||||
activeTeleport = null;
|
activeTeleport = null;
|
||||||
|
stopTeleportLoopAudio();
|
||||||
persistPlayerPosition();
|
persistPlayerPosition();
|
||||||
void refreshAudioSubscriptions(true);
|
void refreshAudioSubscriptions(true);
|
||||||
void audio.playSample(TELEPORT_SOUND_URL, FOOTSTEP_GAIN);
|
void audio.playSample(TELEPORT_SOUND_URL, FOOTSTEP_GAIN);
|
||||||
@@ -1392,6 +1414,7 @@ function disconnect(): void {
|
|||||||
lastSubscriptionRefreshAt = 0;
|
lastSubscriptionRefreshAt = 0;
|
||||||
lastSubscriptionRefreshTileX = Math.round(state.player.x);
|
lastSubscriptionRefreshTileX = Math.round(state.player.x);
|
||||||
lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
lastSubscriptionRefreshTileY = Math.round(state.player.y);
|
||||||
|
stopTeleportLoopAudio();
|
||||||
activeTeleport = null;
|
activeTeleport = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1427,6 +1450,7 @@ const onAppMessage = createOnMessageHandler({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
TELEPORT_SOUND_URL,
|
TELEPORT_SOUND_URL,
|
||||||
|
TELEPORT_START_SOUND_URL,
|
||||||
audioLayers,
|
audioLayers,
|
||||||
pushChatMessage,
|
pushChatMessage,
|
||||||
classifySystemMessageSound,
|
classifySystemMessageSound,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type MessageHandlerDeps = {
|
|||||||
randomFootstepUrl: () => string;
|
randomFootstepUrl: () => string;
|
||||||
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
||||||
TELEPORT_SOUND_URL: string;
|
TELEPORT_SOUND_URL: string;
|
||||||
|
TELEPORT_START_SOUND_URL: string;
|
||||||
audioLayers: { world: boolean };
|
audioLayers: { world: boolean };
|
||||||
pushChatMessage: (message: string) => void;
|
pushChatMessage: (message: string) => void;
|
||||||
classifySystemMessageSound: (message: string) => 'logon' | 'logout' | 'notify' | null;
|
classifySystemMessageSound: (message: string) => 'logon' | 'logout' | 'notify' | null;
|
||||||
@@ -135,7 +136,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
deps.peerManager.setPeerPosition(message.id, message.x, message.y);
|
deps.peerManager.setPeerPosition(message.id, message.x, message.y);
|
||||||
if (peer) {
|
if (peer) {
|
||||||
const movementDelta = Math.hypot(message.x - prevX, message.y - prevY);
|
const movementDelta = Math.hypot(message.x - prevX, message.y - prevY);
|
||||||
const soundUrl = movementDelta > 1.5 ? deps.TELEPORT_SOUND_URL : deps.randomFootstepUrl();
|
const soundUrl = movementDelta > 1.5 ? deps.TELEPORT_START_SOUND_URL : deps.randomFootstepUrl();
|
||||||
if (deps.audioLayers.world) {
|
if (deps.audioLayers.world) {
|
||||||
deps.playRemoteSpatialStepOrTeleport(soundUrl, peer.x, peer.y);
|
deps.playRemoteSpatialStepOrTeleport(soundUrl, peer.x, peer.y);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user