forked from xiangwang25/whale-town-end-v2
feat: integrate invitation access, world NPCs, and deployment
This commit is contained in:
@@ -40,6 +40,9 @@ import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
|
||||
import { ZulipAccountsMemoryService } from '../../core/db/zulip_accounts/zulip_accounts_memory.service';
|
||||
import { AccountProfileService } from '../auth/account_profile.service';
|
||||
import { EconomyService } from '../player/economy.service';
|
||||
|
||||
const WORLD_BULLETIN_COST = 100;
|
||||
|
||||
// ========== 接口定义 ==========
|
||||
|
||||
@@ -63,6 +66,8 @@ export interface ChatMessageRequest {
|
||||
privateContext?: string;
|
||||
/** 是否同步显示角色气泡 */
|
||||
bubble?: boolean;
|
||||
/** 是否发布收费的世界公告 */
|
||||
worldBulletin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +80,10 @@ export interface ChatMessageResponse {
|
||||
messageId?: string;
|
||||
/** 错误信息(失败时返回) */
|
||||
error?: string;
|
||||
/** 本次服务端实际扣费 */
|
||||
charged?: number;
|
||||
/** 扣费后的实时余额 */
|
||||
balance?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +128,12 @@ export interface PositionUpdateRequest {
|
||||
mapId: string;
|
||||
/** 外观同步信息 */
|
||||
appearance?: IPlayerAppearance;
|
||||
/** 面向方向 */
|
||||
direction?: 'down' | 'up' | 'right' | 'left';
|
||||
/** 移动动画状态 */
|
||||
movementState?: 'idle' | 'walk';
|
||||
/** 当前连接内的移动消息序号 */
|
||||
sequence?: number;
|
||||
}
|
||||
|
||||
export interface PlayerPresenceStateUpdateRequest {
|
||||
@@ -150,10 +165,18 @@ export interface MapPlayerSnapshotItem {
|
||||
skinId?: string;
|
||||
/** 头像ID(兼容前端实时位置协议) */
|
||||
avatarId?: string;
|
||||
/** 自定义皮肤资源(兼容前端实时位置协议) */
|
||||
skinAsset?: Record<string, any>;
|
||||
/** 咖啡店陪伴服务状态 */
|
||||
cafeCompanion?: ICafeCompanionPresence | null;
|
||||
/** 是否锁定移动 */
|
||||
movementLocked?: boolean;
|
||||
/** 面向方向 */
|
||||
direction?: 'down' | 'up' | 'right' | 'left';
|
||||
/** 移动动画状态 */
|
||||
movementState?: 'idle' | 'walk';
|
||||
/** 当前连接内的移动消息序号 */
|
||||
sequence?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,6 +219,8 @@ interface GameChatMessage {
|
||||
toUsername?: string;
|
||||
/** 私聊来源上下文:whisper / friends */
|
||||
privateContext?: string;
|
||||
/** 收费世界公告标记 */
|
||||
worldBulletin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,6 +287,7 @@ export class ChatService {
|
||||
@Inject('ZulipAccountsService')
|
||||
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
|
||||
private readonly accountProfileService: AccountProfileService,
|
||||
private readonly economyService: EconomyService,
|
||||
) {
|
||||
this.logger.log('ChatService初始化完成');
|
||||
}
|
||||
@@ -382,7 +408,10 @@ export class ChatService {
|
||||
return { success: false, error: '会话不存在,请重新登录' };
|
||||
}
|
||||
|
||||
const normalizedScope = this.normalizeChatScope(request.scope);
|
||||
// 世界公告的频道和价格均由服务端决定,不信任客户端传值。
|
||||
const normalizedScope = request.worldBulletin
|
||||
? 'global'
|
||||
: this.normalizeChatScope(request.scope);
|
||||
|
||||
if (normalizedScope === 'private' && !request.targetUserId?.trim()) {
|
||||
return { success: false, error: '请选择悄悄话对象' };
|
||||
@@ -410,6 +439,33 @@ export class ChatService {
|
||||
|
||||
const messageContent = validationResult.filteredContent || request.content;
|
||||
const messageId = `game_${Date.now()}_${session.userId}`;
|
||||
let chargedBalance: number | undefined;
|
||||
|
||||
if (request.worldBulletin) {
|
||||
if (!/^\d+$/.test(session.userId)) {
|
||||
return { success: false, error: '钱包服务暂不可用' };
|
||||
}
|
||||
try {
|
||||
const wallet = await this.economyService.spend(
|
||||
BigInt(session.userId),
|
||||
WORLD_BULLETIN_COST,
|
||||
'world_bulletin',
|
||||
messageId,
|
||||
'发布世界公告',
|
||||
);
|
||||
chargedBalance = wallet.balance;
|
||||
} catch (chargeError) {
|
||||
const chargeMessage = (chargeError as Error).message || '';
|
||||
if (chargeMessage.includes('余额不足')) {
|
||||
return {
|
||||
success: false,
|
||||
error: `鲸币余额不足,发布世界公告需要 ${WORLD_BULLETIN_COST} 鲸币`,
|
||||
};
|
||||
}
|
||||
this.logger.error('世界公告扣费失败', { error: chargeMessage, userId: session.userId });
|
||||
return { success: false, error: '钱包服务暂不可用' };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 🚀 立即广播给游戏内玩家(根据scope决定广播范围)
|
||||
const gameMessage: GameChatMessage = {
|
||||
@@ -422,6 +478,7 @@ export class ChatService {
|
||||
messageId,
|
||||
mapId: targetMapId,
|
||||
scope: normalizedScope,
|
||||
worldBulletin: Boolean(request.worldBulletin),
|
||||
};
|
||||
|
||||
if (normalizedScope === 'private') {
|
||||
@@ -432,9 +489,26 @@ export class ChatService {
|
||||
|
||||
// local: 当前地图;global: 所有在线玩家;private: 仅发送者与目标玩家。
|
||||
try {
|
||||
await this.dispatchGameChatMessage(gameMessage, request.socketId);
|
||||
await this.dispatchGameChatMessage(gameMessage, request.socketId, Boolean(request.worldBulletin));
|
||||
this.recordChatHistory(gameMessage);
|
||||
} catch (dispatchError) {
|
||||
if (request.worldBulletin) {
|
||||
try {
|
||||
await this.economyService.earn(
|
||||
BigInt(session.userId),
|
||||
WORLD_BULLETIN_COST,
|
||||
'world_bulletin_refund',
|
||||
messageId,
|
||||
'世界公告发送失败退款',
|
||||
);
|
||||
} catch (refundError) {
|
||||
this.logger.error('世界公告发送失败且退款失败', {
|
||||
messageId,
|
||||
userId: session.userId,
|
||||
error: (refundError as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
const message = (dispatchError as Error).message || '消息发送失败';
|
||||
return { success: false, error: message };
|
||||
}
|
||||
@@ -451,7 +525,12 @@ export class ChatService {
|
||||
duration: Date.now() - startTime,
|
||||
});
|
||||
|
||||
return { success: true, messageId };
|
||||
return {
|
||||
success: true,
|
||||
messageId,
|
||||
charged: request.worldBulletin ? WORLD_BULLETIN_COST : undefined,
|
||||
balance: chargedBalance,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('聊天消息发送失败', { error: (error as Error).message });
|
||||
@@ -477,6 +556,9 @@ export class ChatService {
|
||||
request.y,
|
||||
{
|
||||
appearance: request.appearance,
|
||||
direction: request.direction,
|
||||
movementState: request.movementState,
|
||||
sequence: request.sequence,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -485,6 +567,30 @@ export class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
async updatePlayerPositionAndGetPresence(request: PositionUpdateRequest): Promise<MapPlayerSnapshotItem | null> {
|
||||
try {
|
||||
if (!request.socketId?.trim() || !request.mapId?.trim()) {
|
||||
return null;
|
||||
}
|
||||
const presence = await this.sessionService.updatePlayerPositionWithPresence(
|
||||
request.socketId,
|
||||
request.mapId,
|
||||
request.x,
|
||||
request.y,
|
||||
{
|
||||
appearance: request.appearance,
|
||||
direction: request.direction,
|
||||
movementState: request.movementState,
|
||||
sequence: request.sequence,
|
||||
},
|
||||
);
|
||||
return presence ? this.toMapPlayerSnapshotItem(presence) : null;
|
||||
} catch (error) {
|
||||
this.logger.error('更新位置失败', { error: (error as Error).message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updatePlayerPresenceState(
|
||||
request: PlayerPresenceStateUpdateRequest,
|
||||
): Promise<{ success: boolean; presence?: MapPlayerSnapshotItem; socketId?: string; error?: string }> {
|
||||
@@ -555,6 +661,9 @@ export class ChatService {
|
||||
appearance: updatedSession.appearance,
|
||||
cafeCompanion: updatedSession.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(updatedSession.movementLocked),
|
||||
direction: updatedSession.direction || 'down',
|
||||
movementState: updatedSession.movementState || 'idle',
|
||||
sequence: Number(updatedSession.movementSequence ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -866,7 +975,7 @@ export class ChatService {
|
||||
const clientInstance = await this.zulipClientPool.createUserClient(userId, {
|
||||
username: zulipEmail,
|
||||
apiKey: apiKey,
|
||||
realm: process.env.ZULIP_SERVER_URL || 'https://zulip.xinghangee.icu/',
|
||||
realm: process.env.ZULIP_SERVER_URL || 'https://zulip.novamailio.com/',
|
||||
});
|
||||
|
||||
this.logger.log('Zulip客户端创建成功', {
|
||||
@@ -993,9 +1102,13 @@ export class ChatService {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
private async dispatchGameChatMessage(message: GameChatMessage, senderSocketId: string): Promise<void> {
|
||||
private async dispatchGameChatMessage(
|
||||
message: GameChatMessage,
|
||||
senderSocketId: string,
|
||||
includeSender = false,
|
||||
): Promise<void> {
|
||||
if (message.scope === 'global') {
|
||||
this.broadcastToAllGamePlayers(message, senderSocketId);
|
||||
this.broadcastToAllGamePlayers(message, includeSender ? undefined : senderSocketId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1063,8 +1176,12 @@ export class ChatService {
|
||||
appearance: player.appearance,
|
||||
skinId: player.appearance?.skinId,
|
||||
avatarId: player.appearance?.avatarId,
|
||||
skinAsset: player.appearance?.skinAsset,
|
||||
cafeCompanion: player.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(player.movementLocked),
|
||||
direction: player.direction || 'down',
|
||||
movementState: player.movementState || 'idle',
|
||||
sequence: Number(player.sequence ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1086,6 +1203,9 @@ export class ChatService {
|
||||
avatarId: presence.avatarId,
|
||||
cafeCompanion: presence.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(presence.movementLocked),
|
||||
direction: presence.direction || 'down',
|
||||
movementState: presence.movementState || 'idle',
|
||||
sequence: Number(presence.sequence ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user