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,294 @@
|
|
|
1
|
+
// Singleton Stat
|
|
2
|
+
|
|
3
|
+
let stat;
|
|
4
|
+
|
|
5
|
+
class Stat {
|
|
6
|
+
constructor(config, teams) {
|
|
7
|
+
if (stat) {
|
|
8
|
+
return stat;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
stat = this;
|
|
12
|
+
|
|
13
|
+
this._config = config;
|
|
14
|
+
|
|
15
|
+
this._head = {};
|
|
16
|
+
this._body = {};
|
|
17
|
+
|
|
18
|
+
this._lastHead = [];
|
|
19
|
+
this._lastBody = [];
|
|
20
|
+
|
|
21
|
+
for (const p in teams) {
|
|
22
|
+
if (Object.hasOwn(teams, p)) {
|
|
23
|
+
this._head[teams[p]] = [teams[p], []];
|
|
24
|
+
this._body[teams[p]] = {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// сбрасывает статистику
|
|
30
|
+
reset() {
|
|
31
|
+
for (const teamId in this._body) {
|
|
32
|
+
if (Object.hasOwn(this._body, teamId)) {
|
|
33
|
+
const bodyStats = this._body[teamId];
|
|
34
|
+
|
|
35
|
+
for (const gameId in bodyStats) {
|
|
36
|
+
if (Object.hasOwn(bodyStats, gameId)) {
|
|
37
|
+
const stat = bodyStats[gameId];
|
|
38
|
+
stat[2] = this._getDefaultBody(stat[2]);
|
|
39
|
+
this._lastBody.push(stat);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const teamId in this._head) {
|
|
46
|
+
if (Object.hasOwn(this._head, teamId)) {
|
|
47
|
+
const stat = this._head[teamId];
|
|
48
|
+
|
|
49
|
+
for (const p in this._config) {
|
|
50
|
+
if (Object.hasOwn(this._config, p)) {
|
|
51
|
+
const conf = this._config[p];
|
|
52
|
+
|
|
53
|
+
if (conf.headSync) {
|
|
54
|
+
this._updateHeadSync(teamId, p);
|
|
55
|
+
} else if (typeof conf.headValue !== 'undefined') {
|
|
56
|
+
stat[1][conf.key] = conf.headValue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this._lastHead.push(stat);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// возвращает дефолтные данные для body
|
|
67
|
+
_getDefaultBody(stat = []) {
|
|
68
|
+
for (const p in this._config) {
|
|
69
|
+
if (Object.hasOwn(this._config, p)) {
|
|
70
|
+
const conf = this._config[p];
|
|
71
|
+
|
|
72
|
+
if (typeof conf.bodyValue !== 'undefined') {
|
|
73
|
+
stat[conf.key] = conf.bodyValue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return stat;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// добавляет пользователя
|
|
82
|
+
addUser(gameId, teamId, data) {
|
|
83
|
+
// добавление данных по умолчанию из конфига
|
|
84
|
+
this._body[teamId][gameId] = [gameId, teamId, this._getDefaultBody()];
|
|
85
|
+
|
|
86
|
+
// обновление данных игрока (имя, статус...)
|
|
87
|
+
this.updateUser(gameId, teamId, data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// удаляет пользователя и возвращает его данные
|
|
91
|
+
removeUser(gameId, teamId) {
|
|
92
|
+
let data = this._body[teamId][gameId];
|
|
93
|
+
const stat = {};
|
|
94
|
+
|
|
95
|
+
delete this._body[teamId][gameId];
|
|
96
|
+
this._lastBody.push([data[0], data[1], null]);
|
|
97
|
+
|
|
98
|
+
data = data[2];
|
|
99
|
+
|
|
100
|
+
// преобразование данных пользователя из массива в объект и
|
|
101
|
+
// обновление head
|
|
102
|
+
for (const p in this._config) {
|
|
103
|
+
if (Object.hasOwn(this._config, p)) {
|
|
104
|
+
const conf = this._config[p];
|
|
105
|
+
const value = data[conf.key];
|
|
106
|
+
|
|
107
|
+
stat[p] = value;
|
|
108
|
+
|
|
109
|
+
if (conf.headSync === true) {
|
|
110
|
+
this._updateHeadSync(teamId, p, true);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return stat;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// перемещает пользователя в новую команду
|
|
119
|
+
moveUser(gameId, teamId, newTeamId, data = {}) {
|
|
120
|
+
const userData = this.removeUser(gameId, teamId);
|
|
121
|
+
|
|
122
|
+
this.addUser(gameId, newTeamId, { ...userData, ...data });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// обновляет статистику пользователя
|
|
126
|
+
updateUser(gameId, teamId, data) {
|
|
127
|
+
const stat = this._body[teamId][gameId];
|
|
128
|
+
|
|
129
|
+
for (const p in data) {
|
|
130
|
+
if (Object.hasOwn(data, p)) {
|
|
131
|
+
const conf = this._config[p];
|
|
132
|
+
|
|
133
|
+
// колонки объявляет схема игры (Д7): движковые записи
|
|
134
|
+
// (status/score/deaths/latency) в необъявленные колонки игнорируются
|
|
135
|
+
if (!conf) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const method = conf.bodyMethod;
|
|
140
|
+
const value = data[p];
|
|
141
|
+
|
|
142
|
+
// если метод 'замена'
|
|
143
|
+
if (method === '=') {
|
|
144
|
+
stat[2][conf.key] = value;
|
|
145
|
+
|
|
146
|
+
// иначе если метод 'добавление'
|
|
147
|
+
} else if (method === '+') {
|
|
148
|
+
stat[2][conf.key] += value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (conf.headSync === true) {
|
|
152
|
+
this._updateHeadSync(teamId, p, true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this._lastBody.push(stat);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// обновляет статистику head
|
|
161
|
+
updateHead(teamId, param, value) {
|
|
162
|
+
const stat = this._head[teamId];
|
|
163
|
+
const conf = this._config[param];
|
|
164
|
+
|
|
165
|
+
// необъявленная схемой колонка — запись игнорируется (Д7)
|
|
166
|
+
if (!conf) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const method = conf.headMethod;
|
|
171
|
+
const key = conf.key;
|
|
172
|
+
|
|
173
|
+
// если метод 'добавление'
|
|
174
|
+
if (method === '+') {
|
|
175
|
+
stat[1][key] += value;
|
|
176
|
+
|
|
177
|
+
// если метод 'замена'
|
|
178
|
+
} else if (method === '=') {
|
|
179
|
+
stat[1][key] = value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this._lastHead.push(stat);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// обновляет статистику head синхронизированную с body
|
|
186
|
+
_updateHeadSync(teamId, param, save) {
|
|
187
|
+
const stat = this._head[teamId];
|
|
188
|
+
const bodyStats = this._body[teamId];
|
|
189
|
+
const conf = this._config[param];
|
|
190
|
+
const method = conf.headMethod;
|
|
191
|
+
const key = conf.key;
|
|
192
|
+
let value;
|
|
193
|
+
|
|
194
|
+
// если метод 'количество'
|
|
195
|
+
if (method === '#') {
|
|
196
|
+
value = Object.keys(bodyStats).length;
|
|
197
|
+
|
|
198
|
+
// иначе если метод 'добавление'
|
|
199
|
+
} else if (method === '+') {
|
|
200
|
+
value = 0;
|
|
201
|
+
|
|
202
|
+
for (const p in bodyStats) {
|
|
203
|
+
if (Object.hasOwn(bodyStats, p)) {
|
|
204
|
+
value += bodyStats[p][2][key];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
stat[1][key] = value;
|
|
210
|
+
|
|
211
|
+
if (save === true) {
|
|
212
|
+
this._lastHead.push(stat);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// снимок статистики для эстафеты Worker'ов (Этап 5.2)
|
|
217
|
+
serialize() {
|
|
218
|
+
return { head: this._head, body: this._body };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// восстанавливает статистику из снимка эстафеты; строки участников вне
|
|
222
|
+
// keepIds (не пережившие перенос) удаляются штатно — клиенты получат
|
|
223
|
+
// обновление на первом же кадре
|
|
224
|
+
restore({ head, body }, keepIds = null) {
|
|
225
|
+
this._head = head;
|
|
226
|
+
this._body = body;
|
|
227
|
+
|
|
228
|
+
if (keepIds) {
|
|
229
|
+
for (const teamId in this._body) {
|
|
230
|
+
if (Object.hasOwn(this._body, teamId)) {
|
|
231
|
+
for (const gameId in this._body[teamId]) {
|
|
232
|
+
if (
|
|
233
|
+
Object.hasOwn(this._body[teamId], gameId) &&
|
|
234
|
+
!keepIds.has(gameId)
|
|
235
|
+
) {
|
|
236
|
+
this.removeUser(gameId, teamId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// возвращает последние изменения
|
|
245
|
+
getLast() {
|
|
246
|
+
let stat = [this._lastBody, this._lastHead];
|
|
247
|
+
|
|
248
|
+
this._lastBody = [];
|
|
249
|
+
this._lastHead = [];
|
|
250
|
+
|
|
251
|
+
if (!stat[0].length && !stat[1].length) {
|
|
252
|
+
stat = 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return stat;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// возвращает полную статистику
|
|
259
|
+
getFull() {
|
|
260
|
+
let stat = [];
|
|
261
|
+
|
|
262
|
+
stat[0] = [];
|
|
263
|
+
stat[1] = [];
|
|
264
|
+
|
|
265
|
+
for (const teamId in this._body) {
|
|
266
|
+
if (Object.hasOwn(this._body, teamId)) {
|
|
267
|
+
const bodyStats = this._body[teamId];
|
|
268
|
+
|
|
269
|
+
for (const gameId in bodyStats) {
|
|
270
|
+
if (Object.hasOwn(bodyStats, gameId)) {
|
|
271
|
+
stat[0].push(bodyStats[gameId]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const teamId in this._head) {
|
|
278
|
+
if (Object.hasOwn(this._head, teamId)) {
|
|
279
|
+
stat[1].push(this._head[teamId]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!stat[0].length && !stat[1].length) {
|
|
284
|
+
stat = 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// true указывает, что обновление статистики полное
|
|
288
|
+
stat[2] = true;
|
|
289
|
+
|
|
290
|
+
return stat;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default Stat;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import AbstractTimer from '../../../lib/AbstractTimer.js';
|
|
2
|
+
|
|
3
|
+
// Singleton TimerManager
|
|
4
|
+
|
|
5
|
+
let timerManager;
|
|
6
|
+
|
|
7
|
+
class TimerManager extends AbstractTimer {
|
|
8
|
+
constructor(timers, callbacks) {
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
if (timerManager) {
|
|
12
|
+
return timerManager;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
timerManager = this;
|
|
16
|
+
|
|
17
|
+
this._mapTime = timers.mapTime;
|
|
18
|
+
this._roundTime = timers.roundTime;
|
|
19
|
+
this._timeStep = timers.timeStep;
|
|
20
|
+
this._voteTime = timers.voteTime;
|
|
21
|
+
this._timeBlockedVote = timers.timeBlockedVote;
|
|
22
|
+
this._teamChangeGracePeriod = timers.teamChangeGracePeriod;
|
|
23
|
+
this._roundRestartDelay = timers.roundRestartDelay;
|
|
24
|
+
this._mapChangeDelay = timers.mapChangeDelay;
|
|
25
|
+
this._idleCheckInterval = timers.idleCheckInterval;
|
|
26
|
+
this._rttPingInterval = timers.rttPingInterval;
|
|
27
|
+
|
|
28
|
+
this._callbacks = callbacks;
|
|
29
|
+
|
|
30
|
+
// временные метки для расчетов оставшегося времени
|
|
31
|
+
this._startMapTime = 0;
|
|
32
|
+
this._startRoundTime = 0;
|
|
33
|
+
|
|
34
|
+
// фактическая длительность текущего таймера карты (после эстафеты
|
|
35
|
+
// Worker'ов карта продолжается с остатком времени, а не заново)
|
|
36
|
+
this._currentMapDuration = this._mapTime;
|
|
37
|
+
|
|
38
|
+
// переменные для самокорректирующегося игрового цикла
|
|
39
|
+
this._lastShotTime = 0;
|
|
40
|
+
this._expectedTickTime = 0;
|
|
41
|
+
|
|
42
|
+
// максимально допустимая дельта времени в секундах,
|
|
43
|
+
// для предотвращения "прыжков" в симуляции после долгих пауз
|
|
44
|
+
this._maxDeltaTime = 0.1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// запускает все основные игровые таймеры (карта, игровой цикл, раунд)
|
|
48
|
+
startGameTimers() {
|
|
49
|
+
this.startMapTimer();
|
|
50
|
+
this._startGameLoop();
|
|
51
|
+
this.startRoundTimer();
|
|
52
|
+
this._startRttPingTimer();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// останавливает все основные игровые таймеры
|
|
56
|
+
stopGameTimers() {
|
|
57
|
+
this._stopGameLoop();
|
|
58
|
+
this.stopRoundTimer();
|
|
59
|
+
this.stopMapTimer();
|
|
60
|
+
this._stopRoundRestartDelay();
|
|
61
|
+
this._stopMapChangeDelay();
|
|
62
|
+
this._stopRttPingTimer();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// запускает таймер до конца текущей карты (duration — остаток времени
|
|
66
|
+
// при возобновлении после эстафеты Worker'ов, Этап 5.2)
|
|
67
|
+
startMapTimer(duration = this._mapTime) {
|
|
68
|
+
this.stopMapTimer();
|
|
69
|
+
this._startMapTime = Date.now();
|
|
70
|
+
this._currentMapDuration = duration;
|
|
71
|
+
this._startTimer('map', this._callbacks.onMapTimeEnd, duration);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// останавливает таймер карты
|
|
75
|
+
stopMapTimer() {
|
|
76
|
+
this._stopTimer('map');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// возвращает оставшееся время до конца карты в миллисекундах
|
|
80
|
+
getMapTimeLeft() {
|
|
81
|
+
const timeLeft =
|
|
82
|
+
this._currentMapDuration - (Date.now() - this._startMapTime);
|
|
83
|
+
|
|
84
|
+
return Math.max(0, timeLeft);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// возобновляет игровые таймеры после эстафеты Worker'ов (Этап 5.2):
|
|
88
|
+
// карта — с остатком времени, раунд стартует отдельно (initiateNewRound)
|
|
89
|
+
resumeGameTimers(mapTimeLeft) {
|
|
90
|
+
this.startMapTimer(mapTimeLeft ?? this._mapTime);
|
|
91
|
+
this._startGameLoop();
|
|
92
|
+
this._startRttPingTimer();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// запускает таймер до конца текущего раунда
|
|
96
|
+
startRoundTimer() {
|
|
97
|
+
this.stopRoundTimer();
|
|
98
|
+
this._startRoundTime = Date.now();
|
|
99
|
+
this._startTimer('round', this._callbacks.onRoundTimeEnd, this._roundTime);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// останавливает таймер раунда
|
|
103
|
+
stopRoundTimer() {
|
|
104
|
+
this._stopTimer('round');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// возвращает оставшееся время до конца раунда в секундах
|
|
108
|
+
getRoundTimeLeft() {
|
|
109
|
+
const timeLeft = this._roundTime - (Date.now() - this._startRoundTime);
|
|
110
|
+
|
|
111
|
+
return Math.max(0, Math.floor(timeLeft / 1000));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// проверяет возможно сменить команду игроку в текущем раунде
|
|
115
|
+
canChangeTeamInCurrentRound() {
|
|
116
|
+
const roundTime = Date.now() - this._startRoundTime;
|
|
117
|
+
|
|
118
|
+
return roundTime <= this._teamChangeGracePeriod;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// логика одного "тика" игрового цикла
|
|
122
|
+
_loopTick() {
|
|
123
|
+
const now = performance.now();
|
|
124
|
+
let dt = (now - this._lastShotTime) / 1000;
|
|
125
|
+
this._lastShotTime = now;
|
|
126
|
+
|
|
127
|
+
// если dt аномально большой (система "спала"), ограничить его
|
|
128
|
+
// и сбросить ожидаемое время, чтобы цикл не пытался "наверстать".
|
|
129
|
+
if (dt > this._maxDeltaTime) {
|
|
130
|
+
dt = this._maxDeltaTime;
|
|
131
|
+
this._expectedTickTime = now; // сброс базы для расчета дрейфа
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._callbacks.onShotTick(dt);
|
|
135
|
+
|
|
136
|
+
const drift = now - this._expectedTickTime;
|
|
137
|
+
const nextTimeout = Math.max(0, this._timeStep - drift);
|
|
138
|
+
this._expectedTickTime += this._timeStep;
|
|
139
|
+
|
|
140
|
+
// регистрация следующего шага цикла через _startTimer,
|
|
141
|
+
// чтобы его можно было остановить по ключу 'gameLoop'
|
|
142
|
+
this._startTimer('gameLoop', () => this._loopTick(), nextTimeout);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// инициализирует и запускает игровой цикл (обновление кадров)
|
|
146
|
+
_startGameLoop() {
|
|
147
|
+
this._stopGameLoop();
|
|
148
|
+
|
|
149
|
+
this._lastShotTime = performance.now();
|
|
150
|
+
this._expectedTickTime = this._lastShotTime + this._timeStep;
|
|
151
|
+
|
|
152
|
+
// запуск первого таймаута, который инициирует цикл
|
|
153
|
+
this._startTimer('gameLoop', () => this._loopTick(), this._timeStep);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// останавливает игровой цикл
|
|
157
|
+
_stopGameLoop() {
|
|
158
|
+
this._stopTimer('gameLoop');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// запускает таймер голосования
|
|
162
|
+
startVoteTimer(voteName, onEndCallback) {
|
|
163
|
+
this._startTimer(`vote:${voteName}`, onEndCallback, this._voteTime);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// останавливает все активные таймеры голосования
|
|
167
|
+
stopAllVoteTimers() {
|
|
168
|
+
for (const key of this._timers.keys()) {
|
|
169
|
+
if (key.startsWith('vote:')) {
|
|
170
|
+
this._stopTimer(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// запускает таймер, блокирующий возможность инициировать
|
|
176
|
+
// новое голосование в заданной категории
|
|
177
|
+
startVoteBlockTimer(voteCategory, onEndCallback) {
|
|
178
|
+
this._startTimer(
|
|
179
|
+
`voteBlock:${voteCategory}`,
|
|
180
|
+
onEndCallback,
|
|
181
|
+
this._timeBlockedVote,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// останавливает все таймеры блокировки
|
|
186
|
+
stopAllBlockedVoteTimers() {
|
|
187
|
+
for (const key of this._timers.keys()) {
|
|
188
|
+
if (key.startsWith('voteBlock:')) {
|
|
189
|
+
this._stopTimer(key);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// проверяет наличие блокирующего таймера
|
|
195
|
+
isVoteBlocked(voteCategory) {
|
|
196
|
+
return this._hasTimer(`voteBlock:${voteCategory}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// запускает отложенный перезапуск раунда
|
|
200
|
+
startRoundRestartDelay() {
|
|
201
|
+
this._startTimer(
|
|
202
|
+
'roundRestartDelay',
|
|
203
|
+
this._callbacks.onRoundTimeEnd,
|
|
204
|
+
this._roundRestartDelay,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// останавливает отложенный перезапуск раунда
|
|
209
|
+
_stopRoundRestartDelay() {
|
|
210
|
+
this._stopTimer('roundRestartDelay');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// запускает отложенную смену карты (после голосования)
|
|
214
|
+
startMapChangeDelay(onEndCallback) {
|
|
215
|
+
this._startTimer('mapChangeDelay', onEndCallback, this._mapChangeDelay);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// останавливает отложенную смену карты
|
|
219
|
+
_stopMapChangeDelay() {
|
|
220
|
+
this._stopTimer('mapChangeDelay');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// логика одного "тика" для проверки на бездействие
|
|
224
|
+
_idleCheckTick() {
|
|
225
|
+
this._callbacks.onIdleCheck();
|
|
226
|
+
|
|
227
|
+
// перезапуск таймера для следующей проверки
|
|
228
|
+
this._startTimer(
|
|
229
|
+
'idleCheck',
|
|
230
|
+
() => this._idleCheckTick(),
|
|
231
|
+
this._idleCheckInterval,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// запускает периодическую проверку на бездействие
|
|
236
|
+
startIdleCheckTimer() {
|
|
237
|
+
// если есть интервал и callback
|
|
238
|
+
if (this._idleCheckInterval && this._callbacks.onIdleCheck) {
|
|
239
|
+
this.stopIdleCheckTimer();
|
|
240
|
+
this._idleCheckTick();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// останавливает проверку на бездействие (публичный: пауза эстафеты
|
|
245
|
+
// Worker'ов не должна кикать за бездействие)
|
|
246
|
+
stopIdleCheckTimer() {
|
|
247
|
+
this._stopTimer('idleCheck');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// логика одного "тика" для отправки пингов
|
|
251
|
+
_rttPingTick() {
|
|
252
|
+
this._callbacks.onSendPing();
|
|
253
|
+
|
|
254
|
+
// перезапуск таймера для следующего пинга
|
|
255
|
+
this._startTimer(
|
|
256
|
+
'rttPing',
|
|
257
|
+
() => this._rttPingTick(),
|
|
258
|
+
this._rttPingInterval,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// запускает отправку пингов
|
|
263
|
+
_startRttPingTimer() {
|
|
264
|
+
// если есть интервал и callback
|
|
265
|
+
if (this._rttPingInterval && this._callbacks.onSendPing) {
|
|
266
|
+
this._stopRttPingTimer();
|
|
267
|
+
this._rttPingTick();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// останавливает отправку пингов
|
|
272
|
+
_stopRttPingTimer() {
|
|
273
|
+
this._stopTimer('rttPing');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default TimerManager;
|