Broadcast teleport landing sound to nearby users

This commit is contained in:
Jage9
2026-02-24 20:55:02 -05:00
parent a1132ea22a
commit 297f1c0c1a
9 changed files with 72 additions and 1 deletions

View File

@@ -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.25 R237";
window.CHGRID_WEB_VERSION = "2026.02.25 R238";
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -1095,6 +1095,7 @@ function updateTeleport(): void {
state.player.x = activeTeleport.targetX;
state.player.y = activeTeleport.targetY;
signaling.send({ type: 'update_position', x: activeTeleport.targetX, y: activeTeleport.targetY });
signaling.send({ type: 'teleport_complete' });
activeTeleport = null;
stopTeleportLoopAudio();
persistPlayerPosition();

View File

@@ -158,6 +158,13 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
break;
}
case 'teleport_complete': {
if (deps.getAudioLayers().world) {
deps.playRemoteSpatialStepOrTeleport(deps.TELEPORT_SOUND_URL, message.x, message.y);
}
break;
}
case 'update_nickname': {
const peer = deps.state.peers.get(message.id);
if (peer) {

View File

@@ -103,6 +103,13 @@ export const updatePositionSchema = z.object({
y: z.number().int(),
});
export const teleportCompleteSchema = z.object({
type: z.literal('teleport_complete'),
id: z.string(),
x: z.number().int(),
y: z.number().int(),
});
export const updateNicknameSchema = z.object({
type: z.literal('update_nickname'),
id: z.string(),
@@ -199,6 +206,7 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
welcomeMessageSchema,
signalMessageSchema,
updatePositionSchema,
teleportCompleteSchema,
updateNicknameSchema,
userLeftSchema,
chatMessageSchema,
@@ -217,6 +225,7 @@ export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
export type OutgoingMessage =
| { type: 'signal'; targetId: string; sdp?: RTCSessionDescriptionInit; ice?: RTCIceCandidateInit }
| { type: 'update_position'; x: number; y: number }
| { type: 'teleport_complete' }
| { type: 'update_nickname'; nickname: string }
| { type: 'chat_message'; message: string }
| { type: 'ping'; clientSentAt: number }

View File

@@ -11,6 +11,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
## Client -> Server
- `update_position`: client movement intent; server enforces world bounds and movement rate policy.
- `teleport_complete`: client signals teleport landing; server rebroadcasts spatial landing cue.
- `update_nickname`: nickname change request (server enforces uniqueness).
- `chat_message`: player chat.
- `ping`: latency measurement.
@@ -23,6 +24,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
- `welcome`: initial snapshot with users/items plus server UI/world metadata.
- `signal`: forwarded WebRTC offer/answer/ICE.
- `update_position`, `update_nickname`, `user_left`: presence updates.
- `teleport_complete`: peer teleport landing event with spatial coordinates.
- `chat_message`: system and user chat stream.
- `pong`: ping response.
- `nickname_result`: accepted/rejected nickname result.
@@ -41,6 +43,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
- `item_piano_status` carries machine-readable piano events (`use_mode_entered`, record/playback transitions).
- `item_use_sound` contains absolute item world coordinates (`x`, `y`) and sound path.
- For carried items, source coordinates resolve to the carrier's current position.
- `teleport_complete` contains absolute player world coordinates (`x`, `y`) at teleport landing.
- `item_piano_note` contains:
- `itemId`, `senderId`, `keyId`, `midi`, `on`
- resolved `instrument`, `voiceMode`, `octave`, `attack`, `decay`, `release`, `brightness`, `emitRange`

View File

@@ -39,6 +39,7 @@ Core incoming message effects:
- `signal`: WebRTC negotiation and ICE exchange.
- `update_position`: update peer position; may play movement/teleport world sound.
- `teleport_complete`: play peer teleport landing sound at final tile.
- `update_nickname`: update peer display name.
- `chat_message`: append/readable status; optional system sound class.
- `item_upsert`: replace item snapshot and resync item runtimes.

View File

@@ -25,6 +25,10 @@ class UpdatePositionPacket(BasePacket):
y: int
class TeleportCompletePacket(BasePacket):
type: Literal["teleport_complete"]
class UpdateNicknamePacket(BasePacket):
type: Literal["update_nickname"]
nickname: str = Field(min_length=1, max_length=32)
@@ -91,6 +95,7 @@ class ItemUpdatePacket(BasePacket):
ClientPacket = (
SignalPacket
| UpdatePositionPacket
| TeleportCompletePacket
| UpdateNicknamePacket
| ChatMessagePacket
| PingPacket
@@ -135,6 +140,13 @@ class BroadcastPositionPacket(BasePacket):
y: int
class BroadcastTeleportCompletePacket(BasePacket):
type: Literal["teleport_complete"]
id: str
x: int
y: int
class BroadcastNicknamePacket(BasePacket):
type: Literal["update_nickname"]
id: str

View File

@@ -42,6 +42,7 @@ from .models import (
BroadcastChatMessagePacket,
BroadcastNicknamePacket,
BroadcastPositionPacket,
BroadcastTeleportCompletePacket,
ChatMessagePacket,
ClientPacket,
ForwardSignalPacket,
@@ -63,6 +64,7 @@ from .models import (
PingPacket,
PongPacket,
RemoteUser,
TeleportCompletePacket,
UpdateNicknamePacket,
UpdatePositionPacket,
UserLeftPacket,
@@ -879,6 +881,18 @@ class SignalingServer:
await self._broadcast_item(carried)
return
if isinstance(packet, TeleportCompletePacket):
await self._broadcast(
BroadcastTeleportCompletePacket(
type="teleport_complete",
id=client.id,
x=client.x,
y=client.y,
),
exclude=client.websocket,
)
return
if isinstance(packet, UpdateNicknamePacket):
requested_nickname = packet.nickname.strip()
if not requested_nickname:

View File

@@ -135,3 +135,27 @@ async def test_update_position_enforces_cumulative_budget_per_tick(monkeypatch:
assert client.x == 7
assert client.y == 5
assert len(broadcast_payloads) == 2
@pytest.mark.asyncio
async def test_teleport_complete_broadcasts_spatial_event(monkeypatch: pytest.MonkeyPatch) -> None:
server = SignalingServer("127.0.0.1", 8765, None, None, grid_size=41)
ws = _fake_ws()
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=12, y=13)
server.clients[ws] = client
broadcast_payloads: list[object] = []
async def fake_broadcast(packet: object, exclude: ServerConnection | None = None) -> None:
broadcast_payloads.append(packet)
monkeypatch.setattr(server, "_broadcast", fake_broadcast)
await server._handle_message(client, json.dumps({"type": "teleport_complete"}))
assert len(broadcast_payloads) == 1
packet = broadcast_payloads[0]
assert packet.type == "teleport_complete"
assert packet.id == "u1"
assert packet.x == 12
assert packet.y == 13