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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/bin/vimp-sim.js +2 -2
  3. package/core/Cargo.toml +2 -2
  4. package/package.json +3 -4
  5. package/src/client/boot.js +11 -87
  6. package/src/client/components/controller/Vote.js +26 -0
  7. package/src/client/components/model/Controls.js +9 -1
  8. package/src/client/components/model/RoomMenu.js +1 -1
  9. package/src/client/components/model/Vote.js +202 -0
  10. package/src/client/components/view/Auth.js +3 -3
  11. package/src/client/components/view/Panel.js +176 -6
  12. package/src/client/components/view/Voice.js +6 -1
  13. package/src/client/components/view/Vote.js +71 -2
  14. package/src/client/lib/formBuilder.js +7 -6
  15. package/src/client/lib/hostVoteCommand.js +2 -2
  16. package/src/client/lib/pickActiveGame.js +3 -12
  17. package/src/client/lib/retryOnNetworkError.js +0 -9
  18. package/src/client/main.js +47 -90
  19. package/src/client/network/SessionSupervisor.js +14 -14
  20. package/src/client/network/WebRtcManager.js +1 -2
  21. package/src/client/network/policyClose.js +10 -77
  22. package/src/client/style.css +85 -10
  23. package/src/client/views/gameShell.js +2 -4
  24. package/src/client/views/includes/auth.pug +1 -4
  25. package/src/client/views/includes/lobby.pug +3 -6
  26. package/src/client/voice/MicCapture.js +3 -1
  27. package/src/config/abiOps.js +42 -0
  28. package/src/config/closeCodes.js +7 -9
  29. package/src/config/gameCodes.js +1 -1
  30. package/src/config/hostDefaults.js +2 -2
  31. package/src/config/lobby.js +3 -4
  32. package/src/config/menuName.js +3 -0
  33. package/src/config/wsports.js +1 -1
  34. package/src/devtools/ScenarioRunner.js +1 -1
  35. package/src/devtools/builtinScenario.js +2 -2
  36. package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
  37. package/src/devtools/contract/rules/a4-vite-config.js +1 -1
  38. package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
  39. package/src/devtools/invariants.js +1 -1
  40. package/src/devtools/pluginLoader.js +2 -2
  41. package/src/host/GameCoreAdapter.js +83 -4
  42. package/src/host/HostGame.js +93 -17
  43. package/src/host/PortMachine.js +4 -21
  44. package/src/host/commandFailure.js +2 -2
  45. package/src/host/identity.js +3 -3
  46. package/src/host/meta/core/CommandProcessor.js +2 -2
  47. package/src/host/meta/core/RoundManager.js +8 -1
  48. package/src/host/meta/core/VoteCoordinator.js +56 -1
  49. package/src/host/meta/modules/Accolades.js +1 -1
  50. package/src/host/meta/modules/TimerManager.js +16 -2
  51. package/src/host/meta/modules/Vote.js +65 -0
  52. package/src/host/meta/player/ParticipantManager.js +19 -3
  53. package/src/lib/capabilities.js +17 -0
  54. package/src/lib/createHostRuntime.js +1 -1
  55. package/src/lib/gameConfigView.js +7 -0
  56. package/src/lib/loadGamePackage.js +7 -9
  57. package/src/lib/offlinePlayerData.js +1 -1
  58. package/src/lib/validators.js +2 -2
  59. package/src/lib/voicePacket.js +1 -1
  60. package/src/standalone/index.js +3 -2
  61. package/src/client/lib/footerLink.js +0 -30
  62. 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
- [plugin contract](https://docs.vimp.lgick.dev/en/plugin-api).
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
- Documentation: https://docs.vimp.lgick.dev
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. See § Scenario format at\n' +
44
- 'notice: https://docs.vimp.lgick.dev/en/debugging#scenario-format, then run\n' +
43
+ 'notice: scenario written for your game; the format is in\n' +
44
+ 'notice: VIMP docs: debugging.md § "Scenario format". Then run\n' +
45
45
  'notice: vimp-sim --scenario <file>.\n\n';
46
46
 
47
47
  // типичная опечатка «--core без --game»: фикстурное ядро — обычный JS и
package/core/Cargo.toml CHANGED
@@ -1,10 +1,10 @@
1
1
  [package]
2
2
  name = "vimp-engine-core"
3
- version = "0.23.4"
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://docs.vimp.lgick.dev/en/core"
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.39.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://docs.vimp.lgick.dev",
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/dedicated -w src/lib -w src/config",
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",
@@ -1,33 +1,19 @@
1
1
  // Режим загрузки клиента движка (Этап 2 плана standalone-sdk). Клиент один
2
- // на все контуры, а различает их этот модуль:
2
+ // на оба контура, а различает их этот модуль:
3
3
  //
4
- // lobby — прод: каталог игр и сигналинг мастера, лобби за экраном профиля;
5
- // solo — standalone SDK: хост в этой же вкладке (inline, без Worker'а),
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' | 'dedicated',
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 → GET /config → лобби.
73
- * @param {Function} [fetchImpl] - Реализация fetch (тесты подставляют свою).
74
- * @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
75
- * @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
76
- * сеть не ответила после повторов.
56
+ * Разрешает режим загрузки: инъекция SDK, иначе лобби.
57
+ * @returns {Object} Конфиг загрузки.
77
58
  */
78
- export async function resolveBootConfig(fetchImpl, retry) {
79
- if (bootConfig) {
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/dedicated ссылки на комнату нет
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.vimp.lgick.dev/en/plugin-api "Form schema"), едет по сети в
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, docs.vimp.lgick.dev/en/plugin-api "Form
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;