This commit is contained in:
WangXiang
2025-12-22 18:57:51 +08:00
commit 6119faf53e
159 changed files with 19162 additions and 0 deletions

265
server/index.js Normal file
View File

@@ -0,0 +1,265 @@
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));
}
}