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.
- package/core/Cargo.toml +1 -1
- package/package.json +3 -1
- package/src/client/InputListener.js +3 -0
- package/src/client/SoundManager.js +13 -0
- package/src/client/boot.js +30 -7
- package/src/client/components/controller/ClientReports.js +1 -1
- package/src/client/components/controller/Controls.js +10 -0
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/controller/Voice.js +31 -0
- package/src/client/components/model/ClientReports.js +1 -1
- package/src/client/components/model/Controls.js +64 -0
- package/src/client/components/model/Games.js +24 -0
- package/src/client/components/model/Voice.js +96 -0
- package/src/client/components/model/Vote.js +2 -1
- package/src/client/components/view/ClientReports.js +1 -1
- package/src/client/components/view/Games.js +19 -1
- package/src/client/components/view/Lobby.js +1 -7
- package/src/client/components/view/Voice.js +172 -0
- package/src/client/debug.js +1 -1
- package/src/client/lib/authApi.js +1 -1
- package/src/client/lib/diagnostics.js +19 -4
- package/src/client/lib/engineVersion.js +0 -6
- package/src/client/lib/footerLink.js +15 -14
- package/src/client/lib/retryOnNetworkError.js +60 -0
- package/src/client/lib/voiceState.js +78 -0
- package/src/client/main.js +152 -18
- package/src/client/network/HiddenHostHealthLog.js +40 -3
- package/src/client/network/HostConnectionManager.js +46 -5
- package/src/client/network/HostController.js +47 -4
- package/src/client/network/HostHealthPolicy.js +41 -0
- package/src/client/network/InlineHostBridge.js +8 -1
- package/src/client/network/LoopbackTransport.js +14 -0
- package/src/client/network/SessionSupervisor.js +24 -0
- package/src/client/network/WebRtcManager.js +45 -3
- package/src/client/session/HandoffFlow.js +5 -0
- package/src/client/session/HostRole.js +5 -2
- package/src/client/style.css +151 -9
- package/src/client/views/gameShell.js +3 -3
- package/src/client/views/includes/auth.pug +4 -4
- package/src/client/views/includes/lobby.pug +4 -4
- package/src/client/views/includes/reports.pug +1 -1
- 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/authClient.js +3 -3
- package/src/config/clientServices.js +2 -0
- package/src/config/lobby.js +4 -5
- package/src/config/opcodes.js +2 -1
- 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 +12 -0
- package/src/devtools/contract/loadContext.js +2 -2
- package/src/devtools/contract/rules/a7-package-repository.js +7 -8
- 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/index.js +5 -1
- package/src/devtools/invariants.js +1 -1
- package/src/host/DebugRecorder.js +1 -1
- package/src/host/HostGame.js +93 -15
- package/src/host/PortMachine.js +19 -3
- package/src/host/commandFailure.js +140 -0
- package/src/host/host.worker.js +15 -2
- package/src/host/meta/SocketManager.js +8 -2
- package/src/host/meta/core/CommandProcessor.js +37 -7
- 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/applyRoomOverrides.js +1 -1
- package/src/lib/buildClientConfig.js +7 -0
- package/src/lib/capabilities.js +5 -1
- package/src/lib/checkpointCodec.js +1 -2
- package/src/lib/coreConfig.js +1 -1
- package/src/lib/createHostRuntime.js +9 -1
- package/src/lib/gameConfigView.js +11 -1
- package/src/lib/gameId.js +1 -1
- package/src/lib/gamePlugin.js +6 -2
- package/src/lib/jwt.js +1 -1
- package/src/lib/packageLink.js +18 -61
- package/src/lib/validators.js +3 -3
- 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 +1 -1
- package/tests/fixtures/generations/gen-api3/README.md +2 -2
- package/tests/fixtures/generations/gen-api4/README.md +2 -2
- package/src/devtools/surface/abiParse.js +0 -175
- 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
|
|
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:
|
|
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'
|
|
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
|
package/src/client/style.css
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
@charset "UTF-8";
|
|
2
2
|
|
|
3
|
-
/* База страницы:
|
|
4
|
-
|
|
5
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
/* журнал клиентских
|
|
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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
{ tag: 'p', children: [{ tag: 'a', id: 'auth-
|
|
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
|
-
//-
|
|
20
|
-
//- ячейки остаются пустыми
|
|
18
|
+
//- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
|
|
19
|
+
//- пакета) и ссылка на документацию (только в dev-сборке) —
|
|
20
|
+
//- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
|
|
21
21
|
p
|
|
22
|
-
a#auth-
|
|
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
|
-
//- журнал клиентских
|
|
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
|
-
//- футер:
|
|
71
|
-
//- (engineVersion.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
|
-
//- Журнал клиентских
|
|
1
|
+
//- Журнал клиентских ошибок: только админ.
|
|
2
2
|
//- Панель скрыта по умолчанию — открывается кнопкой «Errors» бейджа
|
|
3
3
|
//- пользователя (#lobby-user в lobby.pug); id элементов перечислены в
|
|
4
4
|
//- config/lobby.js. Подписи — по-английски, как и остальное лобби
|