Commit 4af43c04 authored by nanahira's avatar nanahira

rework random duel

parent 19daed1d
Pipeline #43256 passed with stages
in 2 minutes and 39 seconds
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
- 目录外引用:必须通过 index.ts 引用,如 `'../room'` `'../client'`(指向 index.ts) - 目录外引用:必须通过 index.ts 引用,如 `'../room'` `'../client'`(指向 index.ts)
- 禁止直接引用具体文件:不要使用 `'../room/room'` `'../client/client'` 这样的路径 - 禁止直接引用具体文件:不要使用 `'../room/room'` `'../client/client'` 这样的路径
- 如果正在写的算法代码与 this 和业务无关,那么不要放在类方法里面,而是在 utility 目录新开一个 ts 文件放进去 - 如果正在写的算法代码与 this 和业务无关,那么不要放在类方法里面,而是在 utility 目录新开一个 ts 文件放进去
- 如果正在移植 srvpro 的功能,那么 i18n 必须严格和 srvpro 保持一致,不能改动 i18n 的 key 和 value。新功能或者原有功能的额外部分可以写新的。
## 模块结构 ## 模块结构
......
...@@ -45,6 +45,10 @@ export class ClientHandler { ...@@ -45,6 +45,10 @@ export class ClientHandler {
client.vpass = vpass || ''; client.vpass = vpass || '';
return next(); return next();
}) })
.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
client.roompass = msg.pass || '';
return next();
})
.middleware( .middleware(
YGOProCtosBase, YGOProCtosBase,
async (msg, client, next) => { async (msg, client, next) => {
......
...@@ -213,6 +213,8 @@ export class Client { ...@@ -213,6 +213,8 @@ export class Client {
@ClientRoomField() @ClientRoomField()
roomName?: string; roomName?: string;
@ClientRoomField() @ClientRoomField()
roompass = '';
@ClientRoomField()
isHost = false; isHost = false;
@ClientRoomField() @ClientRoomField()
pos = -1; pos = -1;
......
...@@ -55,6 +55,30 @@ export const TRANSLATIONS = { ...@@ -55,6 +55,30 @@ export const TRANSLATIONS = {
'Match mode room. Password S for single mode, T for tag mode.', 'Match mode room. Password S for single mode, T for tag mode.',
random_duel_enter_room_tag: random_duel_enter_room_tag:
'Tag mode room. Password S for single mode, M for match mode.', 'Tag mode room. Password S for single mode, M for match mode.',
random_banned_part1: 'You have been banned from the game due to ',
random_banned_part2: ', it will last until ',
random_banned_part3: '.',
random_deprecated_part1: 'Because of your ',
random_deprecated_part2:
', in recent games, you will only suffer other players who are under same punishment as you in ',
random_deprecated_part3: '.',
random_warn_part1: 'The system detects that recently you had ',
random_warn_part2:
' in recent games, you will be penalized after 3 offences',
random_ban_reason_separator: '/',
random_ban_reason_flee: 'Flee',
random_ban_reason_AFK: 'AFK',
random_ban_reason_abuse: 'Abusing',
random_ban_reason_zombie: 'Zombie',
unwelcome_warn_part1: 'If you keep doing ',
unwelcome_warn_part2: ', your opponent may leave you.',
unwelcome_tip_part1: 'Your opponent did ',
unwelcome_tip_part2:
', so you can leave the game without being punished.',
banned_duel_tip:
'You are banned from the random duel system for sending inappropriate messages.',
chat_banned: 'is banned from chat.',
surrender_denied: "Please don't surrender in the first 2 turns.",
chat_disabled: 'Chat is disabled in this room.', chat_disabled: 'Chat is disabled in this room.',
chat_warn_level1: 'Please avoid sensitive words.', chat_warn_level1: 'Please avoid sensitive words.',
chat_warn_level2: 'Your message contains blocked words.', chat_warn_level2: 'Your message contains blocked words.',
...@@ -110,6 +134,26 @@ export const TRANSLATIONS = { ...@@ -110,6 +134,26 @@ export const TRANSLATIONS = {
'您进入了比赛模式房间,密码输入 S 进入单局模式,输入 T 进入双打模式。', '您进入了比赛模式房间,密码输入 S 进入单局模式,输入 T 进入双打模式。',
random_duel_enter_room_tag: random_duel_enter_room_tag:
'您进入了双打模式房间,密码输入 S 进入单局模式,输入 M 进入比赛模式。', '您进入了双打模式房间,密码输入 S 进入单局模式,输入 M 进入比赛模式。',
random_banned_part1: '因为您近期在游戏中多次',
random_banned_part2: ',您已被禁止使用随机对战功能,将在',
random_banned_part3: '后解封',
random_deprecated_part1: '因为您近期在游戏中',
random_deprecated_part2: ',在',
random_deprecated_part3: '内您随机对战时只能遇到其他违规玩家。',
random_warn_part1: '系统检测到您近期在游戏中',
random_warn_part2: ',若您违规超过3次,将受到惩罚',
random_ban_reason_separator: '',
random_ban_reason_flee: '强退',
random_ban_reason_AFK: '挂机',
random_ban_reason_abuse: '发言违规',
random_ban_reason_zombie: '挂房间',
unwelcome_warn_part1: '如果您经常',
unwelcome_warn_part2: ',您的对手可能会离你而去。',
unwelcome_tip_part1: '因为您的对手有',
unwelcome_tip_part2: '行为,现在您可以直接离开游戏或投降,不视为强退。',
banned_duel_tip: '您的发言存在严重不适当的内容,禁止您使用随机对战功能!',
chat_banned: '已被禁言!',
surrender_denied: '为保证双方玩家的游戏体验,随机对战中3回合后才能投降。',
chat_disabled: '本房间禁止聊天。', chat_disabled: '本房间禁止聊天。',
chat_warn_level1: '请注意发言,敏感词已被替换。', chat_warn_level1: '请注意发言,敏感词已被替换。',
chat_warn_level2: '消息包含敏感词,已被拦截。', chat_warn_level2: '消息包含敏感词,已被拦截。',
......
import { Client } from '../client';
import { Room } from '../room';
export type RandomDuelWaitTimeoutType = 'ready' | 'hang';
export class OnClientWaitTimeout {
constructor(
public room: Room,
public client: Client,
public type: RandomDuelWaitTimeoutType,
) {}
}
export class OnClientBadwordViolation {
constructor(
public client: Client,
public room: Room | undefined,
public message: string,
public level: number,
public replacedMessage?: string,
) {}
}
This diff is collapsed.
export type RandomDuelPunishReason = 'AFK' | 'ABUSE' | 'FLEE' | 'ZOMBIE';
export const punishReasonToI18nKey = (reason: RandomDuelPunishReason) => {
if (reason === 'AFK') {
return 'random_ban_reason_AFK';
}
if (reason === 'ABUSE') {
return 'random_ban_reason_abuse';
}
if (reason === 'FLEE') {
return 'random_ban_reason_flee';
}
return 'random_ban_reason_zombie';
};
export const renderReasonText = (reasons: RandomDuelPunishReason[]) => {
const entries = [...new Set(reasons)].map(
(reason) => `#{${punishReasonToI18nKey(reason)}}`,
);
if (!entries.length) {
return `#{${punishReasonToI18nKey('ABUSE')}}`;
}
return entries.join('#{random_ban_reason_separator}');
};
export const formatRemainText = (expireAt: number) => {
const remainMs = Math.max(0, expireAt - Date.now());
const remainMinutes = Math.max(1, Math.ceil(remainMs / 60_000));
if (remainMinutes >= 60) {
const hours = Math.floor(remainMinutes / 60);
const minutes = remainMinutes % 60;
if (minutes <= 0) {
return `${hours}h`;
}
return `${hours}h ${minutes}m`;
}
return `${remainMinutes}m`;
};
export const buildFleeFreeKey = (roomName: string, ip: string) =>
`${roomName}:${ip}`;
...@@ -838,7 +838,9 @@ export class Reconnect { ...@@ -838,7 +838,9 @@ export class Reconnect {
const matchCondition = const matchCondition =
room.isLooseReconnectRule || room.isLooseReconnectRule ||
player.ip === newClient.ip || player.ip === newClient.ip ||
(newClient.vpass && newClient.vpass === player.vpass); (newClient.vpass &&
newClient.vpass === player.vpass &&
newClient.roompass === player.roompass);
if (matchCondition) { if (matchCondition) {
return player; return player;
......
...@@ -4,6 +4,7 @@ import { Client } from '../../client'; ...@@ -4,6 +4,7 @@ import { Client } from '../../client';
import { Room, RoomManager } from '../../room'; import { Room, RoomManager } from '../../room';
import { escapeRegExp } from '../../utility/escape-regexp'; import { escapeRegExp } from '../../utility/escape-regexp';
import { ValueContainer } from '../../utility/value-container'; import { ValueContainer } from '../../utility/value-container';
import { OnClientBadwordViolation } from '../random-duel-events';
import { BaseResourceProvider } from './base-resource-provider'; import { BaseResourceProvider } from './base-resource-provider';
import { isObjectRecord } from './resource-util'; import { isObjectRecord } from './resource-util';
import { BadwordsData, EMPTY_BADWORDS_DATA } from './types'; import { BadwordsData, EMPTY_BADWORDS_DATA } from './types';
...@@ -71,7 +72,21 @@ export class BadwordProvider extends BaseResourceProvider<BadwordsData> { ...@@ -71,7 +72,21 @@ export class BadwordProvider extends BaseResourceProvider<BadwordsData> {
const room = client.roomName const room = client.roomName
? this.roomManager.findByName(client.roomName) ? this.roomManager.findByName(client.roomName)
: undefined; : undefined;
const filtered = await this.filterText(msg.msg, room, client); const originalMessage = msg.msg;
const filtered = await this.filterText(originalMessage, room, client);
if (filtered.level >= 0) {
await this.ctx.dispatch(
new OnClientBadwordViolation(
client,
room,
originalMessage,
filtered.level,
filtered.message !== originalMessage ? filtered.message : undefined,
),
client as any,
);
}
if (filtered.blocked) { if (filtered.blocked) {
await client.sendChat('#{chat_warn_level2}', ChatColor.RED); await client.sendChat('#{chat_warn_level2}', ChatColor.RED);
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
Room, Room,
RoomManager, RoomManager,
} from '../room'; } from '../room';
import { OnClientWaitTimeout } from './random-duel-events';
export interface WaitForPlayerConfig { export interface WaitForPlayerConfig {
roomFilter: (room: Room) => boolean; roomFilter: (room: Room) => boolean;
...@@ -394,6 +395,10 @@ export class WaitForPlayerProvider { ...@@ -394,6 +395,10 @@ export class WaitForPlayerProvider {
`${latestTarget.name} #{kicked_by_system}`, `${latestTarget.name} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch(
new OnClientWaitTimeout(room, latestTarget, 'ready'),
latestTarget,
);
latestTarget.disconnect(); latestTarget.disconnect();
} }
...@@ -441,6 +446,10 @@ export class WaitForPlayerProvider { ...@@ -441,6 +446,10 @@ export class WaitForPlayerProvider {
`${waitingPlayer.name} #{kicked_by_system}`, `${waitingPlayer.name} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch(
new OnClientWaitTimeout(room, waitingPlayer, 'hang'),
waitingPlayer,
);
waitingPlayer.disconnect(); waitingPlayer.disconnect();
return; return;
} }
......
import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode'; import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../../app'; import { Context } from '../../app';
import { Client } from '../../client';
import { WindBotProvider } from './windbot-provider'; import { WindBotProvider } from './windbot-provider';
import { RoomManager } from '../../room'; import { RoomManager } from '../../room';
import { MAX_ROOM_NAME_LENGTH } from '../../constants/room'; import { MAX_ROOM_NAME_LENGTH } from '../../constants/room';
...@@ -19,71 +20,86 @@ export class JoinWindbotAi { ...@@ -19,71 +20,86 @@ export class JoinWindbotAi {
return; return;
} }
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => { this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
msg.pass = (msg.pass || '').trim(); if (!(await this.joinByPass(msg.pass, client))) {
if (!msg.pass || !msg.pass.toUpperCase().startsWith('AI')) {
return next(); return next();
} }
return;
});
}
const existingRoom = this.roomManager.findByName(msg.pass); async joinByPass(pass: string, client: Client) {
if (existingRoom) { const normalizedPass = (pass || '').trim();
existingRoom.noHost = true; if (
return existingRoom.join(client); !this.windbotProvider.enabled ||
} !normalizedPass ||
!normalizedPass.toUpperCase().startsWith('AI')
) {
return false;
}
const requestedBotName = this.parseRequestedBotName(msg.pass); const existingRoom = this.roomManager.findByName(normalizedPass);
if ( if (existingRoom) {
requestedBotName && existingRoom.noHost = true;
!this.windbotProvider.getBotByNameOrDeck(requestedBotName) await existingRoom.join(client);
) { return true;
return client.die('#{windbot_deck_not_found}', ChatColor.RED); }
}
const roomName = this.generateWindbotRoomName(msg.pass); const requestedBotName = this.parseRequestedBotName(normalizedPass);
if (!roomName) { if (
return client.die('#{create_room_failed}', ChatColor.RED); requestedBotName &&
} !this.windbotProvider.getBotByNameOrDeck(requestedBotName)
if (getDisplayLength(roomName) > 20) { ) {
return client.die('#{windbot_name_too_long}', ChatColor.RED); await client.die('#{windbot_deck_not_found}', ChatColor.RED);
} return true;
}
const room = await this.roomManager.findOrCreateByName(roomName, { const roomName = this.generateWindbotRoomName(normalizedPass);
rule: 5, if (!roomName) {
lflist: -1, await client.die('#{create_room_failed}', ChatColor.RED);
time_limit: 0, return true;
}); }
room.noHost = true; if (getDisplayLength(roomName) > 20) {
room.noReconnect = true; await client.die('#{windbot_name_too_long}', ChatColor.RED);
room.windbot = { return true;
name: '', }
deck: '',
};
const windbotOptions = parseWindbotOptions(room.name);
await room.join(client); const room = await this.roomManager.findOrCreateByName(roomName, {
const requestCount = room.isTag ? 3 : 1; rule: 5,
for (let i = 0; i < requestCount; i += 1) { lflist: -1,
const requestOk = await this.windbotProvider.requestWindbotJoin( time_limit: 0,
room, });
requestedBotName, room.noHost = true;
windbotOptions, room.noReconnect = true;
); room.windbot = {
if (!requestOk) { name: '',
await room.finalize(); deck: '',
return; };
} const windbotOptions = parseWindbotOptions(room.name);
}
this.logger.debug( await room.join(client);
{ const requestCount = room.isTag ? 3 : 1;
player: client.name, for (let i = 0; i < requestCount; i += 1) {
roomName: room.name, const requestOk = await this.windbotProvider.requestWindbotJoin(
botName: room.windbot?.name, room,
requestCount, requestedBotName,
}, windbotOptions,
'Created windbot room',
); );
return; if (!requestOk) {
}); await room.finalize();
return true;
}
}
this.logger.debug(
{
player: client.name,
roomName: room.name,
botName: room.windbot?.name,
requestCount,
},
'Created windbot room',
);
return true;
} }
private parseRequestedBotName(pass: string) { private parseRequestedBotName(pass: string) {
......
import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app';
import { RandomDuelProvider } from '../feats';
export class JoinBlankPassRandomDuel {
private randomDuelProvider = this.ctx.get(() => RandomDuelProvider);
constructor(private ctx: Context) {
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
msg.pass = (msg.pass || '').trim();
if (msg.pass || !this.randomDuelProvider.enabled) {
return next();
}
const result = await this.randomDuelProvider.findOrCreateRandomRoom(
'',
client.ip,
);
if (result.errorMessage) {
return client.die(result.errorMessage, ChatColor.RED);
}
if (!result.room) {
return client.die('#{create_room_failed}', ChatColor.RED);
}
return result.room.join(client);
});
}
}
import { YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app';
import { JoinWindbotAi } from '../feats/windbot';
export class JoinBlankPassWindbotAi {
private joinWindbotAi = this.ctx.get(() => JoinWindbotAi);
constructor(private ctx: Context) {
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
msg.pass = (msg.pass || '').trim();
if (msg.pass) {
return next();
}
if (await this.joinWindbotAi.joinByPass('AI', client)) {
return;
}
return next();
});
}
}
...@@ -7,6 +7,8 @@ import { JoinFallback } from './fallback'; ...@@ -7,6 +7,8 @@ import { JoinFallback } from './fallback';
import { JoinPrechecks } from './join-prechecks'; import { JoinPrechecks } from './join-prechecks';
import { RandomDuelJoinHandler } from './random-duel-join-handler'; import { RandomDuelJoinHandler } from './random-duel-join-handler';
import { BadwordPlayerInfoChecker } from './badword-player-info-checker'; import { BadwordPlayerInfoChecker } from './badword-player-info-checker';
import { JoinBlankPassRandomDuel } from './join-blank-pass-random-duel';
import { JoinBlankPassWindbotAi } from './join-blank-pass-windbot-ai';
export const JoinHandlerModule = createAppContext<ContextState>() export const JoinHandlerModule = createAppContext<ContextState>()
.provide(ClientVersionCheck) .provide(ClientVersionCheck)
...@@ -16,5 +18,7 @@ export const JoinHandlerModule = createAppContext<ContextState>() ...@@ -16,5 +18,7 @@ export const JoinHandlerModule = createAppContext<ContextState>()
.provide(RandomDuelJoinHandler) .provide(RandomDuelJoinHandler)
.provide(JoinWindbotAi) .provide(JoinWindbotAi)
.provide(JoinRoom) .provide(JoinRoom)
.provide(JoinBlankPassRandomDuel)
.provide(JoinBlankPassWindbotAi)
.provide(JoinFallback) .provide(JoinFallback)
.define(); .define();
...@@ -11,18 +11,24 @@ export class RandomDuelJoinHandler { ...@@ -11,18 +11,24 @@ export class RandomDuelJoinHandler {
} }
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => { this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
msg.pass = (msg.pass || '').trim(); msg.pass = (msg.pass || '').trim();
if (!msg.pass) {
return next();
}
const type = this.randomDuelProvider.resolveRandomType(msg.pass); const type = this.randomDuelProvider.resolveRandomType(msg.pass);
if (type == null) { if (type == null) {
return next(); return next();
} }
const room = await this.randomDuelProvider.findOrCreateRandomRoom( const result = await this.randomDuelProvider.findOrCreateRandomRoom(
type, type,
client.ip, client.ip,
); );
if (!room) { if (result.errorMessage) {
return client.die(result.errorMessage, ChatColor.RED);
}
if (!result.room) {
return client.die('#{create_room_failed}', ChatColor.RED); return client.die('#{create_room_failed}', ChatColor.RED);
} }
return room.join(client); return result.room.join(client);
}); });
} }
} }
import { Room } from '../room';
import { RoomEvent } from './room-event'; import { RoomEvent } from './room-event';
export class OnRoomLeaveObserver extends RoomEvent {} export enum RoomLeaveObserverReason {
Disconnect = 'disconnect',
ToDuelist = 'to_duelist',
}
export class OnRoomLeaveObserver extends RoomEvent {
constructor(
room: Room,
public reason: RoomLeaveObserverReason,
public bySystem = false,
) {
super(room);
}
}
import { Room } from '../room'; import { Room } from '../room';
import { RoomEvent } from './room-event'; import { RoomEvent } from './room-event';
export enum RoomLeavePlayerReason {
Disconnect = 'disconnect',
ToObserver = 'to_observer',
SwitchPosition = 'switch_position',
}
export class OnRoomLeavePlayer extends RoomEvent { export class OnRoomLeavePlayer extends RoomEvent {
constructor( constructor(
room: Room, room: Room,
public oldPos: number, public oldPos: number,
public reason: RoomLeavePlayerReason,
public bySystem = false,
) { ) {
super(room); super(room);
} }
......
...@@ -71,8 +71,14 @@ import { OnRoomLeave } from './room-event/on-room-leave'; ...@@ -71,8 +71,14 @@ import { OnRoomLeave } from './room-event/on-room-leave';
import { OnRoomWin } from './room-event/on-room-win'; import { OnRoomWin } from './room-event/on-room-win';
import { OnRoomJoinPlayer } from './room-event/on-room-join-player'; import { OnRoomJoinPlayer } from './room-event/on-room-join-player';
import { OnRoomJoinObserver } from './room-event/on-room-join-observer'; import { OnRoomJoinObserver } from './room-event/on-room-join-observer';
import { OnRoomLeavePlayer } from './room-event/on-room-leave-player'; import {
import { OnRoomLeaveObserver } from './room-event/on-room-leave-observer'; OnRoomLeavePlayer,
RoomLeavePlayerReason,
} from './room-event/on-room-leave-player';
import {
OnRoomLeaveObserver,
RoomLeaveObserverReason,
} from './room-event/on-room-leave-observer';
import { OnRoomMatchStart } from './room-event/on-room-match-start'; import { OnRoomMatchStart } from './room-event/on-room-match-start';
import { OnRoomGameStart } from './room-event/on-room-game-start'; import { OnRoomGameStart } from './room-event/on-room-game-start';
import YGOProDeck from 'ygopro-deck-encode'; import YGOProDeck from 'ygopro-deck-encode';
...@@ -590,9 +596,24 @@ export class Room { ...@@ -590,9 +596,24 @@ export class Room {
// 触发具体的离开事件 // 触发具体的离开事件
if (wasObserver) { if (wasObserver) {
await this.ctx.dispatch(new OnRoomLeaveObserver(this), client); await this.ctx.dispatch(
new OnRoomLeaveObserver(
this,
RoomLeaveObserverReason.Disconnect,
_msg.bySystem,
),
client,
);
} else { } else {
await this.ctx.dispatch(new OnRoomLeavePlayer(this, oldPos), client); await this.ctx.dispatch(
new OnRoomLeavePlayer(
this,
oldPos,
RoomLeavePlayerReason.Disconnect,
_msg.bySystem,
),
client,
);
} }
client.roomName = undefined; client.roomName = undefined;
...@@ -638,7 +659,10 @@ export class Room { ...@@ -638,7 +659,10 @@ export class Room {
this.allPlayers.forEach((p) => p.send(this.watcherSizeMessage)); this.allPlayers.forEach((p) => p.send(this.watcherSizeMessage));
// 触发事件 // 触发事件
await this.ctx.dispatch(new OnRoomLeavePlayer(this, oldPos), client); await this.ctx.dispatch(
new OnRoomLeavePlayer(this, oldPos, RoomLeavePlayerReason.ToObserver),
client,
);
await this.ctx.dispatch(new OnRoomJoinObserver(this), client); await this.ctx.dispatch(new OnRoomJoinObserver(this), client);
} }
...@@ -675,7 +699,10 @@ export class Room { ...@@ -675,7 +699,10 @@ export class Room {
this.allPlayers.forEach((p) => p.send(this.watcherSizeMessage)); this.allPlayers.forEach((p) => p.send(this.watcherSizeMessage));
// 触发事件 // 触发事件
await this.ctx.dispatch(new OnRoomLeaveObserver(this), client); await this.ctx.dispatch(
new OnRoomLeaveObserver(this, RoomLeaveObserverReason.ToDuelist),
client,
);
await this.ctx.dispatch(new OnRoomJoinPlayer(this), client); await this.ctx.dispatch(new OnRoomJoinPlayer(this), client);
} else if (this.isTag) { } else if (this.isTag) {
// TAG 模式下,已经是玩家,切换到另一个空位 // TAG 模式下,已经是玩家,切换到另一个空位
...@@ -708,7 +735,14 @@ export class Room { ...@@ -708,7 +735,14 @@ export class Room {
await client.sendTypeChange(); await client.sendTypeChange();
// 触发事件 (玩家切换位置) // 触发事件 (玩家切换位置)
await this.ctx.dispatch(new OnRoomLeavePlayer(this, oldPos), client); await this.ctx.dispatch(
new OnRoomLeavePlayer(
this,
oldPos,
RoomLeavePlayerReason.SwitchPosition,
),
client,
);
await this.ctx.dispatch(new OnRoomJoinPlayer(this), client); await this.ctx.dispatch(new OnRoomJoinPlayer(this), client);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment