世界、玩家登录、加入广播。
This commit is contained in:
74
server/src/api/ApiLogin.ts
Normal file
74
server/src/api/ApiLogin.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ApiCall } from "tsrpc";
|
||||
import { ReqLogin, ResLogin, PlayerInfo } from "../shared/protocols/PtlLogin";
|
||||
import { playerManager } from "../managers/PlayerManager";
|
||||
|
||||
/**
|
||||
* 登录API
|
||||
* 处理玩家登录流程:
|
||||
* 1. 查找已有角色
|
||||
* 2. 若无角色,自动注册并创建角色
|
||||
* 3. 分配出生点
|
||||
* 4. 返回角色信息
|
||||
*/
|
||||
export default async function (call: ApiCall<ReqLogin, ResLogin>) {
|
||||
const { playerId, playerName } = call.req;
|
||||
|
||||
// 验证玩家ID
|
||||
if (!playerId || playerId.trim().length === 0) {
|
||||
call.error('玩家ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查玩家是否已存在
|
||||
const isNewPlayer = !playerManager.hasPlayer(playerId);
|
||||
|
||||
// 获取或创建玩家(自动处理注册和创角)
|
||||
const player = playerManager.getOrCreatePlayer(playerId, playerName);
|
||||
|
||||
// 设置玩家在线状态
|
||||
playerManager.setPlayerOnline(playerId, call.conn.id);
|
||||
|
||||
// 保存玩家ID到连接对象,供其他API使用
|
||||
(call.conn as any).playerId = playerId;
|
||||
|
||||
// 转换为协议格式
|
||||
const playerInfo: PlayerInfo = {
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
position: { ...player.position },
|
||||
spawnPoint: { ...player.spawnPoint },
|
||||
hp: player.hp,
|
||||
maxHp: player.maxHp,
|
||||
isAlive: player.isAlive,
|
||||
createdAt: player.createdAt,
|
||||
lastLoginAt: player.lastLoginAt
|
||||
};
|
||||
|
||||
// 广播玩家加入消息给其他在线玩家
|
||||
const { broadcastToAll } = await import('../utils/broadcast');
|
||||
|
||||
broadcastToAll('PlayerJoin', {
|
||||
playerId: player.id,
|
||||
playerName: player.name,
|
||||
position: { ...player.position },
|
||||
isNewPlayer,
|
||||
timestamp: Date.now()
|
||||
}, call.conn.id);
|
||||
|
||||
// 返回成功结果
|
||||
call.succ({
|
||||
success: true,
|
||||
message: isNewPlayer ? '创建角色成功,欢迎来到游戏世界!' : '登录成功,欢迎回来!',
|
||||
player: playerInfo,
|
||||
isNewPlayer
|
||||
});
|
||||
|
||||
console.log(`玩家 ${player.name} (${playerId}) ${isNewPlayer ? '首次登录' : '登录成功'}`);
|
||||
console.log(` 在线玩家数: ${playerManager.getOnlinePlayerCount()}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
call.error(`登录失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
86
server/src/api/ApiMove.ts
Normal file
86
server/src/api/ApiMove.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ApiCall } from "tsrpc";
|
||||
import { ReqMove, ResMove } from "../shared/protocols/PtlMove";
|
||||
import { playerManager } from "../managers/PlayerManager";
|
||||
import { broadcastToAll } from "../utils/broadcast";
|
||||
import { MsgPlayerMove } from "../shared/protocols/MsgPlayerMove";
|
||||
|
||||
/**
|
||||
* 移动API
|
||||
* 处理玩家移动请求,验证位置合法性,更新玩家位置并广播给其他玩家
|
||||
*/
|
||||
export default async function (call: ApiCall<ReqMove, ResMove>) {
|
||||
const { x, y } = call.req;
|
||||
|
||||
// 从连接中获取玩家ID(需要在登录时保存)
|
||||
const playerId = (call.conn as any).playerId;
|
||||
|
||||
if (!playerId) {
|
||||
call.error('未登录,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取玩家信息
|
||||
const player = playerManager.getPlayer(playerId);
|
||||
|
||||
if (!player) {
|
||||
call.error('玩家不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查玩家是否存活
|
||||
if (!player.isAlive) {
|
||||
call.error('玩家已死亡,无法移动');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证坐标是否为数字
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
call.error('坐标格式错误');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证坐标是否为整数
|
||||
if (!Number.isInteger(x) || !Number.isInteger(y)) {
|
||||
call.error('坐标必须为整数');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试更新玩家位置
|
||||
const success = playerManager.updatePlayerPosition(playerId, x, y);
|
||||
|
||||
if (!success) {
|
||||
call.succ({
|
||||
success: false,
|
||||
message: '移动失败,位置无效或超出世界边界'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取更新后的位置
|
||||
const newPosition = { x, y };
|
||||
|
||||
// 广播移动消息给所有其他玩家
|
||||
const moveMsg: MsgPlayerMove = {
|
||||
playerId: player.id,
|
||||
playerName: player.name,
|
||||
position: newPosition,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
broadcastToAll('MsgPlayerMove', moveMsg, call.conn.id);
|
||||
|
||||
// 返回成功结果
|
||||
call.succ({
|
||||
success: true,
|
||||
message: '移动成功',
|
||||
position: newPosition
|
||||
});
|
||||
|
||||
console.log(`玩家 ${player.name} 移动到 (${x}, ${y})`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('移动失败:', error);
|
||||
call.error(`移动失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
26
server/src/api/ApiSend.ts
Normal file
26
server/src/api/ApiSend.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApiCall } from "tsrpc";
|
||||
import { server } from "..";
|
||||
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
|
||||
|
||||
// This is a demo code file
|
||||
// Feel free to delete it
|
||||
|
||||
export default async function (call: ApiCall<ReqSend, ResSend>) {
|
||||
// Error
|
||||
if (call.req.content.length === 0) {
|
||||
call.error('Content is empty')
|
||||
return;
|
||||
}
|
||||
|
||||
// Success
|
||||
let time = new Date();
|
||||
call.succ({
|
||||
time: time
|
||||
});
|
||||
|
||||
// Broadcast
|
||||
server.broadcastMsg('Chat', {
|
||||
content: call.req.content,
|
||||
time: time
|
||||
})
|
||||
}
|
||||
23
server/src/config/world.config.ts
Normal file
23
server/src/config/world.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 世界配置参数
|
||||
*/
|
||||
export const WorldConfig = {
|
||||
/** 世界宽度 */
|
||||
WORLD_WIDTH: 800,
|
||||
|
||||
/** 世界高度 */
|
||||
WORLD_HEIGHT: 800,
|
||||
|
||||
/** 出生区域半径(距离中心点) */
|
||||
SPAWN_RADIUS: 200,
|
||||
|
||||
/** 世界中心点 X 坐标 */
|
||||
get CENTER_X() {
|
||||
return this.WORLD_WIDTH / 2;
|
||||
},
|
||||
|
||||
/** 世界中心点 Y 坐标 */
|
||||
get CENTER_Y() {
|
||||
return this.WORLD_HEIGHT / 2;
|
||||
}
|
||||
} as const;
|
||||
28
server/src/index.ts
Normal file
28
server/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as path from "path";
|
||||
import { WsServer } from "tsrpc";
|
||||
import { serviceProto } from './shared/protocols/serviceProto';
|
||||
import { worldManager } from './managers/WorldManager';
|
||||
|
||||
// Create the Server
|
||||
export const server = new WsServer(serviceProto, {
|
||||
port: 3000,
|
||||
// Remove this to use binary mode (remove from the client too)
|
||||
json: true
|
||||
});
|
||||
|
||||
// Initialize before server start
|
||||
async function init() {
|
||||
await server.autoImplementApi(path.resolve(__dirname, 'api'));
|
||||
|
||||
// 初始化游戏世界
|
||||
console.log('正在初始化游戏世界...');
|
||||
worldManager.initialize();
|
||||
console.log('游戏世界初始化完成');
|
||||
};
|
||||
|
||||
// Entry function
|
||||
async function main() {
|
||||
await init();
|
||||
await server.start();
|
||||
}
|
||||
main();
|
||||
149
server/src/managers/PlayerManager.ts
Normal file
149
server/src/managers/PlayerManager.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Player, createPlayer } from '../models/Player';
|
||||
import { worldManager } from './WorldManager';
|
||||
|
||||
/**
|
||||
* 玩家管理器
|
||||
* 负责玩家数据的存储、查询和管理
|
||||
*/
|
||||
class PlayerManager {
|
||||
/** 玩家数据存储 (key: playerId) */
|
||||
private players: Map<string, Player> = new Map();
|
||||
|
||||
/** 在线玩家连接 (key: playerId, value: connectionId) */
|
||||
private onlinePlayers: Map<string, string> = new Map();
|
||||
|
||||
/**
|
||||
* 根据玩家ID查找玩家
|
||||
*/
|
||||
getPlayer(playerId: string): Player | undefined {
|
||||
return this.players.get(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否存在
|
||||
*/
|
||||
hasPlayer(playerId: string): boolean {
|
||||
return this.players.has(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新玩家
|
||||
*/
|
||||
createPlayer(playerId: string, playerName?: string): Player {
|
||||
if (this.players.has(playerId)) {
|
||||
throw new Error(`玩家 ${playerId} 已存在`);
|
||||
}
|
||||
|
||||
// 生成出生点
|
||||
const spawnPoint = worldManager.generateSpawnPosition();
|
||||
|
||||
// 创建玩家
|
||||
const name = playerName || `玩家${playerId.substring(0, 6)}`;
|
||||
const player = createPlayer(playerId, name, spawnPoint);
|
||||
|
||||
// 保存玩家数据
|
||||
this.players.set(playerId, player);
|
||||
|
||||
console.log(`创建新玩家: ${player.name} (${playerId})`);
|
||||
console.log(` 出生点: (${spawnPoint.x}, ${spawnPoint.y})`);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建玩家(登录流程)
|
||||
*/
|
||||
getOrCreatePlayer(playerId: string, playerName?: string): Player {
|
||||
let player = this.players.get(playerId);
|
||||
|
||||
if (player) {
|
||||
// 玩家已存在,更新最后登录时间
|
||||
player.lastLoginAt = Date.now();
|
||||
console.log(`玩家登录: ${player.name} (${playerId})`);
|
||||
} else {
|
||||
// 新玩家,创建角色
|
||||
player = this.createPlayer(playerId, playerName);
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置玩家在线状态
|
||||
*/
|
||||
setPlayerOnline(playerId: string, connectionId: string): void {
|
||||
this.onlinePlayers.set(playerId, connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置玩家离线
|
||||
*/
|
||||
setPlayerOffline(playerId: string): void {
|
||||
this.onlinePlayers.delete(playerId);
|
||||
console.log(`玩家离线: ${playerId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在线
|
||||
*/
|
||||
isPlayerOnline(playerId: string): boolean {
|
||||
return this.onlinePlayers.has(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家连接ID
|
||||
*/
|
||||
getPlayerConnectionId(playerId: string): string | undefined {
|
||||
return this.onlinePlayers.get(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新玩家位置
|
||||
*/
|
||||
updatePlayerPosition(playerId: string, x: number, y: number): boolean {
|
||||
const player = this.players.get(playerId);
|
||||
if (!player || !player.isAlive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证位置是否合法
|
||||
if (!worldManager.isValidPosition({ x, y })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
player.position.x = x;
|
||||
player.position.y = y;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有在线玩家
|
||||
*/
|
||||
getOnlinePlayers(): Player[] {
|
||||
const players: Player[] = [];
|
||||
for (const playerId of this.onlinePlayers.keys()) {
|
||||
const player = this.players.get(playerId);
|
||||
if (player) {
|
||||
players.push(player);
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家总数
|
||||
*/
|
||||
getTotalPlayers(): number {
|
||||
return this.players.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线玩家数
|
||||
*/
|
||||
getOnlinePlayerCount(): number {
|
||||
return this.onlinePlayers.size;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const playerManager = new PlayerManager();
|
||||
79
server/src/managers/WorldManager.ts
Normal file
79
server/src/managers/WorldManager.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { World, createWorld } from '../models/World';
|
||||
import { Position } from '../models/Position';
|
||||
import { WorldConfig } from '../config/world.config';
|
||||
import { randomPositionInCircle } from '../utils/math';
|
||||
|
||||
/**
|
||||
* 世界管理器
|
||||
* 负责世界初始化和地图管理
|
||||
*/
|
||||
class WorldManager {
|
||||
private world: World | null = null;
|
||||
|
||||
/**
|
||||
* 初始化世界
|
||||
*/
|
||||
initialize(): void {
|
||||
if (this.world) {
|
||||
console.log('世界已经初始化,跳过重复初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
this.world = createWorld(
|
||||
WorldConfig.WORLD_WIDTH,
|
||||
WorldConfig.WORLD_HEIGHT,
|
||||
WorldConfig.SPAWN_RADIUS
|
||||
);
|
||||
|
||||
console.log(`世界初始化完成:`);
|
||||
console.log(` 尺寸: ${this.world.width}x${this.world.height}`);
|
||||
console.log(` 中心点: (${this.world.center.x}, ${this.world.center.y})`);
|
||||
console.log(` 出生区域半径: ${this.world.spawnRadius}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界实例
|
||||
*/
|
||||
getWorld(): World {
|
||||
if (!this.world) {
|
||||
throw new Error('世界尚未初始化');
|
||||
}
|
||||
return this.world;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成出生点位置
|
||||
*/
|
||||
generateSpawnPosition(): Position {
|
||||
if (!this.world) {
|
||||
throw new Error('世界尚未初始化');
|
||||
}
|
||||
|
||||
return randomPositionInCircle(this.world.center, this.world.spawnRadius);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查位置是否在世界范围内
|
||||
*/
|
||||
isValidPosition(pos: Position): boolean {
|
||||
if (!this.world) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pos.x >= 0 && pos.x < this.world.width &&
|
||||
pos.y >= 0 && pos.y < this.world.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界中心点
|
||||
*/
|
||||
getCenter(): Position {
|
||||
if (!this.world) {
|
||||
throw new Error('世界尚未初始化');
|
||||
}
|
||||
return this.world.center;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const worldManager = new WorldManager();
|
||||
71
server/src/models/Player.ts
Normal file
71
server/src/models/Player.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Position } from './Position';
|
||||
|
||||
/**
|
||||
* 玩家角色数据
|
||||
*/
|
||||
export interface Player {
|
||||
/** 玩家唯一ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置 */
|
||||
position: Position;
|
||||
|
||||
/** 出生点位置 */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新玩家
|
||||
*/
|
||||
export function createPlayer(id: string, name: string, spawnPoint: Position): Player {
|
||||
const maxHp = 10; // 初始最大生命值10点
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
position: { ...spawnPoint }, // 初始位置在出生点
|
||||
spawnPoint: { ...spawnPoint },
|
||||
hp: maxHp,
|
||||
maxHp,
|
||||
isAlive: true,
|
||||
createdAt: Date.now(),
|
||||
lastLoginAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家复活
|
||||
*/
|
||||
export function respawnPlayer(player: Player): void {
|
||||
player.position = { ...player.spawnPoint };
|
||||
player.hp = player.maxHp;
|
||||
player.isAlive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家受伤
|
||||
*/
|
||||
export function damagePlayer(player: Player, damage: number): void {
|
||||
player.hp = Math.max(0, player.hp - damage);
|
||||
if (player.hp <= 0) {
|
||||
player.isAlive = false;
|
||||
}
|
||||
}
|
||||
37
server/src/models/Position.ts
Normal file
37
server/src/models/Position.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 位置坐标接口
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建位置对象
|
||||
*/
|
||||
export function createPosition(x: number, y: number): Position {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*/
|
||||
export function getDistance(pos1: Position, pos2: Position): number {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查位置是否在范围内
|
||||
*/
|
||||
export function isInBounds(pos: Position, width: number, height: number): boolean {
|
||||
return pos.x >= 0 && pos.x < width && pos.y >= 0 && pos.y < height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查位置是否在圆形区域内
|
||||
*/
|
||||
export function isInCircle(pos: Position, center: Position, radius: number): boolean {
|
||||
return getDistance(pos, center) <= radius;
|
||||
}
|
||||
37
server/src/models/World.ts
Normal file
37
server/src/models/World.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Position } from './Position';
|
||||
|
||||
/**
|
||||
* 世界数据结构
|
||||
*/
|
||||
export interface World {
|
||||
/** 世界宽度 */
|
||||
width: number;
|
||||
|
||||
/** 世界高度 */
|
||||
height: number;
|
||||
|
||||
/** 出生区域半径 */
|
||||
spawnRadius: number;
|
||||
|
||||
/** 世界中心点 */
|
||||
center: Position;
|
||||
|
||||
/** 世界创建时间 */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建世界实例
|
||||
*/
|
||||
export function createWorld(width: number, height: number, spawnRadius: number): World {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
spawnRadius,
|
||||
center: {
|
||||
x: width / 2,
|
||||
y: height / 2
|
||||
},
|
||||
createdAt: Date.now()
|
||||
};
|
||||
}
|
||||
7
server/src/shared/protocols/MsgChat.ts
Normal file
7
server/src/shared/protocols/MsgChat.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// This is a demo code file
|
||||
// Feel free to delete it
|
||||
|
||||
export interface MsgChat {
|
||||
content: string,
|
||||
time: Date
|
||||
}
|
||||
22
server/src/shared/protocols/MsgPlayerJoin.ts
Normal file
22
server/src/shared/protocols/MsgPlayerJoin.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 玩家加入游戏广播消息
|
||||
* 当有新玩家登录或加入游戏时,广播给所有在线玩家
|
||||
*/
|
||||
export interface MsgPlayerJoin {
|
||||
/** 加入的玩家ID */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 玩家位置 */
|
||||
position: Position;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer: boolean;
|
||||
|
||||
/** 加入时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
19
server/src/shared/protocols/MsgPlayerMove.ts
Normal file
19
server/src/shared/protocols/MsgPlayerMove.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 玩家移动广播消息
|
||||
* 当有玩家移动时,广播给所有在线玩家
|
||||
*/
|
||||
export interface MsgPlayerMove {
|
||||
/** 移动的玩家ID */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
playerName: string;
|
||||
|
||||
/** 移动后的位置 */
|
||||
position: Position;
|
||||
|
||||
/** 移动时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
61
server/src/shared/protocols/PtlLogin.ts
Normal file
61
server/src/shared/protocols/PtlLogin.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
export interface ReqLogin {
|
||||
/** 玩家ID(用于识别玩家) */
|
||||
playerId: string;
|
||||
|
||||
/** 玩家昵称(可选,新玩家时使用) */
|
||||
playerName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家角色信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
/** 玩家ID */
|
||||
id: string;
|
||||
|
||||
/** 玩家昵称 */
|
||||
name: string;
|
||||
|
||||
/** 当前位置 */
|
||||
position: Position;
|
||||
|
||||
/** 出生点 */
|
||||
spawnPoint: Position;
|
||||
|
||||
/** 当前生命值 */
|
||||
hp: number;
|
||||
|
||||
/** 最大生命值 */
|
||||
maxHp: number;
|
||||
|
||||
/** 是否存活 */
|
||||
isAlive: boolean;
|
||||
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface ResLogin {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message: string;
|
||||
|
||||
/** 玩家信息 */
|
||||
player?: PlayerInfo;
|
||||
|
||||
/** 是否新玩家 */
|
||||
isNewPlayer?: boolean;
|
||||
}
|
||||
26
server/src/shared/protocols/PtlMove.ts
Normal file
26
server/src/shared/protocols/PtlMove.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Position } from './base';
|
||||
|
||||
/**
|
||||
* 移动请求
|
||||
*/
|
||||
export interface ReqMove {
|
||||
/** 目标位置 X 坐标 */
|
||||
x: number;
|
||||
|
||||
/** 目标位置 Y 坐标 */
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动响应
|
||||
*/
|
||||
export interface ResMove {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
|
||||
/** 消息 */
|
||||
message?: string;
|
||||
|
||||
/** 实际移动后的位置 */
|
||||
position?: Position;
|
||||
}
|
||||
10
server/src/shared/protocols/PtlSend.ts
Normal file
10
server/src/shared/protocols/PtlSend.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This is a demo code file
|
||||
// Feel free to delete it
|
||||
|
||||
export interface ReqSend {
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface ResSend {
|
||||
time: Date
|
||||
}
|
||||
23
server/src/shared/protocols/base.ts
Normal file
23
server/src/shared/protocols/base.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface BaseRequest {
|
||||
|
||||
}
|
||||
|
||||
export interface BaseResponse {
|
||||
|
||||
}
|
||||
|
||||
export interface BaseConf {
|
||||
|
||||
}
|
||||
|
||||
export interface BaseMessage {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置坐标
|
||||
*/
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
378
server/src/shared/protocols/serviceProto.ts
Normal file
378
server/src/shared/protocols/serviceProto.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { ServiceProto } from 'tsrpc-proto';
|
||||
import { MsgChat } from './MsgChat';
|
||||
import { MsgPlayerJoin } from './MsgPlayerJoin';
|
||||
import { MsgPlayerMove } from './MsgPlayerMove';
|
||||
import { ReqLogin, ResLogin } from './PtlLogin';
|
||||
import { ReqMove, ResMove } from './PtlMove';
|
||||
import { ReqSend, ResSend } from './PtlSend';
|
||||
|
||||
export interface ServiceType {
|
||||
api: {
|
||||
"Login": {
|
||||
req: ReqLogin,
|
||||
res: ResLogin
|
||||
},
|
||||
"Move": {
|
||||
req: ReqMove,
|
||||
res: ResMove
|
||||
},
|
||||
"Send": {
|
||||
req: ReqSend,
|
||||
res: ResSend
|
||||
}
|
||||
},
|
||||
msg: {
|
||||
"Chat": MsgChat,
|
||||
"PlayerJoin": MsgPlayerJoin,
|
||||
"PlayerMove": MsgPlayerMove
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"version": 3,
|
||||
"services": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Chat",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "PlayerJoin",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "PlayerMove",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Login",
|
||||
"type": "api"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Move",
|
||||
"type": "api"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Send",
|
||||
"type": "api"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
"MsgChat/MsgChat": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "content",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "time",
|
||||
"type": {
|
||||
"type": "Date"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MsgPlayerJoin/MsgPlayerJoin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "position",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "base/Position"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "isNewPlayer",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "timestamp",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"base/Position": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MsgPlayerMove/MsgPlayerMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "position",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "base/Position"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "timestamp",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/ReqLogin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"type": "String"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/ResLogin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "success",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "message",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "player",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "PtlLogin/PlayerInfo"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "isNewPlayer",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlLogin/PlayerInfo": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "id",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "name",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "position",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "base/Position"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "spawnPoint",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "base/Position"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "hp",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "maxHp",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "isAlive",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "createdAt",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "lastLoginAt",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ReqMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlMove/ResMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "success",
|
||||
"type": {
|
||||
"type": "Boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "message",
|
||||
"type": {
|
||||
"type": "String"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "position",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "base/Position"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ReqSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "content",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlSend/ResSend": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "time",
|
||||
"type": {
|
||||
"type": "Date"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
67
server/src/utils/broadcast.ts
Normal file
67
server/src/utils/broadcast.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { server } from '../index';
|
||||
import { playerManager } from '../managers/PlayerManager';
|
||||
|
||||
/**
|
||||
* 广播消息给所有在线玩家
|
||||
* @param msgName 消息名称
|
||||
* @param msg 消息内容
|
||||
* @param excludeConnectionId 排除的连接ID(可选,用于排除发送者自己)
|
||||
*/
|
||||
export function broadcastToAll<T>(msgName: string, msg: T, excludeConnectionId?: string): void {
|
||||
const onlinePlayers = playerManager.getOnlinePlayers();
|
||||
const targetConns = [];
|
||||
|
||||
for (const player of onlinePlayers) {
|
||||
const connectionId = playerManager.getPlayerConnectionId(player.id);
|
||||
|
||||
if (!connectionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果指定了排除的连接ID,跳过该连接
|
||||
if (excludeConnectionId && connectionId === excludeConnectionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const conn = server.connections.find(c => c.id === connectionId);
|
||||
if (conn) {
|
||||
targetConns.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 broadcastMsg 批量发送,只执行一次序列化
|
||||
if (targetConns.length > 0) {
|
||||
(server as any).broadcastMsg(msgName, msg, targetConns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给指定玩家
|
||||
* @param playerId 玩家ID
|
||||
* @param msgName 消息名称
|
||||
* @param msg 消息内容
|
||||
*/
|
||||
export function broadcastToPlayer<T>(playerId: string, msgName: string, msg: T): void {
|
||||
const connectionId = playerManager.getPlayerConnectionId(playerId);
|
||||
|
||||
if (!connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = server.connections.find(c => c.id === connectionId);
|
||||
if (conn) {
|
||||
(conn as any).sendMsg(msgName, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给多个玩家
|
||||
* @param playerIds 玩家ID列表
|
||||
* @param msgName 消息名称
|
||||
* @param msg 消息内容
|
||||
*/
|
||||
export function broadcastToPlayers<T>(playerIds: string[], msgName: string, msg: T): void {
|
||||
for (const playerId of playerIds) {
|
||||
broadcastToPlayer(playerId, msgName, msg);
|
||||
}
|
||||
}
|
||||
54
server/src/utils/math.ts
Normal file
54
server/src/utils/math.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Position } from '../models/Position';
|
||||
|
||||
/**
|
||||
* 生成指定范围内的随机整数
|
||||
*/
|
||||
export function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机浮点数
|
||||
*/
|
||||
export function randomFloat(min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在圆形区域内生成随机位置
|
||||
* @param center 圆心
|
||||
* @param radius 半径
|
||||
*/
|
||||
export function randomPositionInCircle(center: Position, radius: number): Position {
|
||||
// 使用极坐标转换,确保均匀分布
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const r = Math.sqrt(Math.random()) * radius;
|
||||
|
||||
return {
|
||||
x: Math.round(center.x + r * Math.cos(angle)),
|
||||
y: Math.round(center.y + r * Math.sin(angle))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的欧几里得距离
|
||||
*/
|
||||
export function distance(pos1: Position, pos2: Position): number {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的曼哈顿距离
|
||||
*/
|
||||
export function manhattanDistance(pos1: Position, pos2: Position): number {
|
||||
return Math.abs(pos1.x - pos2.x) + Math.abs(pos1.y - pos2.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制数值在指定范围内
|
||||
*/
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
Reference in New Issue
Block a user