Files
whale_town/server/index.js
WangXiang 6119faf53e init
2025-12-22 18:57:51 +08:00

266 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}