feat(multiplayer): Phase 14 MVP — agnostic core + /multiplayer demo route#20
Open
DanielMartinezSebastian wants to merge 10 commits into
Open
feat(multiplayer): Phase 14 MVP — agnostic core + /multiplayer demo route#20DanielMartinezSebastian wants to merge 10 commits into
DanielMartinezSebastian wants to merge 10 commits into
Conversation
…yer route engine-core (agnostic, no network/window): - MultiplayerPort + NetEnvelope + HeadlessMultiplayerAdapter/InMemoryHub - event classification (world/presence/private, exhaustive satisfies) - PlayerDescriptor + remotePlayersStore; net:* events/commands - MultiplayerSession (bus<->port bridge, origin-tagging anti-loop, presence throttle) - HLC clock + LWW worldState + claim resolution + optimistic reconciler engine-renderer-r3f: - RemotePlayers (reuses DavidSprite, per-player lerp interpolation) apps/web-demo: - PartyKit server (party/multiplayer.ts, cap 4) + client adapter (partysocket) - roomSession (code + localStorage persist + solo/reset + N/4) - createMultiplayerRuntime + RoomLobby + /multiplayer route - GameTouchCanvas extraCanvasChildren slot Tests: +29 core (273 total) + 6 roomSession, all green. Core agnosticism test added. Pre-existing publicApi.test.ts failures unaffected. E2E two-tab pending manual run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ploy runbook - Bump partykit to ^0.0.115 (fixes ERR_INVALID_URL on `partykit dev` on Windows) - Add .claude/launch.json (Next.js + PartyKit dev server configs) - Add DEPLOY.md: dev + prod (Vercel + PartyKit) runbook with env setup - validation-report: live dev verification (room FK4CFY, WS 101) + next build EXIT 0 Verified locally: PartyKit :1999, Next :3000, /multiplayer creates room and connects via WebSocket. Production `next build` succeeds with /multiplayer route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remote player sprites now apply the same depth-based scale interpolation (SPRITE_MIN_SCALE=1.4 .. SPRITE_MAX_SCALE=2.94, DEPTH_FAR_Z=-16 .. DEPTH_NEAR_Z=8) as GameTouchSpriteRuntime, so avatars change apparent size with Z position just like the local character does. Also includes UX improvements to RoomLobby: clipboard copy button with feedback, auto-uppercase input with Enter-to-join, (n/6) counter while typing the room code, and "Abrir 2ª pestaña" button for self-testing. The /multiplayer page now auto-joins via ?room=CODE URL param. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The local player sprite lives inside a <RigidBody> with a local Y offset of (spriteScale - 0.95), keeping the sprite's feet at a fixed position relative to the physics collider. RemotePlayerSprite was placing the mesh directly at the physics body coordinates (no offset), so remote avatars appeared 0.45–2 units lower than their actual position. Fix: compute the same visual Y = physicsY + spriteScale - 0.95 and lerp manually to [tx, ty, tz] each frame. Also switched depth factor to use target.current.z (the true position) instead of the lerped m.position.z. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ale threshold Previously presence was only sent on movement, so idle players disappeared after 8 seconds. Now MultiplayerSession sends a heartbeat every 30s so remote clients know the player is still connected. The prune threshold is raised to 10 minutes so a player only disappears if truly unreachable for that long (or immediately on window close, handled by the server __left broadcast). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remote players now use a kinematicPosition RigidBody + CuboidCollider (same dimensions as the local player: 0.55×0.95×0.18) so the local player's dynamic body collides against them. Position is lerped each frame via setNextKinematicTranslation, keeping smooth movement while Rapier handles the collision response. Sprite Y offset (spriteScale-0.95) is now applied as a local mesh offset inside the RigidBody, consistent with how the local player sprite is positioned. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… blocks path The wall-slide detection (slideBlockX/Z) fires every other frame when a kinematic remote-player body is blocking the local player's movement axis. This alternated `horizontal` between 0 and the true value at 60 fps, causing resolveAction(0,0)="idle" on alternate frames and resetting the DavidSprite animation every ~16 ms. Fix: snapshot rawHorizontal/rawVertical before slide-zeroing and feed them to resolveAction for animation purposes. Physics still receives the zeroed velocity (correct collision response), but the animation keeps showing the intended walk direction until the player genuinely stops. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esence update Passing a dynamic `position` prop to <RigidBody> caused @react-three/rapier to snap the kinematic body to the raw network position on every ~12 Hz presence re-render, overriding the smooth setNextKinematicTranslation lerp and producing visible stutter. Fix: freeze the `position` prop at mount via `spawnPosition` ref; all subsequent movement flows exclusively through setNextKinematicTranslation in useFrame. Also drive the depth-scale factor from smoothPos.current.z (was target.current.z) so sprite scale tracks the smooth path, not the 12 Hz raw position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…licate Without `id: opts.selfId`, PartySocket generates its own ID and the server echoes it back as status.selfId. MultiplayerSession then updates self.playerId to the server-assigned conn.id, so all subsequent presences use a different playerId than the initial one. Peers who stored the UUID-based initial presence see the new conn.id as a brand-new player — spawning a ghost avatar at the original spawn position and a real one following the actual movement. Passing `id: opts.selfId` pins the PartyKit party-key (_pk query param) so conn.id === our UUID throughout the session. status.selfId now matches our self.playerId, no ghost entry is created, and onClose correctly resolves the disconnected player by UUID. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…te join
Root cause: item:dropped only carried {itemId, outcome, interactionId}.
Remote clients received the event but had no data to render the placed item,
and placedItems lived in a local useState unreachable from the net layer.
Changes:
- item:dropped GameEvent now carries placedItem?: PlacedSceneItem (only for
outcome "place") and promotes "pickup-success" to the outcome union.
- RuntimeDropEvent carries the same placedItem field; legacyAdapter passes
it through so the full item data reaches the event bus.
- placedItems migrated from useState to usePlacedItemsStore (Zustand) so any
addItem/removeItemById call — local or remote — triggers reactive re-render.
Store gains addItem, removeItemByInteractionId, removeItemById actions.
- applyRemoteEvent in createMultiplayerRuntime applies item:dropped to the
store before emitting to the bus (no double-apply; server excludes sender).
- MultiplayerSession accepts applySnapshot option; server now stores full
placedItem in the world snapshot so late joiners hydrate placed items on
connect.
- Party server snapshot updated to persist {value, placedItem} for placed
items and clears value on pickup/consume.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implementación del MVP de multijugador (Phase 14) sobre el plan ya mergeado (PR #19). Probado en local con vitest.
Qué incluye
engine-core (agnóstico — sin red/
window, test de agnosticismo verde):MultiplayerPort+NetEnvelope+HeadlessMultiplayerAdapter/InMemoryHubworld/presence/private(exhaustiva víasatisfies)PlayerDescriptor+remotePlayersStore; eventos/comandosnet:*MultiplayerSession: puente bus↔port con origin-tagging anti-bucle y throttle de presenceworldState+resolveClaim+ reconciliador optimistaengine-renderer-r3f:
RemotePlayers(reutilizaDavidSprite, interpolación lerp por jugador)apps/web-demo:
party/multiplayer.ts, cap 4) + adapter cliente (partysocket)roomSession(código + persistencia localStorage + solo/reset +N/4)createMultiplayerRuntime+RoomLobby+ ruta/multiplayerextraCanvasChildrenenGameTouchCanvasVerificación local
engine-coretestsroomSessionteststsc --noEmitapp — archivos nuevospublicApi.test.ts+ 10 errores tsc son pre-existentes en main, verificado)Pendiente (no verificable headless)
E2E de dos pestañas con servidor real:
Detalle en
docs/phases/phase-14-multiplayer/validation-report.md. Item-sync world completo(puerta/llave/claim con rollback) queda como 2ª iteración.
🤖 Generated with Claude Code