vimp-engine 0.38.2 → 0.39.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.
Files changed (99) hide show
  1. package/core/Cargo.toml +1 -1
  2. package/package.json +3 -1
  3. package/src/client/InputListener.js +3 -0
  4. package/src/client/SoundManager.js +13 -0
  5. package/src/client/boot.js +30 -7
  6. package/src/client/components/controller/ClientReports.js +1 -1
  7. package/src/client/components/controller/Controls.js +10 -0
  8. package/src/client/components/controller/Games.js +1 -1
  9. package/src/client/components/controller/Voice.js +31 -0
  10. package/src/client/components/model/ClientReports.js +1 -1
  11. package/src/client/components/model/Controls.js +64 -0
  12. package/src/client/components/model/Games.js +24 -0
  13. package/src/client/components/model/Voice.js +96 -0
  14. package/src/client/components/model/Vote.js +2 -1
  15. package/src/client/components/view/ClientReports.js +1 -1
  16. package/src/client/components/view/Games.js +19 -1
  17. package/src/client/components/view/Lobby.js +1 -7
  18. package/src/client/components/view/Voice.js +172 -0
  19. package/src/client/debug.js +1 -1
  20. package/src/client/lib/authApi.js +1 -1
  21. package/src/client/lib/diagnostics.js +19 -4
  22. package/src/client/lib/engineVersion.js +0 -6
  23. package/src/client/lib/footerLink.js +15 -14
  24. package/src/client/lib/retryOnNetworkError.js +60 -0
  25. package/src/client/lib/voiceState.js +78 -0
  26. package/src/client/main.js +152 -18
  27. package/src/client/network/HiddenHostHealthLog.js +40 -3
  28. package/src/client/network/HostConnectionManager.js +46 -5
  29. package/src/client/network/HostController.js +47 -4
  30. package/src/client/network/HostHealthPolicy.js +41 -0
  31. package/src/client/network/InlineHostBridge.js +8 -1
  32. package/src/client/network/LoopbackTransport.js +14 -0
  33. package/src/client/network/SessionSupervisor.js +24 -0
  34. package/src/client/network/WebRtcManager.js +45 -3
  35. package/src/client/session/HandoffFlow.js +5 -0
  36. package/src/client/session/HostRole.js +5 -2
  37. package/src/client/style.css +151 -9
  38. package/src/client/views/gameShell.js +3 -3
  39. package/src/client/views/includes/auth.pug +4 -4
  40. package/src/client/views/includes/lobby.pug +4 -4
  41. package/src/client/views/includes/reports.pug +1 -1
  42. package/src/client/voice/JitterBuffer.js +137 -0
  43. package/src/client/voice/MicCapture.js +307 -0
  44. package/src/client/voice/PttMachine.js +369 -0
  45. package/src/client/voice/VoiceClient.js +336 -0
  46. package/src/client/voice/VoiceCues.js +295 -0
  47. package/src/client/voice/VoiceEncoder.js +174 -0
  48. package/src/client/voice/VoicePlayback.js +558 -0
  49. package/src/client/voice/audioData.js +35 -0
  50. package/src/client/voice/captureProcessor.worklet.js +136 -0
  51. package/src/client/voice/opusConfig.js +49 -0
  52. package/src/client/voice/voiceOutput.js +72 -0
  53. package/src/client/voice/voiceSupport.js +71 -0
  54. package/src/client/voice/voiceTones.js +90 -0
  55. package/src/config/authClient.js +3 -3
  56. package/src/config/clientServices.js +2 -0
  57. package/src/config/lobby.js +4 -5
  58. package/src/config/opcodes.js +2 -1
  59. package/src/config/voiceDefaults.js +44 -0
  60. package/src/config/voiceProtocol.js +80 -0
  61. package/src/config/voteKeys.js +4 -0
  62. package/src/config/wsports.js +12 -0
  63. package/src/devtools/contract/loadContext.js +2 -2
  64. package/src/devtools/contract/rules/a7-package-repository.js +7 -8
  65. package/src/devtools/contract/rules/b11-voice-config.js +34 -0
  66. package/src/devtools/contract/rules/c12-voice-hotkey.js +70 -0
  67. package/src/devtools/contract/rules/index.js +5 -1
  68. package/src/devtools/invariants.js +1 -1
  69. package/src/host/DebugRecorder.js +1 -1
  70. package/src/host/HostGame.js +93 -15
  71. package/src/host/PortMachine.js +19 -3
  72. package/src/host/commandFailure.js +140 -0
  73. package/src/host/host.worker.js +15 -2
  74. package/src/host/meta/SocketManager.js +8 -2
  75. package/src/host/meta/core/CommandProcessor.js +37 -7
  76. package/src/host/meta/modules/voice/Voice.js +430 -0
  77. package/src/host/meta/modules/voice/channels.js +58 -0
  78. package/src/host/meta/modules/voice/index.js +1 -0
  79. package/src/lib/VoiceRelay.js +200 -0
  80. package/src/lib/applyRoomOverrides.js +1 -1
  81. package/src/lib/buildClientConfig.js +7 -0
  82. package/src/lib/capabilities.js +5 -1
  83. package/src/lib/checkpointCodec.js +1 -2
  84. package/src/lib/coreConfig.js +1 -1
  85. package/src/lib/createHostRuntime.js +9 -1
  86. package/src/lib/gameConfigView.js +11 -1
  87. package/src/lib/gameId.js +1 -1
  88. package/src/lib/gamePlugin.js +6 -2
  89. package/src/lib/jwt.js +1 -1
  90. package/src/lib/packageLink.js +18 -61
  91. package/src/lib/validators.js +3 -3
  92. package/src/lib/voiceConfig.js +136 -0
  93. package/src/lib/voiceMessages.js +172 -0
  94. package/src/lib/voicePacket.js +142 -0
  95. package/src/standalone/index.js +1 -1
  96. package/tests/fixtures/generations/gen-api3/README.md +2 -2
  97. package/tests/fixtures/generations/gen-api4/README.md +2 -2
  98. package/src/devtools/surface/abiParse.js +0 -175
  99. package/src/devtools/surface/collect.js +0 -413
@@ -56,6 +56,7 @@ export default class HostHealthPolicy {
56
56
  this._successor = false;
57
57
  this._hiddenSince = null;
58
58
  this._hiddenTimer = null;
59
+ this._hiddenOutcome = null;
59
60
  this._lastAutoAt = null;
60
61
  this._cooldownBefore = null;
61
62
  this._samples = [];
@@ -80,6 +81,12 @@ export default class HostHealthPolicy {
80
81
  this._successor = false;
81
82
  this._resetSamples();
82
83
 
84
+ // исход прошлой роли уже прочитан её журналом (flush при потере роли);
85
+ // новой роли он не принадлежит
86
+ if (isHost) {
87
+ this._hiddenOutcome = null;
88
+ }
89
+
83
90
  // роль получена скрытой вкладкой: скрытие отсчитывается от получения
84
91
  if (isHost && this._hiddenSince !== null) {
85
92
  this._hiddenSince = this._hostSince;
@@ -98,6 +105,7 @@ export default class HostHealthPolicy {
98
105
  setHidden(hidden) {
99
106
  if (hidden && this._hiddenSince === null) {
100
107
  this._hiddenSince = this._now();
108
+ this._hiddenOutcome = null;
101
109
  this._armHiddenTimer();
102
110
  } else if (!hidden) {
103
111
  this._hiddenSince = null;
@@ -105,6 +113,19 @@ export default class HostHealthPolicy {
105
113
  }
106
114
  }
107
115
 
116
+ /**
117
+ * Исход передачи для последнего эпизода скрытия (живёт до следующего
118
+ * скрытия или новой роли хоста, переживает возврат видимости и потерю
119
+ * роли — его читает журнал закрываемого эпизода): null — порог
120
+ * hiddenHandoffMs не достигнут; 'started' — передача началась;
121
+ * 'disabled' | 'no-successor' | 'blocked' (срок роли или кулдаун) —
122
+ * ожидаемый отказ; 'unexplained' — всё разрешено, а передача не началась.
123
+ * @returns {string|null}
124
+ */
125
+ hiddenHandoff() {
126
+ return this._hiddenOutcome;
127
+ }
128
+
108
129
  // сэмпл здоровья Worker'а
109
130
  addHealth(health) {
110
131
  if (this._hostSince === null || !health) {
@@ -182,6 +203,8 @@ export default class HostHealthPolicy {
182
203
  }
183
204
 
184
205
  if (this._handOverNow('hidden')) {
206
+ this._hiddenOutcome = 'started';
207
+
185
208
  return true;
186
209
  }
187
210
 
@@ -191,9 +214,27 @@ export default class HostHealthPolicy {
191
214
  this._armHiddenTimer(waitMs);
192
215
  }
193
216
 
217
+ // начатая передача исход не меняет: следующие проверки её уже не стартуют
218
+ if (this._hiddenOutcome !== 'started') {
219
+ this._hiddenOutcome = this._refusal(waitMs);
220
+ }
221
+
194
222
  return false;
195
223
  }
196
224
 
225
+ // почему передача со скрытой вкладки не началась
226
+ _refusal(waitMs) {
227
+ if (!this._enabled()) {
228
+ return 'disabled';
229
+ }
230
+
231
+ if (!this._successor) {
232
+ return 'no-successor';
233
+ }
234
+
235
+ return waitMs > 0 ? 'blocked' : 'unexplained';
236
+ }
237
+
197
238
  // немедленная передача: ждущую границы — ускорить, иначе начать
198
239
  _handOverNow(reason) {
199
240
  if (!this._enabled() || !this._successor) {
@@ -11,7 +11,7 @@ import { createGuestIdentity } from '../../host/identity.js';
11
11
  // Почему inline, а не Worker: HostPlugin принципиально непередаваем в Worker
12
12
  // (postMessage не несёт функции), а standalone SDK получает от репозитория
13
13
  // игры именно живой объект плагина. Прод-путь (хост в Worker'е) не меняется —
14
- // расхождение dev/prod осознанное, см. plan/standalone-sdk/README.md.
14
+ // расхождение dev/prod осознанное.
15
15
  //
16
16
  // Мастера в этом контуре нет: личность гостевая (ник из формы, identity.js),
17
17
  // профиль offline-заглушкой (lib/offlinePlayerData.js).
@@ -102,6 +102,13 @@ export default class InlineHostBridge {
102
102
 
103
103
  this._runtime = runtime;
104
104
 
105
+ // сбой чат-команды игры виден в консоли вкладки вместе с кадрами обработчика
106
+ runtime.host.setDiagnosticSink(report =>
107
+ console.error(
108
+ `[vimp] ${report.message}${report.stack ? `\n${report.stack}` : ''}`,
109
+ ),
110
+ );
111
+
105
112
  this._portMachine = new PortMachine({
106
113
  host: runtime.host,
107
114
  socketManager: runtime.socketManager,
@@ -32,6 +32,13 @@ export default class LoopbackTransport {
32
32
  resume: this._resume,
33
33
  });
34
34
 
35
+ // голос хоста-игрока не идёт через Worker: ретранслятор живёт здесь же, в
36
+ // главном потоке, и отдаёт пакеты прямым вызовом. ?. — у InlineHostBridge
37
+ // (standalone) голоса нет (D4)
38
+ this._controller.attachVoice?.(this._socketId, {
39
+ send: bytes => this.publisher.emit('voice', bytes),
40
+ });
41
+
35
42
  // подписчики появляются после connect() (супервизор берёт уже
36
43
  // подключённый транспорт) — 'open' уходит следующей микрозадачей
37
44
  if (this._resume) {
@@ -50,6 +57,13 @@ export default class LoopbackTransport {
50
57
  }
51
58
  }
52
59
 
60
+ // голосовой пакет хоста-игрока — ретранслятору, минуя Worker
61
+ sendVoice(bytes) {
62
+ if (!this._closed) {
63
+ this._controller.voiceIngress?.(this._socketId, bytes);
64
+ }
65
+ }
66
+
53
67
  close() {
54
68
  this._emitClose();
55
69
  }
@@ -54,6 +54,8 @@ export default class SessionSupervisor {
54
54
  * транспорта (строка или ArrayBuffer).
55
55
  * @param {Function} opts.onTerminal - (closeCode) сессия закрыта
56
56
  * окончательно: путь прежнего handleDisconnect.
57
+ * @param {Function} [opts.onVoice] - (data) голосовой пакет текущего
58
+ * транспорта в состоянии inGame (бинарный, без разбора).
57
59
  * @param {Function} [opts.onStateChange] - (state, prev).
58
60
  * @param {Function} [opts.onResumed] - место возвращено (RESUME_RESULT ok).
59
61
  * @param {Function} [opts.onResumeRejected] - (reason) хост отказал.
@@ -76,6 +78,7 @@ export default class SessionSupervisor {
76
78
  constructor({
77
79
  onMessage,
78
80
  onTerminal,
81
+ onVoice = null,
79
82
  onStateChange = () => {},
80
83
  onResumed = () => {},
81
84
  onResumeRejected = () => {},
@@ -88,6 +91,7 @@ export default class SessionSupervisor {
88
91
  }) {
89
92
  this._onMessage = onMessage;
90
93
  this._onTerminal = onTerminal;
94
+ this._onVoice = onVoice;
91
95
  this._onStateChange = onStateChange;
92
96
  this._onResumed = onResumed;
93
97
  this._onResumeRejected = onResumeRejected;
@@ -166,6 +170,17 @@ export default class SessionSupervisor {
166
170
  this._transport?.send(data, reliable);
167
171
  }
168
172
 
173
+ /**
174
+ * Голосовой пакет текущему транспорту (только в inGame; транспорт без
175
+ * голоса — выброс).
176
+ * @param {Uint8Array} bytes
177
+ */
178
+ sendVoice(bytes) {
179
+ if (this._state === S.inGame) {
180
+ this._transport?.sendVoice?.(bytes);
181
+ }
182
+ }
183
+
169
184
  /**
170
185
  * Пользователь уходит сам (или сессию закрывает политика): закрытие
171
186
  * транспорта терминально, переподключения не будет.
@@ -477,6 +492,7 @@ export default class SessionSupervisor {
477
492
 
478
493
  const handlers = {
479
494
  message: data => this._handleMessage(transport, data),
495
+ voice: data => this._handleVoice(transport, data),
480
496
  close: closeCode => this._handleClose(transport, closeCode),
481
497
  open: () => this._handleOpen(transport),
482
498
  };
@@ -523,6 +539,14 @@ export default class SessionSupervisor {
523
539
  this._onMessage(data);
524
540
  }
525
541
 
542
+ // голос не обновляет метку живости: поток голоса не доказывает живость
543
+ // протокола хоста
544
+ _handleVoice(transport, data) {
545
+ if (transport === this._transport && this._state === S.inGame) {
546
+ this._onVoice?.(data);
547
+ }
548
+ }
549
+
526
550
  _handleOpen(transport) {
527
551
  if (transport !== this._transport) {
528
552
  return;
@@ -1,12 +1,15 @@
1
+ import { VOICE_BACKPRESSURE_BYTES } from '../../config/voiceProtocol.js';
1
2
  import Publisher from '../../lib/Publisher.js';
2
3
 
3
4
  // Транспорт P2P-соединения клиента с браузерным хостом. Заменяет игровой
4
- // WebSocket: два RTCDataChannel вместо одного сокета.
5
+ // WebSocket: три RTCDataChannel вместо одного сокета.
5
6
  //
6
7
  // - meta (reliable-ordered) — весь JSON-протокол [portId, payload] и
7
8
  // бинарные кадры с одноразовыми событиями;
8
9
  // - state (unreliable-unordered) — чисто позиционные бинарные кадры, их
9
- // потерю компенсирует следующий кадр.
10
+ // потерю компенсирует следующий кадр;
11
+ // - voice (unreliable-unordered) — только голосовые пакеты,
12
+ // в готовность соединения не входит.
10
13
  //
11
14
  // Классификация кадров meta/state — на стороне хоста при упаковке. Клиент лишь
12
15
  // принимает данные из обоих каналов и отдаёт их одним потоком (событие
@@ -103,6 +106,7 @@ export default class WebRtcManager {
103
106
  this._pc = null;
104
107
  this._meta = null;
105
108
  this._state = null;
109
+ this._voice = null;
106
110
  this._roomId = null;
107
111
  // id вкладки — хост хранит его рядом с clientId пира
108
112
  this._memberId = memberId ?? null;
@@ -164,8 +168,18 @@ export default class WebRtcManager {
164
168
  });
165
169
  this._state.binaryType = 'arraybuffer';
166
170
 
171
+ // voice — голос: ненадёжный неупорядоченный, как state,
172
+ // но в готовность соединения не входит — связь не обязательна для матча, а
173
+ // потеря пакета голоса лечится следующим
174
+ this._voice = pc.createDataChannel('voice', {
175
+ ordered: false,
176
+ maxRetransmits: 0,
177
+ });
178
+ this._voice.binaryType = 'arraybuffer';
179
+
167
180
  this._wireChannel(this._meta);
168
181
  this._wireChannel(this._state);
182
+ this._wireVoiceChannel(this._voice);
169
183
 
170
184
  // канал standby открывает хост — только преемнику (этап 6)
171
185
  pc.ondatachannel = event => {
@@ -319,6 +333,28 @@ export default class WebRtcManager {
319
333
  }
320
334
  }
321
335
 
336
+ // голосовой пакет хосту; закрытый или переполненный канал — выброс (живой
337
+ // голос важнее старого)
338
+ sendVoice(bytes) {
339
+ const channel = this._voice;
340
+
341
+ if (
342
+ !channel ||
343
+ channel.readyState !== 'open' ||
344
+ channel.bufferedAmount > VOICE_BACKPRESSURE_BYTES
345
+ ) {
346
+ return;
347
+ }
348
+
349
+ // канал мог закрыться между проверкой и отправкой — исключение не должно
350
+ // уйти в колбэк энкодера
351
+ try {
352
+ channel.send(bytes);
353
+ } catch {
354
+ // пакет потерян, как и при закрытом канале
355
+ }
356
+ }
357
+
322
358
  close() {
323
359
  this._emitClose();
324
360
  }
@@ -420,7 +456,7 @@ export default class WebRtcManager {
420
456
 
421
457
  // поздние события каналов закрытого менеджера не должны ничего эмитить
422
458
  _dropChannelHandlers() {
423
- for (const channel of [this._meta, this._state]) {
459
+ for (const channel of [this._meta, this._state, this._voice]) {
424
460
  if (channel) {
425
461
  channel.onopen = null;
426
462
  channel.onmessage = null;
@@ -451,6 +487,12 @@ export default class WebRtcManager {
451
487
  channel.onclose = () => this._emitClose();
452
488
  }
453
489
 
490
+ // onopen/onclose не ставим: голос не трогает _openChannels и не закрывает
491
+ // менеджер
492
+ _wireVoiceChannel(channel) {
493
+ channel.onmessage = event => this.publisher.emit('voice', event.data);
494
+ }
495
+
454
496
  // кода закрытия у data channel нет: 'close' эмитится без аргумента, как
455
497
  // и раньше (код приходит только от WebSocket-транспорта dedicated)
456
498
  _emitClose() {
@@ -261,6 +261,11 @@ export default class HandoffFlow {
261
261
  this._policy?.addHealth(health);
262
262
  }
263
263
 
264
+ // исход передачи для последнего эпизода скрытия (HostHealthPolicy)
265
+ hiddenHandoff() {
266
+ return this._policy?.hiddenHandoff() ?? null;
267
+ }
268
+
264
269
  // «Leave server» из меню комнаты (этап 8.3). Хост при других людях отдаёт
265
270
  // роль бете без отката и уходит; один в комнате — закрывает её
266
271
  // (host_leaving: людей нет — мастер закрывает комнату сразу). Гость
@@ -321,11 +321,14 @@ export default class HostRole {
321
321
  onStats: this._debug ? stats => this._debug('standby sent', stats) : null,
322
322
  });
323
323
 
324
- // метрики здоровья Worker'а (этап 9a): скрытая вкладка троттлит цикл —
325
- // насколько, уходит в журнал одной сводкой за эпизод
324
+ // метрики здоровья Worker'а: эпизод скрытия, в котором матч просел,
325
+ // задел удалённых игроков, а автопередача не началась без видимой
326
+ // причины, уходит в журнал одной сводкой (HiddenHostHealthLog)
326
327
  this._hiddenLog = this._factories.hiddenLog({
327
328
  warn: (code, summary) =>
328
329
  this._diagnostics.warn(code, summary, { source: 'client' }),
330
+ minHealthyTickRate: migration.auto.overloadTickRate,
331
+ handoffOutcome: () => this._getHandoff().hiddenHandoff(),
329
332
  hidden: this._ui.isHidden(),
330
333
  });
331
334
  this._healthReporter = this._isLobbyMode
@@ -1,10 +1,17 @@
1
1
  @charset "UTF-8";
2
2
 
3
- /* База страницы: раньше жила инлайном в packages/engine/index.html, но в
4
- репозитории игры этого файла нет — а без правил ниже интерфейс движка
5
- остался бы без своей типографики и раскладки. Standalone SDK импортирует
6
- этот файл целиком, поэтому всё, что здесь есть, применяется и к
7
- встраивающей странице. */
3
+ /* База страницы: типографика и раскладка интерфейса движка живут здесь, а
4
+ не в index.html, потому что в репозитории игры такого файла нет.
5
+ Standalone SDK импортирует этот файл целиком, поэтому всё, что здесь
6
+ есть, применяется и к встраивающей странице. */
7
+ /* Цвета команд 1…3: общие у ника в чате и в HUD связи; игра, перекрасившая
8
+ команды, переопределяет переменные. */
9
+ :root {
10
+ --vimp-team-color-1: #f56;
11
+ --vimp-team-color-2: #7ed;
12
+ --vimp-team-color-3: #fe5;
13
+ }
14
+
8
15
  html,
9
16
  body {
10
17
  width: 100%;
@@ -307,6 +314,141 @@ body.hide-cursor {
307
314
  text-align: right;
308
315
  }
309
316
 
317
+ /*voice*/
318
+
319
+ /* Нейтральный вид HUD связи; игра перестилизует его по #voice-hud и
320
+ классам. Селектор по id перебивает `.vimp-shell > * { display: none }` */
321
+ #voice-hud {
322
+ display: none;
323
+ position: absolute;
324
+ right: 24px;
325
+ top: 50%;
326
+ transform: translateY(-50%);
327
+ z-index: 4;
328
+ pointer-events: none;
329
+ flex-direction: column;
330
+ align-items: flex-end;
331
+ gap: 8px;
332
+ color: #fff;
333
+ font-size: 14px;
334
+ text-shadow: 1px 1px 1px #000;
335
+ }
336
+
337
+ #voice-hud.is-talking,
338
+ #voice-hud.is-busy,
339
+ #voice-hud.is-cooldown,
340
+ #voice-hud.is-error,
341
+ #voice-hud.is-incoming {
342
+ display: flex;
343
+ }
344
+
345
+ #voice-hud .voice-own,
346
+ #voice-hud .voice-incoming {
347
+ display: none;
348
+ position: relative;
349
+ align-items: center;
350
+ gap: 8px;
351
+ }
352
+
353
+ #voice-hud.is-talking .voice-own,
354
+ #voice-hud.is-busy .voice-own,
355
+ #voice-hud.is-cooldown .voice-own,
356
+ #voice-hud.is-error .voice-own,
357
+ #voice-hud.is-incoming .voice-incoming {
358
+ display: flex;
359
+ }
360
+
361
+ #voice-hud .voice-icon {
362
+ width: 32px;
363
+ height: 32px;
364
+ fill: none;
365
+ stroke: currentColor;
366
+ stroke-width: 2;
367
+ stroke-linecap: round;
368
+ }
369
+
370
+ #voice-hud .voice-icon rect {
371
+ fill: currentColor;
372
+ }
373
+
374
+ #voice-hud .voice-cross {
375
+ display: none;
376
+ }
377
+
378
+ #voice-hud .voice-ring {
379
+ display: none;
380
+ position: absolute;
381
+ left: -2px;
382
+ top: -2px;
383
+ width: 36px;
384
+ height: 36px;
385
+ fill: none;
386
+ stroke: currentColor;
387
+ stroke-width: 2;
388
+ transform: rotate(-90deg);
389
+ }
390
+
391
+ #voice-hud.is-talking .voice-ring {
392
+ display: block;
393
+ }
394
+
395
+ #voice-hud .voice-own {
396
+ flex-direction: row-reverse;
397
+ }
398
+
399
+ #voice-hud.is-talking .voice-own {
400
+ color: #3ad16b;
401
+ }
402
+
403
+ #voice-hud.is-busy .voice-own {
404
+ color: #e5484d;
405
+ }
406
+
407
+ #voice-hud.is-cooldown .voice-own,
408
+ #voice-hud.is-error .voice-own {
409
+ color: #8a8f98;
410
+ }
411
+
412
+ #voice-hud.is-busy .voice-cross,
413
+ #voice-hud.is-error .voice-cross {
414
+ display: inline;
415
+ }
416
+
417
+ #voice-hud .voice-incoming {
418
+ flex-direction: row-reverse;
419
+ color: #f5a524;
420
+ animation: voice-pulse 1s ease-in-out infinite;
421
+ }
422
+
423
+ #voice-hud .voice-name {
424
+ color: var(--voice-name-color, #fff);
425
+ font-weight: bold;
426
+ }
427
+
428
+ #voice-hud .voice-team1 {
429
+ color: var(--voice-name-color, var(--vimp-team-color-1));
430
+ }
431
+
432
+ #voice-hud .voice-team2 {
433
+ color: var(--voice-name-color, var(--vimp-team-color-2));
434
+ }
435
+
436
+ #voice-hud .voice-team3 {
437
+ color: var(--voice-name-color, var(--vimp-team-color-3));
438
+ }
439
+
440
+ @keyframes voice-pulse {
441
+ 50% {
442
+ opacity: 0.55;
443
+ }
444
+ }
445
+
446
+ @media (prefers-reduced-motion: reduce) {
447
+ #voice-hud .voice-incoming {
448
+ animation: none;
449
+ }
450
+ }
451
+
310
452
  /*vote*/
311
453
 
312
454
  #vote {
@@ -457,15 +599,15 @@ body.hide-cursor {
457
599
  }
458
600
 
459
601
  #chat-box .line1:before {
460
- color: var(--chat-name-color, #f56);
602
+ color: var(--chat-name-color, var(--vimp-team-color-1));
461
603
  }
462
604
 
463
605
  #chat-box .line2:before {
464
- color: var(--chat-name-color, #7ed);
606
+ color: var(--chat-name-color, var(--vimp-team-color-2));
465
607
  }
466
608
 
467
609
  #chat-box .line3:before {
468
- color: var(--chat-name-color, #fe5);
610
+ color: var(--chat-name-color, var(--vimp-team-color-3));
469
611
  }
470
612
 
471
613
  #cmd {
@@ -1329,7 +1471,7 @@ body.hide-cursor {
1329
1471
  color: #d66;
1330
1472
  }
1331
1473
 
1332
- /* журнал клиентских ошибок (plan/client-reports, этап 5): тот же каркас и
1474
+ /* журнал клиентских ошибок: тот же каркас и
1333
1475
  палитра, что у панели реестра игр; графы — .games-filters/.games-filter-btn */
1334
1476
  #reports-panel {
1335
1477
  background-color: #222;
@@ -55,9 +55,9 @@ const SHELL = [
55
55
  tag: 'div',
56
56
  id: 'auth-link',
57
57
  children: [
58
- // ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
59
- // из метаданных пакета в манифесте
60
- { tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
58
+ // версия пакета ИГРЫ (из манифеста) и ссылка на документацию
59
+ // (только в dev-сборке) — заполняет main.js по AUTH_DATA
60
+ { tag: 'p', children: [{ tag: 'a', id: 'auth-docs-link' }] },
61
61
  { tag: 'p', id: 'auth-version' },
62
62
  { tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
63
63
  ],
@@ -15,10 +15,10 @@ div#auth
15
15
  div#auth-informs
16
16
 
17
17
  div#auth-link
18
- //- ссылка и версия пакета ИГРЫ (мастер кладёт их в манифест, читая
19
- //- package.json пакета) — проставляет main.js по AUTH_DATA; без данных
20
- //- ячейки остаются пустыми
18
+ //- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
19
+ //- пакета) и ссылка на документацию (только в dev-сборке) —
20
+ //- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
21
21
  p
22
- a#auth-package-link
22
+ a#auth-docs-link
23
23
  p#auth-version
24
24
  p © #{new Date().getFullYear()} VIMP
@@ -21,7 +21,7 @@ div#lobby(style='display:none')
21
21
  input#profile-open(type='button', value='Profile')
22
22
  input#games-open-mine(type='button', value='My games')
23
23
  input#games-open-moderation(type='button', value='Moderation', style='display:none')
24
- //- журнал клиентских ошибок (plan/client-reports, этап 5): только админ
24
+ //- журнал клиентских ошибок: только админ
25
25
  input#reports-open(type='button', value='Errors', style='display:none')
26
26
  hr.lobby-divider
27
27
  h3.lobby-section-title= 'HOST A GAME'
@@ -67,8 +67,8 @@ div#lobby(style='display:none')
67
67
  span#leaderboard-total
68
68
  ol#lobby-leaderboard-list
69
69
  p#lobby-my-placement
70
- //- футер: ссылку и версию пакета движка проставляет LobbyView
71
- //- (engineVersion.js + lib/packageLink.js)
70
+ //- футер: ссылка на документацию статична, версию пакета движка
71
+ //- проставляет LobbyView (engineVersion.js)
72
72
  div#lobby-footer
73
73
  p
74
74
  a#lobby-terms-link(target='_blank', rel='noopener') Terms of Service
@@ -76,6 +76,6 @@ div#lobby(style='display:none')
76
76
  |
77
77
  a#lobby-privacy-link(target='_blank', rel='noopener') Privacy Policy
78
78
  p
79
- a#lobby-link
79
+ a#lobby-link(href='https://docs.vimp.lgick.dev', target='_blank', rel='noopener') docs
80
80
  p#lobby-version
81
81
  p © #{new Date().getFullYear()} VIMP
@@ -1,4 +1,4 @@
1
- //- Журнал клиентских ошибок (plan/client-reports, этап 5): только админ.
1
+ //- Журнал клиентских ошибок: только админ.
2
2
  //- Панель скрыта по умолчанию — открывается кнопкой «Errors» бейджа
3
3
  //- пользователя (#lobby-user в lobby.pug); id элементов перечислены в
4
4
  //- config/lobby.js. Подписи — по-английски, как и остальное лобби