const WebSocket = require('ws'); const PORT = 8910; const wss = new WebSocket.Server({ port: PORT }); // Game State let nextPlayerId = 1; const players = new Map(); // id -> { id, name, x, y, skin_id, ws } // Mock Data const NOTICES = [ { "text": "欢迎来到 [color=#3399ff]Datawhale Town (Online)[/color]!\n\n这里是开源学习者的家园。在这里,我们一同探索知识,分享成长。\n\n[center]🐋[/center]", "image_path": "res://Assets/Assets/background2.png" }, { "text": "最新活动 (Server):\n\n- 镇长刚刚搬进来了,就在喷泉左边。\n- 欢迎板已经设立,查看最新动态。\n- 玩家名字现在显示在头顶了!", "image_path": "res://Assets/MayorWhale.png" }, { "text": "操作提示:\n\n- 按 [color=#ffaa00]F[/color] 键可以与物体互动。\n- 在下方输入框输入文字并在气泡中显示。\n- 点击右下角按钮发送聊天。", "image_path": "res://Assets/NoticeBoard.png" } ]; console.log(`PokePlaza Server listening on ws://localhost:${PORT}`); wss.on('connection', (ws) => { let playerId = null; ws.on('message', (rawData) => { let msg; try { msg = JSON.parse(rawData.toString()); } catch (e) { console.error('Invalid JSON:', rawData.toString()); return; } switch (msg.type) { case 'login': handleLogin(ws, msg); break; case 'move': handleMove(msg); break; case 'chat': handleChat(msg); break; case 'private_chat': handlePrivateChat(msg); break; case 'change_scene': handleChangeScene(msg); break; case 'get_notices': handleGetNotices(ws); break; function handlePrivateChat(msg) { if (playerId === null) return; const targetId = msg.target_id; const targetPlayer = players.get(targetId); if (targetPlayer) { // Send to Target send(targetPlayer.ws, { type: 'private_chat', sender_id: playerId, text: msg.text }); // Send back to Sender (so they know it sent) // Actually, client can handle simulated display, but server confirmation is nice. // In this design, let's just let client handle its own "Sent" display to avoid double-echo if we aren't strict. // But wait, our client code relies on "private_chat_message_received" for INCOMING. // For OUTGOING, the client code manually appends to log. So we DON'T echo back. } } default: console.warn('Unknown message type:', msg.type); } }); ws.on('close', () => { if (playerId !== null) { console.log(`Player ${playerId} disconnected`); const player = players.get(playerId); const currentScene = player ? player.scene : 'world'; players.delete(playerId); broadcastExcept(playerId, { type: 'player_leave', id: playerId }, currentScene); } }); function handleLogin(ws, msg) { playerId = nextPlayerId++; const player = { id: playerId, name: msg.name || 'Anonymous', x: 240, // Center of 480x270 viewport y: 135, skin_id: msg.skin_id || 0, scene: 'world', // Default scene ws: ws }; players.set(playerId, player); // Send login_ok to this player with all current players IN THE SAME SCENE const sameScenePlayers = []; for (const [id, p] of players) { if (p.scene === player.scene) { sameScenePlayers.push({ id: p.id, name: p.name, x: p.x, y: p.y, skin_id: p.skin_id }); } } send(ws, { type: 'login_ok', id: playerId, players: sameScenePlayers }); // Broadcast to others in the SAME SCENE that a new player joined broadcastExcept(playerId, { type: 'player_join', id: playerId, name: player.name, x: player.x, y: player.y, skin_id: player.skin_id }, player.scene); console.log(`Player ${playerId} (${player.name}) logged in to ${player.scene}`); } function handleMove(msg) { if (playerId === null) return; const player = players.get(playerId); // console.log(`Server: handleMove from PlayerID ${playerId}. Msg: ${JSON.stringify(msg)}`); if (!player) return; player.x = msg.x; player.y = msg.y; // Broadcast to all OTHER players IN THE SAME SCENE broadcastExcept(playerId, { type: 'player_move', id: playerId, x: msg.x, y: msg.y, flip_h: msg.flip_h, frame: msg.frame }, player.scene); } function handleChangeScene(msg) { if (playerId === null) return; const player = players.get(playerId); if (!player) return; const oldScene = player.scene; const newScene = msg.scene; if (oldScene === newScene) return; // 1. Tell everyone in the OLD scene that this player LEFT broadcastExcept(playerId, { type: 'player_leave', id: playerId }, oldScene); // 2. Update player scene AND position if provided player.scene = newScene; if (typeof msg.x === 'number' && typeof msg.y === 'number') { player.x = msg.x; player.y = msg.y; } console.log(`Player ${playerId} changed scene to ${newScene} at (${player.x}, ${player.y})`); // 3. Tell everyone in the NEW scene that this player JOINED broadcastExcept(playerId, { type: 'player_join', id: playerId, name: player.name, x: player.x, y: player.y, skin_id: player.skin_id }, newScene); // 4. Send the player the list of existing players in the NEW scene const newScenePlayers = []; for (const [id, p] of players) { if (p.id !== playerId && p.scene === newScene) { newScenePlayers.push({ id: p.id, name: p.name, x: p.x, y: p.y, skin_id: p.skin_id }); } } // Use a special sync message or re-use login_ok? // Re-using login_ok might confuse the client if it resets ID, but since ID matches, it might be fine. // Better: custom 'scene_peers' message. send(ws, { type: 'scene_peers', players: newScenePlayers }); } function handleChat(msg) { if (playerId === null) return; const player = players.get(playerId); // Broadcast to ALL players IN THE SAME SCENE broadcast({ type: 'chat', id: playerId, text: msg.text }, player ? player.scene : 'world'); } function handlePrivateChat(msg) { if (playerId === null) return; const targetId = msg.target_id; const targetPlayer = players.get(targetId); if (targetPlayer) { // Send to Target send(targetPlayer.ws, { type: 'private_chat', sender_id: playerId, text: msg.text }); // No echo needed for outgoing, client handles it. } } }); function send(ws, obj) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(obj)); } } function broadcast(obj, sceneFilter) { const data = JSON.stringify(obj); for (const [id, player] of players) { if (player.ws.readyState === WebSocket.OPEN) { if (!sceneFilter || player.scene === sceneFilter) { player.ws.send(data); } } } } function broadcastExcept(excludeId, obj, sceneFilter) { const data = JSON.stringify(obj); for (const [id, player] of players) { if (id !== excludeId && player.ws.readyState === WebSocket.OPEN) { if (!sceneFilter || player.scene === sceneFilter) { // console.log(`Server: Sending to ${id}: ${data}`); player.ws.send(data); } } } } function handleGetNotices(ws) { const response = { type: 'notices_list', data: NOTICES }; if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(response)); } }