vimp-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +34 -0
- package/src/config/authClient.js +47 -0
- package/src/config/clientDefaults.js +113 -0
- package/src/config/hostDefaults.js +79 -0
- package/src/config/lobby.js +104 -0
- package/src/config/master.js +103 -0
- package/src/config/opcodes.js +29 -0
- package/src/config/wsports.js +35 -0
- package/src/host/GameCoreAdapter.js +206 -0
- package/src/host/HostGame.js +857 -0
- package/src/host/host.worker.js +438 -0
- package/src/host/meta/SocketManager.js +431 -0
- package/src/host/meta/core/CommandProcessor.js +99 -0
- package/src/host/meta/core/RoundManager.js +646 -0
- package/src/host/meta/core/VoteCoordinator.js +71 -0
- package/src/host/meta/modules/Panel.js +181 -0
- package/src/host/meta/modules/PlayerDataSync.js +176 -0
- package/src/host/meta/modules/RTTManager.js +168 -0
- package/src/host/meta/modules/Stat.js +294 -0
- package/src/host/meta/modules/TimerManager.js +277 -0
- package/src/host/meta/modules/Vote.js +179 -0
- package/src/host/meta/modules/chat/Chat.js +66 -0
- package/src/host/meta/modules/chat/index.js +1 -0
- package/src/host/meta/modules/chat/systemMessages.js +51 -0
- package/src/host/meta/player/HumanParticipant.js +36 -0
- package/src/host/meta/player/Participant.js +22 -0
- package/src/host/meta/player/ParticipantManager.js +247 -0
- package/src/host/meta/player/ScriptedParticipant.js +19 -0
- package/src/lib/AbstractTimer.js +84 -0
- package/src/lib/Publisher.js +43 -0
- package/src/lib/applyRoomOverrides.js +48 -0
- package/src/lib/buildClientConfig.js +48 -0
- package/src/lib/clientCoreConfig.js +59 -0
- package/src/lib/config.js +83 -0
- package/src/lib/coreConfig.js +60 -0
- package/src/lib/factory.js +21 -0
- package/src/lib/formatters.js +33 -0
- package/src/lib/gamePlugin.js +95 -0
- package/src/lib/jwt.js +103 -0
- package/src/lib/math.js +75 -0
- package/src/lib/rateLimiter.js +40 -0
- package/src/lib/sanitizers.js +17 -0
- package/src/lib/security.js +45 -0
- package/src/lib/validators.js +52 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { isValidName } from '../../../lib/validators.js';
|
|
2
|
+
|
|
3
|
+
// масштабирование данных карты (физика, респауны) под mapScale
|
|
4
|
+
function scaleMapData(mapData) {
|
|
5
|
+
const scale = mapData.scale;
|
|
6
|
+
const step = mapData.step * scale;
|
|
7
|
+
|
|
8
|
+
const physicsDynamic = (mapData.physicsDynamic || []).map(item => ({
|
|
9
|
+
...item,
|
|
10
|
+
position: item.position.map(v => v * scale),
|
|
11
|
+
width: item.width * scale,
|
|
12
|
+
height: item.height * scale,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const respawns = Object.fromEntries(
|
|
16
|
+
Object.entries(mapData.respawns || {}).map(([team, arr]) => [
|
|
17
|
+
team,
|
|
18
|
+
arr.map(([x, y, angle]) => [x * scale, y * scale, angle]),
|
|
19
|
+
]),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...mapData,
|
|
24
|
+
step,
|
|
25
|
+
physicsDynamic,
|
|
26
|
+
respawns,
|
|
27
|
+
scale,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Менеджер раундов, команд и карт. Владеет жизненным циклом раунда/карты
|
|
32
|
+
// и состоянием: currentMap/currentMapData/scaledMapData, isRoundEnding,
|
|
33
|
+
// startMapNumber и списком игроков для удаления с полотна.
|
|
34
|
+
class RoundManager {
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this._participants = deps.participants;
|
|
37
|
+
this._game = deps.game;
|
|
38
|
+
this._panel = deps.panel;
|
|
39
|
+
this._stat = deps.stat;
|
|
40
|
+
this._chat = deps.chat;
|
|
41
|
+
this._socketManager = deps.socketManager;
|
|
42
|
+
this._timerManager = deps.timerManager;
|
|
43
|
+
this._scripted = deps.scripted;
|
|
44
|
+
this._voteCoordinator = deps.voteCoordinator;
|
|
45
|
+
this._snapshotManager = deps.snapshotManager;
|
|
46
|
+
this._playerDataSync = deps.playerDataSync || null;
|
|
47
|
+
|
|
48
|
+
// конфигурация
|
|
49
|
+
this._teams = deps.teams;
|
|
50
|
+
this._spectatorTeam = deps.spectatorTeam;
|
|
51
|
+
this._spectatorId = deps.spectatorId;
|
|
52
|
+
this._maps = deps.maps;
|
|
53
|
+
this._mapList = deps.mapList;
|
|
54
|
+
this._mapsInVote = deps.mapsInVote;
|
|
55
|
+
this._mapScale = deps.mapScale;
|
|
56
|
+
this._mapSetId = deps.mapSetId;
|
|
57
|
+
|
|
58
|
+
// состояние раунда/карты
|
|
59
|
+
this._currentMap = deps.currentMap;
|
|
60
|
+
this._currentMapData = null;
|
|
61
|
+
this._scaledMapData = null;
|
|
62
|
+
this._startMapNumber = 0;
|
|
63
|
+
this._isRoundEnding = false;
|
|
64
|
+
this._removedPlayersList = []; // gameId игроков для удаления с полотна
|
|
65
|
+
|
|
66
|
+
// эстафета Worker'ов (Этап 5.2): колбэк переноса на границе раунда
|
|
67
|
+
this._handoffCallback = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get currentMap() {
|
|
71
|
+
return this._currentMap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get currentMapData() {
|
|
75
|
+
return this._currentMapData;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get removedPlayersList() {
|
|
79
|
+
return this._removedPlayersList;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// запускает голосование за смену карты по истечении времени карты
|
|
83
|
+
onMapTimeEnd() {
|
|
84
|
+
this._voteCoordinator.reset();
|
|
85
|
+
this.changeMap();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// подготавливает данные текущей карты (масштабирование) без пересоздания
|
|
89
|
+
// мира — общий шаг createMap и восстановления эстафеты (Этап 5.2)
|
|
90
|
+
_prepareMapData() {
|
|
91
|
+
this._currentMapData = {
|
|
92
|
+
scale: this._mapScale,
|
|
93
|
+
...this._maps[this._currentMap],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// если нет индивидуального конструктора для создания карты
|
|
97
|
+
if (!this._currentMapData.setId) {
|
|
98
|
+
this._currentMapData.setId = this._mapSetId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._scaledMapData = scaleMapData(this._currentMapData);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// восстанавливает карту после эстафеты Worker'ов (Этап 5.2): без
|
|
105
|
+
// пересоздания мира и рассылки клиентам — мир соберёт первый
|
|
106
|
+
// initiateNewRound после переноса
|
|
107
|
+
restoreMap(mapName) {
|
|
108
|
+
this._currentMap = mapName;
|
|
109
|
+
this._prepareMapData();
|
|
110
|
+
this._scripted.createMap(this._scaledMapData);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// создает карту
|
|
114
|
+
createMap() {
|
|
115
|
+
this._prepareMapData();
|
|
116
|
+
this._scripted.createMap(this._scaledMapData);
|
|
117
|
+
const scriptedCounts = this._scripted.getCountsPerTeam();
|
|
118
|
+
this._scripted.removeScripted();
|
|
119
|
+
|
|
120
|
+
// остановка всех игровых таймеров и отложенных вызовов
|
|
121
|
+
this._timerManager.stopGameTimers();
|
|
122
|
+
|
|
123
|
+
this._participants.resetTeamSizes();
|
|
124
|
+
|
|
125
|
+
this._participants.clearActive();
|
|
126
|
+
this._removedPlayersList = [];
|
|
127
|
+
|
|
128
|
+
this._panel.reset();
|
|
129
|
+
this._stat.reset();
|
|
130
|
+
this._voteCoordinator.reset();
|
|
131
|
+
|
|
132
|
+
// синхронизация накопленных rank/state на мастер перед сменой карты
|
|
133
|
+
// (Этап B4) — естественная граница жизненного цикла
|
|
134
|
+
this._playerDataSync?.flushAll();
|
|
135
|
+
|
|
136
|
+
this._snapshotManager.reset();
|
|
137
|
+
|
|
138
|
+
this._game.clear();
|
|
139
|
+
this._game.createMap(this._scaledMapData);
|
|
140
|
+
|
|
141
|
+
for (const user of this._participants.getHumans()) {
|
|
142
|
+
const gameId = user.gameId;
|
|
143
|
+
|
|
144
|
+
this._socketManager.sendClear(user.socketId);
|
|
145
|
+
|
|
146
|
+
// перемещение пользователя в наблюдатели
|
|
147
|
+
this._stat.moveUser(gameId, user.teamId, this._spectatorId);
|
|
148
|
+
|
|
149
|
+
// обнулить параметры
|
|
150
|
+
user.team = this._spectatorTeam;
|
|
151
|
+
user.teamId = this._spectatorId;
|
|
152
|
+
user.status = 'spectator';
|
|
153
|
+
user.isWatching = true;
|
|
154
|
+
user.watchedGameId = this._participants.getActiveList()[0] || null;
|
|
155
|
+
user.forceCameraReset = true;
|
|
156
|
+
|
|
157
|
+
this._participants.addToTeam(gameId, this._spectatorTeam);
|
|
158
|
+
|
|
159
|
+
this.sendMap(gameId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// воссоздание scripted-участников на новой карте
|
|
163
|
+
for (const [team, count] of Object.entries(scriptedCounts)) {
|
|
164
|
+
if (count > 0) {
|
|
165
|
+
this._scripted.createScripted(count, team);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._timerManager.startGameTimers();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// отправляет карту игроку
|
|
173
|
+
sendMap(gameId) {
|
|
174
|
+
const user = this._participants.get(gameId);
|
|
175
|
+
const socketId = user.socketId;
|
|
176
|
+
|
|
177
|
+
user.isReady = false;
|
|
178
|
+
user.currentMap = this._currentMap;
|
|
179
|
+
this._socketManager.sendTechInform(socketId, 'loading');
|
|
180
|
+
this._socketManager.sendMap(socketId, this._currentMapData);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// немедленная смена карты (когда голосование не требуется)
|
|
184
|
+
forceChangeMap(mapName) {
|
|
185
|
+
this._currentMap = mapName;
|
|
186
|
+
this.createMap();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// эстафета Worker'ов (Этап 5.2): запросить перенос на ближайшей границе
|
|
190
|
+
// раунда — вместо старта нового раунда сработает колбэк (сбор состояния)
|
|
191
|
+
requestHandoff(cb) {
|
|
192
|
+
this._handoffCallback = cb;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// отмена запрошенного переноса (своп не состоялся)
|
|
196
|
+
cancelHandoff() {
|
|
197
|
+
this._handoffCallback = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// запуск нового раунда
|
|
201
|
+
initiateNewRound() {
|
|
202
|
+
this._timerManager.stopRoundTimer();
|
|
203
|
+
|
|
204
|
+
// граница раунда достигнута — отдать состояние эстафете; новый раунд
|
|
205
|
+
// стартует уже новый Worker после переноса
|
|
206
|
+
if (this._handoffCallback) {
|
|
207
|
+
const cb = this._handoffCallback;
|
|
208
|
+
|
|
209
|
+
this._handoffCallback = null;
|
|
210
|
+
cb();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this._startRound();
|
|
215
|
+
this._timerManager.startRoundTimer();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// начало раунда
|
|
219
|
+
_startRound() {
|
|
220
|
+
this._isRoundEnding = false; // сброс флага завершения раунда
|
|
221
|
+
|
|
222
|
+
const respawns = this._scaledMapData.respawns;
|
|
223
|
+
const respId = Object.keys(this._teams)
|
|
224
|
+
.filter(key => key !== this._spectatorTeam)
|
|
225
|
+
.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
|
|
226
|
+
|
|
227
|
+
function getRespawnData(team) {
|
|
228
|
+
const number = respId[team];
|
|
229
|
+
|
|
230
|
+
respId[team] += 1;
|
|
231
|
+
|
|
232
|
+
return respawns[team][number];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// очищение списка играющих
|
|
236
|
+
this._participants.clearActive();
|
|
237
|
+
|
|
238
|
+
this._panel.reset();
|
|
239
|
+
|
|
240
|
+
const setIdList = this._game.removePlayersAndShots();
|
|
241
|
+
|
|
242
|
+
this._game.createMap(this._scaledMapData);
|
|
243
|
+
|
|
244
|
+
for (const user of this._participants.getHumans()) {
|
|
245
|
+
if (user.isReady === false) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const socketId = user.socketId;
|
|
250
|
+
const team = user.team;
|
|
251
|
+
|
|
252
|
+
this._socketManager.sendClear(socketId, setIdList);
|
|
253
|
+
|
|
254
|
+
if (team === this._spectatorTeam) {
|
|
255
|
+
user.isWatching = true;
|
|
256
|
+
user.forceCameraReset = true;
|
|
257
|
+
this._socketManager.sendSpectatorDefaultShot(socketId);
|
|
258
|
+
} else {
|
|
259
|
+
this._setActivePlayer(user, getRespawnData(team));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this._socketManager.sendSoundCue(socketId, 'roundStart');
|
|
263
|
+
this._socketManager.sendGameInform(socketId, 'roundStart');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// размещение scripted-участников на карте
|
|
267
|
+
for (const participant of this._participants.getScripted()) {
|
|
268
|
+
this._setActivePlayer(participant, getRespawnData(participant.team));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// меняет ник игрока
|
|
273
|
+
changeName(gameId, name) {
|
|
274
|
+
const user = this._participants.get(gameId);
|
|
275
|
+
const oldName = user.name;
|
|
276
|
+
|
|
277
|
+
if (isValidName(name)) {
|
|
278
|
+
name = this._participants.checkName(name);
|
|
279
|
+
user.name = name;
|
|
280
|
+
this._game.changeName(gameId, name);
|
|
281
|
+
this._stat.updateUser(gameId, user.teamId, { name });
|
|
282
|
+
this._chat.pushSystem('NAME_CHANGED', [oldName, name]);
|
|
283
|
+
this._socketManager.sendName(user.socketId, name);
|
|
284
|
+
} else {
|
|
285
|
+
this._chat.pushSystemByUser(gameId, 'NAME_INVALID');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// меняет команду игрока
|
|
290
|
+
changeTeam(gameId, newTeam) {
|
|
291
|
+
const user = this._participants.get(gameId);
|
|
292
|
+
const currentTeam = user.team;
|
|
293
|
+
const respawns = this._scaledMapData.respawns;
|
|
294
|
+
|
|
295
|
+
// если игрок выбирает свою текущую команду
|
|
296
|
+
if (newTeam === currentTeam) {
|
|
297
|
+
this._chat.pushSystemByUser(gameId, 'TEAMS_YOUR_TEAM', [newTeam]);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// если новая команда не наблюдатель и нет свободных респаунов
|
|
302
|
+
if (
|
|
303
|
+
newTeam !== this._spectatorTeam &&
|
|
304
|
+
respawns[newTeam].length <= this._participants.getTeamSize(newTeam)
|
|
305
|
+
) {
|
|
306
|
+
// попытка удалить одного scripted-участника, чтобы освободить место
|
|
307
|
+
const slotFreed = this._scripted.removeOneForHuman(newTeam);
|
|
308
|
+
|
|
309
|
+
if (!slotFreed) {
|
|
310
|
+
this._chat.pushSystemByUser(gameId, 'TEAMS_TEAM_FULL', [
|
|
311
|
+
newTeam,
|
|
312
|
+
currentTeam,
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// на этом этапе смена команды доступна
|
|
320
|
+
this._participants.removeFromTeam(gameId, currentTeam);
|
|
321
|
+
this._participants.addToTeam(gameId, newTeam);
|
|
322
|
+
|
|
323
|
+
const oldTeamId = user.teamId;
|
|
324
|
+
const newTeamId = this._teams[newTeam];
|
|
325
|
+
|
|
326
|
+
user.team = newTeam;
|
|
327
|
+
user.teamId = newTeamId;
|
|
328
|
+
|
|
329
|
+
this._stat.moveUser(gameId, oldTeamId, newTeamId);
|
|
330
|
+
|
|
331
|
+
// если активных игроков меньше 2-х, рестарт раунда
|
|
332
|
+
if (
|
|
333
|
+
this._participants
|
|
334
|
+
.getHumans()
|
|
335
|
+
.filter(u => u.teamId !== this._spectatorId && u.gameId !== gameId)
|
|
336
|
+
.length < 2
|
|
337
|
+
) {
|
|
338
|
+
this._stat.reset();
|
|
339
|
+
this.initiateNewRound();
|
|
340
|
+
this._chat.pushSystemByUser(gameId, 'TEAMS_NEW_TEAM', [newTeam]);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// если переход из активного игрока в наблюдатели
|
|
345
|
+
if (newTeamId === this._spectatorId) {
|
|
346
|
+
this._setSpectatorFromActivePlayer(user);
|
|
347
|
+
this._chat.pushSystemByUser(gameId, 'TEAMS_NOW_SPECTATOR');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// сообщение о смене команды
|
|
352
|
+
this._chat.pushSystemByUser(gameId, 'TEAMS_NEW_TEAM', [newTeam]);
|
|
353
|
+
|
|
354
|
+
// если игра активным игроком невозможна в текущем раунде
|
|
355
|
+
if (!this._timerManager.canChangeTeamInCurrentRound()) {
|
|
356
|
+
this._stat.updateUser(gameId, newTeamId, {
|
|
357
|
+
status: 'dead',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// если пользователь был активным игроком,
|
|
361
|
+
// перевести его в наблюдатели до следующего раунда
|
|
362
|
+
if (oldTeamId !== this._spectatorId) {
|
|
363
|
+
this._setSpectatorFromActivePlayer(user);
|
|
364
|
+
}
|
|
365
|
+
// игра активным игроком возможна в текущем раунде
|
|
366
|
+
} else {
|
|
367
|
+
const respawnIndex = this._participants.getTeamSize(newTeam) - 1;
|
|
368
|
+
const respawnData = respawns[newTeam][respawnIndex];
|
|
369
|
+
|
|
370
|
+
// переход из наблюдателя в игрока
|
|
371
|
+
if (oldTeamId === this._spectatorId) {
|
|
372
|
+
this._setActivePlayer(user, respawnData);
|
|
373
|
+
// смена игровой команды
|
|
374
|
+
} else {
|
|
375
|
+
this._game.changePlayerData(gameId, {
|
|
376
|
+
respawnData,
|
|
377
|
+
teamId: newTeamId,
|
|
378
|
+
gameId,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// перевод активного игрока в наблюдатели
|
|
385
|
+
_setSpectatorFromActivePlayer(user) {
|
|
386
|
+
const gameId = user.gameId;
|
|
387
|
+
const model = user.model;
|
|
388
|
+
|
|
389
|
+
user.status = 'spectator';
|
|
390
|
+
user.isWatching = true;
|
|
391
|
+
user.watchedGameId = this._participants.getActiveList()[0] || null;
|
|
392
|
+
user.forceCameraReset = true;
|
|
393
|
+
|
|
394
|
+
this._participants.removeActive(gameId);
|
|
395
|
+
this._removedPlayersList.push({ gameId, model });
|
|
396
|
+
this._game.removePlayer(gameId);
|
|
397
|
+
this._socketManager.sendSpectatorDefaultShot(user.socketId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// перевод игрока в активные игроки
|
|
401
|
+
_setActivePlayer(user, respawnData) {
|
|
402
|
+
const gameId = user.gameId;
|
|
403
|
+
const teamId = user.teamId;
|
|
404
|
+
const name = user.name;
|
|
405
|
+
const model = user.model;
|
|
406
|
+
|
|
407
|
+
user.status = 'active';
|
|
408
|
+
user.isWatching = false;
|
|
409
|
+
user.watchedGameId = null;
|
|
410
|
+
user.forceCameraReset = true;
|
|
411
|
+
|
|
412
|
+
// если это реальный игрок (есть сокет)
|
|
413
|
+
if (user.isNetworked) {
|
|
414
|
+
this._socketManager.sendPlayerDefaultShot(user.socketId, gameId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
this._stat.updateUser(gameId, teamId, { status: '' });
|
|
418
|
+
this._game.createPlayer(gameId, model, name, teamId, respawnData);
|
|
419
|
+
this._participants.addActive(gameId);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// обрабатывает уничтожение игрока, обновляет статистику
|
|
423
|
+
reportKill(victimId, killerId = null) {
|
|
424
|
+
const victimUser = this._participants.get(victimId);
|
|
425
|
+
|
|
426
|
+
if (!victimUser) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
victimUser.status = 'dead';
|
|
431
|
+
victimUser.isWatching = true;
|
|
432
|
+
this._stat.updateUser(victimId, victimUser.teamId, {
|
|
433
|
+
deaths: 1,
|
|
434
|
+
status: 'dead',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// отмена всех запланированных обновлений панели
|
|
438
|
+
this._panel.invalidate(victimId);
|
|
439
|
+
|
|
440
|
+
if (victimUser.isNetworked) {
|
|
441
|
+
this._socketManager.sendSpectatorDefaultShot(victimUser.socketId);
|
|
442
|
+
this._socketManager.sendSoundCue(victimUser.socketId, 'death');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (killerId) {
|
|
446
|
+
const killerUser = this._participants.get(killerId);
|
|
447
|
+
|
|
448
|
+
// убийца мог покинуть игру до попадания (например, взрыв его бомбы) —
|
|
449
|
+
// фраг не начисляется, но раунд обязан корректно завершиться
|
|
450
|
+
if (!killerUser) {
|
|
451
|
+
this._checkTeamWipe(victimUser.teamId, null);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// если это не самоубийство
|
|
456
|
+
if (victimId !== killerId) {
|
|
457
|
+
// отслеживание противника
|
|
458
|
+
victimUser.watchedGameId = killerId;
|
|
459
|
+
victimUser.forceCameraReset = true;
|
|
460
|
+
|
|
461
|
+
// если кто-то наблюдал за victimId — переназначить на killerId
|
|
462
|
+
this._participants.replaceWatched(victimId, killerId);
|
|
463
|
+
|
|
464
|
+
// если это не убийство игрока своей команды
|
|
465
|
+
if (victimUser.teamId !== killerUser.teamId) {
|
|
466
|
+
this._stat.updateUser(killerId, killerUser.teamId, { score: 1 });
|
|
467
|
+
this._playerDataSync?.addRank(killerId, 1);
|
|
468
|
+
// иначе если это огонь по своим
|
|
469
|
+
} else {
|
|
470
|
+
this._stat.updateUser(killerId, killerUser.teamId, { score: -1 });
|
|
471
|
+
this._playerDataSync?.addRank(killerId, -1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (killerUser.isNetworked) {
|
|
475
|
+
this._socketManager.sendSoundCue(killerUser.socketId, 'frag');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this._chat.pushSystem('REPORT_KILL', [killerUser.name, victimUser.name]);
|
|
480
|
+
|
|
481
|
+
// проверка на уничтожение всей команды противника
|
|
482
|
+
this._checkTeamWipe(victimUser.teamId, killerUser.teamId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// проверяет уничтожение всей команды
|
|
487
|
+
_checkTeamWipe(victimTeamId, killerTeamId) {
|
|
488
|
+
// если раунд уже в процессе завершения
|
|
489
|
+
if (this._isRoundEnding) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let winnerTeam = null;
|
|
494
|
+
|
|
495
|
+
// если команда наблюдателей, проверка не требуется
|
|
496
|
+
if (victimTeamId === this._spectatorId) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// проверка на живых участников в команде (люди и scripted)
|
|
501
|
+
for (const participant of this._participants.getAll()) {
|
|
502
|
+
// если нашелся живой участник, команда не уничтожена
|
|
503
|
+
if (
|
|
504
|
+
participant.teamId === victimTeamId &&
|
|
505
|
+
this._game.isAlive(participant.gameId)
|
|
506
|
+
) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// активация флага завершения раунда
|
|
512
|
+
this._isRoundEnding = true;
|
|
513
|
+
|
|
514
|
+
// запись поражения команде
|
|
515
|
+
this._stat.updateHead(victimTeamId, 'deaths', 1);
|
|
516
|
+
|
|
517
|
+
// если убийца из другой команды, фраг для команды-победителя
|
|
518
|
+
if (killerTeamId && killerTeamId !== victimTeamId) {
|
|
519
|
+
this._stat.updateHead(killerTeamId, 'score', 1);
|
|
520
|
+
|
|
521
|
+
winnerTeam = Object.keys(this._teams).find(
|
|
522
|
+
key => this._teams[key] === killerTeamId,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// синхронизация накопленных rank/state на мастер по итогам раунда
|
|
527
|
+
// (Этап B4) — естественная граница жизненного цикла
|
|
528
|
+
this._playerDataSync?.flushAll();
|
|
529
|
+
|
|
530
|
+
if (winnerTeam) {
|
|
531
|
+
this._participants.getHumans().forEach(user => {
|
|
532
|
+
const socketId = user.socketId;
|
|
533
|
+
|
|
534
|
+
if (user.teamId === victimTeamId) {
|
|
535
|
+
this._socketManager.sendSoundCue(socketId, 'defeat');
|
|
536
|
+
} else {
|
|
537
|
+
this._socketManager.sendSoundCue(socketId, 'victory');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this._socketManager.sendRoundEnd(socketId, winnerTeam);
|
|
541
|
+
});
|
|
542
|
+
} else {
|
|
543
|
+
this._participants.getHumans().forEach(user => {
|
|
544
|
+
const socketId = user.socketId;
|
|
545
|
+
|
|
546
|
+
this._socketManager.sendSoundCue(socketId, 'defeat');
|
|
547
|
+
this._socketManager.sendRoundEnd(socketId);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this._timerManager.stopRoundTimer();
|
|
552
|
+
this._timerManager.startRoundRestartDelay(); // отложенный перезапуск раунда
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// возвращает список карт для голосования
|
|
556
|
+
_getMapList() {
|
|
557
|
+
if (this._mapList.length <= this._mapsInVote) {
|
|
558
|
+
return this._mapList;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let endNumber = this._startMapNumber + this._mapsInVote;
|
|
562
|
+
let maps = this._mapList.slice(this._startMapNumber, endNumber);
|
|
563
|
+
|
|
564
|
+
if (maps.length < this._mapsInVote) {
|
|
565
|
+
endNumber = this._mapsInVote - maps.length;
|
|
566
|
+
maps = maps.concat(this._mapList.slice(0, endNumber));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this._startMapNumber = endNumber;
|
|
570
|
+
|
|
571
|
+
return maps;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// отправляет голосование за новую карту
|
|
575
|
+
changeMap(gameId, mapName) {
|
|
576
|
+
const voteCategory = 'mapChange';
|
|
577
|
+
|
|
578
|
+
if (!this._voteCoordinator.canCreateVote(voteCategory, gameId)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// если есть gameId и карта (голосование создает пользователь)
|
|
583
|
+
if (typeof gameId !== 'undefined' && typeof mapName === 'string') {
|
|
584
|
+
const voteName = 'mapChangeByUser';
|
|
585
|
+
|
|
586
|
+
const userName = this._participants.get(gameId).name;
|
|
587
|
+
const userList = this._participants
|
|
588
|
+
.getHumans()
|
|
589
|
+
.map(u => u.gameId)
|
|
590
|
+
.filter(id => id !== gameId);
|
|
591
|
+
const payload = { name: voteName, params: [userName, mapName] };
|
|
592
|
+
|
|
593
|
+
this._voteCoordinator.createVote({
|
|
594
|
+
voteName,
|
|
595
|
+
voteCategory,
|
|
596
|
+
payload,
|
|
597
|
+
resultFunc: result => {
|
|
598
|
+
if (result === 'Yes' && this._maps[mapName]) {
|
|
599
|
+
this._chat.pushSystem('VOTE_PASSED');
|
|
600
|
+
this._chat.pushSystem('MAP_NEXT', [mapName]);
|
|
601
|
+
this._timerManager.startMapChangeDelay(() => {
|
|
602
|
+
this._currentMap = mapName;
|
|
603
|
+
this.createMap();
|
|
604
|
+
});
|
|
605
|
+
} else {
|
|
606
|
+
this._chat.pushSystem('VOTE_FAILED');
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
userList,
|
|
610
|
+
gameId,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// иначе голосование создает игра
|
|
614
|
+
} else {
|
|
615
|
+
const voteName = 'mapChangeBySystem';
|
|
616
|
+
|
|
617
|
+
const mapList = this._getMapList(); // список карт
|
|
618
|
+
|
|
619
|
+
const payload = { name: voteName, values: mapList };
|
|
620
|
+
|
|
621
|
+
this._voteCoordinator.createVote({
|
|
622
|
+
voteName,
|
|
623
|
+
voteCategory,
|
|
624
|
+
payload,
|
|
625
|
+
resultFunc: resultingMapName => {
|
|
626
|
+
if (resultingMapName && this._maps[resultingMapName]) {
|
|
627
|
+
this._chat.pushSystem('VOTE_PASSED');
|
|
628
|
+
this._chat.pushSystem('MAP_NEXT', [resultingMapName]);
|
|
629
|
+
this._timerManager.startMapChangeDelay(() => {
|
|
630
|
+
this._currentMap = resultingMapName;
|
|
631
|
+
this.createMap();
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
// если никто не проголосовал, продлеваем время текущей карты
|
|
635
|
+
this._timerManager.stopMapTimer();
|
|
636
|
+
this._timerManager.startMapTimer();
|
|
637
|
+
this._chat.pushSystem('VOTE_FAILED');
|
|
638
|
+
this._chat.pushSystem('MAP_CURRENT', [this._currentMap]);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export default RoundManager;
|