vimp-engine 0.40.0 → 0.42.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 +1 -1
- package/src/client/components/controller/RoomMenu.js +38 -10
- package/src/client/components/controller/Vote.js +26 -0
- package/src/client/components/model/Controls.js +9 -1
- package/src/client/components/model/RoomMenu.js +42 -57
- package/src/client/components/model/Vote.js +202 -0
- package/src/client/components/view/Panel.js +176 -6
- package/src/client/components/view/RoomMenu.js +78 -24
- package/src/client/components/view/Voice.js +6 -1
- package/src/client/components/view/Vote.js +71 -2
- package/src/client/lib/autoRejoin.js +161 -0
- package/src/client/lib/hostUnloadGuard.js +40 -15
- package/src/client/lib/roomLink.js +32 -0
- package/src/client/main.js +120 -36
- package/src/client/network/HostConnectionManager.js +27 -1
- package/src/client/network/HostController.js +1 -30
- package/src/client/network/HostHealthPolicy.js +14 -59
- package/src/client/network/PlannedHandoff.js +15 -100
- package/src/client/network/SessionSupervisor.js +58 -9
- package/src/client/network/SignalingClient.js +1 -1
- package/src/client/network/WebRtcManager.js +38 -3
- package/src/client/session/GuestSession.js +42 -7
- package/src/client/session/HandoffFlow.js +26 -51
- package/src/client/session/HostRole.js +8 -6
- package/src/client/session/PromotionFlow.js +28 -1
- package/src/client/session/RouteBoot.js +28 -9
- package/src/client/style.css +112 -20
- package/src/client/views/includes/roomMenu.pug +7 -8
- package/src/client/voice/MicCapture.js +3 -1
- package/src/config/abiOps.js +42 -0
- package/src/config/chatMessages.js +1 -1
- package/src/config/lobby.js +22 -23
- package/src/config/menuName.js +3 -0
- package/src/devtools/ScenarioRunner.js +1 -0
- package/src/devtools/invariants.js +24 -2
- package/src/devtools/report.js +2 -2
- package/src/host/GameCoreAdapter.js +83 -4
- package/src/host/HostGame.js +103 -39
- package/src/host/PortMachine.js +34 -17
- package/src/host/host.worker.js +1 -13
- package/src/host/meta/core/RoundManager.js +8 -22
- package/src/host/meta/core/VoteCoordinator.js +56 -1
- package/src/host/meta/modules/TimerManager.js +16 -2
- package/src/host/meta/modules/Vote.js +65 -0
- package/src/host/meta/modules/chat/systemMessages.js +1 -1
- package/src/host/meta/player/ParticipantManager.js +19 -3
- package/src/lib/capabilities.js +19 -4
- package/src/lib/gameConfigView.js +6 -5
|
@@ -5,10 +5,9 @@ import PlannedHandoff, {
|
|
|
5
5
|
import { SESSION_STATES } from '../network/SessionSupervisor.js';
|
|
6
6
|
import TokenHandoffTimer from '../network/TokenHandoffTimer.js';
|
|
7
7
|
|
|
8
|
-
// Плановая передача хоста
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (host-migration-review этап 15).
|
|
8
|
+
// Плановая передача хоста и уход из комнаты: кнопка Exit, автотриггеры
|
|
9
|
+
// (перегрузка, скрытая вкладка), просьба мастера (сеть, голосование),
|
|
10
|
+
// передача по сроку входа.
|
|
12
11
|
//
|
|
13
12
|
// Без DOM: сигналинг, роль хоста, супервизор, UI-действия и фабрики
|
|
14
13
|
// инъектирует сборщик (client/main.js).
|
|
@@ -27,7 +26,7 @@ export default class HandoffFlow {
|
|
|
27
26
|
* @param {Object} deps.config - lobby.migration.
|
|
28
27
|
* @param {Object} deps.ui - { showSessionOverlay(text|null),
|
|
29
28
|
* disableControls(), enableControls(), mute(), unmute(), isHidden(),
|
|
30
|
-
*
|
|
29
|
+
* setExitStatus(state|null), sendLeave(), reloadPage(hash) }.
|
|
31
30
|
* @param {Object} [deps.factories] - конструкторы (тесты).
|
|
32
31
|
*/
|
|
33
32
|
constructor({
|
|
@@ -81,6 +80,11 @@ export default class HandoffFlow {
|
|
|
81
80
|
return this._planned?.leaving === true;
|
|
82
81
|
}
|
|
83
82
|
|
|
83
|
+
// матч заморожен до прихода нового хоста
|
|
84
|
+
get frozen() {
|
|
85
|
+
return this._planned?.frozen === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
// подписки сигналинга передачи (лобби-режим, один раз на страницу)
|
|
85
89
|
bind() {
|
|
86
90
|
const migration = this._config;
|
|
@@ -91,8 +95,7 @@ export default class HandoffFlow {
|
|
|
91
95
|
getController: () => this._hostRole.controller,
|
|
92
96
|
slowAfterMs: migration.handoffSlowMs,
|
|
93
97
|
deadlineMs: migration.handoffDeadlineMs,
|
|
94
|
-
|
|
95
|
-
onSlow: () => this._ui.setHandoffMenu('slow'),
|
|
98
|
+
onSlow: () => this._ui.setExitStatus('slow'),
|
|
96
99
|
onFrozen: () => this._onFrozen(),
|
|
97
100
|
onReleased: msg => this._onReleased(msg),
|
|
98
101
|
onAborted: msg => this._onAborted(msg),
|
|
@@ -103,29 +106,13 @@ export default class HandoffFlow {
|
|
|
103
106
|
this._policy = this._factories.healthPolicy({
|
|
104
107
|
config: migration.auto,
|
|
105
108
|
handoff: {
|
|
106
|
-
start: ({ reason
|
|
107
|
-
hurry: reason => this._planned.hurry(reason),
|
|
108
|
-
// нагрузка нормализовалась до границы раунда — передача не нужна
|
|
109
|
-
cancelDeferred: () => {
|
|
110
|
-
const cancelled = this._planned.cancelDeferred();
|
|
111
|
-
|
|
112
|
-
if (cancelled) {
|
|
113
|
-
this._ui.setHandoffMenu(null);
|
|
114
|
-
// нагрузка нормализовалась до границы раунда — передача не
|
|
115
|
-
// нужна, а отложенное или вытесненное ею обновление кода — нужно
|
|
116
|
-
this._hostRole.resumeCodeUpdate();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return cancelled;
|
|
120
|
-
},
|
|
121
|
-
deferredReason: () =>
|
|
122
|
-
this._planned.deferred ? this._planned.reason : null,
|
|
109
|
+
start: ({ reason }) => this.start({ reason, stay: true }),
|
|
123
110
|
},
|
|
124
111
|
});
|
|
125
112
|
this._policy.setHidden(this._ui.isHidden());
|
|
126
113
|
|
|
127
114
|
// мастер просит отдать роль: сеть хоста заметно хуже, чем у беты (этап
|
|
128
|
-
// 9c)
|
|
115
|
+
// 9c) — передача сразу; выключенные автотриггеры — отказ
|
|
129
116
|
this._signaling.publisher.on('request_handoff', msg => {
|
|
130
117
|
const room = this._hostRole.room;
|
|
131
118
|
const ownRoom =
|
|
@@ -134,13 +121,10 @@ export default class HandoffFlow {
|
|
|
134
121
|
msg.epoch === room.epoch;
|
|
135
122
|
|
|
136
123
|
// хоста сняли голосованием (этап 10): передача сразу, при любых
|
|
137
|
-
// настройках автотриггеров — иначе мастер снимет его
|
|
124
|
+
// настройках автотриггеров — иначе мастер снимет его аварийно. Уже
|
|
125
|
+
// идущая передача (start её не повторяет) — её исход решит мастер
|
|
138
126
|
if (msg.reason === 'vote' && ownRoom) {
|
|
139
|
-
|
|
140
|
-
// идущая — её исход решит мастер
|
|
141
|
-
if (!this._planned?.hurry('vote')) {
|
|
142
|
-
this.start({ reason: 'vote', stay: true, defer: false });
|
|
143
|
-
}
|
|
127
|
+
this.start({ reason: 'vote', stay: true });
|
|
144
128
|
|
|
145
129
|
return;
|
|
146
130
|
}
|
|
@@ -150,11 +134,7 @@ export default class HandoffFlow {
|
|
|
150
134
|
migration.auto.enabled !== false &&
|
|
151
135
|
ownRoom
|
|
152
136
|
) {
|
|
153
|
-
this.start({
|
|
154
|
-
reason: 'network',
|
|
155
|
-
stay: true,
|
|
156
|
-
defer: msg.defer !== false,
|
|
157
|
-
});
|
|
137
|
+
this.start({ reason: 'network', stay: true });
|
|
158
138
|
}
|
|
159
139
|
});
|
|
160
140
|
|
|
@@ -172,22 +152,17 @@ export default class HandoffFlow {
|
|
|
172
152
|
}
|
|
173
153
|
|
|
174
154
|
/**
|
|
175
|
-
* Отдать роль хоста бете без отката:
|
|
176
|
-
*
|
|
177
|
-
* время идущей передачи игнорируется.
|
|
178
|
-
*
|
|
179
|
-
* defer (по умолчанию — для stay): передача ждёт границы раунда, если игра
|
|
180
|
-
* не умеет продолжать посреди него (без migration.midRound мягкая точка
|
|
181
|
-
* начала бы раунд у беты заново); решает Worker, потолок —
|
|
182
|
-
* lobby.migration.deferMaxMs. Уходящий хост не ждёт.
|
|
155
|
+
* Отдать роль хоста бете без отката: Exit (stay: false — затем в лобби),
|
|
156
|
+
* автотриггеры и просьбы мастера (stay: true — дальше гостем). Повторный
|
|
157
|
+
* вызов во время идущей передачи игнорируется. Передача начинается сразу, посреди
|
|
158
|
+
* раунда: бета продолжает его с того же тика.
|
|
183
159
|
* @param {Object} options
|
|
184
160
|
* @param {string} options.reason - 'leave' | 'handover' | 'overload' |
|
|
185
161
|
* 'hidden' | 'network' | 'vote'.
|
|
186
162
|
* @param {boolean} [options.stay]
|
|
187
|
-
* @param {boolean} [options.defer]
|
|
188
163
|
* @returns {boolean} передача началась.
|
|
189
164
|
*/
|
|
190
|
-
start({ reason, stay = true
|
|
165
|
+
start({ reason, stay = true }) {
|
|
191
166
|
const controller = this._hostRole.controller;
|
|
192
167
|
|
|
193
168
|
// промоушен сам владеет Worker'ом; передача уже идёт; комната ещё не
|
|
@@ -207,14 +182,14 @@ export default class HandoffFlow {
|
|
|
207
182
|
return false;
|
|
208
183
|
}
|
|
209
184
|
|
|
210
|
-
const started = this._planned.start({ reason, stay
|
|
185
|
+
const started = this._planned.start({ reason, stay });
|
|
211
186
|
|
|
212
187
|
// эстафета Worker'ов уступает только начавшейся передаче (преемник и так
|
|
213
188
|
// поднимется на актуальном коде); всё синхронно — состояние свопа между
|
|
214
189
|
// проверкой и вытеснением не меняется
|
|
215
190
|
if (started) {
|
|
216
191
|
this._hostRole.preemptSwap();
|
|
217
|
-
this._ui.
|
|
192
|
+
this._ui.setExitStatus(stay ? 'switching' : 'pending');
|
|
218
193
|
}
|
|
219
194
|
|
|
220
195
|
return started;
|
|
@@ -235,7 +210,7 @@ export default class HandoffFlow {
|
|
|
235
210
|
tryStart: () =>
|
|
236
211
|
Boolean(this._hostRole.controller) &&
|
|
237
212
|
this._hostRole.successorMemberId !== null &&
|
|
238
|
-
this.start({ reason: 'handover', stay: true
|
|
213
|
+
this.start({ reason: 'handover', stay: true }),
|
|
239
214
|
});
|
|
240
215
|
this._tokenHandoff.arm();
|
|
241
216
|
}
|
|
@@ -266,7 +241,7 @@ export default class HandoffFlow {
|
|
|
266
241
|
return this._policy?.hiddenHandoff() ?? null;
|
|
267
242
|
}
|
|
268
243
|
|
|
269
|
-
//
|
|
244
|
+
// Exit (подтверждён в окне). Хост при других людях отдаёт
|
|
270
245
|
// роль бете без отката и уходит; один в комнате — закрывает её
|
|
271
246
|
// (host_leaving: людей нет — мастер закрывает комнату сразу). Гость
|
|
272
247
|
// снимается у хоста сразу (LEAVE), а не через resumeGraceMs
|
|
@@ -358,7 +333,7 @@ export default class HandoffFlow {
|
|
|
358
333
|
// продолжается (Worker разморожен, гостям ушла полная синхронизация)
|
|
359
334
|
_onAborted({ reason, frozen }) {
|
|
360
335
|
console.warn(`[handoff] not completed: ${reason}`);
|
|
361
|
-
this._ui.
|
|
336
|
+
this._ui.setExitStatus(null);
|
|
362
337
|
|
|
363
338
|
const { hideOverlay, restoreInput } = controlsAfterAbort({
|
|
364
339
|
frozen,
|
|
@@ -44,7 +44,7 @@ export default class HostRole {
|
|
|
44
44
|
* @param {Function} deps.onPromotionRegistered - (hostPromotion) комната
|
|
45
45
|
* занята преемником (PromotionFlow.finish).
|
|
46
46
|
* @param {Object} deps.ui - { showRoomLink(gameId, roomId),
|
|
47
|
-
* refreshRoomControls(),
|
|
47
|
+
* refreshRoomControls(), setExitStatus(state), isHidden(),
|
|
48
48
|
* informTech(text), ensureWebRtc() → bool, startSession(transport),
|
|
49
49
|
* closeLobby(), onStartFailed(msg) }.
|
|
50
50
|
* @param {boolean} [deps.isLobbyMode]
|
|
@@ -285,7 +285,9 @@ export default class HostRole {
|
|
|
285
285
|
|
|
286
286
|
this._diagnostics.setContext({ role: 'host' });
|
|
287
287
|
|
|
288
|
-
// удалённые клиенты по WebRTC
|
|
288
|
+
// удалённые клиенты по WebRTC; adopt без teardown не оставляет прежний
|
|
289
|
+
// менеджер подписанным на офферы
|
|
290
|
+
this._connections?.destroy();
|
|
289
291
|
this._connections = this._factories.connections(
|
|
290
292
|
this._signaling,
|
|
291
293
|
controller,
|
|
@@ -458,7 +460,7 @@ export default class HostRole {
|
|
|
458
460
|
this._roomConfig = null;
|
|
459
461
|
this._promotion = null;
|
|
460
462
|
this._successorMemberId = null;
|
|
461
|
-
this._ui.
|
|
463
|
+
this._ui.setExitStatus(null);
|
|
462
464
|
this._ui.refreshRoomControls();
|
|
463
465
|
}
|
|
464
466
|
|
|
@@ -698,9 +700,9 @@ export default class HostRole {
|
|
|
698
700
|
}
|
|
699
701
|
|
|
700
702
|
/**
|
|
701
|
-
* Передача, отложившая или вытеснившая эстафету,
|
|
702
|
-
*
|
|
703
|
-
*
|
|
703
|
+
* Передача, отложившая или вытеснившая эстафету, сорвалась — обновление
|
|
704
|
+
* кода нужно снова. Без отложенного обновления ничего не делает:
|
|
705
|
+
* манифесты не качаются зря.
|
|
704
706
|
* @returns {Promise<void>}
|
|
705
707
|
*/
|
|
706
708
|
async resumeCodeUpdate() {
|
|
@@ -75,6 +75,9 @@ export default class PromotionFlow {
|
|
|
75
75
|
|
|
76
76
|
// промоушен из контрольной точки, Worker которого ещё поднимается
|
|
77
77
|
this._inFlight = null;
|
|
78
|
+
// роль хоста, с которой вкладка сама объявила уход (host_leaving из
|
|
79
|
+
// диалога закрытия): { roomId, epoch } | null
|
|
80
|
+
this._leaveAnnounced = null;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
get inFlight() {
|
|
@@ -112,11 +115,34 @@ export default class PromotionFlow {
|
|
|
112
115
|
this._hostRole.controller &&
|
|
113
116
|
msg.roomId === this._hostRole.room?.roomId
|
|
114
117
|
) {
|
|
115
|
-
|
|
118
|
+
// уход объявлен самой вкладкой («Cancel» в диалоге закрытия):
|
|
119
|
+
// «связь потеряна» было бы ложью
|
|
120
|
+
this.demote(msg.epoch, { notice: !this._leftThisRole() });
|
|
116
121
|
}
|
|
117
122
|
});
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
// вкладка-хост сама объявила уход (host_leaving из диалога закрытия):
|
|
126
|
+
// отметка — за текущей ролью, другую роль она не касается
|
|
127
|
+
markLeaveAnnounced() {
|
|
128
|
+
const room = this._hostRole.room;
|
|
129
|
+
|
|
130
|
+
this._leaveAnnounced = room
|
|
131
|
+
? { roomId: room.roomId, epoch: room.epoch }
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// снимают ту роль хоста, с которой вкладка объявила уход
|
|
136
|
+
_leftThisRole() {
|
|
137
|
+
const room = this._hostRole.room;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
this._leaveAnnounced !== null &&
|
|
141
|
+
room?.roomId === this._leaveAnnounced.roomId &&
|
|
142
|
+
room.epoch === this._leaveAnnounced.epoch
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
handlePromote(msg) {
|
|
121
147
|
// повтор promote нашей плановой передачи режимом checkpoint: хост пропал
|
|
122
148
|
// посреди неё — финальную точку больше не ждём
|
|
@@ -282,6 +308,7 @@ export default class PromotionFlow {
|
|
|
282
308
|
return;
|
|
283
309
|
}
|
|
284
310
|
|
|
311
|
+
this._leaveAnnounced = null;
|
|
285
312
|
this._hostRole.teardown();
|
|
286
313
|
this._standby.teardown();
|
|
287
314
|
this._ctx.epoch = minEpoch;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
quickPlayCreateDelay,
|
|
8
8
|
} from '../lib/roomLink.js';
|
|
9
9
|
import { takePendingPromotion } from '../network/Promotion.js';
|
|
10
|
+
import { takeAutoRejoin } from '../lib/autoRejoin.js';
|
|
10
11
|
|
|
11
12
|
// Прямые ссылки (host-migration, этап 3): без hash — лобби, #/<gameId> —
|
|
12
13
|
// быстрая игра, #/<gameId>/<roomId> — вход в комнату (client/lib/roomLink.js).
|
|
@@ -57,8 +58,12 @@ export default class RouteBoot {
|
|
|
57
58
|
* setRoute(hash), reloadPage(hash) }.
|
|
58
59
|
* @param {Function} [deps.fetch]
|
|
59
60
|
* @param {Function} [deps.getStorage] - () → sessionStorage (может бросить).
|
|
61
|
+
* @param {Object|null} [deps.autoRejoin] - client/lib/autoRejoin.js
|
|
62
|
+
* AutoRejoin: взводится, если маршрут ведёт в комнату метки автовхода.
|
|
60
63
|
* @param {Function} [deps.sleep] - (ms) → Promise.
|
|
61
64
|
* @param {Function} [deps.now] - монотонные часы, мс.
|
|
65
|
+
* @param {Function} [deps.wallNow] - часы, переживающие перезагрузку
|
|
66
|
+
* (срок метки автовхода), мс.
|
|
62
67
|
*/
|
|
63
68
|
constructor({
|
|
64
69
|
ctx,
|
|
@@ -73,8 +78,10 @@ export default class RouteBoot {
|
|
|
73
78
|
ui,
|
|
74
79
|
fetch = (...args) => globalThis.fetch(...args),
|
|
75
80
|
getStorage = () => window.sessionStorage,
|
|
81
|
+
autoRejoin = null,
|
|
76
82
|
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
77
83
|
now = () => performance.now(),
|
|
84
|
+
wallNow = () => Date.now(),
|
|
78
85
|
}) {
|
|
79
86
|
this._ctx = ctx;
|
|
80
87
|
this._signaling = signaling;
|
|
@@ -88,8 +95,10 @@ export default class RouteBoot {
|
|
|
88
95
|
this._ui = ui;
|
|
89
96
|
this._fetch = fetch;
|
|
90
97
|
this._getStorage = getStorage;
|
|
98
|
+
this._autoRejoin = autoRejoin;
|
|
91
99
|
this._sleep = sleep;
|
|
92
100
|
this._now = now;
|
|
101
|
+
this._wallNow = wallNow;
|
|
93
102
|
|
|
94
103
|
// маршрут разбирается один раз: повторный welcome (реконнект
|
|
95
104
|
// сигналинга) бутстрап не повторяет
|
|
@@ -162,6 +171,8 @@ export default class RouteBoot {
|
|
|
162
171
|
// разбор маршрута после логина и welcome (только лобби-режим)
|
|
163
172
|
async _bootRoute() {
|
|
164
173
|
const route = parseRoute(this._ui.getHash());
|
|
174
|
+
// метка автовхода снимается с любым маршрутом: живёт только до него
|
|
175
|
+
const rejoin = takeAutoRejoin(this._storage(), this._wallNow());
|
|
165
176
|
const pending = this._takeRoutePromotion(route);
|
|
166
177
|
|
|
167
178
|
if (pending && (await this._promoteCold(pending))) {
|
|
@@ -184,6 +195,13 @@ export default class RouteBoot {
|
|
|
184
195
|
|
|
185
196
|
switch (decision.action) {
|
|
186
197
|
case 'join':
|
|
198
|
+
if (
|
|
199
|
+
rejoin?.gameId === decision.gameId &&
|
|
200
|
+
rejoin.roomId === decision.roomId
|
|
201
|
+
) {
|
|
202
|
+
this._autoRejoin?.arm(rejoin);
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
await this._joinFromRoute(decision.gameId, decision.roomId);
|
|
188
206
|
break;
|
|
189
207
|
|
|
@@ -375,15 +393,7 @@ export default class RouteBoot {
|
|
|
375
393
|
// холодный промоушен (host-migration этап 7.4): страница перезагружена в
|
|
376
394
|
// комнату, чтобы поднять её матч заново. Запись снимается при любом исходе
|
|
377
395
|
_takeRoutePromotion(route) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
try {
|
|
381
|
-
storage = this._getStorage();
|
|
382
|
-
} catch {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const pending = takePendingPromotion(storage);
|
|
396
|
+
const pending = takePendingPromotion(this._storage());
|
|
387
397
|
|
|
388
398
|
return pending &&
|
|
389
399
|
route.kind === 'room' &&
|
|
@@ -393,6 +403,15 @@ export default class RouteBoot {
|
|
|
393
403
|
: null;
|
|
394
404
|
}
|
|
395
405
|
|
|
406
|
+
// sessionStorage или null (хранилище запрещено)
|
|
407
|
+
_storage() {
|
|
408
|
+
try {
|
|
409
|
+
return this._getStorage();
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
396
415
|
// занимает комнату холодным промоушеном. false — вкладка хостить не может,
|
|
397
416
|
// маршрут разбирается как обычный вход гостем
|
|
398
417
|
async _promoteCold(pending) {
|
package/src/client/style.css
CHANGED
|
@@ -177,6 +177,39 @@ body.hide-cursor {
|
|
|
177
177
|
background-color: #333;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/* ячейки с подписью (panel.cells): подпись, значение и шкала */
|
|
181
|
+
#panel table td {
|
|
182
|
+
position: relative;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#panel table td .panel-label {
|
|
186
|
+
opacity: 0.6;
|
|
187
|
+
margin-right: 0.4em;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#panel table td.is-empty {
|
|
191
|
+
opacity: 0.5;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#panel table td div.panel-gauge {
|
|
195
|
+
position: absolute;
|
|
196
|
+
left: 0;
|
|
197
|
+
bottom: 0;
|
|
198
|
+
width: 0;
|
|
199
|
+
height: 2px;
|
|
200
|
+
background-color: currentColor;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@keyframes panel-gauge-fill {
|
|
204
|
+
from {
|
|
205
|
+
width: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
to {
|
|
209
|
+
width: 100%;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
180
213
|
/* bar-поля панели (type: 'bar' в схеме игры) — классы нейтральные,
|
|
181
214
|
без привязки к игровым DOM-id */
|
|
182
215
|
#panel table td div.panel-bar-wrapper {
|
|
@@ -211,6 +244,11 @@ body.hide-cursor {
|
|
|
211
244
|
#panel table td div.panel-bar-blink {
|
|
212
245
|
animation: none;
|
|
213
246
|
}
|
|
247
|
+
|
|
248
|
+
#panel table td div.panel-gauge {
|
|
249
|
+
animation: none !important;
|
|
250
|
+
width: 100%;
|
|
251
|
+
}
|
|
214
252
|
}
|
|
215
253
|
|
|
216
254
|
/*stat*/
|
|
@@ -314,6 +352,29 @@ body.hide-cursor {
|
|
|
314
352
|
text-align: right;
|
|
315
353
|
}
|
|
316
354
|
|
|
355
|
+
/*menu hint*/
|
|
356
|
+
|
|
357
|
+
/* Подсказка личного меню (корзина) под местом HUD связи; игра
|
|
358
|
+
перестилизует её по #menu-hints и .menu-hint.is-on. Селектор по id
|
|
359
|
+
перебивает `.vimp-shell > * { display: none }` */
|
|
360
|
+
#menu-hints {
|
|
361
|
+
display: block;
|
|
362
|
+
position: absolute;
|
|
363
|
+
right: 24px;
|
|
364
|
+
top: calc(50% + 44px);
|
|
365
|
+
z-index: 4;
|
|
366
|
+
pointer-events: none;
|
|
367
|
+
color: #fff;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#menu-hints .menu-hint {
|
|
371
|
+
display: none;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#menu-hints .menu-hint.is-on {
|
|
375
|
+
display: block;
|
|
376
|
+
}
|
|
377
|
+
|
|
317
378
|
/*voice*/
|
|
318
379
|
|
|
319
380
|
/* Нейтральный вид HUD связи; игра перестилизует его по #voice-hud и
|
|
@@ -331,7 +392,7 @@ body.hide-cursor {
|
|
|
331
392
|
gap: 8px;
|
|
332
393
|
color: #fff;
|
|
333
394
|
font-size: 14px;
|
|
334
|
-
text-shadow:
|
|
395
|
+
text-shadow: 0 0 1px #000;
|
|
335
396
|
}
|
|
336
397
|
|
|
337
398
|
#voice-hud.is-talking,
|
|
@@ -358,6 +419,12 @@ body.hide-cursor {
|
|
|
358
419
|
display: flex;
|
|
359
420
|
}
|
|
360
421
|
|
|
422
|
+
/* тёмная обводка: серый и зелёный значок читаются на светлой карте */
|
|
423
|
+
#voice-hud .voice-icon,
|
|
424
|
+
#voice-hud .voice-ring {
|
|
425
|
+
filter: drop-shadow(0 0 1px #000);
|
|
426
|
+
}
|
|
427
|
+
|
|
361
428
|
#voice-hud .voice-icon {
|
|
362
429
|
width: 32px;
|
|
363
430
|
height: 32px;
|
|
@@ -375,17 +442,22 @@ body.hide-cursor {
|
|
|
375
442
|
display: none;
|
|
376
443
|
}
|
|
377
444
|
|
|
445
|
+
#voice-hud .voice-mark {
|
|
446
|
+
position: relative;
|
|
447
|
+
display: flex;
|
|
448
|
+
}
|
|
449
|
+
|
|
378
450
|
#voice-hud .voice-ring {
|
|
379
451
|
display: none;
|
|
380
452
|
position: absolute;
|
|
381
|
-
left:
|
|
382
|
-
top:
|
|
453
|
+
left: 50%;
|
|
454
|
+
top: 50%;
|
|
383
455
|
width: 36px;
|
|
384
456
|
height: 36px;
|
|
385
457
|
fill: none;
|
|
386
458
|
stroke: currentColor;
|
|
387
459
|
stroke-width: 2;
|
|
388
|
-
transform: rotate(-90deg);
|
|
460
|
+
transform: translate(-50%, -50%) rotate(-90deg);
|
|
389
461
|
}
|
|
390
462
|
|
|
391
463
|
#voice-hud.is-talking .voice-ring {
|
|
@@ -489,6 +561,11 @@ body.hide-cursor {
|
|
|
489
561
|
overflow: hidden;
|
|
490
562
|
}
|
|
491
563
|
|
|
564
|
+
/* недоступный пункт личного меню */
|
|
565
|
+
#vote .vote-list li.disabled {
|
|
566
|
+
opacity: 0.4;
|
|
567
|
+
}
|
|
568
|
+
|
|
492
569
|
#vote .vote-nav {
|
|
493
570
|
padding: 0 0 0 5px;
|
|
494
571
|
margin-top: 10px;
|
|
@@ -503,9 +580,9 @@ body.hide-cursor {
|
|
|
503
580
|
display: block;
|
|
504
581
|
}
|
|
505
582
|
|
|
506
|
-
/*room menu
|
|
507
|
-
|
|
508
|
-
|
|
583
|
+
/*room menu: кнопка Exit внутри #panel справа от таблицы
|
|
584
|
+
(RoomMenuView.placeInPanel); order — таблицу PanelView достраивает позже,
|
|
585
|
+
по CONFIG_DATA. Без панели (игра её не показывает) — угол экрана;
|
|
509
586
|
z-index 7 — выше прозрачного #game-informer (6) и ниже лобби (8)*/
|
|
510
587
|
|
|
511
588
|
#room-menu {
|
|
@@ -523,7 +600,7 @@ body.hide-cursor {
|
|
|
523
600
|
flex: 0 0 auto;
|
|
524
601
|
}
|
|
525
602
|
|
|
526
|
-
#room-menu-
|
|
603
|
+
#room-menu-exit {
|
|
527
604
|
display: block;
|
|
528
605
|
margin-left: auto;
|
|
529
606
|
height: 24px;
|
|
@@ -532,29 +609,44 @@ body.hide-cursor {
|
|
|
532
609
|
background: transparent;
|
|
533
610
|
}
|
|
534
611
|
|
|
535
|
-
/* fixed — иначе
|
|
536
|
-
#room-menu-
|
|
612
|
+
/* fixed — иначе статус обрезал бы overflow: hidden панели */
|
|
613
|
+
#room-menu-status {
|
|
537
614
|
position: fixed;
|
|
538
615
|
top: 24px;
|
|
539
616
|
right: 0;
|
|
540
|
-
padding: 4px;
|
|
617
|
+
padding: 2px 4px;
|
|
541
618
|
background: #fff;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
#room-menu-list input {
|
|
545
|
-
display: block;
|
|
546
|
-
width: 100%;
|
|
619
|
+
/* тёмный: фон белый */
|
|
620
|
+
color: #333;
|
|
547
621
|
white-space: nowrap;
|
|
548
|
-
margin-top: 5px;
|
|
549
622
|
}
|
|
550
623
|
|
|
551
|
-
#
|
|
552
|
-
|
|
553
|
-
|
|
624
|
+
/* окно «Leave the game?»: по центру поверх игры, ниже #session-overlay (8) —
|
|
625
|
+
пауза сессии важнее */
|
|
626
|
+
#room-exit-confirm {
|
|
627
|
+
position: fixed;
|
|
628
|
+
top: 50%;
|
|
629
|
+
left: 50%;
|
|
630
|
+
transform: translate(-50%, -50%);
|
|
631
|
+
flex-wrap: wrap;
|
|
632
|
+
justify-content: center;
|
|
633
|
+
gap: 8px;
|
|
634
|
+
padding: 12px 16px;
|
|
635
|
+
background: #fff;
|
|
554
636
|
color: #333;
|
|
637
|
+
font-size: 14px;
|
|
555
638
|
white-space: nowrap;
|
|
556
639
|
}
|
|
557
640
|
|
|
641
|
+
#room-exit-confirm-text {
|
|
642
|
+
flex: 0 0 100%;
|
|
643
|
+
text-align: center;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#room-exit-confirm input {
|
|
647
|
+
min-width: 64px;
|
|
648
|
+
}
|
|
649
|
+
|
|
558
650
|
/*chat*/
|
|
559
651
|
|
|
560
652
|
#chat {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
//-
|
|
2
|
-
//- вкладка в комнате. «Hand over host» — только у хоста, когда передавать
|
|
3
|
-
//- есть кому; статус передачи — на месте заблокированных пунктов
|
|
1
|
+
//- выход из комнаты: только лобби-режим, пока вкладка в комнате
|
|
4
2
|
div#room-menu(style='display:none')
|
|
5
|
-
input#room-menu-
|
|
6
|
-
div#room-menu-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
input#room-menu-exit(type='button', value='Exit', title='Leave the game')
|
|
4
|
+
div#room-menu-status(style='display:none')
|
|
5
|
+
div#room-exit-confirm(style='display:none')
|
|
6
|
+
div#room-exit-confirm-text Leave the game?
|
|
7
|
+
input#room-exit-confirm-yes(type='button', value='Yes')
|
|
8
|
+
input#room-exit-confirm-cancel(type='button', value='Cancel')
|
|
@@ -186,7 +186,9 @@ export default class MicCapture {
|
|
|
186
186
|
try {
|
|
187
187
|
stream = await this._mediaDevices.getUserMedia({
|
|
188
188
|
audio: {
|
|
189
|
-
|
|
189
|
+
// эхоподавление включает системную обработку голоса (macOS) и
|
|
190
|
+
// приглушает весь остальной звук на время открытия и закрытия
|
|
191
|
+
echoCancellation: false,
|
|
190
192
|
noiseSuppression: true,
|
|
191
193
|
autoGainControl: true,
|
|
192
194
|
channelCount: 1,
|
package/src/config/abiOps.js
CHANGED
|
@@ -23,6 +23,43 @@ export const abiOps = createRegistry('abiOps', [
|
|
|
23
23
|
payload: 'none',
|
|
24
24
|
response: 'json',
|
|
25
25
|
},
|
|
26
|
+
// ответ игрока на личное меню: { id, name, path } (путь листа «2.1»)
|
|
27
|
+
{
|
|
28
|
+
value: 'menu.answer',
|
|
29
|
+
since: 4,
|
|
30
|
+
payload: 'json',
|
|
31
|
+
response: 'none',
|
|
32
|
+
},
|
|
33
|
+
// участник покинул матч: { id } — ядро игры удаляет всё, что хранит о нём
|
|
34
|
+
{
|
|
35
|
+
value: 'actor.forget',
|
|
36
|
+
since: 4,
|
|
37
|
+
payload: 'json',
|
|
38
|
+
response: 'none',
|
|
39
|
+
},
|
|
40
|
+
// новая игра: движок обнулил счёт, ядро игры сбрасывает накопленное
|
|
41
|
+
// между раундами
|
|
42
|
+
{
|
|
43
|
+
value: 'match.reset',
|
|
44
|
+
since: 4,
|
|
45
|
+
payload: 'json',
|
|
46
|
+
response: 'none',
|
|
47
|
+
},
|
|
48
|
+
// состояние игры между раундами: запрос { roundOver }, ответ — JSON игры,
|
|
49
|
+
// который переживает мягкую миграцию хоста и эстафету Worker'а
|
|
50
|
+
{
|
|
51
|
+
value: 'state.persist',
|
|
52
|
+
since: 4,
|
|
53
|
+
payload: 'json',
|
|
54
|
+
response: 'json',
|
|
55
|
+
},
|
|
56
|
+
// тот же JSON обратно новому ядру до первого раунда
|
|
57
|
+
{
|
|
58
|
+
value: 'state.restore',
|
|
59
|
+
since: 4,
|
|
60
|
+
payload: 'json',
|
|
61
|
+
response: 'none',
|
|
62
|
+
},
|
|
26
63
|
]);
|
|
27
64
|
|
|
28
65
|
// плоский список активных опкодов — то, что вправе позвать движок
|
|
@@ -33,5 +70,10 @@ export const ABI_OPS = abiOps.values();
|
|
|
33
70
|
// опкод уехал бы в прод, не попав ни в слепок поверхности, ни в
|
|
34
71
|
// CHANGELOG. Имя опкода читается ТОЛЬКО отсюда.
|
|
35
72
|
export const ABI_OP_DEBUG_JSON = 'debug.json';
|
|
73
|
+
export const ABI_OP_MENU_ANSWER = 'menu.answer';
|
|
74
|
+
export const ABI_OP_ACTOR_FORGET = 'actor.forget';
|
|
75
|
+
export const ABI_OP_MATCH_RESET = 'match.reset';
|
|
76
|
+
export const ABI_OP_STATE_PERSIST = 'state.persist';
|
|
77
|
+
export const ABI_OP_STATE_RESTORE = 'state.restore';
|
|
36
78
|
|
|
37
79
|
export default abiOps;
|