vimp-engine 0.39.0 → 0.41.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/boot.js +11 -87
- 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 +1 -1
- package/src/client/components/model/Vote.js +202 -0
- package/src/client/components/view/Auth.js +3 -3
- package/src/client/components/view/Panel.js +176 -6
- package/src/client/components/view/Voice.js +6 -1
- package/src/client/components/view/Vote.js +71 -2
- 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/main.js +47 -90
- package/src/client/network/SessionSupervisor.js +14 -14
- package/src/client/network/WebRtcManager.js +1 -2
- package/src/client/network/policyClose.js +10 -77
- package/src/client/style.css +85 -10
- 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/MicCapture.js +3 -1
- package/src/config/abiOps.js +42 -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/menuName.js +3 -0
- package/src/config/wsports.js +1 -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/c4-component-dependencies.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +2 -2
- package/src/host/GameCoreAdapter.js +83 -4
- package/src/host/HostGame.js +93 -17
- package/src/host/PortMachine.js +4 -21
- package/src/host/commandFailure.js +2 -2
- package/src/host/identity.js +3 -3
- package/src/host/meta/core/CommandProcessor.js +2 -2
- package/src/host/meta/core/RoundManager.js +8 -1
- package/src/host/meta/core/VoteCoordinator.js +56 -1
- package/src/host/meta/modules/Accolades.js +1 -1
- package/src/host/meta/modules/TimerManager.js +16 -2
- package/src/host/meta/modules/Vote.js +65 -0
- package/src/host/meta/player/ParticipantManager.js +19 -3
- package/src/lib/capabilities.js +17 -0
- package/src/lib/createHostRuntime.js +1 -1
- package/src/lib/gameConfigView.js +7 -0
- package/src/lib/loadGamePackage.js +7 -9
- package/src/lib/offlinePlayerData.js +1 -1
- package/src/lib/validators.js +2 -2
- package/src/lib/voicePacket.js +1 -1
- package/src/standalone/index.js +3 -2
- package/src/client/lib/footerLink.js +0 -30
- package/src/client/network/WebSocketTransport.js +0 -78
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;
|
|
@@ -726,8 +803,8 @@ body.hide-cursor {
|
|
|
726
803
|
display: block;
|
|
727
804
|
}
|
|
728
805
|
|
|
729
|
-
/* футеры лобби
|
|
730
|
-
|
|
806
|
+
/* футеры лобби (Terms/Privacy | версия движка | копирайт) и формы входа
|
|
807
|
+
(версия игры | копирайт): одна полоса, оба заполняет JS, поэтому вид
|
|
731
808
|
держится в одном месте (#lobby-footer лежит в секции lobby ниже только
|
|
732
809
|
разметкой) */
|
|
733
810
|
#auth-link,
|
|
@@ -761,9 +838,8 @@ body.hide-cursor {
|
|
|
761
838
|
padding-left: 10px;
|
|
762
839
|
}
|
|
763
840
|
|
|
764
|
-
/*
|
|
765
|
-
|
|
766
|
-
и при space-between версия съезжала бы с центра */
|
|
841
|
+
/* равные ячейки: версия игры у левого края, копирайт у правого; пустая
|
|
842
|
+
версия (у пакета игры может не быть метаданных) не сдвигает копирайт */
|
|
767
843
|
#auth-link p {
|
|
768
844
|
margin: 0;
|
|
769
845
|
flex: 1;
|
|
@@ -781,7 +857,6 @@ body.hide-cursor {
|
|
|
781
857
|
text-align: left;
|
|
782
858
|
}
|
|
783
859
|
|
|
784
|
-
#auth-version,
|
|
785
860
|
#lobby-version {
|
|
786
861
|
text-align: center;
|
|
787
862
|
}
|
|
@@ -55,9 +55,7 @@ const SHELL = [
|
|
|
55
55
|
tag: 'div',
|
|
56
56
|
id: 'auth-link',
|
|
57
57
|
children: [
|
|
58
|
-
// версия пакета ИГРЫ (из манифеста)
|
|
59
|
-
// (только в dev-сборке) — заполняет main.js по AUTH_DATA
|
|
60
|
-
{ tag: 'p', children: [{ tag: 'a', id: 'auth-docs-link' }] },
|
|
58
|
+
// версия пакета ИГРЫ (из манифеста) — заполняет main.js по AUTH_DATA
|
|
61
59
|
{ tag: 'p', id: 'auth-version' },
|
|
62
60
|
{ tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
|
|
63
61
|
],
|
|
@@ -67,7 +65,7 @@ const SHELL = [
|
|
|
67
65
|
],
|
|
68
66
|
},
|
|
69
67
|
{ tag: 'div', id: 'game-informer' },
|
|
70
|
-
// оверлей переподключения (host-migration этап 4): в solo
|
|
68
|
+
// оверлей переподключения (host-migration этап 4): в solo не
|
|
71
69
|
// показывается, но разметка общая с informer.pug
|
|
72
70
|
{
|
|
73
71
|
tag: 'div',
|
|
@@ -16,9 +16,6 @@ div#auth
|
|
|
16
16
|
|
|
17
17
|
div#auth-link
|
|
18
18
|
//- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
|
|
19
|
-
//- пакета)
|
|
20
|
-
//- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
|
|
21
|
-
p
|
|
22
|
-
a#auth-docs-link
|
|
19
|
+
//- пакета) — проставляет main.js по AUTH_DATA; без данных ячейка пустая
|
|
23
20
|
p#auth-version
|
|
24
21
|
p © #{new Date().getFullYear()} VIMP
|
|
@@ -32,8 +32,7 @@ div#lobby(style='display:none')
|
|
|
32
32
|
label.form-label(for='lobby-game')= 'Select game'
|
|
33
33
|
select#lobby-game.field-select
|
|
34
34
|
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
35
|
-
//- манифеста активной игры (docs
|
|
36
|
-
//- schema")
|
|
35
|
+
//- манифеста активной игры (VIMP docs: plugin-api.md § "Form schema")
|
|
37
36
|
div#lobby-fields
|
|
38
37
|
//- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
|
|
39
38
|
//- плагина выбранной игры — строкой в форме, а не в #tech-informer:
|
|
@@ -67,15 +66,13 @@ div#lobby(style='display:none')
|
|
|
67
66
|
span#leaderboard-total
|
|
68
67
|
ol#lobby-leaderboard-list
|
|
69
68
|
p#lobby-my-placement
|
|
70
|
-
//- футер:
|
|
71
|
-
//-
|
|
69
|
+
//- футер: Terms/Privacy, версия пакета движка (проставляет LobbyView,
|
|
70
|
+
//- engineVersion.js), копирайт
|
|
72
71
|
div#lobby-footer
|
|
73
72
|
p
|
|
74
73
|
a#lobby-terms-link(target='_blank', rel='noopener') Terms of Service
|
|
75
74
|
| ·
|
|
76
75
|
|
|
|
77
76
|
a#lobby-privacy-link(target='_blank', rel='noopener') Privacy Policy
|
|
78
|
-
p
|
|
79
|
-
a#lobby-link(href='https://docs.vimp.lgick.dev', target='_blank', rel='noopener') docs
|
|
80
77
|
p#lobby-version
|
|
81
78
|
p © #{new Date().getFullYear()} VIMP
|
|
@@ -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;
|
package/src/config/closeCodes.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
// коды закрытия транспорта — общий контракт
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// каждому коду из этой карты
|
|
1
|
+
// коды закрытия транспорта — общий контракт сигналинга мастера и
|
|
2
|
+
// Worker-хоста, тот же приём, что в gameCodes.js: значение не должно
|
|
3
|
+
// расходиться между ними молча. Игровой транспорт клиента кода не передаёт
|
|
4
|
+
// (у data channel его нет): причину кика или отказа клиент узнаёт по
|
|
5
|
+
// TECH_INFORM перед закрытием
|
|
7
6
|
export default {
|
|
8
7
|
staleHost: 4000, // master/SignalingServer.js — сигнальный WS хоста протух
|
|
9
|
-
invalidOrigin: 4001, // master/SignalingServer.js
|
|
8
|
+
invalidOrigin: 4001, // master/SignalingServer.js
|
|
10
9
|
// 4002 retired (server rating removed) — номер не переиспользуется
|
|
11
10
|
kickForMaxLatency: 4003, // host/HostGame.js
|
|
12
11
|
kickForMissedPings: 4004, // host/HostGame.js
|
|
13
12
|
kickIdle: 4005, // host/HostGame.js
|
|
14
13
|
roomFull: 4006, // host/PortMachine.js
|
|
15
14
|
// 4007 свободен
|
|
16
|
-
handshakeTimeout: 4008, //
|
|
17
|
-
tooManyConnections: 4009, // dedicated/main.js
|
|
15
|
+
handshakeTimeout: 4008, // host/PortMachine.js — нет RESUME_REQUEST вовремя
|
|
18
16
|
};
|
package/src/config/gameCodes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
|
|
2
|
-
// docs.
|
|
2
|
+
// VIMP docs: snapshot.md) — общий контракт хоста (SocketManager) и
|
|
3
3
|
// клиента (main.js): значение не должно расходиться между ними молча
|
|
4
4
|
export default {
|
|
5
5
|
winnerTeam: [0],
|
|
@@ -66,8 +66,8 @@ export default {
|
|
|
66
66
|
|
|
67
67
|
// возобновление сессии (host-migration этап 4). Сколько хост держит место
|
|
68
68
|
// участника, чей транспорт оборвался без LEAVE: слот занят, актор в мире,
|
|
69
|
-
// клавиши отпущены. Включает его только Worker лобби —
|
|
70
|
-
//
|
|
69
|
+
// клавиши отпущены. Включает его только Worker лобби — standalone
|
|
70
|
+
// поднимает PortMachine без этой опции (0 — снять сразу)
|
|
71
71
|
resumeGraceMs: 20000,
|
|
72
72
|
|
|
73
73
|
// сколько возобновляющее соединение может молчать до RESUME_REQUEST
|
package/src/config/lobby.js
CHANGED
|
@@ -50,9 +50,8 @@ export default {
|
|
|
50
50
|
// клиент берёт не из assetsBase, а по отдельному URL, и версионность
|
|
51
51
|
// каталога обязана доехать и сюда, иначе комната на застейдженной версии
|
|
52
52
|
// играла бы на картах одобренной. `mapsBase` проставляет мастер при
|
|
53
|
-
// ребейзе версионного каталога; его отсутствие (dev, standalone,
|
|
54
|
-
//
|
|
55
|
-
// путь по id
|
|
53
|
+
// ребейзе версионного каталога; его отсутствие (dev, standalone, старый
|
|
54
|
+
// мастер) — законный случай, тогда работает путь по id
|
|
56
55
|
maps: {
|
|
57
56
|
manifestUrl: manifest =>
|
|
58
57
|
`${manifest.mapsBase ?? `/games/${manifest.id}/maps`}/manifest.json`,
|
|
@@ -305,7 +304,7 @@ export default {
|
|
|
305
304
|
debugReportUrl: '/debug/report',
|
|
306
305
|
|
|
307
306
|
// журнал клиентских ошибок: приём на том же боксе,
|
|
308
|
-
// что раздал
|
|
307
|
+
// что раздал страницу
|
|
309
308
|
clientReportUrl: '/client-reports',
|
|
310
309
|
|
|
311
310
|
// размер страницы для «Загрузить ещё» (offset/limit к мастеру)
|
package/src/config/wsports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Номера портов JSON-канала (docs.
|
|
1
|
+
// Номера портов JSON-канала (VIMP docs: snapshot.md). Реестр
|
|
2
2
|
// append-only, как и остальные словари плагинной поверхности (этап 3 плана
|
|
3
3
|
// plugin-forward-compat): номер НИКОГДА не переиспользуется и не
|
|
4
4
|
// перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
|
|
@@ -39,7 +39,7 @@ const RECORDED_PORTS = {
|
|
|
39
39
|
const FLUSHING_METHODS = new Set(['sendClear', 'sendMap', 'sendFirstShot']);
|
|
40
40
|
|
|
41
41
|
// В headless-контуре нет ни мастера, ни auth-сервиса — участник стартует с
|
|
42
|
-
// пустым профилем (lib/offlinePlayerData.js, общее со standalone
|
|
42
|
+
// пустым профилем (lib/offlinePlayerData.js, общее со standalone).
|
|
43
43
|
const emptyProfileFetch = offlinePlayerData();
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -13,7 +13,7 @@ import { isFixture } from './pluginLoader.js';
|
|
|
13
13
|
// Ввод начинается на 40-м тике, а не сразу: кадр спавна с force_reset
|
|
14
14
|
// приходит примерно на interpolation.delay (движковый дефолт — 100 мс, тут
|
|
15
15
|
// ~3× запаса) позже входа и чистит удержанные клавиши предиктора
|
|
16
|
-
// (docs.
|
|
16
|
+
// (VIMP docs: debugging.md) — встроенный сценарий не должен
|
|
17
17
|
// демонстрировать ровно ту ловушку, от которой предостерегает документация
|
|
18
18
|
const INPUT_DOWN_TICK = 40;
|
|
19
19
|
const INPUT_UP_TICK = 100;
|
|
@@ -26,7 +26,7 @@ const FIXTURE_UNUSED_KEYS = ['e1'];
|
|
|
26
26
|
const firstKey = dict => Object.keys(dict ?? {})[0];
|
|
27
27
|
|
|
28
28
|
// Клавиша для смоука обязана быть удерживаемой: по конвенции playerKeys
|
|
29
|
-
// (docs
|
|
29
|
+
// (VIMP docs: plugin-api.md) `type: 1` — триггер, у которого 'up'
|
|
30
30
|
// игнорируется, и «нажал — отпустил» не даёт ни удержания, ни движения.
|
|
31
31
|
// Порядок объявления в playerKeys контрактом не задан, поэтому берём первую
|
|
32
32
|
// без type; если игра объявила одни триггеры — первую любую, смоук всё равно
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ERROR, skip, verdict } from '../result.js';
|
|
2
2
|
|
|
3
|
-
// Набор npm-скриптов пакета игры (docs.
|
|
3
|
+
// Набор npm-скриптов пакета игры (VIMP docs: packaging.md). Это не
|
|
4
4
|
// вкусовое требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
5
|
// имени, а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
6
6
|
// ядра.
|
|
@@ -3,7 +3,7 @@ import { ERROR, skip, verdict } from '../result.js';
|
|
|
3
3
|
// vite.config.js проверяется как текст, а не импортом: конфиг —
|
|
4
4
|
// функция от mode и вне Vite не исполняется. Каждый пункт здесь ловит
|
|
5
5
|
// молчаливую поломку сборки, а не стилистику
|
|
6
|
-
// (docs.
|
|
6
|
+
// (VIMP docs: packaging.md): без preserveEntrySignatures
|
|
7
7
|
// default-экспорт плагина вытряхивает tree-shaking, а build.lib всегда инлайнит
|
|
8
8
|
// ассеты.
|
|
9
9
|
const REQUIRED = [
|
|
@@ -4,7 +4,7 @@ import { SERVICES } from '../../../config/clientServices.js';
|
|
|
4
4
|
// Движковый пул сервисов держит реестр (config/clientServices.js), но пул
|
|
5
5
|
// ими не исчерпывается: игра доливает туда свои через
|
|
6
6
|
// ClientPlugin.hooks.services(core) — например геометрию предсказанной
|
|
7
|
-
// динамики карты (docs
|
|
7
|
+
// динамики карты (VIMP docs: plugin-api.md § "Services").
|
|
8
8
|
// Незнакомое имя не ошибка для движка: part получает undefined и рисует пустоту
|
|
9
9
|
// — карта без assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
10
|
// ради этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
@@ -484,7 +484,7 @@ function predictionDrift({ clients, scenario }) {
|
|
|
484
484
|
// Компоненты player-блока — игровая раскладка, движок знает только их
|
|
485
485
|
// порядок, поэтому нарушение адресуется индексом. Исключение — уровень 0
|
|
486
486
|
// (source 'camera'): там движок сравнивает ровно мировые x/y, и называть их
|
|
487
|
-
// «#0/#1» значит прятать причину (см. docs.
|
|
487
|
+
// «#0/#1» значит прятать причину (см. VIMP docs: debugging.md).
|
|
488
488
|
const CAMERA_COMPONENTS = ['x', 'y'];
|
|
489
489
|
|
|
490
490
|
function formatDivergence(record) {
|
|
@@ -12,7 +12,7 @@ import { loadGamePackage } from '../lib/loadGamePackage.js';
|
|
|
12
12
|
// 3. ничего — фикстура miniGame из этого репозитория.
|
|
13
13
|
//
|
|
14
14
|
// entries.wasmNode — новое ОПЦИОНАЛЬНОЕ поле манифеста: конвенция
|
|
15
|
-
// core/pkg-node/ уже описана в docs.
|
|
15
|
+
// core/pkg-node/ уже описана в VIMP docs: packaging.md, не хватало
|
|
16
16
|
// только пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
17
|
// прогоняется headless своим настоящим ядром.
|
|
18
18
|
|
|
@@ -51,7 +51,7 @@ export async function loadGameForSim({ game = null, core = null } = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// путь --game указывает либо на пакет игры, либо прямо на манифест; сама
|
|
54
|
-
// загрузка
|
|
54
|
+
// загрузка — lib/loadGamePackage.js.
|
|
55
55
|
// client: true — прогону виртуальные клиенты обязательны, и отказ на
|
|
56
56
|
// загрузке пакета лучше отказа посреди сценария
|
|
57
57
|
async function loadFromManifest(game, core) {
|
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
// (spawn_scripted_actor/remove_scripted_actor), человек — только танк
|
|
11
11
|
// (spawn_actor/remove_actor).
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ABI_OP_ACTOR_FORGET,
|
|
15
|
+
ABI_OP_DEBUG_JSON,
|
|
16
|
+
ABI_OP_MATCH_RESET,
|
|
17
|
+
ABI_OP_MENU_ANSWER,
|
|
18
|
+
ABI_OP_STATE_PERSIST,
|
|
19
|
+
ABI_OP_STATE_RESTORE,
|
|
20
|
+
} from '../config/abiOps.js';
|
|
14
21
|
import { readCoreAbi, dispatchCoreOp } from '../lib/coreAbi.js';
|
|
15
22
|
|
|
16
23
|
export default class GameCoreAdapter {
|
|
@@ -26,7 +33,7 @@ export default class GameCoreAdapter {
|
|
|
26
33
|
this._core = core;
|
|
27
34
|
this._participants = participants;
|
|
28
35
|
this._onCoreEvent = onCoreEvent;
|
|
29
|
-
this._services = {}; // { vimp, panel } — инъекция как у Game.js
|
|
36
|
+
this._services = {}; // { vimp, panel, vote } — инъекция как у Game.js
|
|
30
37
|
|
|
31
38
|
// Метода нет (или самоописание нечитаемо) — ядро собрано до появления
|
|
32
39
|
// механизма. Это не ошибка: поколение 0, ни одного опционального опкода
|
|
@@ -44,7 +51,7 @@ export default class GameCoreAdapter {
|
|
|
44
51
|
return this._abi;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// получает сервисы (аналог Game.injectServices): { vimp, panel }
|
|
54
|
+
// получает сервисы (аналог Game.injectServices): { vimp, panel, vote }
|
|
48
55
|
injectServices(services) {
|
|
49
56
|
Object.assign(this._services, services);
|
|
50
57
|
}
|
|
@@ -159,7 +166,7 @@ export default class GameCoreAdapter {
|
|
|
159
166
|
// vimp }); 'custom' (игровой смысл вне словаря) — в HostPlugin.onCoreEvent
|
|
160
167
|
_drainEvents() {
|
|
161
168
|
const events = JSON.parse(this._core.take_events());
|
|
162
|
-
const { panel, vimp } = this._services;
|
|
169
|
+
const { panel, vimp, vote } = this._services;
|
|
163
170
|
|
|
164
171
|
for (const event of events) {
|
|
165
172
|
switch (event.type) {
|
|
@@ -182,6 +189,20 @@ export default class GameCoreAdapter {
|
|
|
182
189
|
});
|
|
183
190
|
break;
|
|
184
191
|
|
|
192
|
+
case 'menu':
|
|
193
|
+
this._pushPersonal(vote, event.id, { menu: event.menu });
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'menuClose':
|
|
197
|
+
this._pushPersonal(vote, event.id, { menuClose: event.name });
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'menuHint':
|
|
201
|
+
this._pushPersonal(vote, event.id, {
|
|
202
|
+
menuHint: { name: event.name, on: event.on },
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
|
|
185
206
|
case 'custom':
|
|
186
207
|
this._onCoreEvent?.(event.data, this._services);
|
|
187
208
|
break;
|
|
@@ -189,6 +210,60 @@ export default class GameCoreAdapter {
|
|
|
189
210
|
}
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
// личное сообщение меню адресату; получатель — только человек с сокетом
|
|
214
|
+
// (бот и ушедший id отбрасываются: у них нет личной очереди)
|
|
215
|
+
_pushPersonal(vote, id, payload) {
|
|
216
|
+
const gameId = String(id);
|
|
217
|
+
|
|
218
|
+
if (vote && this._participants.get(gameId)?.isNetworked) {
|
|
219
|
+
vote.pushPersonal(gameId, payload);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ***** опкоды игры ***** //
|
|
224
|
+
|
|
225
|
+
// ответ игрока на личное меню; ядро без опкода молча игнорирует
|
|
226
|
+
menuAnswer(gameId, name, path) {
|
|
227
|
+
this._op(
|
|
228
|
+
ABI_OP_MENU_ANSWER,
|
|
229
|
+
encodeJson({ id: Number(gameId), name, path }),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// участник покинул матч (в том числе наблюдатель)
|
|
234
|
+
forgetActor(gameId) {
|
|
235
|
+
this._op(ABI_OP_ACTOR_FORGET, encodeJson({ id: Number(gameId) }));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// новая игра: движок обнулил счёт
|
|
239
|
+
resetMatch() {
|
|
240
|
+
this._op(ABI_OP_MATCH_RESET, encodeJson({}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Состояние игры между раундами для мягкой миграции и эстафеты.
|
|
245
|
+
* @param {{roundOver: boolean}} request - `roundOver` — раунд закончился,
|
|
246
|
+
* но ядро его ещё не подвело (эстафета на границе раунда).
|
|
247
|
+
* @returns {Object|null} JSON игры; `null` — ядро опкода не знает.
|
|
248
|
+
*/
|
|
249
|
+
persistState({ roundOver }) {
|
|
250
|
+
const { handled, bytes } = this._op(
|
|
251
|
+
ABI_OP_STATE_PERSIST,
|
|
252
|
+
encodeJson({ roundOver }),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (handled && bytes !== null) {
|
|
256
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// отдаёт новому ядру JSON, снятый persistState
|
|
263
|
+
restorePersisted(state) {
|
|
264
|
+
this._op(ABI_OP_STATE_RESTORE, encodeJson(state));
|
|
265
|
+
}
|
|
266
|
+
|
|
192
267
|
// ***** упаковка снапшота ***** //
|
|
193
268
|
|
|
194
269
|
// пакует broadcast-тело кадра (один раз за отправляемый кадр)
|
|
@@ -333,6 +408,10 @@ export default class GameCoreAdapter {
|
|
|
333
408
|
}
|
|
334
409
|
}
|
|
335
410
|
|
|
411
|
+
function encodeJson(value) {
|
|
412
|
+
return new TextEncoder().encode(JSON.stringify(value));
|
|
413
|
+
}
|
|
414
|
+
|
|
336
415
|
// JsError wasm-bindgen и обычный Error — у обоих есть message; строка из
|
|
337
416
|
// throw внутри фейка — тоже ответ
|
|
338
417
|
function errorText(e) {
|