airframe.io / src /hooks /useMultiplayer.ts
LATTICEWORKS's picture
Implement terrain, map coordinate, and GLB collision upgrades, configure WebSockets keepalive, and fix client MapSchema callbacks TypeError
93f151d
Raw
History Blame Contribute Delete
3.62 kB
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useRef, useCallback } from "react";
import { MatchMode } from "../types";
import { getMultiplayerSessionId } from "./useProgression";
import { Client, Room } from "@colyseus/sdk";
import { ChatMessage, MultiplayerMatchContext } from "./multiplayer/types";
import { setupRoomListeners } from "./multiplayer/listeners";
export function useMultiplayer() {
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const roomRef = useRef<Room | null>(null);
const myPilotIdRef = useRef<string>("");
const disconnectMultiplayer = useCallback(() => {
if (roomRef.current) {
try {
roomRef.current.leave();
} catch (err) {
console.warn("Error leaving room:", err);
}
roomRef.current = null;
}
}, []);
const connectMultiplayer = useCallback(
(
engine: MultiplayerMatchContext,
_renderer3D: any,
mapId: string,
mode: MatchMode,
nickname: string,
skin: string,
onLocalPlayerHit: (tgtId: string, isGround: boolean) => void,
onMatchRejected: (reason: string) => void
) => {
disconnectMultiplayer();
const sessionId = getMultiplayerSessionId();
engine.isMultiplayer = true;
engine.isHost = false;
// Discard locally-spawned bots — the server is authoritative and will
// deliver the real roster via Colyseus state sync (onAdd).
engine.pilots = engine.pilots.filter(p => p.id === "player");
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
const client = new Client(`${protocol}${window.location.host}`);
console.log(`[Multiplayer] Connecting to Colyseus at ${window.location.host}...`);
client.joinOrCreate("air_combat", {
token: sessionId,
nickname: nickname || "Maverick",
aircraftId: engine.pilots.find(p => p.id === "player")?.specs.id || "falcon-mk2",
skin: skin || "default",
mapId,
mode
})
.then((room) => {
roomRef.current = room;
myPilotIdRef.current = room.sessionId;
console.log(`[Multiplayer] Joined room: ${room.roomId}`);
// Register all schema sync & message listeners from separate module
setupRoomListeners(room, engine, setChatMessages, onLocalPlayerHit);
})
.catch((err) => {
const reason: string = err?.message || String(err);
console.error("[Multiplayer] Join failed:", reason, err);
fetch("/api/client-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context: "connectMultiplayer.catch",
message: reason,
stack: err?.stack || ""
})
}).catch(() => {});
onMatchRejected(reason);
});
// Bind local player shoots callback to nothing on server auth
engine.onProjectileSpawn = () => {};
engine.onGroundTargetDamage = () => {};
engine.onLocalPlayerKill = () => {};
engine.onPlayerDamage = () => {};
},
[disconnectMultiplayer]
);
const sendChat = useCallback((text: string, nickname: string) => {
if (roomRef.current) {
roomRef.current.send("chat", text);
} else {
setChatMessages((prev) => [
...prev.slice(-49),
{ sender: nickname || "Cadet", text, ts: Date.now() }
]);
}
}, []);
return {
chatMessages,
setChatMessages,
connectMultiplayer,
disconnectMultiplayer,
sendChat,
roomRef,
myPilotIdRef
};
}