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.
- package/README.md +2 -2
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +2 -2
- package/package.json +3 -4
- package/src/client/InputListener.js +3 -0
- package/src/client/SoundManager.js +13 -0
- package/src/client/boot.js +11 -87
- package/src/client/components/controller/Controls.js +10 -0
- package/src/client/components/controller/Voice.js +31 -0
- package/src/client/components/model/Controls.js +64 -0
- package/src/client/components/model/RoomMenu.js +1 -1
- package/src/client/components/model/Voice.js +96 -0
- package/src/client/components/model/Vote.js +2 -1
- package/src/client/components/view/Auth.js +3 -3
- package/src/client/components/view/Voice.js +172 -0
- package/src/client/lib/formBuilder.js +7 -6
- package/src/client/lib/hostVoteCommand.js +2 -2
- package/src/client/lib/pickActiveGame.js +3 -12
- package/src/client/lib/retryOnNetworkError.js +0 -9
- package/src/client/lib/voiceState.js +78 -0
- package/src/client/main.js +160 -90
- package/src/client/network/HostConnectionManager.js +46 -5
- package/src/client/network/HostController.js +43 -0
- package/src/client/network/LoopbackTransport.js +14 -0
- package/src/client/network/SessionSupervisor.js +38 -14
- package/src/client/network/WebRtcManager.js +46 -5
- package/src/client/network/policyClose.js +10 -77
- package/src/client/style.css +150 -9
- package/src/client/views/gameShell.js +2 -4
- package/src/client/views/includes/auth.pug +1 -4
- package/src/client/views/includes/lobby.pug +3 -6
- package/src/client/voice/JitterBuffer.js +137 -0
- package/src/client/voice/MicCapture.js +307 -0
- package/src/client/voice/PttMachine.js +369 -0
- package/src/client/voice/VoiceClient.js +336 -0
- package/src/client/voice/VoiceCues.js +295 -0
- package/src/client/voice/VoiceEncoder.js +174 -0
- package/src/client/voice/VoicePlayback.js +558 -0
- package/src/client/voice/audioData.js +35 -0
- package/src/client/voice/captureProcessor.worklet.js +136 -0
- package/src/client/voice/opusConfig.js +49 -0
- package/src/client/voice/voiceOutput.js +72 -0
- package/src/client/voice/voiceSupport.js +71 -0
- package/src/client/voice/voiceTones.js +90 -0
- package/src/config/clientServices.js +2 -0
- package/src/config/closeCodes.js +7 -9
- package/src/config/gameCodes.js +1 -1
- package/src/config/hostDefaults.js +2 -2
- package/src/config/lobby.js +3 -4
- package/src/config/voiceDefaults.js +44 -0
- package/src/config/voiceProtocol.js +80 -0
- package/src/config/voteKeys.js +4 -0
- package/src/config/wsports.js +13 -1
- package/src/devtools/ScenarioRunner.js +1 -1
- package/src/devtools/builtinScenario.js +2 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
- package/src/devtools/contract/rules/a4-vite-config.js +1 -1
- package/src/devtools/contract/rules/b11-voice-config.js +34 -0
- package/src/devtools/contract/rules/c12-voice-hotkey.js +70 -0
- package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
- package/src/devtools/contract/rules/index.js +4 -0
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +2 -2
- package/src/host/HostGame.js +62 -4
- package/src/host/PortMachine.js +23 -24
- package/src/host/commandFailure.js +2 -2
- package/src/host/host.worker.js +7 -0
- package/src/host/identity.js +3 -3
- package/src/host/meta/SocketManager.js +6 -0
- package/src/host/meta/core/CommandProcessor.js +2 -2
- package/src/host/meta/modules/Accolades.js +1 -1
- package/src/host/meta/modules/voice/Voice.js +430 -0
- package/src/host/meta/modules/voice/channels.js +58 -0
- package/src/host/meta/modules/voice/index.js +1 -0
- package/src/lib/VoiceRelay.js +200 -0
- package/src/lib/buildClientConfig.js +7 -0
- package/src/lib/capabilities.js +4 -0
- package/src/lib/createHostRuntime.js +8 -0
- package/src/lib/gameConfigView.js +11 -1
- package/src/lib/loadGamePackage.js +7 -9
- package/src/lib/offlinePlayerData.js +1 -1
- package/src/lib/validators.js +2 -2
- package/src/lib/voiceConfig.js +136 -0
- package/src/lib/voiceMessages.js +172 -0
- package/src/lib/voicePacket.js +142 -0
- package/src/standalone/index.js +3 -2
- package/src/client/lib/footerLink.js +0 -30
- 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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
'notice:
|
|
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.
|
|
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://
|
|
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.
|
|
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://
|
|
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/
|
|
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
|
*/
|
package/src/client/boot.js
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
// Режим загрузки клиента движка (Этап 2 плана standalone-sdk). Клиент один
|
|
2
|
-
// на
|
|
2
|
+
// на оба контура, а различает их этот модуль:
|
|
3
3
|
//
|
|
4
|
-
// lobby
|
|
5
|
-
// solo
|
|
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'
|
|
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
|
|
73
|
-
* @
|
|
74
|
-
* @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
|
|
75
|
-
* @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
|
|
76
|
-
* сеть не ответила после повторов.
|
|
56
|
+
* Разрешает режим загрузки: инъекция SDK, иначе лобби.
|
|
57
|
+
* @returns {Object} Конфиг загрузки.
|
|
77
58
|
*/
|
|
78
|
-
export
|
|
79
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
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;
|