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/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.
|
|
3
|
+
version = "0.24.0"
|
|
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.41.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",
|
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
|
}
|
|
@@ -24,6 +24,27 @@ export default class VoteCtrl {
|
|
|
24
24
|
// {name: 'templateName', params: ['p1',..], values: ['v1',..] || 'values'}
|
|
25
25
|
// ['val1', 'val2'];
|
|
26
26
|
open(data) {
|
|
27
|
+
// личное меню хоста: проверяется раньше шаблона голосования (тоже объект)
|
|
28
|
+
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
29
|
+
if (Object.hasOwn(data, 'menu')) {
|
|
30
|
+
this._model.openPersonal(data.menu);
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Object.hasOwn(data, 'menuClose')) {
|
|
36
|
+
this._model.closePersonal(data.menuClose);
|
|
37
|
+
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Object.hasOwn(data, 'menuHint')) {
|
|
42
|
+
this._model.setHint(data.menuHint?.name, data.menuHint?.on);
|
|
43
|
+
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
// если данные - массив (values для созданного голосования)
|
|
28
49
|
if (Array.isArray(data)) {
|
|
29
50
|
this._model.updateValues(data);
|
|
@@ -64,6 +85,11 @@ export default class VoteCtrl {
|
|
|
64
85
|
this._model.closeEngineVote(name);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
// CLEAR: личное меню закрывается, подсказки гаснут
|
|
89
|
+
clearPersonal() {
|
|
90
|
+
this._model.clearPersonal();
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
// голосования хоста не пережили переподключение
|
|
68
94
|
removeHostVotes() {
|
|
69
95
|
this._model.removeHostVotes();
|
|
@@ -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 ControlsModel
|
|
4
5
|
|
|
@@ -120,8 +121,15 @@ export default class ControlsModel {
|
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// цифры открытого окна голосования/меню — только ответ окну, в игру
|
|
125
|
+
// не уходят; прочие клавиши идут и в игру (танк ездит при меню)
|
|
126
|
+
const isVoteAnswer =
|
|
127
|
+
this._currentModes.vote &&
|
|
128
|
+
keyCode >= VOTE_ANSWER_KEYS[0] &&
|
|
129
|
+
keyCode <= VOTE_ANSWER_KEYS[1];
|
|
130
|
+
|
|
123
131
|
// если клавиша ещё не зажата
|
|
124
|
-
if (!this._pressedKeys[keyCode]) {
|
|
132
|
+
if (!isVoteAnswer && !this._pressedKeys[keyCode]) {
|
|
125
133
|
const name = this._currentKeySet[keyCode];
|
|
126
134
|
|
|
127
135
|
if (name) {
|
|
@@ -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 = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
2
|
import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
3
|
+
import { MENU_NAME_PATTERN } from '../../../config/menuName.js';
|
|
3
4
|
|
|
4
5
|
// Singleton VoteModel
|
|
5
6
|
|
|
@@ -7,6 +8,44 @@ import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
|
7
8
|
// его ведёт мастер, а не хост, и игра таких имён не объявляет
|
|
8
9
|
const isEngineVote = name => typeof name === 'string' && name.startsWith('@');
|
|
9
10
|
|
|
11
|
+
// ограничения данных личного меню: хост недоверенный
|
|
12
|
+
const PERSONAL_MAX_ITEMS = 7;
|
|
13
|
+
const PERSONAL_MAX_DEPTH = 2;
|
|
14
|
+
const PERSONAL_MAX_TEXT = 64;
|
|
15
|
+
|
|
16
|
+
// узлы личного меню: усечённые копии, глубина ≤ 2, пунктов ≤ 7
|
|
17
|
+
function sanitizeNodes(nodes, depth) {
|
|
18
|
+
if (!Array.isArray(nodes)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (nodes.length > PERSONAL_MAX_ITEMS) {
|
|
23
|
+
console.warn(
|
|
24
|
+
`[vote] personal menu level has ${nodes.length} items, max ${PERSONAL_MAX_ITEMS}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return nodes.slice(0, PERSONAL_MAX_ITEMS).map(node => {
|
|
29
|
+
const out = {
|
|
30
|
+
text: String(node?.text ?? '').slice(0, PERSONAL_MAX_TEXT),
|
|
31
|
+
disabled: node?.disabled === true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (depth < PERSONAL_MAX_DEPTH) {
|
|
35
|
+
const children = sanitizeNodes(node?.children, depth + 1);
|
|
36
|
+
|
|
37
|
+
if (children.length > 0) {
|
|
38
|
+
out.children = children;
|
|
39
|
+
} else if (node?.children !== undefined) {
|
|
40
|
+
// объявленная группа без валидных детей не должна стать листом
|
|
41
|
+
out.disabled = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
10
49
|
let voteModel;
|
|
11
50
|
|
|
12
51
|
export default class VoteModel {
|
|
@@ -41,6 +80,11 @@ export default class VoteModel {
|
|
|
41
80
|
// затирает чужое и не затирается им (этап 10)
|
|
42
81
|
this._queue = [];
|
|
43
82
|
|
|
83
|
+
// личное меню хоста (type 'personal'): { name, style, title, items,
|
|
84
|
+
// path }, path — индексы открытых групп; подсказки — имя → горит ли
|
|
85
|
+
this._personal = null;
|
|
86
|
+
this._hints = new Map();
|
|
87
|
+
|
|
44
88
|
this._title = null; // заголовок голосования
|
|
45
89
|
this._values = []; // все значения голосования
|
|
46
90
|
|
|
@@ -124,6 +168,137 @@ export default class VoteModel {
|
|
|
124
168
|
if (!(this._type === 'vote' && isEngineVote(this._voteName))) {
|
|
125
169
|
this.complete();
|
|
126
170
|
}
|
|
171
|
+
|
|
172
|
+
this._clearHints();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Личное меню хоста: заменяет любое содержимое окна и начинается с
|
|
177
|
+
* 1-го уровня; открытое голосование движка уходит в начало очереди.
|
|
178
|
+
* @param {Object} menu - { name, style, title, items }.
|
|
179
|
+
* @returns {boolean} данные приняты.
|
|
180
|
+
*/
|
|
181
|
+
openPersonal(menu) {
|
|
182
|
+
if (
|
|
183
|
+
!menu ||
|
|
184
|
+
!MENU_NAME_PATTERN.test(menu.name) ||
|
|
185
|
+
!MENU_NAME_PATTERN.test(menu.style)
|
|
186
|
+
) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
this._showing &&
|
|
192
|
+
this._type === 'vote' &&
|
|
193
|
+
isEngineVote(this._voteName)
|
|
194
|
+
) {
|
|
195
|
+
this._queue.unshift({
|
|
196
|
+
engine: {
|
|
197
|
+
name: this._voteName,
|
|
198
|
+
title: this._title,
|
|
199
|
+
values: this._values,
|
|
200
|
+
deadline: this._deadline,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this._waitingValues = false;
|
|
206
|
+
this._timeOff = true;
|
|
207
|
+
this._deadline = null;
|
|
208
|
+
this._type = 'personal';
|
|
209
|
+
this._voteName = '';
|
|
210
|
+
this._personal = {
|
|
211
|
+
name: menu.name,
|
|
212
|
+
style: menu.style,
|
|
213
|
+
title: String(menu.title ?? '').slice(0, PERSONAL_MAX_TEXT),
|
|
214
|
+
items: sanitizeNodes(menu.items, 1),
|
|
215
|
+
path: [],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.show();
|
|
219
|
+
this.open();
|
|
220
|
+
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// закрывает окно, если оно показывает личное меню `name`
|
|
225
|
+
closePersonal(name) {
|
|
226
|
+
if (
|
|
227
|
+
this._showing &&
|
|
228
|
+
this._type === 'personal' &&
|
|
229
|
+
this._personal.name === name
|
|
230
|
+
) {
|
|
231
|
+
this.complete();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// зажигает/гасит подсказку меню
|
|
236
|
+
setHint(name, on) {
|
|
237
|
+
if (!MENU_NAME_PATTERN.test(name)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._hints.set(name, on === true);
|
|
242
|
+
this.publisher.emit('hint', { name, on: on === true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// CLEAR: личное меню закрывается, подсказки гаснут
|
|
246
|
+
clearPersonal() {
|
|
247
|
+
if (this._showing && this._type === 'personal') {
|
|
248
|
+
this.complete();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._clearHints();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_clearHints() {
|
|
255
|
+
for (const [name, on] of this._hints) {
|
|
256
|
+
if (on) {
|
|
257
|
+
this.setHint(name, false);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// пункты текущего уровня личного меню
|
|
263
|
+
_personalLevel() {
|
|
264
|
+
let nodes = this._personal.items;
|
|
265
|
+
|
|
266
|
+
for (const index of this._personal.path) {
|
|
267
|
+
nodes = nodes[index].children;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return nodes;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// цифра в личном меню: 0 — выход, 8/9 — страниц нет, 1–7 — пункт
|
|
274
|
+
_updatePersonal(number) {
|
|
275
|
+
if (number === 0) {
|
|
276
|
+
this.complete();
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const node = this._personalLevel()[number - 1];
|
|
282
|
+
|
|
283
|
+
if (number >= 8 || !node || node.disabled) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (node.children) {
|
|
288
|
+
this._personal.path.push(number - 1);
|
|
289
|
+
this.show();
|
|
290
|
+
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const answer = [...this._personal.path, number - 1]
|
|
295
|
+
.map(index => index + 1)
|
|
296
|
+
.join('.');
|
|
297
|
+
const name = this._personal.name;
|
|
298
|
+
|
|
299
|
+
// окно закрывается ДО отправки (синхронный транспорт, см. ниже)
|
|
300
|
+
this.complete();
|
|
301
|
+
this.publisher.emit('socket', [`menu:${name}`, answer]);
|
|
127
302
|
}
|
|
128
303
|
|
|
129
304
|
_startEngineVote({ name, title, values, deadline }) {
|
|
@@ -220,6 +395,12 @@ export default class VoteModel {
|
|
|
220
395
|
number = String.fromCharCode(keyCode);
|
|
221
396
|
number = parseInt(number, 10);
|
|
222
397
|
|
|
398
|
+
if (this._type === 'personal') {
|
|
399
|
+
this._updatePersonal(number);
|
|
400
|
+
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
223
404
|
// exit
|
|
224
405
|
if (number === 0) {
|
|
225
406
|
this.complete();
|
|
@@ -277,6 +458,27 @@ export default class VoteModel {
|
|
|
277
458
|
|
|
278
459
|
// отображает голосование
|
|
279
460
|
show() {
|
|
461
|
+
if (this._type === 'personal') {
|
|
462
|
+
this._showing = true;
|
|
463
|
+
this.publisher.emit('clear', this._timerId);
|
|
464
|
+
|
|
465
|
+
this.publisher.emit('vote', {
|
|
466
|
+
title: this._personal.title,
|
|
467
|
+
list: this._personalLevel().map(node => ({
|
|
468
|
+
text: node.text,
|
|
469
|
+
disabled: node.disabled,
|
|
470
|
+
group: Boolean(node.children),
|
|
471
|
+
})),
|
|
472
|
+
back: false,
|
|
473
|
+
more: false,
|
|
474
|
+
time: null,
|
|
475
|
+
style: this._personal.style,
|
|
476
|
+
level: this._personal.path.length + 1,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
280
482
|
const begin = this._currentPage * 7;
|
|
281
483
|
const max = begin + 7;
|
|
282
484
|
let currentValues = [];
|
|
@@ -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;
|