vimp-engine 0.40.0 → 0.41.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.
@@ -56,6 +56,43 @@ class Vote {
56
56
  this._userList[gameId].push(arr);
57
57
  }
58
58
 
59
+ // добавляет личное сообщение меню/подсказки (`menu`, `menuClose`,
60
+ // `menuHint`). Ещё не отправленные сообщения того же вида и имени
61
+ // вытесняются: очередь не растёт от частых обновлений, порядок
62
+ // «закрыть → открыть» сохраняется. Списки `teams`/`maps` не трогаются
63
+ pushPersonal(gameId, payload) {
64
+ const queue = this._userList[gameId];
65
+
66
+ if (!queue) {
67
+ return;
68
+ }
69
+
70
+ const key = personalKey(payload);
71
+ const name = personalName(payload);
72
+
73
+ this._userList[gameId] = queue.filter(item => {
74
+ const itemKey = personalKey(item);
75
+
76
+ return (
77
+ itemKey === null ||
78
+ personalName(item) !== name ||
79
+ personalGroup(itemKey) !== personalGroup(key)
80
+ );
81
+ });
82
+
83
+ this._userList[gameId].push(payload);
84
+ }
85
+
86
+ // убирает неотправленные личные сообщения: CLEAR уходит синхронно, а
87
+ // очередь — по сообщению на кадр, и меню прошлого раунда пришло бы после
88
+ dropPersonal(gameId) {
89
+ const queue = this._userList[gameId];
90
+
91
+ if (queue) {
92
+ this._userList[gameId] = queue.filter(item => personalKey(item) === null);
93
+ }
94
+ }
95
+
59
96
  // возвращает данные
60
97
  shift() {
61
98
  return this._list.shift();
@@ -178,6 +215,34 @@ class Vote {
178
215
  }
179
216
  }
180
217
 
218
+ const PERSONAL_KEYS = ['menu', 'menuClose', 'menuHint'];
219
+
220
+ // ключ личного сообщения (`menu`/`menuClose`/`menuHint`) или null — это
221
+ // список голосования (массив или шаблон)
222
+ function personalKey(item) {
223
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) {
224
+ return null;
225
+ }
226
+
227
+ return PERSONAL_KEYS.find(key => Object.hasOwn(item, key)) ?? null;
228
+ }
229
+
230
+ // имя меню, к которому относится личное сообщение
231
+ function personalName(item) {
232
+ const key = personalKey(item);
233
+
234
+ if (key === 'menu') {
235
+ return item.menu.name;
236
+ }
237
+
238
+ return key === 'menuHint' ? item.menuHint.name : item[key];
239
+ }
240
+
241
+ // `menu` и `menuClose` вытесняют друг друга, `menuHint` — только себя
242
+ function personalGroup(key) {
243
+ return key === 'menuHint' ? 'hint' : 'menu';
244
+ }
245
+
181
246
  // Сброс синглтона. Нужен только тем, кто крутит больше одного матча в
182
247
  // процессе — headless-runner (devtools/resetHostSingletons.js) и тесты;
183
248
  // в браузерной вкладке матч всегда один, поэтому прод его не зовёт.
@@ -24,6 +24,8 @@ class ParticipantManager {
24
24
  this._participants = new Map(); // gameId -> Participant
25
25
  this._teamSizes = {}; // team -> Set<gameId>
26
26
  this._activePlayersList = []; // gameId[]
27
+ // жив ли игрок в ядре; ядро появляется позже менеджера (setAliveCheck)
28
+ this._isAlive = () => true;
27
29
 
28
30
  this.resetTeamSizes();
29
31
  }
@@ -62,7 +64,7 @@ class ParticipantManager {
62
64
  team: this._joinTeam,
63
65
  teamId: this._joinTeamId,
64
66
  socketId,
65
- watchedGameId: this._activePlayersList[0] || null,
67
+ watchedGameId: this.firstWatchable(),
66
68
  token: params.token,
67
69
  identityName: params.name,
68
70
  });
@@ -113,7 +115,7 @@ class ParticipantManager {
113
115
  team,
114
116
  teamId,
115
117
  socketId,
116
- watchedGameId: this._activePlayersList[0] || null,
118
+ watchedGameId: this.firstWatchable(),
117
119
  token: record.token ?? null,
118
120
  identityName: record.identityName ?? null,
119
121
  });
@@ -335,11 +337,25 @@ class ParticipantManager {
335
337
  // перецепление наблюдателей на другого активного игрока
336
338
  for (const participant of this._participants.values()) {
337
339
  if (participant.watchedGameId === gameId) {
338
- participant.watchedGameId = this._activePlayersList[0] || null;
340
+ participant.watchedGameId = this.firstWatchable();
339
341
  }
340
342
  }
341
343
  }
342
344
 
345
+ setAliveCheck(isAlive) {
346
+ this._isAlive = isAlive;
347
+ }
348
+
349
+ // первая цель наблюдателя: живой активный игрок; живых нет — первый из
350
+ // списка (остов), списка нет — null
351
+ firstWatchable() {
352
+ return (
353
+ this._activePlayersList.find(id => this._isAlive(id)) ??
354
+ this._activePlayersList[0] ??
355
+ null
356
+ );
357
+ }
358
+
343
359
  clearActive() {
344
360
  this._activePlayersList = [];
345
361
  }
@@ -55,6 +55,23 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
55
55
  // хуки `hooks.voiceSounds`/`hooks.voiceFilter`); опциональна — игра не
56
56
  // пишет её в `requires`
57
57
  { value: 'voice', since: '0.39.0' },
58
+ // личное меню игрока в окне `#vote` и подсказка под значком рации:
59
+ // события ядра menu/menuClose/menuHint, опкод `menu.answer`
60
+ { value: 'menu.personal', since: '0.41.0' },
61
+ // опкод `actor.forget`: участник покинул матч (в том числе наблюдатель);
62
+ // без него id ушедшего наследует новичок вместе с профилем игры
63
+ { value: 'actor.forget', since: '0.41.0' },
64
+ // опкод `match.reset`: новая игра (движок обнулил счёт); игра, чей баланс
65
+ // сбрасывается вместе со счётом, объявляет её в `requires`
66
+ { value: 'match.reset', since: '0.41.0' },
67
+ // опкоды `state.persist`/`state.restore`: состояние игры между раундами
68
+ // переживает мягкую миграцию хоста и эстафету Worker'а (мета `persist`)
69
+ { value: 'state.persist', since: '0.41.0' },
70
+ // поле конфига игры `voteHoldTime`: удержание голосований в начале раунда
71
+ { value: 'vote.hold', since: '0.41.0' },
72
+ // свойства ячейки панели (modules.panel.fields[]): label, labelFrom,
73
+ // hidden, format, prefix, suffix, emptyWhen, flashOnChange, countdown
74
+ { value: 'panel.cells', since: '0.41.0' },
58
75
  ]);
59
76
 
60
77
  // плоский список имён — то, чем правило контракта B2 проверяет `requires`
@@ -68,6 +68,13 @@ const FIELDS = {
68
68
 
69
69
  spectatorTeam: { derive: deriveSpectatorTeam },
70
70
 
71
+ // мс от старта раунда, в которые голосования удерживаются: запущенное
72
+ // игроком для других отклоняется, запущенное игрой откладывается; 0 — без
73
+ // удержания. Поле верхнего уровня, а не `timers.*`: applyRoomOverrides
74
+ // сливает конфиг поверхностно, и игра, задавшая `timers`, потеряла бы
75
+ // остальные таймеры движка
76
+ voteHoldTime: { default: 0 },
77
+
71
78
  // смена хоста посреди раунда (host-migration этап 5): true — игра
72
79
  // гарантирует, что GameSim::serialize/deserialize и хуки модулей
73
80
  // serializeState/restoreState переносят всё её состояние. Без поля —