working ha

This commit is contained in:
2026-02-23 13:00:16 -07:00
parent 3de4cdac62
commit ffb0d9075b
11 changed files with 685 additions and 38 deletions

View File

@@ -1,35 +1,232 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { useEffect, useRef, useState } from "react";
import { Socket, Channel } from "phoenix";
import "./App.css";
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
interface Player {
x: number;
y: number;
}
export default App
interface GameState {
[playerId: string]: Player;
}
// List of WebSocket servers - we'll connect to all of them
const WS_SERVERS = [
"ws://localhost:4001/socket",
"ws://localhost:4002/socket",
"ws://localhost:4003/socket",
];
function App() {
const [players, setPlayers] = useState<GameState>({});
const [myPlayerId, setMyPlayerId] = useState<string | null>(null);
const [connectionStatus, setConnectionStatus] =
useState<string>("connecting");
const socketsRef = useRef<Socket[]>([]);
const channelsRef = useRef<Channel[]>([]);
const keysPressed = useRef<Set<string>>(new Set());
useEffect(() => {
// Connect to all servers concurrently
const sockets = WS_SERVERS.map((serverUrl) => {
console.log(`Connecting to ${serverUrl}`);
const socket = new Socket(serverUrl, {
timeout: 3000,
reconnectAfterMs: () => 2000, // Keep trying to reconnect
});
// Handle connection events
socket.onOpen(() => {
console.log(`✓ Connected to ${serverUrl}`);
updateConnectionStatus();
});
socket.onError((error) => {
console.error(`✗ Error on ${serverUrl}:`, error);
updateConnectionStatus();
});
socket.onClose(() => {
console.log(`✗ Disconnected from ${serverUrl}`);
updateConnectionStatus();
});
socket.connect();
return socket;
});
socketsRef.current = sockets;
// Join game channel on all connected sockets
const channels = sockets.map((socket, index) => {
const channel = socket.channel("game:lobby", {});
channel
.join()
.receive("ok", () => {
console.log(`✓ Joined channel on ${WS_SERVERS[index]}`);
updateConnectionStatus();
})
.receive("error", (resp) => {
console.log(`✗ Failed to join on ${WS_SERVERS[index]}:`, resp);
})
.receive("timeout", () => {
console.log(`✗ Timeout joining on ${WS_SERVERS[index]}`);
});
// Listen for game state updates from any server
channel.on("game_state", (payload: { players: GameState }) => {
setPlayers(payload.players);
// Set our player ID from the first state update if not set
if (!myPlayerId && Object.keys(payload.players).length > 0) {
const playerIds = Object.keys(payload.players);
if (playerIds.length > 0) {
setMyPlayerId(playerIds[playerIds.length - 1]);
}
}
});
return channel;
});
channelsRef.current = channels;
const updateConnectionStatus = () => {
const joined = channels.filter((c) => c.state === "joined").length;
setConnectionStatus(`${joined}/${WS_SERVERS.length} servers active`);
};
// Periodic status update
const statusInterval = setInterval(updateConnectionStatus, 1000);
// Cleanup on unmount
return () => {
clearInterval(statusInterval);
channels.forEach((channel) => channel.leave());
sockets.forEach((socket) => socket.disconnect());
};
}, [myPlayerId]);
// Handle keyboard input - send to first available channel
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (["w", "a", "s", "d"].includes(key)) {
e.preventDefault();
// Only send if not already pressed (prevent repeat)
if (!keysPressed.current.has(key)) {
keysPressed.current.add(key);
// Send to first joined channel (they all share same game state)
const activeChannel = channelsRef.current.find(
(c) => c.state === "joined",
);
if (activeChannel) {
activeChannel.push("move", { direction: key });
}
}
}
};
const handleKeyUp = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (["w", "a", "s", "d"].includes(key)) {
keysPressed.current.delete(key);
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#1a1a2e",
overflow: "hidden",
position: "relative",
}}
>
{/* Connection status */}
<div
style={{
position: "absolute",
top: 10,
right: 10,
color: "white",
fontFamily: "monospace",
background: "rgba(0,0,0,0.5)",
padding: "10px",
borderRadius: "5px",
fontSize: "12px",
}}
>
<div>Status: {connectionStatus}</div>
<div>Players: {Object.keys(players).length}</div>
</div>
{/* Game canvas */}
<div
style={{
position: "relative",
width: "800px",
height: "600px",
background: "#16213e",
margin: "50px auto",
border: "2px solid #0f3460",
overflow: "hidden",
}}
>
{Object.entries(players).map(([id, player]) => (
<div
key={id}
style={{
position: "absolute",
left: player.x,
top: player.y,
width: "20px",
height: "20px",
borderRadius: "50%",
background: id === myPlayerId ? "#e94560" : "#53a8b6",
border:
id === myPlayerId ? "3px solid #ff6b6b" : "2px solid #48d6e0",
transition: "all 0.1s linear",
transform: "translate(-50%, -50%)",
boxShadow:
id === myPlayerId ? "0 0 10px #e94560" : "0 0 5px #53a8b6",
}}
>
{id === myPlayerId && (
<div
style={{
position: "absolute",
top: "-25px",
left: "50%",
transform: "translateX(-50%)",
color: "#fff",
fontSize: "10px",
whiteSpace: "nowrap",
}}
>
You
</div>
)}
</div>
))}
</div>
</div>
);
}
export default App;