Broadcast teleport landing sound to nearby users
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.25 R237";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R238";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -1095,6 +1095,7 @@ function updateTeleport(): void {
|
|||||||
state.player.x = activeTeleport.targetX;
|
state.player.x = activeTeleport.targetX;
|
||||||
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 });
|
||||||
|
signaling.send({ type: 'teleport_complete' });
|
||||||
activeTeleport = null;
|
activeTeleport = null;
|
||||||
stopTeleportLoopAudio();
|
stopTeleportLoopAudio();
|
||||||
persistPlayerPosition();
|
persistPlayerPosition();
|
||||||
|
|||||||
@@ -158,6 +158,13 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'teleport_complete': {
|
||||||
|
if (deps.getAudioLayers().world) {
|
||||||
|
deps.playRemoteSpatialStepOrTeleport(deps.TELEPORT_SOUND_URL, message.x, message.y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'update_nickname': {
|
case 'update_nickname': {
|
||||||
const peer = deps.state.peers.get(message.id);
|
const peer = deps.state.peers.get(message.id);
|
||||||
if (peer) {
|
if (peer) {
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ export const updatePositionSchema = z.object({
|
|||||||
y: z.number().int(),
|
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({
|
export const updateNicknameSchema = z.object({
|
||||||
type: z.literal('update_nickname'),
|
type: z.literal('update_nickname'),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -199,6 +206,7 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
|
|||||||
welcomeMessageSchema,
|
welcomeMessageSchema,
|
||||||
signalMessageSchema,
|
signalMessageSchema,
|
||||||
updatePositionSchema,
|
updatePositionSchema,
|
||||||
|
teleportCompleteSchema,
|
||||||
updateNicknameSchema,
|
updateNicknameSchema,
|
||||||
userLeftSchema,
|
userLeftSchema,
|
||||||
chatMessageSchema,
|
chatMessageSchema,
|
||||||
@@ -217,6 +225,7 @@ export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|
|||||||
export type OutgoingMessage =
|
export type OutgoingMessage =
|
||||||
| { type: 'signal'; targetId: string; sdp?: RTCSessionDescriptionInit; ice?: RTCIceCandidateInit }
|
| { type: 'signal'; targetId: string; sdp?: RTCSessionDescriptionInit; ice?: RTCIceCandidateInit }
|
||||||
| { type: 'update_position'; x: number; y: number }
|
| { type: 'update_position'; x: number; y: number }
|
||||||
|
| { type: 'teleport_complete' }
|
||||||
| { type: 'update_nickname'; nickname: string }
|
| { type: 'update_nickname'; nickname: string }
|
||||||
| { type: 'chat_message'; message: string }
|
| { type: 'chat_message'; message: string }
|
||||||
| { type: 'ping'; clientSentAt: number }
|
| { type: 'ping'; clientSentAt: number }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
## Client -> Server
|
## Client -> Server
|
||||||
|
|
||||||
- `update_position`: client movement intent; server enforces world bounds and movement rate policy.
|
- `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).
|
- `update_nickname`: nickname change request (server enforces uniqueness).
|
||||||
- `chat_message`: player chat.
|
- `chat_message`: player chat.
|
||||||
- `ping`: latency measurement.
|
- `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.
|
- `welcome`: initial snapshot with users/items plus server UI/world metadata.
|
||||||
- `signal`: forwarded WebRTC offer/answer/ICE.
|
- `signal`: forwarded WebRTC offer/answer/ICE.
|
||||||
- `update_position`, `update_nickname`, `user_left`: presence updates.
|
- `update_position`, `update_nickname`, `user_left`: presence updates.
|
||||||
|
- `teleport_complete`: peer teleport landing event with spatial coordinates.
|
||||||
- `chat_message`: system and user chat stream.
|
- `chat_message`: system and user chat stream.
|
||||||
- `pong`: ping response.
|
- `pong`: ping response.
|
||||||
- `nickname_result`: accepted/rejected nickname result.
|
- `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_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.
|
- `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.
|
- 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:
|
- `item_piano_note` contains:
|
||||||
- `itemId`, `senderId`, `keyId`, `midi`, `on`
|
- `itemId`, `senderId`, `keyId`, `midi`, `on`
|
||||||
- resolved `instrument`, `voiceMode`, `octave`, `attack`, `decay`, `release`, `brightness`, `emitRange`
|
- resolved `instrument`, `voiceMode`, `octave`, `attack`, `decay`, `release`, `brightness`, `emitRange`
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Core incoming message effects:
|
|||||||
|
|
||||||
- `signal`: WebRTC negotiation and ICE exchange.
|
- `signal`: WebRTC negotiation and ICE exchange.
|
||||||
- `update_position`: update peer position; may play movement/teleport world sound.
|
- `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.
|
- `update_nickname`: update peer display name.
|
||||||
- `chat_message`: append/readable status; optional system sound class.
|
- `chat_message`: append/readable status; optional system sound class.
|
||||||
- `item_upsert`: replace item snapshot and resync item runtimes.
|
- `item_upsert`: replace item snapshot and resync item runtimes.
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class UpdatePositionPacket(BasePacket):
|
|||||||
y: int
|
y: int
|
||||||
|
|
||||||
|
|
||||||
|
class TeleportCompletePacket(BasePacket):
|
||||||
|
type: Literal["teleport_complete"]
|
||||||
|
|
||||||
|
|
||||||
class UpdateNicknamePacket(BasePacket):
|
class UpdateNicknamePacket(BasePacket):
|
||||||
type: Literal["update_nickname"]
|
type: Literal["update_nickname"]
|
||||||
nickname: str = Field(min_length=1, max_length=32)
|
nickname: str = Field(min_length=1, max_length=32)
|
||||||
@@ -91,6 +95,7 @@ class ItemUpdatePacket(BasePacket):
|
|||||||
ClientPacket = (
|
ClientPacket = (
|
||||||
SignalPacket
|
SignalPacket
|
||||||
| UpdatePositionPacket
|
| UpdatePositionPacket
|
||||||
|
| TeleportCompletePacket
|
||||||
| UpdateNicknamePacket
|
| UpdateNicknamePacket
|
||||||
| ChatMessagePacket
|
| ChatMessagePacket
|
||||||
| PingPacket
|
| PingPacket
|
||||||
@@ -135,6 +140,13 @@ class BroadcastPositionPacket(BasePacket):
|
|||||||
y: int
|
y: int
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastTeleportCompletePacket(BasePacket):
|
||||||
|
type: Literal["teleport_complete"]
|
||||||
|
id: str
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
|
||||||
|
|
||||||
class BroadcastNicknamePacket(BasePacket):
|
class BroadcastNicknamePacket(BasePacket):
|
||||||
type: Literal["update_nickname"]
|
type: Literal["update_nickname"]
|
||||||
id: str
|
id: str
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from .models import (
|
|||||||
BroadcastChatMessagePacket,
|
BroadcastChatMessagePacket,
|
||||||
BroadcastNicknamePacket,
|
BroadcastNicknamePacket,
|
||||||
BroadcastPositionPacket,
|
BroadcastPositionPacket,
|
||||||
|
BroadcastTeleportCompletePacket,
|
||||||
ChatMessagePacket,
|
ChatMessagePacket,
|
||||||
ClientPacket,
|
ClientPacket,
|
||||||
ForwardSignalPacket,
|
ForwardSignalPacket,
|
||||||
@@ -63,6 +64,7 @@ from .models import (
|
|||||||
PingPacket,
|
PingPacket,
|
||||||
PongPacket,
|
PongPacket,
|
||||||
RemoteUser,
|
RemoteUser,
|
||||||
|
TeleportCompletePacket,
|
||||||
UpdateNicknamePacket,
|
UpdateNicknamePacket,
|
||||||
UpdatePositionPacket,
|
UpdatePositionPacket,
|
||||||
UserLeftPacket,
|
UserLeftPacket,
|
||||||
@@ -879,6 +881,18 @@ class SignalingServer:
|
|||||||
await self._broadcast_item(carried)
|
await self._broadcast_item(carried)
|
||||||
return
|
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):
|
if isinstance(packet, UpdateNicknamePacket):
|
||||||
requested_nickname = packet.nickname.strip()
|
requested_nickname = packet.nickname.strip()
|
||||||
if not requested_nickname:
|
if not requested_nickname:
|
||||||
|
|||||||
@@ -135,3 +135,27 @@ async def test_update_position_enforces_cumulative_budget_per_tick(monkeypatch:
|
|||||||
assert client.x == 7
|
assert client.x == 7
|
||||||
assert client.y == 5
|
assert client.y == 5
|
||||||
assert len(broadcast_payloads) == 2
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user