266 lines
8.7 KiB
JavaScript
266 lines
8.7 KiB
JavaScript
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));
|
||
}
|
||
}
|