vimp-engine 0.22.0 → 0.23.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 (65) hide show
  1. package/bin/vimp-surface.js +88 -0
  2. package/core/Cargo.toml +1 -1
  3. package/package.json +1 -1
  4. package/src/client/lib/formBuilder.js +27 -8
  5. package/src/client/lib/socketDispatch.js +34 -0
  6. package/src/client/main.js +55 -7
  7. package/src/config/abiOps.js +31 -0
  8. package/src/config/clientServices.js +27 -0
  9. package/src/config/opcodes.js +11 -2
  10. package/src/config/wsports.js +9 -0
  11. package/src/devtools/contract/loadContext.js +25 -2
  12. package/src/devtools/contract/rules/b10-respawns.js +5 -3
  13. package/src/devtools/contract/rules/b2-engine-api.js +31 -8
  14. package/src/devtools/contract/rules/b3-game-config-shape.js +46 -5
  15. package/src/devtools/contract/rules/b4-teams.js +6 -3
  16. package/src/devtools/contract/rules/b5-room-form.js +40 -8
  17. package/src/devtools/contract/rules/c4-component-dependencies.js +10 -15
  18. package/src/devtools/surface/abiParse.js +175 -0
  19. package/src/devtools/surface/collect.js +413 -0
  20. package/src/host/GameCoreAdapter.js +49 -0
  21. package/src/host/meta/core/RoundManager.js +14 -1
  22. package/src/lib/applyRoomOverrides.js +14 -2
  23. package/src/lib/capabilities.js +32 -0
  24. package/src/lib/coreConfig.js +3 -3
  25. package/src/lib/createHostRuntime.js +5 -3
  26. package/src/lib/formControls.js +89 -0
  27. package/src/lib/gameConfigView.js +211 -0
  28. package/src/lib/gamePlugin.js +65 -87
  29. package/src/lib/loadGamePackage.js +9 -2
  30. package/src/lib/registry.js +94 -0
  31. package/src/standalone/index.js +27 -13
  32. package/tests/fixtures/generations/gen-api3/README.md +7 -0
  33. package/tests/fixtures/generations/gen-api3/client/fakeClientCore.js +307 -0
  34. package/tests/fixtures/generations/gen-api3/client/index.js +38 -0
  35. package/tests/fixtures/generations/gen-api3/client/parts/Actor.js +18 -0
  36. package/tests/fixtures/generations/gen-api3/client/parts/ActorRadar.js +7 -0
  37. package/tests/fixtures/generations/gen-api3/config/auth.js +41 -0
  38. package/tests/fixtures/generations/gen-api3/config/client.js +151 -0
  39. package/tests/fixtures/generations/gen-api3/config/game.js +167 -0
  40. package/tests/fixtures/generations/gen-api3/core/pkg-node/core.js +4 -0
  41. package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +129 -0
  42. package/tests/fixtures/generations/gen-api3/host/createModules.js +6 -0
  43. package/tests/fixtures/generations/gen-api3/host/fakeCore.js +263 -0
  44. package/tests/fixtures/generations/gen-api3/host/index.js +30 -0
  45. package/tests/fixtures/generations/gen-api3/host/spawnCommand.js +14 -0
  46. package/tests/fixtures/generations/gen-api3/host/systemMessages.js +6 -0
  47. package/tests/fixtures/generations/gen-api3/manifest.json +25 -0
  48. package/tests/fixtures/generations/gen-api4/README.md +7 -0
  49. package/tests/fixtures/generations/gen-api4/client/fakeClientCore.js +307 -0
  50. package/tests/fixtures/generations/gen-api4/client/index.js +38 -0
  51. package/tests/fixtures/generations/gen-api4/client/parts/Actor.js +18 -0
  52. package/tests/fixtures/generations/gen-api4/client/parts/ActorRadar.js +7 -0
  53. package/tests/fixtures/generations/gen-api4/config/auth.js +41 -0
  54. package/tests/fixtures/generations/gen-api4/config/client.js +151 -0
  55. package/tests/fixtures/generations/gen-api4/config/game.js +156 -0
  56. package/tests/fixtures/generations/gen-api4/core/pkg-node/core.js +4 -0
  57. package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +129 -0
  58. package/tests/fixtures/generations/gen-api4/host/createModules.js +6 -0
  59. package/tests/fixtures/generations/gen-api4/host/fakeCore.js +263 -0
  60. package/tests/fixtures/generations/gen-api4/host/index.js +30 -0
  61. package/tests/fixtures/generations/gen-api4/host/spawnCommand.js +14 -0
  62. package/tests/fixtures/generations/gen-api4/host/systemMessages.js +6 -0
  63. package/tests/fixtures/generations/gen-api4/manifest.json +17 -0
  64. package/tests/fixtures/miniGame/client/fakeClientCore.js +17 -0
  65. package/tests/fixtures/miniGame/host/fakeCore.js +38 -8
@@ -1,4 +1,7 @@
1
- import { assertEngineApiCompatible } from '../lib/gamePlugin.js';
1
+ import {
2
+ assertGameConfigShape,
3
+ checkPluginCompatibility,
4
+ } from '../lib/gamePlugin.js';
2
5
  import { setBootConfig } from '../client/boot.js';
3
6
  import { ensureGameShell } from '../client/views/gameShell.js';
4
7
 
@@ -55,21 +58,24 @@ export async function startStandaloneGame({
55
58
  requireOption(clientPlugin, 'clientPlugin');
56
59
  requireOption(wasmUrl, 'wasmUrl');
57
60
 
58
- // несовпадение ENGINE_API_VERSION иначе всплыло бы где-то в середине
59
- // хендшейка непрозрачной ошибкой
60
- assertEngineApiCompatible(hostPlugin);
61
- assertEngineApiCompatible(clientPlugin);
61
+ // возможность, которой в этой сборке движка нет, иначе всплыла бы где-то
62
+ // в середине хендшейка непрозрачной ошибкой
63
+ requireCompatible(hostPlugin);
64
+ requireCompatible(clientPlugin);
65
+
66
+ // одна view на прогон: умолчания вместо обязательных полей (И2 этапа 2)
67
+ const configView = assertGameConfigShape(hostPlugin);
62
68
 
63
69
  ensureGameShell(container);
64
70
 
65
71
  setBootConfig({
66
72
  mode: 'solo',
67
73
  container,
68
- manifest: buildManifest(hostPlugin, { wasmUrl, assetsBase }),
74
+ manifest: buildManifest(hostPlugin, configView, { wasmUrl, assetsBase }),
69
75
  clientPlugin,
70
76
  hostPlugin,
71
77
  room: {
72
- ...hostPlugin.gameConfig.roomDefaults,
78
+ ...configView.roomDefaults,
73
79
  ...room,
74
80
  isDevMode: devMode,
75
81
  },
@@ -91,6 +97,16 @@ export async function startStandaloneGame({
91
97
  };
92
98
  }
93
99
 
100
+ // половина плагина просит возможность, которой в этой сборке движка нет:
101
+ // подменить её в SDK нечем — матч не поднимется
102
+ function requireCompatible(plugin) {
103
+ const compat = checkPluginCompatibility(plugin);
104
+
105
+ if (!compat.ok) {
106
+ throw new Error(compat.text);
107
+ }
108
+ }
109
+
94
110
  // незаданное поле — это «взять дефолт схемы», а не «отправить undefined»:
95
111
  // JSON.stringify ключ с undefined выбрасывает, и хост отвечает
96
112
  // 'Property is missing' (main.js накрывает autoAuth поверх дефолтов схемы)
@@ -103,18 +119,16 @@ function pruneUndefined(source) {
103
119
  // манифест-подобный объект в памяти: мастера нет, а клиент читает из
104
120
  // манифеста id/version, entries.wasm и assetsBase. entries.client/host не
105
121
  // нужны — оба плагина уже живые объекты
106
- function buildManifest(hostPlugin, { wasmUrl, assetsBase }) {
107
- const { gameConfig } = hostPlugin;
108
-
122
+ function buildManifest(hostPlugin, configView, { wasmUrl, assetsBase }) {
109
123
  return {
110
124
  id: hostPlugin.id,
111
125
  engineApi: hostPlugin.engineApi,
112
126
  version: 'dev',
113
- title: gameConfig.title ?? hostPlugin.id,
127
+ title: configView.title ?? hostPlugin.id,
114
128
  entries: { wasm: wasmUrl },
115
129
  assetsBase,
116
- maps: { version: 'dev', list: Object.keys(gameConfig.maps) },
117
- roomDefaults: gameConfig.roomDefaults,
130
+ maps: { version: 'dev', list: Object.keys(configView.maps) },
131
+ roomDefaults: configView.roomDefaults,
118
132
  };
119
133
  }
120
134
 
@@ -0,0 +1,7 @@
1
+ # gen-api3 — ЗАМОРОЖЕНО
2
+
3
+ Снимок плагина 2026-08-29, поколение `engineApi: 3`.
4
+ Не править: правка фикстуры маскирует слом совместимости
5
+ (этап 1 плана `plan/plugin-forward-compat`). Новое поколение
6
+ добавляется копированием текущего `../../miniGame`, а не правкой
7
+ этого каталога.
@@ -0,0 +1,307 @@
1
+ // ЗАМОРОЖЕНО. Снимок плагина 2026-08-29 (поколение engineApi: 3).
2
+ // Не править: правка фикстуры маскирует слом совместимости.
3
+ // Фейковое клиентское ядро миниигры-фикстуры: минимальная реализация
4
+ // движкового клиентского ABI (см. export_client_core_abi!, PLAN.md §3.4).
5
+ //
6
+ // Кадры фикстуры — не бинарный кодек ядра, а JSON (см. host/fakeCore.js:
7
+ // frame_bytes), поэтому push_frame их просто разбирает, а интерполяции нет:
8
+ // sample() отдаёт последний принятый кадр. Раскладка hot-буфера при этом
9
+ // настоящая (флаги, две группы, ширина записи = 2 + поля схемы) — именно её
10
+ // читает движковый reconstructHot, и именно она замыкает headless-контур
11
+ // «хост → байты → клиентское ядро → сцена» без WASM.
12
+
13
+ // флаги hot-буфера на момент снимка (движковый src/config/opcodes.js)
14
+ const HOT_FLAGS = { GAME: 1, CAMERA: 2, PREDICTED: 4, FRAMES: 8 };
15
+
16
+ export default class FakeClientCore {
17
+ constructor(configJson) {
18
+ this._config = JSON.parse(configJson);
19
+ this._active = false;
20
+ this._frame = null;
21
+ this._hot = new Float32Array(0);
22
+
23
+ // обратный индекс снапшот-схемы: keyId → { key, kind, fields }
24
+ const keys = this._config.engine?.snapshot?.keys ?? {};
25
+
26
+ this._hotKey = Object.entries(keys).find(
27
+ ([, spec]) => spec.class === 'hot' && spec.kind === 'indexed8',
28
+ );
29
+
30
+ // детектор рассинхрона предикта (этап 5): включается секцией divergence
31
+ // движкового конфига, как и в Rust-ядре
32
+ const cfg = this._config.engine?.divergence;
33
+
34
+ this._divergenceCfg = cfg ?? null;
35
+ this._divergence = cfg
36
+ ? { samples: 0, violations: 0, dropped: 0, maxDelta: [0, 0], records: [] }
37
+ : null;
38
+
39
+ // предикт фикстуры: та же таблица скоростей, что у host/fakeCore, плюс
40
+ // история её изменений — реконсиляция идёт по времени кадра, поэтому
41
+ // история переигрывается от момента авторитетного состояния, а не от seq
42
+ this._vy = 0;
43
+ this._inputs = []; // [{ time, vy }] в локальном времени
44
+ this._base = null; // { x, y, time, vy } — последнее авторитетное, в серверном
45
+ this._offset = 0;
46
+ }
47
+
48
+ push_frame(data, localNow = 0) {
49
+ let frame;
50
+
51
+ try {
52
+ frame = JSON.parse(new TextDecoder().decode(data));
53
+ } catch {
54
+ return false;
55
+ }
56
+
57
+ this._offset = frame.serverTime - localNow;
58
+ this._observeDivergence(frame);
59
+ this._frame = frame;
60
+
61
+ return true;
62
+ }
63
+
64
+ my_game_id() {
65
+ return this._frame?.playerId ?? null;
66
+ }
67
+
68
+ offset() {
69
+ return 0;
70
+ }
71
+
72
+ sample() {
73
+ if (!this._frame || !this._hotKey) {
74
+ this._hot = new Float32Array(0);
75
+
76
+ return 0;
77
+ }
78
+
79
+ const [, spec] = this._hotKey;
80
+ const body = this._frame.body ?? [];
81
+ const camera = this._frame.camera;
82
+ const values = [
83
+ HOT_FLAGS.GAME | (camera ? HOT_FLAGS.CAMERA : 0),
84
+ camera ? camera[0] : 0,
85
+ camera ? camera[1] : 0,
86
+ body.length,
87
+ ];
88
+
89
+ for (const record of body) {
90
+ values.push(spec.id, record.id);
91
+
92
+ for (const field of spec.fields) {
93
+ values.push(Number(record[field.name] ?? 0));
94
+ }
95
+ }
96
+
97
+ // группа IndexedNoNull8 (динамика карты) — у фикстуры пуста
98
+ values.push(0);
99
+
100
+ this._hot = new Float32Array(values);
101
+
102
+ return this._hot.length;
103
+ }
104
+
105
+ hot_ptr() {
106
+ return 0;
107
+ }
108
+
109
+ hot_values() {
110
+ return this._hot;
111
+ }
112
+
113
+ take_frames() {
114
+ return '[]';
115
+ }
116
+
117
+ apply_input(action, name, localNow = 0) {
118
+ const magnitude = action === 'down' ? 40 : 0;
119
+
120
+ if (name === 'forward') {
121
+ this._vy = -magnitude;
122
+ } else if (name === 'back') {
123
+ this._vy = magnitude;
124
+ } else {
125
+ return;
126
+ }
127
+
128
+ this._inputs.push({ time: localNow, vy: this._vy });
129
+ }
130
+
131
+ // записи расхождения предикта; 'null' — детектор выключен конфигом
132
+ take_divergence() {
133
+ if (!this._divergence) {
134
+ return 'null';
135
+ }
136
+
137
+ const { records, ...stats } = this._divergence;
138
+
139
+ this._divergence.records = [];
140
+
141
+ return JSON.stringify({ ...stats, records });
142
+ }
143
+
144
+ // сравнение предсказанного своего актора с авторитетным player-блоком —
145
+ // до того, как авторитетное состояние станет новой базой предикта
146
+ _observeDivergence(frame) {
147
+ if (!this._divergence) {
148
+ return;
149
+ }
150
+
151
+ // playerId приходит в кадре хоста строкой (gameId меты) — наблюдатель
152
+ // помечен -1 и своего актора не имеет
153
+ const id = Number(frame.playerId);
154
+ const authoritative = (frame.body ?? []).find(row => row.id === id);
155
+
156
+ if (!Number.isInteger(id) || id < 0 || !authoritative) {
157
+ return;
158
+ }
159
+
160
+ const time = frame.serverTime;
161
+
162
+ if (this._base) {
163
+ const predicted = this._predictAt(time);
164
+
165
+ this._compare(predicted, authoritative, frame);
166
+ }
167
+
168
+ // реконсиляция: база — авторитетное состояние на serverTime кадра,
169
+ // история ввода до этого момента больше не переигрывается
170
+ const vy = this._velocityAt(time);
171
+
172
+ this._base = { x: authoritative.x, y: authoritative.y, time, vy };
173
+ this._inputs = this._inputs.filter(
174
+ input => input.time + this._offset > time,
175
+ );
176
+ }
177
+
178
+ _compare(predicted, authoritative, frame) {
179
+ const dump = this._divergence;
180
+ const cfg = this._divergenceCfg;
181
+ const pair = [
182
+ [predicted.x, authoritative.x],
183
+ [predicted.y, authoritative.y],
184
+ ];
185
+ const delta = pair.map(([a, b]) => a - b);
186
+ const thresholds = pair.map(
187
+ (_, index) => cfg.thresholds?.[index] ?? cfg.defaultThreshold ?? 1,
188
+ );
189
+ const exceeded = delta
190
+ .map((value, index) => (Math.abs(value) > thresholds[index] ? index : -1))
191
+ .filter(index => index !== -1);
192
+
193
+ dump.samples += 1;
194
+ delta.forEach((value, index) => {
195
+ dump.maxDelta[index] = Math.max(dump.maxDelta[index], Math.abs(value));
196
+ });
197
+
198
+ if (!exceeded.length) {
199
+ return;
200
+ }
201
+
202
+ dump.violations += 1;
203
+
204
+ if (dump.records.length >= (cfg.capacity ?? 64)) {
205
+ dump.records.shift();
206
+ dump.dropped += 1;
207
+ }
208
+
209
+ dump.records.push({
210
+ source: 'state',
211
+ serverTime: frame.serverTime,
212
+ localNow: frame.serverTime - this._offset,
213
+ offset: this._offset,
214
+ inputSeq: frame.seq,
215
+ replayed: {
216
+ from: this._base.time,
217
+ to: frame.serverTime,
218
+ count: this._inputs.length,
219
+ },
220
+ predicted: [predicted.x, predicted.y],
221
+ authoritative: [authoritative.x, authoritative.y],
222
+ delta,
223
+ exceeded,
224
+ thresholds,
225
+ });
226
+ }
227
+
228
+ // интегрирование скорости от базы до момента t (серверное время):
229
+ // кусочно-линейно по истории ввода — предикт переигрывает именно её
230
+ _predictAt(t) {
231
+ let { y, time, vy } = this._base;
232
+
233
+ for (const input of this._inputs) {
234
+ const at = input.time + this._offset;
235
+ const until = Math.min(at, t);
236
+
237
+ if (until > time) {
238
+ y += (vy * (until - time)) / 1000;
239
+ time = until;
240
+ }
241
+
242
+ if (at > t) {
243
+ return { x: this._base.x, y };
244
+ }
245
+
246
+ vy = input.vy;
247
+ }
248
+
249
+ if (t > time) {
250
+ y += (vy * (t - time)) / 1000;
251
+ }
252
+
253
+ return { x: this._base.x, y };
254
+ }
255
+
256
+ _velocityAt(time) {
257
+ let vy = this._base?.vy ?? 0;
258
+
259
+ for (const input of this._inputs) {
260
+ if (input.time + this._offset <= time) {
261
+ vy = input.vy;
262
+ }
263
+ }
264
+
265
+ return vy;
266
+ }
267
+
268
+ set_active(active) {
269
+ this._active = active;
270
+ }
271
+
272
+ set_map() {}
273
+
274
+ reset() {
275
+ this._frame = null;
276
+ this._hot = new Float32Array(0);
277
+ this._base = null;
278
+ this._inputs = [];
279
+ }
280
+
281
+ // зеркало ClientState::debug_json; у фикстуры буфер — один последний
282
+ // кадр, поэтому окно seq вырождено
283
+ debug_json() {
284
+ return JSON.stringify({
285
+ myGameId: this._frame?.playerId ?? null,
286
+ offset: 0,
287
+ interpolator: {
288
+ buffered: this._frame ? 1 : 0,
289
+ seqWindow: this._frame ? [this._frame.seq, this._frame.seq] : null,
290
+ lastFrame: this._frame
291
+ ? { seq: this._frame.seq, serverTime: this._frame.serverTime }
292
+ : null,
293
+ },
294
+ hotLen: this._hot.length,
295
+ framesOut: 0,
296
+ active: this._active,
297
+ });
298
+ }
299
+
300
+ decode_frame(data) {
301
+ try {
302
+ return JSON.stringify(JSON.parse(new TextDecoder().decode(data)));
303
+ } catch {
304
+ return 'null';
305
+ }
306
+ }
307
+ }
@@ -0,0 +1,38 @@
1
+ // ЗАМОРОЖЕНО. Снимок плагина 2026-08-29 (поколение engineApi: 3).
2
+ // Не править: правка фикстуры маскирует слом совместимости.
3
+ import FakeClientCore from './fakeClientCore.js';
4
+ import Actor from './parts/Actor.js';
5
+ import ActorRadar from './parts/ActorRadar.js';
6
+
7
+ // ClientPlugin миниигры-фикстуры (Этап 7 плана отделения движка, PLAN.md
8
+ // §3.3): минимальные заглушечные parts (без PixiJS) — доказывает, что
9
+ // gamePlugin.js/Factory работают с любым ClientPlugin, а не только с
10
+ // @vimp-games/tanks.
11
+ export default {
12
+ id: 'gen-api3',
13
+ engineApi: 3,
14
+
15
+ async createClientCore(clientConfigJson) {
16
+ return { core: new FakeClientCore(clientConfigJson), memory: null };
17
+ },
18
+
19
+ parts: { Actor, ActorRadar },
20
+ bakers: {},
21
+ styles: '',
22
+
23
+ hooks: {
24
+ onAuth(core, authData) {
25
+ core.set_model?.(authData.model);
26
+ },
27
+
28
+ onPanel() {},
29
+
30
+ onLocalAction(core, action, name, now) {
31
+ if (action === 'down' && name === 'fire') {
32
+ return core.try_fire?.(now) || null;
33
+ }
34
+
35
+ return null;
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,18 @@
1
+ // Заглушечный part фикстуры (ClientPlugin.parts, PLAN.md §3.3) — минимальная
2
+ // форма рендер-сущности: конструктор + update(state)/destroy(), без PixiJS
3
+ // (тесты не поднимают живой канвас). Доказывает, что Factory/CanvasManager
4
+ // не требуют конкретно PixiJS-Container-класса — только этот интерфейс.
5
+ export default class Actor {
6
+ constructor(id) {
7
+ this.id = id;
8
+ this.destroyed = false;
9
+ }
10
+
11
+ update(state) {
12
+ this.state = state;
13
+ }
14
+
15
+ destroy() {
16
+ this.destroyed = true;
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import Actor from './Actor.js';
2
+
3
+ // Второй заглушечный part фикстуры — радар-проекция того же актёра,
4
+ // зеркало пары Tank/TankRadar танков (доказывает, что gameSets/
5
+ // entitiesOnCanvas умеют распределять одну сущность по нескольким
6
+ // полотнам произвольным числом part-классов).
7
+ export default class ActorRadar extends Actor {}
@@ -0,0 +1,41 @@
1
+ import gameConfig from './game.js';
2
+
3
+ // Схема авторизации фикстуры (HostPlugin.authSchema, PLAN.md §3.2) —
4
+ // зеркало vimp-tanks/src/config/auth.js с единственной моделью.
5
+ export default {
6
+ elems: {
7
+ authId: 'auth',
8
+ fieldsId: 'auth-form',
9
+ errorId: 'auth-error',
10
+ enterId: 'auth-enter',
11
+ titleId: 'auth-title',
12
+ informsId: 'auth-informs',
13
+ },
14
+ // собственные тексты фикстуры (Д2): каркас auth.pug нейтрален,
15
+ // заголовок/подсказки — данные игры
16
+ texts: {
17
+ title: 'Mini Game Fixture',
18
+ sections: [
19
+ {
20
+ heading: 'Controls',
21
+ lines: [
22
+ { keys: 'W, S', text: 'move' },
23
+ { keys: 'J', text: 'fire' },
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ params: [
29
+ {
30
+ name: 'model',
31
+ value: 'm1',
32
+ options: {
33
+ validator: 'isValidModel',
34
+ storage: 'model',
35
+ },
36
+ },
37
+ ],
38
+ validators: {
39
+ isValidModel: model => model in gameConfig.parts.models,
40
+ },
41
+ };
@@ -0,0 +1,151 @@
1
+ // Игровая половина клиентского CONFIG_DATA (HostPlugin.buildClientGameConfig,
2
+ // PLAN.md §3.5) — зеркало vimp-tanks/src/config/client.js, минимальная
3
+ // схема: одно полотно, одна колонка статистики сверх имени, один keySet.
4
+ export default {
5
+ parts: {
6
+ gameSets: {
7
+ // ключ снапшот-схемы (a1) — по нему клиент разводит записи кадра по
8
+ // parts; setId карты (m1) — по нему создаётся статика полотна
9
+ a1: ['Actor'],
10
+ m1: ['Actor'],
11
+ // event-ключ схемы: строк он в фикстуре не даёт, но запись нужна —
12
+ // ключ снапшота без набора партов это чёрный холст на первом же
13
+ // кадре, где он ожил
14
+ e1: ['Actor'],
15
+ },
16
+ entitiesOnCanvas: {
17
+ Actor: 'vimp',
18
+ },
19
+ bakedAssets: {},
20
+ // пул сервисов v3 — без 'accolades' (он появился только в v4)
21
+ componentDependencies: {
22
+ renderer: ['Actor'],
23
+ localPlayer: ['Actor'],
24
+ },
25
+ sounds: {},
26
+ },
27
+
28
+ initIdList: ['vimp', 'panel', 'chat'],
29
+
30
+ modules: {
31
+ canvasManager: {
32
+ canvases: {
33
+ vimp: {
34
+ width: 640,
35
+ height: 400,
36
+ aspectRatio: '16:10',
37
+ baseScale: '5:1',
38
+ dynamicCamera: false,
39
+ shakeCamera: false,
40
+ },
41
+ },
42
+ },
43
+
44
+ controls: {
45
+ keySetList: [
46
+ // spectator keyset: переключение камеры — механизм движка
47
+ // (hostDefaults.spectatorKeys), без него наблюдатель заперт
48
+ {
49
+ 78: 'nextPlayer', // next player (n)
50
+ 80: 'prevPlayer', // prev player (p)
51
+ },
52
+ // player keyset
53
+ {
54
+ 87: 'forward', // forward (w)
55
+ 83: 'back', // back (s)
56
+ 74: 'fire', // fire (j)
57
+ },
58
+ ],
59
+ },
60
+
61
+ chat: {
62
+ params: {
63
+ messages: {
64
+ s: [
65
+ 'Team {0} is full. Your current team: {1}',
66
+ 'Your team: {0}',
67
+ 'Your new team: {0}',
68
+ 'Your new status: spectator',
69
+ '{0} killed {1}',
70
+ '{0} joined the game',
71
+ '{0} left the game',
72
+ ],
73
+ v: [
74
+ 'A vote has been created',
75
+ 'Voting has started',
76
+ 'Your vote has been accepted',
77
+ 'Voting is temporarily unavailable',
78
+ 'Vote passed',
79
+ 'Vote failed',
80
+ ],
81
+ m: ['Current map: {0}', 'Next map: {0}'],
82
+ c: ['Command not found'],
83
+ n: ['Invalid name', '{0} changed name to {1}'],
84
+ g: ['{0} scripted participant(s) spawned'],
85
+ },
86
+ },
87
+ },
88
+
89
+ panel: {
90
+ // поле нарочно называется не health: бар обязан работать по
91
+ // type: 'bar', а не по имени поля (Д2)
92
+ // 't' — движковый ключ времени раунда: его шлёт Panel хоста, поэтому
93
+ // без маппинга клиент получал бы значение с именем undefined
94
+ keys: {
95
+ h: 'energy',
96
+ t: 'time',
97
+ },
98
+ fields: [
99
+ {
100
+ name: 'energy',
101
+ elem: 'panel-energy',
102
+ type: 'bar',
103
+ max: 100,
104
+ blocks: 10,
105
+ },
106
+ {
107
+ name: 'time',
108
+ elem: 'panel-time',
109
+ type: 'time',
110
+ },
111
+ ],
112
+ },
113
+
114
+ stat: {
115
+ params: {
116
+ columns: ['names', 'status', 'score', 'deaths', 'latency'],
117
+ heads: {
118
+ 1: 'team1',
119
+ },
120
+ bodies: {
121
+ 1: 'team1',
122
+ 2: 'spectators',
123
+ },
124
+ sortList: {
125
+ team1: [
126
+ [2, true],
127
+ [3, false],
128
+ ],
129
+ },
130
+ },
131
+ },
132
+
133
+ vote: {
134
+ params: {
135
+ templates: {
136
+ teamChange: ['Choose a team', 'teams', true],
137
+ mapChangeBySystem: ['Choose the next map'],
138
+ mapChangeByUser: ['{0} suggested the map: {1}', ['Yes', 'No']],
139
+ },
140
+ menu: [
141
+ ['teamChange', ['Switch team', 'teams']],
142
+ ['mapChange', ['Suggest map', 'maps']],
143
+ ],
144
+ },
145
+ },
146
+ },
147
+
148
+ gameInform: {
149
+ list: ['{0} WINS!', 'ROUND START!', 'GAME OVER!'],
150
+ },
151
+ };