mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Add Phase 1 plan: URL rooms, animation triggers, and Svelte overlay
This commit is contained in:
parent
14d6bf8c8e
commit
fd3ce9f7a1
285
docs/multiplayer-rooms-and-animations.md
Normal file
285
docs/multiplayer-rooms-and-animations.md
Normal file
@ -0,0 +1,285 @@
|
||||
# Phase 1: URL Rooms, Animation Triggers, and Svelte Overlay
|
||||
|
||||
## Context
|
||||
|
||||
The multiplayer backend works: Cloudflare Durable Objects relay, binary protocol (15Hz player state, world events), remote player rendering, host election. But there's no user-facing interface -- room ID is hardcoded to `"default"` in `ISLE/emscripten/config.cpp`, everyone auto-joins one room. The isle.pizza Svelte 5 frontend hosts the WASM game but has zero multiplayer screens.
|
||||
|
||||
The relay can be extended. Max 5 players per room (5 playable characters). No auth system.
|
||||
|
||||
## What We're Building
|
||||
|
||||
URL-based rooms + animation trigger system + Svelte overlay.
|
||||
|
||||
- **Room code in URL**: `isle.pizza/r/BRICK42`
|
||||
- **Svelte overlay** with buttons that trigger animations on the local player (presented as "emotes" in the UI, but internally just animation triggers)
|
||||
- **Room preview** on isle.pizza (player count before joining)
|
||||
- **No text chat** -- emotes are the communication
|
||||
- **Connection lifecycle handled by URL** at startup (no `mp_connect`/`mp_disconnect` from Svelte)
|
||||
|
||||
### Key Design Principle: Animations, Not Emotes
|
||||
|
||||
The UI presents these as "emotes" (Wave, Dance, Cheer, etc.), but the internal system is a **generic animation trigger mechanism**. The protocol, C++ exports, and network messages all deal in animation IDs -- not emote-specific concepts. This means:
|
||||
|
||||
- The protocol message carries an `animationId`, not an `emoteId`
|
||||
- The C++ export is `mp_trigger_animation(int animationId)`, not `mp_trigger_emote`
|
||||
- The mapping from UI label ("Wave") to animation ID lives entirely in the Svelte overlay
|
||||
- New animations can be added without changing the protocol or C++ code
|
||||
|
||||
## User Flow
|
||||
|
||||
```
|
||||
isle.pizza → solo play
|
||||
isle.pizza/r/BRICK42 → auto-joins room BRICK42
|
||||
```
|
||||
|
||||
To play together:
|
||||
|
||||
1. Click "Create Room" → generates code → navigates to `/r/CODE`
|
||||
2. Share the URL with friends
|
||||
3. Friends open URL → auto-join the same room
|
||||
|
||||
## In-Game Overlay
|
||||
|
||||
```
|
||||
┌──────────────── Game Canvas ─────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ Room: BRICK42 │ │
|
||||
│ │ Players: 3/5 │ │
|
||||
│ │ [Copy Link] │ │
|
||||
│ │ │ │
|
||||
│ │ ── Emotes ── │ │
|
||||
│ │ [Wave] [Dance] │ │
|
||||
│ │ [Cheer] [Bow] │ │
|
||||
│ └────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Clicking a button calls a C++ function that triggers an animation on the local player character. The animation state is synced to other players through the multiplayer protocol as a one-shot animation message.
|
||||
|
||||
## Svelte → WASM Bridge
|
||||
|
||||
One-way, minimal. The C++ side exports only what the Svelte overlay needs:
|
||||
|
||||
```cpp
|
||||
extern "C" {
|
||||
// Trigger an animation on the local player by ID.
|
||||
// The mapping from UI emote labels to animation IDs lives in Svelte.
|
||||
EMSCRIPTEN_KEEPALIVE void mp_trigger_animation(int animationId);
|
||||
|
||||
// Room info -- for the overlay to display
|
||||
EMSCRIPTEN_KEEPALIVE int mp_get_player_count();
|
||||
}
|
||||
```
|
||||
|
||||
No `mp_connect` / `mp_disconnect` -- the connection is established at WASM startup based on the room ID passed in via config. The Svelte frontend sets the room ID before WASM init:
|
||||
|
||||
```
|
||||
URL → Svelte reads /r/BRICK42 → sets Module config → WASM starts → auto-connects to BRICK42
|
||||
```
|
||||
|
||||
## Protocol: MSG_ANIMATION
|
||||
|
||||
A new one-shot message sent when any player triggers an animation:
|
||||
|
||||
```
|
||||
MSG_ANIMATION = 9
|
||||
|
||||
struct AnimationMsg {
|
||||
MessageHeader header; // 9 bytes (type + peerId + sequence)
|
||||
uint8_t animationId; // 1 byte
|
||||
};
|
||||
```
|
||||
|
||||
Total: 10 bytes per animation trigger.
|
||||
|
||||
The relay broadcasts it to all peers (it already broadcasts unknown message types). Remote clients receive the message and play the corresponding animation on that peer's character.
|
||||
|
||||
### Why a One-Shot Message (Not Player State)
|
||||
|
||||
Animations are discrete events, not continuous state. Piggybacking on the 15Hz `PlayerStateMsg` would mean:
|
||||
|
||||
- Wasting bandwidth sending an animation field every 66ms even when idle
|
||||
- Needing "started at" timestamps or frame counters to avoid replaying
|
||||
- Difficulty distinguishing "no animation" from "animation just ended"
|
||||
|
||||
A one-shot message is cleaner: fire once, play once, done.
|
||||
|
||||
### Animation ID Space
|
||||
|
||||
The `animationId` field is a `uint8_t`, giving 256 possible animations. The initial set depends on what animations the original game characters already have. Potential candidates:
|
||||
|
||||
| ID | UI Label | Animation Source | Notes |
|
||||
|----|----------|-----------------|-------|
|
||||
| 0 | Wave | Arm wave | May need new animation or repurpose existing |
|
||||
| 1 | Dance | Character dance | Original game has some dance animations |
|
||||
| 2 | Cheer | Jump + arms up | Could reuse victory animations |
|
||||
| 3 | Bow | Forward lean | May need new animation |
|
||||
| 4 | Point | Point forward | Reuse directing animation |
|
||||
|
||||
The exact set requires investigation of available animation assets. The protocol supports adding more at any time -- just add a new ID mapping in the Svelte overlay and a handler on the C++ side.
|
||||
|
||||
## Room ID Configuration
|
||||
|
||||
### Current State
|
||||
|
||||
Room name is set in `ISLE/emscripten/config.cpp` as a default INI override:
|
||||
|
||||
```cpp
|
||||
iniparser_set(p_dictionary, "multiplayer:room", "default");
|
||||
```
|
||||
|
||||
The extension reads it in `MultiplayerExt::Initialize()`:
|
||||
|
||||
```cpp
|
||||
room = options["multiplayer:room"];
|
||||
```
|
||||
|
||||
### Change
|
||||
|
||||
The Svelte frontend reads the room code from the URL path (`/r/BRICK42`) and passes it to the WASM module before initialization. The default remains `""` (empty = solo play, no connection).
|
||||
|
||||
The Emscripten config override should read the room from a JS `Module` property:
|
||||
|
||||
```cpp
|
||||
// In Emscripten_SetupDefaultConfigOverrides():
|
||||
EM_ASM({
|
||||
var room = Module['multiplayerRoom'] || '';
|
||||
if (room) {
|
||||
stringToUTF8(room, $0, 64);
|
||||
} else {
|
||||
stringToUTF8('', $0, 64);
|
||||
}
|
||||
}, roomBuf);
|
||||
iniparser_set(p_dictionary, "multiplayer:room", roomBuf);
|
||||
```
|
||||
|
||||
The relay URL can follow the same pattern, or remain hardcoded to the production relay.
|
||||
|
||||
### Connection Logic
|
||||
|
||||
- **Empty room** (`""` or missing): multiplayer extension skips initialization entirely (current behavior when `room.empty()` returns true)
|
||||
- **Non-empty room**: connect to relay at `/room/{roomId}` as currently implemented
|
||||
|
||||
No `mp_connect`/`mp_disconnect` exports needed. The connection is established once at startup and torn down when the page unloads.
|
||||
|
||||
## Relay: Room Preview Endpoint
|
||||
|
||||
Add an HTTP response to the Durable Object for non-WebSocket requests, so the Svelte UI can show player count before joining:
|
||||
|
||||
```typescript
|
||||
// In GameRoom.fetch(), before WebSocket upgrade:
|
||||
if (request.headers.get("Upgrade") !== "websocket") {
|
||||
return new Response(JSON.stringify({
|
||||
players: this.connections.size,
|
||||
maxPlayers: 5
|
||||
}), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cross-Origin-Resource-Policy": "same-site"
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `same-site` header is needed because isle.pizza serves `Cross-Origin-Embedder-Policy: require-corp` (required for SharedArrayBuffer / pthreads). WebSocket connections are unaffected by COEP; only HTTP fetch needs this header.
|
||||
|
||||
## Hosting: relay.isle.pizza
|
||||
|
||||
Works with the COOP/COEP setup:
|
||||
|
||||
- **WebSocket** (`wss://relay.isle.pizza/room/...`): unaffected by COEP
|
||||
- **HTTP fetch** (`GET https://relay.isle.pizza/room/.../info`): needs `Cross-Origin-Resource-Policy: same-site` (relay and isle.pizza share the same registrable domain)
|
||||
|
||||
## C++ Implementation: Animation Trigger
|
||||
|
||||
When `mp_trigger_animation(animationId)` is called:
|
||||
|
||||
1. Look up the animation for the current player character
|
||||
2. Play the animation locally (using existing animation system)
|
||||
3. Send `MSG_ANIMATION` with the `animationId` to all peers via the relay
|
||||
|
||||
When a remote `MSG_ANIMATION` is received:
|
||||
|
||||
1. Look up the remote player by `peerId` from the message header
|
||||
2. Look up the animation for that character
|
||||
3. Play the animation on the remote player's character model
|
||||
|
||||
The animation lookup and playback depend on the existing animation assets and system (`LegoROI::ApplyAnimationTransformation()` for remote players, or triggering through the entity system for the local player). The exact mechanism needs investigation of available character animations.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `extensions/include/extensions/multiplayer/protocol.h` | Add `MSG_ANIMATION` (type 9) and `AnimationMsg` struct |
|
||||
| `extensions/src/multiplayer/networkmanager.cpp` | Handle incoming `MSG_ANIMATION`, add `SendAnimation(uint8_t animationId)` method |
|
||||
| `extensions/include/extensions/multiplayer/networkmanager.h` | Declare `SendAnimation()` |
|
||||
| `extensions/src/multiplayer.cpp` | Add `mp_trigger_animation()` and `mp_get_player_count()` C exports |
|
||||
| `ISLE/emscripten/config.cpp` | Read `multiplayerRoom` from JS `Module` property instead of hardcoding `"default"` |
|
||||
| `CMakeLists.txt` | Add `mp_trigger_animation`, `mp_get_player_count` to `EXPORTED_FUNCTIONS` |
|
||||
| `extensions/src/multiplayer/server/gameroom.ts` | Add HTTP response for non-WebSocket requests (room preview) |
|
||||
| isle.pizza `src/App.svelte` | Read `/r/:code` from URL, pass room to WASM `Module` config, mount overlay |
|
||||
| isle.pizza: new overlay component | Emote buttons that call `Module.ccall('mp_trigger_animation', ...)`, room info, copy link |
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **URL rooms** -- read room ID from URL in Svelte, pass to WASM via `Module` property, connect on startup. Solo play when no room in URL.
|
||||
2. **Animation trigger protocol** -- `MSG_ANIMATION` message type, `AnimationMsg` struct, send/receive in `NetworkManager`.
|
||||
3. **C++ animation exports** -- `mp_trigger_animation()` that plays locally and broadcasts, `mp_get_player_count()` for overlay display.
|
||||
4. **Svelte overlay** -- emote buttons calling `mp_trigger_animation`, room info badge, copy link button.
|
||||
5. **Room preview** -- relay HTTP endpoint returning player count, Svelte pre-join display.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Available animations**: What character animations exist in the original game assets? The emote set depends entirely on this. Need to investigate `LegoCharacterManager`, animation trees, and existing animation data files.
|
||||
2. **Animation playback for remote players**: Remote players use `LegoROI::ApplyAnimationTransformation()` with static animation data. Can triggered animations be played through this system, or do they need a different approach? The current remote player rendering applies walking animations at the network tick rate -- triggered animations may need to run for a fixed duration and then return to the idle/walking state.
|
||||
3. **Animation interruption**: What happens if a player triggers an animation while walking? Should movement stop, or should the animation play on top of movement? The local player's path actor state may need to be considered.
|
||||
4. **Room code generation**: What format for generated room codes? Short alphanumeric strings (e.g., `BRICK42`, `SURF99`) are memorable and URL-safe. A simple random generator in Svelte is sufficient.
|
||||
5. **Room capacity enforcement**: The relay should reject WebSocket upgrades when a room has 5 players. Currently the relay doesn't enforce this -- needs a connection count check in `GameRoom.fetch()`.
|
||||
|
||||
---
|
||||
|
||||
## Future Layers (Reference)
|
||||
|
||||
These build on URL rooms. Each layers on the previous:
|
||||
|
||||
### Layer 1: Public Lobby Browser
|
||||
|
||||
Browse active rooms. Requires cross-DO aggregation (Cloudflare KV or D1) for room listing.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ PUBLIC ROOMS [Create] │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ BRICK42 ●●●○○ 3/5 [Join]│ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ SURF99 ●○○○○ 1/5 [Join]│ │
|
||||
│ └──────────────────────────────┘ │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Relay extensions needed**: KV writes on join/leave (with TTL), Worker endpoint to list rooms, room visibility flag (public/private).
|
||||
|
||||
### Layer 2: Quick Match / Auto-Join
|
||||
|
||||
One button, auto-join an open room. Requires same infra as lobby browser. Best with a large active player base.
|
||||
|
||||
### Layer 3: Persistent Social Lobby
|
||||
|
||||
Global chat room at isle.pizza. See who's online, organize games. Separate Durable Object for lobby chat (independent of game rooms).
|
||||
|
||||
### Layer 4: Spatial Island Lobby
|
||||
|
||||
The game world IS the lobby. Everyone loads into a shared overworld, walks up to activities. Most immersive, most complex. Long-term vision.
|
||||
|
||||
### Composition
|
||||
|
||||
```
|
||||
Layer 0 (Foundation): URL Rooms + Animation Triggers ← building this now
|
||||
Layer 1 (Discovery): Lobby Browser / Quick Match
|
||||
Layer 2 (Social): Persistent Lobby + Chat
|
||||
Layer 3 (Immersive): Spatial Island Lobby
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user