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
@@ -0,0 +1,156 @@
1
+ // Игровая конфигурация фикстуры «миниигра» (Этап 7 плана отделения
2
+ // движка — HostPlugin.gameConfig): нарочно минимальна и структурно
3
+ // отличается от vimp-tanks/src/config/game.js — доказывает, что
4
+ // движковая мета (Panel/Stat/RoundManager/ParticipantManager/
5
+ // CommandProcessor) настраивается произвольной схемой, а не только
6
+ // схемой танков. Одна играющая команда (team1) вместо двух у танков —
7
+ // проверка нижней границы настраиваемости Stat/RoundManager.
8
+ export default {
9
+ parts: {
10
+ models: { m1: { radius: 10 } },
11
+ weapons: {},
12
+ friendlyFire: false,
13
+ },
14
+
15
+ // собственная снапшот-схема (нарочно не танковая: один hot-ключ актора
16
+ // с 4 полями и один event-ключ) — движок обязан работать с любой
17
+ // раскладкой из gameConfig.snapshot, а не только m1/w1/…/c2
18
+ snapshot: {
19
+ a1: {
20
+ id: 1,
21
+ kind: 'indexed8',
22
+ class: 'hot',
23
+ fields: [
24
+ { name: 'x', ty: 'f32', interp: 'lerp' },
25
+ { name: 'y', ty: 'f32', interp: 'lerp' },
26
+ { name: 'angle', ty: 'f32', interp: 'lerpAngle' },
27
+ { name: 'team', ty: 'u8' },
28
+ ],
29
+ },
30
+ e1: {
31
+ id: 2,
32
+ kind: 'list16',
33
+ class: 'event',
34
+ fields: [
35
+ { name: 'x', ty: 'f32' },
36
+ { name: 'y', ty: 'f32' },
37
+ ],
38
+ },
39
+ },
40
+
41
+ soundCues: {},
42
+ initialVote: 'teamChange',
43
+
44
+ maps: {
45
+ arena: {
46
+ setId: 'm1',
47
+ scale: 1,
48
+ // имя файла — инертные данные: фикстура host-only, рендера в ней нет
49
+ // и картинку никто не грузит. Реальные тайлы везёт пакет игры
50
+ // (`${assetsBase}img/`), движок картинок не раздаёт
51
+ spriteSheet: { img: 'fixture-tiles.png', frames: [[0, 0, 32, 32]] },
52
+ layers: { 1: [0] },
53
+ physicsStatic: [1],
54
+ physicsDynamic: [],
55
+ step: 32,
56
+ // точек не меньше, чем roomDefaults.maxPlayers: длина списка —
57
+ // жёсткая вместимость команды на карте (RoundManager), и комната
58
+ // молча вмещала бы меньше, чем обещает лобби
59
+ respawns: {
60
+ team1: [
61
+ [100, 100, 0],
62
+ [200, 100, 0],
63
+ [100, 200, 0],
64
+ [200, 200, 0],
65
+ ],
66
+ },
67
+ map: [
68
+ [1, 1, 1, 1],
69
+ [1, 0, 0, 1],
70
+ [1, 0, 0, 1],
71
+ [1, 1, 1, 1],
72
+ ],
73
+ },
74
+ },
75
+ mapScale: 1,
76
+ currentMap: 'arena',
77
+ mapsInVote: 1,
78
+ mapSetId: 'm1',
79
+
80
+ roomDefaults: {
81
+ maxPlayers: 4,
82
+ },
83
+
84
+ // RoundManager/RTTManager (движковые) обновляют status/score/deaths/latency
85
+ // безусловно (не через схему) — поэтому набор колонок такой же, как у
86
+ // танков; отличие фикстуры — ОДНА играющая команда (см. teams ниже)
87
+ stat: {
88
+ name: {
89
+ key: 0,
90
+ bodyMethod: '=',
91
+ headSync: true,
92
+ headMethod: '#',
93
+ },
94
+ status: {
95
+ key: 1,
96
+ bodyMethod: '=',
97
+ bodyValue: '',
98
+ headValue: '',
99
+ },
100
+ score: {
101
+ key: 2,
102
+ bodyMethod: '+',
103
+ bodyValue: 0,
104
+ headMethod: '+',
105
+ headValue: 0,
106
+ },
107
+ deaths: {
108
+ key: 3,
109
+ bodyMethod: '+',
110
+ bodyValue: 0,
111
+ headMethod: '+',
112
+ headValue: 0,
113
+ },
114
+ latency: {
115
+ key: 4,
116
+ bodyMethod: '=',
117
+ },
118
+ },
119
+
120
+ panel: {
121
+ // поле нарочно не health (Д2): семантику ячейки клиенту задаёт
122
+ // type в схеме client.js, а не имя поля
123
+ fields: {
124
+ energy: {
125
+ key: 'h',
126
+ value: 100,
127
+ },
128
+ },
129
+ activeKey: null,
130
+ },
131
+
132
+ scripted: {
133
+ namePrefix: 'Scripted',
134
+ defaultModel: 'm1',
135
+ },
136
+
137
+ spectatorTeam: 'spectators',
138
+
139
+ teams: {
140
+ team1: 1,
141
+ spectators: 2,
142
+ },
143
+
144
+ playerKeys: {
145
+ forward: {
146
+ key: 1 << 0,
147
+ },
148
+ back: {
149
+ key: 1 << 1,
150
+ },
151
+ fire: {
152
+ key: 1 << 2,
153
+ type: 1,
154
+ },
155
+ },
156
+ };
@@ -0,0 +1,4 @@
1
+ // ЗАМОРОЖЕНО. Заглушка node-сборки ядра: ядро поколения — обычный JS
2
+ // (host/fakeCore.js), но манифест обязан объявлять entries.wasmNode,
3
+ // а загрузчик пакета проверяет, что файл на месте.
4
+ export default {};
@@ -0,0 +1,129 @@
1
+ // Игровой scripted-модуль фикстуры: зеркало
2
+ // vimp-tanks/src/host/TanksBotManager.js — тот же контракт (createMap,
3
+ // createScripted, removeScripted, removeOneForHuman, getCount,
4
+ // getCountsPerTeam), проверяющий, что RoundManager/HostGame/CommandProcessor
5
+ // работают с любым HostPlugin.createModules(ctx), а не только с ботами
6
+ // танков. Worker-safe.
7
+ export default class ScriptedManager {
8
+ constructor({ participants, coreAdapter, panel, stat, scripted }) {
9
+ this._participants = participants;
10
+ this._coreAdapter = coreAdapter;
11
+ this._panel = panel;
12
+ this._stat = stat;
13
+
14
+ this._model = scripted.defaultModel;
15
+ this._respawns = null;
16
+ }
17
+
18
+ createMap(mapData) {
19
+ this._respawns = mapData.respawns;
20
+ }
21
+
22
+ createScripted(count, teamName = null) {
23
+ if (!this._respawns) {
24
+ return 0;
25
+ }
26
+
27
+ const playableTeams = this._participants.getPlayableTeams();
28
+ let createdCount = 0;
29
+
30
+ for (let i = 0; i < count; i += 1) {
31
+ if (this._participants.isFull) {
32
+ break;
33
+ }
34
+
35
+ let targetTeam = teamName;
36
+
37
+ if (!targetTeam) {
38
+ targetTeam = playableTeams.sort(
39
+ (a, b) =>
40
+ this._participants.getTeamSize(a) -
41
+ this._participants.getTeamSize(b),
42
+ )[0];
43
+ }
44
+
45
+ if (
46
+ !targetTeam ||
47
+ !this._respawns[targetTeam] ||
48
+ this._participants.getTeamSize(targetTeam) >=
49
+ this._respawns[targetTeam].length
50
+ ) {
51
+ continue;
52
+ }
53
+
54
+ const gameId = this._participants.createScripted({
55
+ team: targetTeam,
56
+ model: this._model,
57
+ });
58
+ const participant = this._participants.get(gameId);
59
+
60
+ this._stat.addUser(gameId, participant.teamId, {
61
+ name: participant.name,
62
+ status: 'dead',
63
+ latency: 'SCRIPTED',
64
+ });
65
+ this._panel.addUser(gameId);
66
+
67
+ createdCount += 1;
68
+ }
69
+
70
+ return createdCount;
71
+ }
72
+
73
+ removeScripted(teamName = null) {
74
+ const toRemove = teamName
75
+ ? this._participants.getScripted().filter(p => p.team === teamName)
76
+ : this._participants.getScripted();
77
+
78
+ toRemove.forEach(p => this._removeById(p.gameId));
79
+ }
80
+
81
+ removeOneForHuman(teamName) {
82
+ for (const participant of this._participants.getScripted()) {
83
+ if (participant.team === teamName) {
84
+ this._removeById(participant.gameId);
85
+ return true;
86
+ }
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ _removeById(gameId) {
93
+ const participant = this._participants.get(gameId);
94
+
95
+ if (!participant || !participant.isScripted) {
96
+ return;
97
+ }
98
+
99
+ this._stat.removeUser(gameId, participant.teamId);
100
+ this._panel.removeUser(gameId);
101
+ this._coreAdapter.removePlayer(gameId);
102
+
103
+ this._participants.remove(gameId);
104
+ }
105
+
106
+ getScripted() {
107
+ return this._participants.getScripted();
108
+ }
109
+
110
+ getCount() {
111
+ return this._participants.getScripted().length;
112
+ }
113
+
114
+ getCountForTeam(teamName) {
115
+ return this._participants
116
+ .getScripted()
117
+ .filter(p => p.team === teamName).length;
118
+ }
119
+
120
+ getCountsPerTeam() {
121
+ const counts = {};
122
+
123
+ for (const participant of this._participants.getScripted()) {
124
+ counts[participant.team] = (counts[participant.team] || 0) + 1;
125
+ }
126
+
127
+ return counts;
128
+ }
129
+ }
@@ -0,0 +1,6 @@
1
+ import ScriptedManager from './ScriptedManager.js';
2
+
3
+ // Фабрика игровых host-модулей фикстуры (HostPlugin.createModules).
4
+ export default function createModules(ctx) {
5
+ return { scripted: new ScriptedManager(ctx) };
6
+ }
@@ -0,0 +1,263 @@
1
+ // Фейковое JS-ядро миниигры-фикстуры (Этап 7 плана отделения движка,
2
+ // PLAN.md §4/Wasm Host ABI §3.4): реализует полную поверхность методов,
3
+ // которую вызывает GameCoreAdapter, без Rust/WASM — доказательство, что
4
+ // движок не завязан на конкретную реализацию ядра. Актёры — плоские
5
+ // объекты { x, y, angle, team, alive }; тик — тривиальная линейная
6
+ // интеграция скорости (та же идея, что у Rust-фикстуры TestClient,
7
+ // core/src/client/game.rs: форма ABI важнее физики).
8
+ export default class FakeGameCore {
9
+ constructor(configJson) {
10
+ this._config = JSON.parse(configJson);
11
+ this._actors = new Map(); // gameId -> { x, y, angle, team, alive, vx, vy, lastInputSeq }
12
+ this._map = null;
13
+ this._events = [];
14
+ this._lastBody = [];
15
+ this._lastFrame = null;
16
+ }
17
+
18
+ // ***** карта ***** //
19
+
20
+ load_map(mapJson) {
21
+ this._map = JSON.parse(mapJson);
22
+ }
23
+
24
+ map_info() {
25
+ if (!this._map) {
26
+ return 'null';
27
+ }
28
+
29
+ return JSON.stringify({
30
+ setId: this._map.setId,
31
+ respawns: this._map.respawns || {},
32
+ });
33
+ }
34
+
35
+ clear() {
36
+ this._actors.clear();
37
+ this._events.length = 0;
38
+ this._lastBody = [];
39
+ }
40
+
41
+ // ***** участники ***** //
42
+
43
+ spawn_actor(gameId, model, teamId, x, y, angle) {
44
+ this._actors.set(gameId, {
45
+ x,
46
+ y,
47
+ angle,
48
+ team: teamId,
49
+ alive: true,
50
+ vx: 0,
51
+ vy: 0,
52
+ lastInputSeq: 0,
53
+ });
54
+
55
+ this._events.push({ type: 'panelSet', id: gameId, field: 'energy', value: 100 });
56
+ }
57
+
58
+ remove_actor(gameId) {
59
+ this._actors.delete(gameId);
60
+ }
61
+
62
+ reset_actor(gameId, teamId, x, y, angle) {
63
+ const actor = this._actors.get(gameId);
64
+
65
+ if (actor) {
66
+ Object.assign(actor, { x, y, angle, team: teamId, alive: true, vx: 0, vy: 0 });
67
+ }
68
+ }
69
+
70
+ reset_all_vitals() {
71
+ for (const actor of this._actors.values()) {
72
+ actor.alive = true;
73
+ }
74
+ }
75
+
76
+ spawn_scripted_actor(gameId, model, teamId, x, y, angle) {
77
+ this.spawn_actor(gameId, model, teamId, x, y, angle);
78
+ this._actors.get(gameId).scripted = true;
79
+ }
80
+
81
+ remove_scripted_actor(gameId) {
82
+ this.remove_actor(gameId);
83
+ }
84
+
85
+ remove_players_and_shots() {
86
+ const names = [...this._actors.keys()].map(String);
87
+
88
+ this._actors.clear();
89
+
90
+ return JSON.stringify(names);
91
+ }
92
+
93
+ // ***** ввод ***** //
94
+
95
+ apply_input(gameId, seq, action, name) {
96
+ const actor = this._actors.get(gameId);
97
+
98
+ if (!actor) {
99
+ return;
100
+ }
101
+
102
+ actor.lastInputSeq = seq;
103
+
104
+ const magnitude = action === 'down' ? 40 : 0;
105
+
106
+ if (name === 'forward') {
107
+ actor.vy = -magnitude;
108
+ } else if (name === 'back') {
109
+ actor.vy = magnitude;
110
+ } else if (name === 'fire' && action === 'down') {
111
+ this._events.push({ type: 'custom', data: { kind: 'fire', id: gameId } });
112
+ }
113
+ }
114
+
115
+ // ввод указателем: миниигра просто запоминает последнюю точку и биты —
116
+ // движковым тестам важно, что канал доходит до ядра
117
+ apply_aim(gameId, seq, x, y, flags) {
118
+ const actor = this._actors.get(gameId);
119
+
120
+ if (!actor) {
121
+ return;
122
+ }
123
+
124
+ actor.lastInputSeq = seq;
125
+ actor.aim = { x, y, flags };
126
+ }
127
+
128
+ aim_of(gameId) {
129
+ return this._actors.get(gameId)?.aim ?? null;
130
+ }
131
+
132
+ last_input_seq(gameId) {
133
+ return this._actors.get(gameId)?.lastInputSeq ?? 0;
134
+ }
135
+
136
+ // ***** запросы состояния ***** //
137
+
138
+ is_alive(gameId) {
139
+ return Boolean(this._actors.get(gameId)?.alive);
140
+ }
141
+
142
+ position_of(gameId) {
143
+ const actor = this._actors.get(gameId);
144
+
145
+ return actor ? [actor.x, actor.y] : [];
146
+ }
147
+
148
+ // FIRST_SHOT_DATA: полный снапшот игроков в раскладке снапшот-схемы
149
+ // (ключ схемы -> id -> поля), а не плоским списком: ровно её разбирает
150
+ // клиент (applyShot браузера и VirtualClient._applyGameData). Плоский
151
+ // список доезжал до клиента ключами '0', '1' — сущность первого кадра
152
+ // не появлялась на холсте, и видно это только со второго участника
153
+ players_data() {
154
+ const entry = Object.entries(
155
+ this._config.engine?.snapshot?.keys ?? {},
156
+ ).find(([, spec]) => spec.class === 'hot' && spec.kind === 'indexed8');
157
+
158
+ if (!entry) {
159
+ return '{}';
160
+ }
161
+
162
+ const [key, spec] = entry;
163
+ const rows = {};
164
+
165
+ for (const [id, actor] of this._actors) {
166
+ rows[id] = spec.fields.map(field => Number(actor[field.name] ?? 0));
167
+ }
168
+
169
+ return JSON.stringify({ [key]: rows });
170
+ }
171
+
172
+ // ***** игровой тик ***** //
173
+
174
+ step(dt) {
175
+ for (const actor of this._actors.values()) {
176
+ actor.x += actor.vx * dt;
177
+ actor.y += actor.vy * dt;
178
+ }
179
+ }
180
+
181
+ take_events() {
182
+ const events = this._events;
183
+
184
+ this._events = [];
185
+
186
+ return JSON.stringify(events);
187
+ }
188
+
189
+ // ***** упаковка снапшота (фикстурный формат — не бинарный кодек ядра;
190
+ // достаточно для тестов меты, которые не декодируют реальный фрейминг) ***** //
191
+
192
+ // поля записи — по снапшот-схеме фикстуры (config/game.js, ключ a1):
193
+ // x, y, angle, team; клиентская половина фикстуры раскладывает их в
194
+ // hot-буфер той же ширины, что заявлена схемой
195
+ pack_body() {
196
+ this._lastBody = [...this._actors.entries()].map(([id, a]) => ({
197
+ id: Number(id),
198
+ x: a.x,
199
+ y: a.y,
200
+ angle: a.angle,
201
+ team: a.team,
202
+ }));
203
+ }
204
+
205
+ body_has_events() {
206
+ return false;
207
+ }
208
+
209
+ pack_frame(serverTime, seq, hasCamera, camX, camY, forceReset, shake, playerId) {
210
+ this._lastFrame = {
211
+ serverTime,
212
+ seq,
213
+ camera: hasCamera ? [camX, camY, Boolean(forceReset), shake ?? null] : null,
214
+ playerId,
215
+ body: this._lastBody,
216
+ };
217
+ }
218
+
219
+ frame_bytes() {
220
+ return new TextEncoder().encode(JSON.stringify(this._lastFrame));
221
+ }
222
+
223
+ // ***** отладка ***** //
224
+
225
+ // форма дампа — та же, что у движкового crate::debug (тела/карта/rng),
226
+ // с поправкой на фикстуру без физики: актёры и есть тела
227
+ debug_json() {
228
+ return JSON.stringify({
229
+ bodies: [...this._actors.entries()].map(([id, a]) => ({
230
+ handle: Number(id),
231
+ tag: 2,
232
+ userData: String(id),
233
+ translation: [a.x, a.y],
234
+ rotation: a.angle,
235
+ linvel: [a.vx, a.vy],
236
+ angvel: 0,
237
+ mass: 1,
238
+ bodyType: 'Dynamic',
239
+ ccd: false,
240
+ })),
241
+ colliders: [],
242
+ map: this._map
243
+ ? { setId: this._map.setId, step: this._map.step ?? null }
244
+ : null,
245
+ nav: null,
246
+ spatial: { cells: 0, entities: 0, cellCounts: [] },
247
+ rng: { state: '0' },
248
+ step: { timeStep: this._config.timeStep ?? null, accumulator: 0 },
249
+ });
250
+ }
251
+
252
+ // ***** handoff (задел, PLAN.md §6 «открытые вопросы») ***** //
253
+
254
+ serialize_state() {
255
+ return new TextEncoder().encode(JSON.stringify([...this._actors.entries()]));
256
+ }
257
+
258
+ deserialize_state(bytes) {
259
+ const entries = JSON.parse(new TextDecoder().decode(bytes));
260
+
261
+ this._actors = new Map(entries);
262
+ }
263
+ }
@@ -0,0 +1,30 @@
1
+ // ЗАМОРОЖЕНО. Снимок плагина 2026-08-29 (поколение engineApi: 4).
2
+ // Не править: правка фикстуры маскирует слом совместимости.
3
+ import FakeGameCore from './fakeCore.js';
4
+ import gameConfig from '../config/game.js';
5
+ import authSchema from '../config/auth.js';
6
+ import clientConfig from '../config/client.js';
7
+ import systemMessages from './systemMessages.js';
8
+ import spawnCommand from './spawnCommand.js';
9
+ import createModules from './createModules.js';
10
+
11
+ // HostPlugin миниигры-фикстуры (Этап 7 плана отделения движка, PLAN.md
12
+ // §3.2): доказывает, что HostGame и движковая мета работают с любым
13
+ // HostPlugin, реализующим контракт — не только с @vimp-games/tanks. createCore
14
+ // не грузит WASM (fake-core — обычный JS-класс), поэтому фикстура не
15
+ // требует собранного Rust-ядра игры.
16
+ export default {
17
+ id: 'gen-api4',
18
+ engineApi: 4,
19
+
20
+ async createCore(coreConfigJson) {
21
+ return new FakeGameCore(coreConfigJson);
22
+ },
23
+
24
+ gameConfig,
25
+ authSchema,
26
+ chatCommands: [spawnCommand],
27
+ systemMessages,
28
+ createModules,
29
+ buildClientGameConfig: () => clientConfig,
30
+ };
@@ -0,0 +1,14 @@
1
+ // Игровая чат-команда фикстуры: '/spawn <count>' — создаёт scripted-
2
+ // участников без голосования (зеркало vimp-tanks/src/host/botCommand.js,
3
+ // упрощённое — доказывает только регистрацию HostPlugin.chatCommands).
4
+ // Регистрируется в движковом CommandProcessor.
5
+ export default {
6
+ name: '/spawn',
7
+ handler(ctx, gameId, args) {
8
+ const count = Number(args[0]) || 1;
9
+ const created = ctx.scripted.createScripted(count);
10
+
11
+ ctx.chat.pushSystem('SCRIPTED_SPAWNED', [created]);
12
+ ctx.roundManager.initiateNewRound();
13
+ },
14
+ };
@@ -0,0 +1,6 @@
1
+ // Игровые коды системных сообщений фикстуры (группа g:*) — зеркало
2
+ // vimp-tanks/src/host/systemMessages.js (группа b:*). Merge в движковый
3
+ // реестр через registerCodes (HostGame конструктор).
4
+ export default {
5
+ SCRIPTED_SPAWNED: 'g:0', // {0} scripted participant(s) spawned
6
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "gen-api4",
3
+ "engineApi": 4,
4
+ "assetsBase": "",
5
+ "entries": {
6
+ "host": "host/index.js",
7
+ "client": "client/index.js",
8
+ "wasm": "core/pkg-web/core_bg.wasm",
9
+ "wasmNode": "core/pkg-node/core.js"
10
+ },
11
+ "roomDefaults": {
12
+ "maxPlayers": 4
13
+ },
14
+ "maps": [
15
+ "arena"
16
+ ]
17
+ }
@@ -301,4 +301,21 @@ export default class FakeClientCore {
301
301
  return 'null';
302
302
  }
303
303
  }
304
+
305
+ // ***** расширение (этап 4 плана plugin-forward-compat) ***** //
306
+
307
+ // самоописание: фикстура представляет ядро текущего поколения, поэтому
308
+ // отвечает так же, как раскрытие движкового макроса
309
+ abi_describe() {
310
+ return JSON.stringify({ abi: 1, core: 'fixture', ops: ['debug.json'] });
311
+ }
312
+
313
+ // опкод вместо нового символа: пустой ответ — «не обработан»
314
+ dispatch(op) {
315
+ if (op === 'debug.json') {
316
+ return new TextEncoder().encode(this.debug_json());
317
+ }
318
+
319
+ return new Uint8Array(0);
320
+ }
304
321
  }