feat: add in-game social test lab

This commit is contained in:
ANG-Server
2026-07-22 12:16:31 +08:00
parent fe52ab1696
commit a3341b6a3b
19 changed files with 827 additions and 14 deletions

View File

@@ -34,6 +34,13 @@ import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/commo
import * as WebSocket from 'ws';
import { ChatService } from '../../business/chat/chat.service';
import { SocialService } from '../../business/social/social.service';
import {
getTestLabPresence,
getTestLabPresences,
removeTestLabPresence,
TestLabPresence,
upsertTestLabPresence,
} from '../../business/admin/test_lab_presence.registry';
/** WebSocket 服务器默认端口 */
const DEFAULT_WEBSOCKET_PORT = 3001;
@@ -911,6 +918,58 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
return players;
}
/** 管理端测试实验室使用:返回真实在线玩家,绝不包含合成假人。 */
public getOnlineWorldPlayers(): Array<{ userId: string; username: string; mapId: string }> {
return [...this.clients.values()]
.filter((client) => client.authenticated && client.worldReady && client.userId && client.currentMap)
.map((client) => ({ userId: String(client.userId), username: String(client.username || ''), mapId: String(client.currentMap) }));
}
public setTestLabPresence(presence: TestLabPresence): void {
const previous = getTestLabPresence(presence.userId);
const current = upsertTestLabPresence(presence);
if (previous?.online && (!current.online || previous.mapId !== current.mapId)) {
this.broadcastToMap(previous.mapId, { t: 'player_left', userId: previous.userId, username: previous.nickname, mapId: previous.mapId });
}
if (!current.online) return;
const payload = {
t: previous?.online && previous.mapId === current.mapId ? 'position_update' : 'player_joined',
userId: current.userId,
username: current.nickname,
mapId: current.mapId,
x: current.x,
y: current.y,
skinId: current.skinId,
avatarId: current.avatarId,
appearance: { skinId: current.skinId, avatarId: current.avatarId },
};
this.broadcastToMap(current.mapId, payload);
}
public removeTestLabActor(userId: string): void {
const previous = removeTestLabPresence(userId);
if (previous?.online) {
this.broadcastToMap(previous.mapId, { t: 'player_left', userId: previous.userId, username: previous.nickname, mapId: previous.mapId });
}
}
public broadcastTestLabChat(actor: TestLabPresence, content: string, scope: 'local' | 'global' = 'local'): void {
const payload = {
t: 'chat_render',
from: actor.nickname,
fromUserId: actor.userId,
txt: content,
bubble: true,
timestamp: new Date().toISOString(),
messageId: `test_${Date.now()}_${actor.userId}`,
mapId: actor.mapId,
scope,
testLab: true,
};
if (scope === 'global') this.broadcastToAll(payload);
else this.broadcastToMap(actor.mapId, payload);
}
// ========== 私有辅助方法 ==========
private sendMessage(ws: ExtendedWebSocket, data: any) {
@@ -976,10 +1035,22 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
});
const players = (await this.chatService.getMapPlayerSnapshot(normalizedMapId, ws.id))
.filter((player) => activeUserIds.has(String(player.userId)));
const testPlayers = getTestLabPresences(normalizedMapId).map((player) => ({
userId: player.userId,
username: player.nickname,
mapId: player.mapId,
x: player.x,
y: player.y,
skinId: player.skinId,
avatarId: player.avatarId,
appearance: { skinId: player.skinId, avatarId: player.avatarId },
cafeCompanion: null,
movementLocked: false,
}));
this.sendMessage(ws, {
t: 'map_players_snapshot',
mapId: normalizedMapId,
players,
players: [...players, ...testPlayers].filter((player) => String(player.userId) !== String(ws.userId)),
});
}