vimp-engine 0.38.3 → 0.40.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 (88) hide show
  1. package/README.md +2 -2
  2. package/bin/vimp-sim.js +2 -2
  3. package/core/Cargo.toml +2 -2
  4. package/package.json +3 -4
  5. package/src/client/InputListener.js +3 -0
  6. package/src/client/SoundManager.js +13 -0
  7. package/src/client/boot.js +11 -87
  8. package/src/client/components/controller/Controls.js +10 -0
  9. package/src/client/components/controller/Voice.js +31 -0
  10. package/src/client/components/model/Controls.js +64 -0
  11. package/src/client/components/model/RoomMenu.js +1 -1
  12. package/src/client/components/model/Voice.js +96 -0
  13. package/src/client/components/model/Vote.js +2 -1
  14. package/src/client/components/view/Auth.js +3 -3
  15. package/src/client/components/view/Voice.js +172 -0
  16. package/src/client/lib/formBuilder.js +7 -6
  17. package/src/client/lib/hostVoteCommand.js +2 -2
  18. package/src/client/lib/pickActiveGame.js +3 -12
  19. package/src/client/lib/retryOnNetworkError.js +0 -9
  20. package/src/client/lib/voiceState.js +78 -0
  21. package/src/client/main.js +160 -90
  22. package/src/client/network/HostConnectionManager.js +46 -5
  23. package/src/client/network/HostController.js +43 -0
  24. package/src/client/network/LoopbackTransport.js +14 -0
  25. package/src/client/network/SessionSupervisor.js +38 -14
  26. package/src/client/network/WebRtcManager.js +46 -5
  27. package/src/client/network/policyClose.js +10 -77
  28. package/src/client/style.css +150 -9
  29. package/src/client/views/gameShell.js +2 -4
  30. package/src/client/views/includes/auth.pug +1 -4
  31. package/src/client/views/includes/lobby.pug +3 -6
  32. package/src/client/voice/JitterBuffer.js +137 -0
  33. package/src/client/voice/MicCapture.js +307 -0
  34. package/src/client/voice/PttMachine.js +369 -0
  35. package/src/client/voice/VoiceClient.js +336 -0
  36. package/src/client/voice/VoiceCues.js +295 -0
  37. package/src/client/voice/VoiceEncoder.js +174 -0
  38. package/src/client/voice/VoicePlayback.js +558 -0
  39. package/src/client/voice/audioData.js +35 -0
  40. package/src/client/voice/captureProcessor.worklet.js +136 -0
  41. package/src/client/voice/opusConfig.js +49 -0
  42. package/src/client/voice/voiceOutput.js +72 -0
  43. package/src/client/voice/voiceSupport.js +71 -0
  44. package/src/client/voice/voiceTones.js +90 -0
  45. package/src/config/clientServices.js +2 -0
  46. package/src/config/closeCodes.js +7 -9
  47. package/src/config/gameCodes.js +1 -1
  48. package/src/config/hostDefaults.js +2 -2
  49. package/src/config/lobby.js +3 -4
  50. package/src/config/voiceDefaults.js +44 -0
  51. package/src/config/voiceProtocol.js +80 -0
  52. package/src/config/voteKeys.js +4 -0
  53. package/src/config/wsports.js +13 -1
  54. package/src/devtools/ScenarioRunner.js +1 -1
  55. package/src/devtools/builtinScenario.js +2 -2
  56. package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
  57. package/src/devtools/contract/rules/a4-vite-config.js +1 -1
  58. package/src/devtools/contract/rules/b11-voice-config.js +34 -0
  59. package/src/devtools/contract/rules/c12-voice-hotkey.js +70 -0
  60. package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
  61. package/src/devtools/contract/rules/index.js +4 -0
  62. package/src/devtools/invariants.js +1 -1
  63. package/src/devtools/pluginLoader.js +2 -2
  64. package/src/host/HostGame.js +62 -4
  65. package/src/host/PortMachine.js +23 -24
  66. package/src/host/commandFailure.js +2 -2
  67. package/src/host/host.worker.js +7 -0
  68. package/src/host/identity.js +3 -3
  69. package/src/host/meta/SocketManager.js +6 -0
  70. package/src/host/meta/core/CommandProcessor.js +2 -2
  71. package/src/host/meta/modules/Accolades.js +1 -1
  72. package/src/host/meta/modules/voice/Voice.js +430 -0
  73. package/src/host/meta/modules/voice/channels.js +58 -0
  74. package/src/host/meta/modules/voice/index.js +1 -0
  75. package/src/lib/VoiceRelay.js +200 -0
  76. package/src/lib/buildClientConfig.js +7 -0
  77. package/src/lib/capabilities.js +4 -0
  78. package/src/lib/createHostRuntime.js +8 -0
  79. package/src/lib/gameConfigView.js +11 -1
  80. package/src/lib/loadGamePackage.js +7 -9
  81. package/src/lib/offlinePlayerData.js +1 -1
  82. package/src/lib/validators.js +2 -2
  83. package/src/lib/voiceConfig.js +136 -0
  84. package/src/lib/voiceMessages.js +172 -0
  85. package/src/lib/voicePacket.js +142 -0
  86. package/src/standalone/index.js +3 -2
  87. package/src/client/lib/footerLink.js +0 -30
  88. package/src/client/network/WebSocketTransport.js +0 -78
package/README.md CHANGED
@@ -4,7 +4,7 @@ The VIMP engine SDK for game plugins: the browser host and client runtime,
4
4
  the standalone mode (`vimp-engine/standalone`) and the CLI tools `vimp-sim`
5
5
  and `vimp-contract`. Game rules live in separate plugin packages that the
6
6
  engine loads at runtime through the
7
- [plugin contract](https://docs.vimp.lgick.dev/en/plugin-api).
7
+ plugin contract (VIMP docs: plugin-api.md).
8
8
 
9
9
  Start a game:
10
10
 
@@ -12,4 +12,4 @@ Start a game:
12
12
  npm create vimp-game my-game
13
13
  ```
14
14
 
15
- Documentation: https://docs.vimp.lgick.dev
15
+ Platform: https://vimp.lgick.dev
package/bin/vimp-sim.js CHANGED
@@ -40,8 +40,8 @@ const BUILTIN_NOTICE =
40
40
  'notice: the built-in smoke scenario is running against --game. It only\n' +
41
41
  'notice: proves the loop closes on this plugin: key coverage (invariant 2)\n' +
42
42
  'notice: and prediction drift (invariant 9) are skipped — both need a\n' +
43
- 'notice: scenario written for your game. See § Scenario format at\n' +
44
- 'notice: https://docs.vimp.lgick.dev/en/debugging#scenario-format, then run\n' +
43
+ 'notice: scenario written for your game; the format is in\n' +
44
+ 'notice: VIMP docs: debugging.md § "Scenario format". Then run\n' +
45
45
  'notice: vimp-sim --scenario <file>.\n\n';
46
46
 
47
47
  // типичная опечатка «--core без --game»: фикстурное ядро — обычный JS и
package/core/Cargo.toml CHANGED
@@ -1,10 +1,10 @@
1
1
  [package]
2
2
  name = "vimp-engine-core"
3
- version = "0.23.4"
3
+ version = "0.23.5"
4
4
  edition = "2024"
5
5
  description = "VIMP engine simulation core: physics, map, snapshot framing, interpolation/prediction/raycast primitives, navigation utilities. No wasm-bindgen — the game crate builds the WASM ABI wrappers."
6
6
  license = "MIT"
7
- homepage = "https://docs.vimp.lgick.dev/en/core"
7
+ homepage = "https://vimp.lgick.dev"
8
8
  documentation = "https://docs.rs/vimp-engine-core"
9
9
  readme = "README.md"
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.38.3",
3
+ "version": "0.40.0",
4
4
  "description": "VIMP engine SDK: browser host and client runtime, standalone mode and CLI tools (vimp-sim, vimp-contract) for VIMP game plugins",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,7 +8,7 @@
8
8
  "url": "git+ssh://git@github.com/lgick/vimp.git",
9
9
  "directory": "packages/engine"
10
10
  },
11
- "homepage": "https://docs.vimp.lgick.dev",
11
+ "homepage": "https://vimp.lgick.dev",
12
12
  "type": "module",
13
13
  "engines": {
14
14
  "node": ">=20.11.0"
@@ -44,8 +44,7 @@
44
44
  ],
45
45
  "scripts": {
46
46
  "predev": "node scripts/sync-pixi-vendor.mjs",
47
- "dev": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/dedicated -w src/lib -w src/config",
48
- "dedicated": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/dedicated -w src/host -w src/lib -w src/config",
47
+ "dev": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/lib -w src/config",
49
48
  "start": "NODE_ENV=production node --env-file ../../.env src/master/main.js",
50
49
  "prebuild": "node scripts/sync-pixi-vendor.mjs",
51
50
  "build": "vite build",
@@ -20,6 +20,9 @@ export default class InputListener {
20
20
  this.publisher.emit('keyUp', event),
21
21
  );
22
22
 
23
+ // keyup за пределами окна не приходит — эфир залип бы до таймаута
24
+ window.addEventListener('blur', () => this.publisher.emit('blur'));
25
+
23
26
  const emitMouse = () => this.publisher.emit('mouseAction');
24
27
 
25
28
  window.addEventListener('mouseup', emitMouse);
@@ -892,6 +892,19 @@ export default class SoundManager {
892
892
  }
893
893
  }
894
894
 
895
+ /**
896
+ * Выход Web Audio движка для голосовой связи: контекст Howler и его master
897
+ * gain. Голос подчиняется общей громкости и mute (скрытая вкладка, пауза
898
+ * миграции).
899
+ * @returns {{ ctx: AudioContext, destination: AudioNode } | null} null —
900
+ * Web Audio недоступен.
901
+ */
902
+ getAudioOutput() {
903
+ return Howler.usingWebAudio && Howler.ctx && Howler.masterGain
904
+ ? { ctx: Howler.ctx, destination: Howler.masterGain }
905
+ : null;
906
+ }
907
+
895
908
  /**
896
909
  * Выключает все звуки.
897
910
  */
@@ -1,33 +1,19 @@
1
1
  // Режим загрузки клиента движка (Этап 2 плана standalone-sdk). Клиент один
2
- // на все контуры, а различает их этот модуль:
2
+ // на оба контура, а различает их этот модуль:
3
3
  //
4
- // lobby — прод: каталог игр и сигналинг мастера, лобби за экраном профиля;
5
- // solo — standalone SDK: хост в этой же вкладке (inline, без Worker'а),
6
- // мастера нет вовсе;
7
- // dedicated — Node-сервер: прямой WebSocket к игровому процессу.
4
+ // lobby — прод: каталог игр и сигналинг мастера, лобби за экраном профиля;
5
+ // solo — standalone SDK: хост в этой же вкладке (inline, без Worker'а),
6
+ // мастера нет вовсе.
8
7
  //
9
8
  // Канал передачи конфига от SDK к main.js — модульное состояние: и SDK, и
10
9
  // main.js резолвят этот модуль в один экземпляр графа бандлера. Глобалов на
11
10
  // window не вводим: они переживают перезагрузку бандла и ломают вторую
12
11
  // вкладку SDK в том же документе.
13
12
 
14
- import {
15
- bodyReadFailure,
16
- fetchRetrying,
17
- isNetworkFailure,
18
- } from './lib/retryOnNetworkError.js';
19
-
20
- // эндпоинт режима: его отдаёт dedicated-сервер, у мастера его нет —
21
- // 404 и мусор означают прод-лобби; сбой сети — отказ загрузки
22
- const CONFIG_URL = '/config';
23
-
24
- // умолчание игрового WS dedicated-сервера: тот же хост, путь /game
25
- const DEDICATED_WS_PATH = '/game';
26
-
27
13
  /**
28
14
  * Конфиг загрузки:
29
15
  * {
30
- * mode: 'lobby' | 'solo' | 'dedicated',
16
+ * mode: 'lobby' | 'solo',
31
17
  * container, // solo: точка монтирования каркаса и канвасов
32
18
  * manifest, // solo: манифест-подобный объект в памяти
33
19
  * clientPlugin, // solo: живой ClientPlugin (минуя dynamic import)
@@ -37,15 +23,13 @@ const DEDICATED_WS_PATH = '/game';
37
23
  * startupVotes, // solo: ответы на initialVote (выход из наблюдателей)
38
24
  * startupCommands, // solo: чат-команды игры (спавн ботов)
39
25
  * reportUrl, // solo: приём журнала клиентских ошибок (null — выкл.)
40
- * wsUrl, // dedicated: адрес игрового WS
41
- * gameId, // dedicated: какую игру каталога считать активной
42
26
  * }
43
27
  */
44
28
  let bootConfig = null;
45
29
 
46
30
  /**
47
31
  * Задаёт конфиг загрузки. SDK вызывает его до import('./main.js') — main.js
48
- * читает уже готовое состояние и сети не касается.
32
+ * читает уже готовое состояние.
49
33
  * @param {Object} cfg - Конфиг загрузки (см. выше).
50
34
  */
51
35
  export function setBootConfig(cfg) {
@@ -53,7 +37,7 @@ export function setBootConfig(cfg) {
53
37
  }
54
38
 
55
39
  /**
56
- * Текущий конфиг загрузки (null, пока не задан и не разрешён).
40
+ * Текущий конфиг загрузки (null, пока SDK его не задал).
57
41
  * @returns {Object|null}
58
42
  */
59
43
  export function getBootConfig() {
@@ -69,69 +53,9 @@ export function resetBootConfig() {
69
53
  }
70
54
 
71
55
  /**
72
- * Разрешает режим загрузки: инъекция SDK → GET /config → лобби.
73
- * @param {Function} [fetchImpl] - Реализация fetch (тесты подставляют свою).
74
- * @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
75
- * @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
76
- * сеть не ответила после повторов.
56
+ * Разрешает режим загрузки: инъекция SDK, иначе лобби.
57
+ * @returns {Object} Конфиг загрузки.
77
58
  */
78
- export async function resolveBootConfig(fetchImpl, retry) {
79
- if (bootConfig) {
80
- return bootConfig;
81
- }
82
-
83
- const doFetch = fetchImpl ?? ((...args) => fetch(...args));
84
- let res = null;
85
-
86
- try {
87
- res = await fetchRetrying(doFetch, CONFIG_URL, retry);
88
- } catch (e) {
89
- // сети нет: режим неизвестен, и лобби здесь — догадка, а не факт
90
- if (isNetworkFailure(e)) {
91
- throw e;
92
- }
93
- }
94
-
95
- if (res?.ok) {
96
- try {
97
- bootConfig = normalize(await res.json());
98
- } catch (e) {
99
- // мусор в JSON — лобби; обрыв при чтении тела — сбой сети
100
- const failure = bodyReadFailure(e);
101
-
102
- if (isNetworkFailure(failure)) {
103
- throw failure;
104
- }
105
-
106
- bootConfig = null;
107
- }
108
- }
109
-
110
- bootConfig = bootConfig ?? { mode: 'lobby' };
111
-
112
- return bootConfig;
113
- }
114
-
115
- // приводит ответ /config к валидному конфигу; всё, что не dedicated, —
116
- // лобби: неизвестный режим не должен уронить клиент в чёрный экран
117
- function normalize(raw) {
118
- if (!raw || typeof raw !== 'object' || raw.mode !== 'dedicated') {
119
- return null;
120
- }
121
-
122
- return {
123
- ...raw,
124
- mode: 'dedicated',
125
- // сервер объявляет свой путь в /config: константа — только умолчание
126
- wsUrl: raw.wsUrl || defaultDedicatedWsUrl(raw.wsPath),
127
- };
128
- }
129
-
130
- function defaultDedicatedWsUrl(wsPath) {
131
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
132
- // пустой или отсутствующий путь — это «умолчание», а не корень: `''` дал бы
133
- // ws://host, null — ws://hostnull
134
- const path = wsPath || DEDICATED_WS_PATH;
135
-
136
- return `${protocol}//${location.host}${path}`;
59
+ export function resolveBootConfig() {
60
+ return bootConfig ?? { mode: 'lobby' };
137
61
  }
@@ -54,6 +54,16 @@ export default class ControlsCtrl {
54
54
  this._model.resendHeld();
55
55
  }
56
56
 
57
+ // назначает клавишу связи (push-to-talk)
58
+ setPushToTalk(keyCode) {
59
+ this._model.setPushToTalk(keyCode);
60
+ }
61
+
62
+ // отпускает связь (потеря фокуса, скрытая вкладка)
63
+ releasePtt() {
64
+ this._model.releasePtt();
65
+ }
66
+
57
67
  // скрывает курсор
58
68
  resetCursorHideTimer() {
59
69
  this._view.resetCursorHideTimer();
@@ -0,0 +1,31 @@
1
+ // Singleton VoiceCtrl
2
+
3
+ let voiceCtrl;
4
+
5
+ export default class VoiceCtrl {
6
+ constructor(model, view) {
7
+ if (voiceCtrl) {
8
+ return voiceCtrl;
9
+ }
10
+
11
+ voiceCtrl = this;
12
+
13
+ this._model = model;
14
+ this._view = view;
15
+ }
16
+
17
+ // состояние своего эфира
18
+ showOwn(kind, data) {
19
+ this._model.showOwn(kind, data);
20
+ }
21
+
22
+ // чужой эфир (null — скрыть)
23
+ setIncoming(speaker) {
24
+ this._model.setIncoming(speaker);
25
+ }
26
+
27
+ destroy() {
28
+ this._model.destroy();
29
+ this._view.destroy();
30
+ }
31
+ }
@@ -2,6 +2,17 @@ import Publisher from '../../../lib/Publisher.js';
2
2
 
3
3
  // Singleton ControlsModel
4
4
 
5
+ // keyCode самих модификаторов: Ctrl, Alt, Meta (два кода и Firefox)
6
+ const MODIFIER_CODES = new Set([17, 18, 91, 93, 224]);
7
+
8
+ // фокус в поле ввода: клавиша печатает букву
9
+ function isEditableTarget(target) {
10
+ return (
11
+ target?.isContentEditable === true ||
12
+ ['INPUT', 'TEXTAREA', 'SELECT'].includes(target?.tagName)
13
+ );
14
+ }
15
+
5
16
  let controlsModel;
6
17
 
7
18
  export default class ControlsModel {
@@ -25,6 +36,8 @@ export default class ControlsModel {
25
36
  // возобновления сессии должен знать, что игрок держит на самом деле
26
37
  this._heldCodes = new Set();
27
38
  this._areKeysEnabled = false; // статус возможности нажатия клавиш
39
+ this._pttKey = null; // keyCode клавиши связи, null — связи нет
40
+ this._pttHeld = false;
28
41
 
29
42
  // канал указателя: игра, не объявившая modules.controls.pointer, его не
30
43
  // получает вовсе — ни одного лишнего сообщения по проводу
@@ -79,6 +92,34 @@ export default class ControlsModel {
79
92
  this.publisher.emit('mode', mode);
80
93
  }
81
94
  } else {
95
+ // Cmd, нажатый при уже зажатой клавише связи: её keyup на macOS не
96
+ // придёт — эфир отпускается сразу
97
+ if (event.metaKey && this._pttHeld && keyCode !== this._pttKey) {
98
+ this.releasePtt();
99
+ }
100
+
101
+ // клавиша связи никогда не уходит в игру командой. Автоповтор keydown
102
+ // игнорируется — удержание и так длится до keyup. Фокус в поле ввода
103
+ // (любом, не только в строке чата) — клавиша печатает букву. С
104
+ // Ctrl/Cmd/Alt — системная комбинация (вставка), не связь: на macOS при
105
+ // Cmd keyup обычной клавиши не приходит и связь залипла бы
106
+ // Сам модификатор может быть клавишей связи: его keydown несёт свой флаг
107
+ if (
108
+ keyCode === this._pttKey &&
109
+ (MODIFIER_CODES.has(keyCode) ||
110
+ !(event.ctrlKey || event.metaKey || event.altKey)) &&
111
+ !isEditableTarget(event.target)
112
+ ) {
113
+ event.preventDefault();
114
+
115
+ if (!event.repeat && !this._pttHeld) {
116
+ this._pttHeld = true;
117
+ this.publisher.emit('ptt', true);
118
+ }
119
+
120
+ return;
121
+ }
122
+
82
123
  // если клавиша ещё не зажата
83
124
  if (!this._pressedKeys[keyCode]) {
84
125
  const name = this._currentKeySet[keyCode];
@@ -111,6 +152,11 @@ export default class ControlsModel {
111
152
 
112
153
  this._heldCodes.delete(keyCode);
113
154
 
155
+ // отпускание засчитывается всегда, без проверки цели
156
+ if (keyCode === this._pttKey && this._pttHeld) {
157
+ this.releasePtt();
158
+ }
159
+
114
160
  // если клавиша была зажата
115
161
  if (this._pressedKeys[keyCode]) {
116
162
  const name = this._currentKeySet[keyCode];
@@ -131,6 +177,10 @@ export default class ControlsModel {
131
177
  if (status === 'opened') {
132
178
  this._currentModes[mode] = true;
133
179
 
180
+ if (mode === 'chat') {
181
+ this.releasePtt();
182
+ }
183
+
134
184
  // иначе змейка так и ехала бы к последней точке, пока игрок набирает
135
185
  // сообщение в чат
136
186
  this._releasePointer(this._lastTapX, this._lastTapY);
@@ -260,6 +310,20 @@ export default class ControlsModel {
260
310
 
261
311
  if (isEnabled === false) {
262
312
  this._releasePointer(this._lastTapX, this._lastTapY);
313
+ this.releasePtt();
314
+ }
315
+ }
316
+
317
+ // клавиша связи (push-to-talk); null выключает
318
+ setPushToTalk(keyCode) {
319
+ this._pttKey = keyCode ?? null;
320
+ }
321
+
322
+ // отпускает связь (клавиша вверх, потеря фокуса, чат, запрет ввода)
323
+ releasePtt() {
324
+ if (this._pttHeld) {
325
+ this._pttHeld = false;
326
+ this.publisher.emit('ptt', false);
263
327
  }
264
328
  }
265
329
  }
@@ -8,7 +8,7 @@ import Publisher from '../../../lib/Publisher.js';
8
8
  // - «Hand over host» — только у текущего хоста, когда в комнате есть другие
9
9
  // люди и мастер назначил бету (иначе передавать некому).
10
10
  // Пока идёт передача, пункты заблокированы, на их месте статус. Только
11
- // лобби-режим: в solo/dedicated ссылки на комнату нет
11
+ // лобби-режим: в solo ссылки на комнату нет
12
12
 
13
13
  // handoff: null | 'pending' | 'slow' | 'failed'
14
14
  const STATUS_TEXT = {
@@ -0,0 +1,96 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+
3
+ // вспышка ошибки гаснет сама
4
+ const ERROR_FLASH_MS = 1500;
5
+
6
+ // Singleton VoiceModel
7
+
8
+ let voiceModel;
9
+
10
+ export default class VoiceModel {
11
+ /**
12
+ * @param {Object} data
13
+ * @param {Object} data.texts - CONFIG_DATA.voice.texts.
14
+ */
15
+ constructor({ texts }) {
16
+ if (voiceModel) {
17
+ return voiceModel;
18
+ }
19
+
20
+ voiceModel = this;
21
+
22
+ this._texts = texts ?? {};
23
+ // один таймер авто-скрытия: новый показ снимает прежний, иначе таймер
24
+ // старой ошибки погасил бы эфир, начатый после неё
25
+ this._timer = null;
26
+
27
+ // 'own' ({ kind, text, endsAt, totalMs }) и 'incoming' (speaker | null)
28
+ this.publisher = new Publisher();
29
+ }
30
+
31
+ /**
32
+ * Состояние своего эфира.
33
+ * @param {'hidden'|'talking'|'busy'|'cooldown'|'error'} kind
34
+ * @param {Object} [data] - talking: { endsAt, totalMs }, cooldown:
35
+ * { endsAt }, error: { textKey }.
36
+ */
37
+ showOwn(kind, data = {}) {
38
+ this._clearTimer();
39
+
40
+ let text = '';
41
+ let endsAt = 0;
42
+ let totalMs = 0;
43
+
44
+ if (kind === 'busy') {
45
+ text = this._texts.busy ?? '';
46
+ } else if (kind === 'cooldown') {
47
+ text = this._texts.cooldown ?? '';
48
+ endsAt = data.endsAt;
49
+ } else if (kind === 'error') {
50
+ text = this._texts[data.textKey] ?? '';
51
+ } else if (kind === 'talking') {
52
+ endsAt = data.endsAt;
53
+ totalMs = data.totalMs;
54
+ }
55
+
56
+ this.publisher.emit('own', { kind, text, endsAt, totalMs });
57
+
58
+ if (kind === 'error') {
59
+ this._arm(ERROR_FLASH_MS);
60
+ } else if (kind === 'cooldown') {
61
+ this._arm(Math.max(0, endsAt - performance.now()));
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Чужой эфир.
67
+ * @param {{ name: string, teamId: number|null, color: string|null }|null}
68
+ * speaker - null скрывает.
69
+ */
70
+ setIncoming(speaker) {
71
+ this.publisher.emit('incoming', speaker);
72
+ }
73
+
74
+ destroy() {
75
+ this._clearTimer();
76
+ }
77
+
78
+ _arm(ms) {
79
+ this._timer = setTimeout(() => {
80
+ this._timer = null;
81
+ this.publisher.emit('own', {
82
+ kind: 'hidden',
83
+ text: '',
84
+ endsAt: 0,
85
+ totalMs: 0,
86
+ });
87
+ }, ms);
88
+ }
89
+
90
+ _clearTimer() {
91
+ if (this._timer !== null) {
92
+ clearTimeout(this._timer);
93
+ this._timer = null;
94
+ }
95
+ }
96
+ }
@@ -1,4 +1,5 @@
1
1
  import Publisher from '../../../lib/Publisher.js';
2
+ import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
2
3
 
3
4
  // Singleton VoteModel
4
5
 
@@ -215,7 +216,7 @@ export default class VoteModel {
215
216
  }
216
217
 
217
218
  // если keyCode это число от 0 до 9
218
- if (48 <= keyCode && keyCode <= 57) {
219
+ if (VOTE_ANSWER_KEYS[0] <= keyCode && keyCode <= VOTE_ANSWER_KEYS[1]) {
219
220
  number = String.fromCharCode(keyCode);
220
221
  number = parseInt(number, 10);
221
222
 
@@ -13,7 +13,7 @@ export default class AuthView {
13
13
  // texts — игровые тексты формы (authSchema.texts: title, sections);
14
14
  // сам каркас (auth.pug) нейтрален и текстов игры не содержит.
15
15
  // params — та же дескрипторная схема формы, что и у room-формы
16
- // (docs.vimp.lgick.dev/en/plugin-api "Form schema"), едет по сети в
16
+ // (VIMP docs: plugin-api.md § "Form schema"), едет по сети в
17
17
  // PS_AUTH_DATA
18
18
  constructor(model, elems, texts = null, params = null) {
19
19
  if (authView) {
@@ -64,8 +64,8 @@ export default class AuthView {
64
64
  }
65
65
 
66
66
  // строит контролы формы игрока из той же дескрипторной схемы, что и
67
- // room-форма (единый formBuilder, docs.vimp.lgick.dev/en/plugin-api "Form
68
- // schema")
67
+ // room-форма (единый formBuilder,
68
+ // VIMP docs: plugin-api.md § "Form schema")
69
69
  _renderFields(elems, params) {
70
70
  if (!Array.isArray(params)) {
71
71
  return;