vimp-engine 0.22.1 → 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.
- package/bin/vimp-surface.js +88 -0
- package/core/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/client/lib/formBuilder.js +27 -8
- package/src/client/lib/socketDispatch.js +34 -0
- package/src/client/main.js +55 -7
- package/src/config/abiOps.js +31 -0
- package/src/config/clientServices.js +27 -0
- package/src/config/opcodes.js +11 -2
- package/src/config/wsports.js +9 -0
- package/src/devtools/contract/loadContext.js +25 -2
- package/src/devtools/contract/rules/b10-respawns.js +5 -3
- package/src/devtools/contract/rules/b2-engine-api.js +31 -8
- package/src/devtools/contract/rules/b3-game-config-shape.js +46 -5
- package/src/devtools/contract/rules/b4-teams.js +6 -3
- package/src/devtools/contract/rules/b5-room-form.js +40 -8
- package/src/devtools/contract/rules/c4-component-dependencies.js +10 -15
- package/src/devtools/surface/abiParse.js +175 -0
- package/src/devtools/surface/collect.js +413 -0
- package/src/host/GameCoreAdapter.js +49 -0
- package/src/lib/applyRoomOverrides.js +14 -2
- package/src/lib/capabilities.js +32 -0
- package/src/lib/coreConfig.js +3 -3
- package/src/lib/createHostRuntime.js +5 -3
- package/src/lib/formControls.js +89 -0
- package/src/lib/gameConfigView.js +211 -0
- package/src/lib/gamePlugin.js +65 -87
- package/src/lib/loadGamePackage.js +9 -2
- package/src/lib/registry.js +94 -0
- package/src/standalone/index.js +27 -13
- package/tests/fixtures/generations/gen-api3/README.md +7 -0
- package/tests/fixtures/generations/gen-api3/client/fakeClientCore.js +307 -0
- package/tests/fixtures/generations/gen-api3/client/index.js +38 -0
- package/tests/fixtures/generations/gen-api3/client/parts/Actor.js +18 -0
- package/tests/fixtures/generations/gen-api3/client/parts/ActorRadar.js +7 -0
- package/tests/fixtures/generations/gen-api3/config/auth.js +41 -0
- package/tests/fixtures/generations/gen-api3/config/client.js +151 -0
- package/tests/fixtures/generations/gen-api3/config/game.js +167 -0
- package/tests/fixtures/generations/gen-api3/core/pkg-node/core.js +4 -0
- package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +129 -0
- package/tests/fixtures/generations/gen-api3/host/createModules.js +6 -0
- package/tests/fixtures/generations/gen-api3/host/fakeCore.js +263 -0
- package/tests/fixtures/generations/gen-api3/host/index.js +30 -0
- package/tests/fixtures/generations/gen-api3/host/spawnCommand.js +14 -0
- package/tests/fixtures/generations/gen-api3/host/systemMessages.js +6 -0
- package/tests/fixtures/generations/gen-api3/manifest.json +25 -0
- package/tests/fixtures/generations/gen-api4/README.md +7 -0
- package/tests/fixtures/generations/gen-api4/client/fakeClientCore.js +307 -0
- package/tests/fixtures/generations/gen-api4/client/index.js +38 -0
- package/tests/fixtures/generations/gen-api4/client/parts/Actor.js +18 -0
- package/tests/fixtures/generations/gen-api4/client/parts/ActorRadar.js +7 -0
- package/tests/fixtures/generations/gen-api4/config/auth.js +41 -0
- package/tests/fixtures/generations/gen-api4/config/client.js +151 -0
- package/tests/fixtures/generations/gen-api4/config/game.js +156 -0
- package/tests/fixtures/generations/gen-api4/core/pkg-node/core.js +4 -0
- package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +129 -0
- package/tests/fixtures/generations/gen-api4/host/createModules.js +6 -0
- package/tests/fixtures/generations/gen-api4/host/fakeCore.js +263 -0
- package/tests/fixtures/generations/gen-api4/host/index.js +30 -0
- package/tests/fixtures/generations/gen-api4/host/spawnCommand.js +14 -0
- package/tests/fixtures/generations/gen-api4/host/systemMessages.js +6 -0
- package/tests/fixtures/generations/gen-api4/manifest.json +17 -0
- package/tests/fixtures/miniGame/client/fakeClientCore.js +17 -0
- package/tests/fixtures/miniGame/host/fakeCore.js +38 -8
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
// 'range' — контрол, который v3 умел и v4 выкинул: поколение написано с
|
|
85
|
+
// ним нарочно, это и есть нарушение И1, за которое игра состарилась
|
|
86
|
+
roomForm: [
|
|
87
|
+
{
|
|
88
|
+
name: 'maxPlayers',
|
|
89
|
+
control: 'range',
|
|
90
|
+
min: 2,
|
|
91
|
+
max: 4,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
|
|
95
|
+
// RoundManager/RTTManager (движковые) обновляют status/score/deaths/latency
|
|
96
|
+
// безусловно (не через схему) — поэтому набор колонок такой же, как у
|
|
97
|
+
// танков; отличие фикстуры — ОДНА играющая команда (см. teams ниже)
|
|
98
|
+
stat: {
|
|
99
|
+
name: {
|
|
100
|
+
key: 0,
|
|
101
|
+
bodyMethod: '=',
|
|
102
|
+
headSync: true,
|
|
103
|
+
headMethod: '#',
|
|
104
|
+
},
|
|
105
|
+
status: {
|
|
106
|
+
key: 1,
|
|
107
|
+
bodyMethod: '=',
|
|
108
|
+
bodyValue: '',
|
|
109
|
+
headValue: '',
|
|
110
|
+
},
|
|
111
|
+
score: {
|
|
112
|
+
key: 2,
|
|
113
|
+
bodyMethod: '+',
|
|
114
|
+
bodyValue: 0,
|
|
115
|
+
headMethod: '+',
|
|
116
|
+
headValue: 0,
|
|
117
|
+
},
|
|
118
|
+
deaths: {
|
|
119
|
+
key: 3,
|
|
120
|
+
bodyMethod: '+',
|
|
121
|
+
bodyValue: 0,
|
|
122
|
+
headMethod: '+',
|
|
123
|
+
headValue: 0,
|
|
124
|
+
},
|
|
125
|
+
latency: {
|
|
126
|
+
key: 4,
|
|
127
|
+
bodyMethod: '=',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
panel: {
|
|
132
|
+
// поле нарочно не health (Д2): семантику ячейки клиенту задаёт
|
|
133
|
+
// type в схеме client.js, а не имя поля
|
|
134
|
+
fields: {
|
|
135
|
+
energy: {
|
|
136
|
+
key: 'h',
|
|
137
|
+
value: 100,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
activeKey: null,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
scripted: {
|
|
144
|
+
namePrefix: 'Scripted',
|
|
145
|
+
defaultModel: 'm1',
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
spectatorTeam: 'spectators',
|
|
149
|
+
|
|
150
|
+
teams: {
|
|
151
|
+
team1: 1,
|
|
152
|
+
spectators: 2,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
playerKeys: {
|
|
156
|
+
forward: {
|
|
157
|
+
key: 1 << 0,
|
|
158
|
+
},
|
|
159
|
+
back: {
|
|
160
|
+
key: 1 << 1,
|
|
161
|
+
},
|
|
162
|
+
fire: {
|
|
163
|
+
key: 1 << 2,
|
|
164
|
+
type: 1,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
@@ -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,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: 3).
|
|
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-api3',
|
|
18
|
+
engineApi: 3,
|
|
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,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gen-api3",
|
|
3
|
+
"engineApi": 3,
|
|
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
|
+
"roomForm": [
|
|
18
|
+
{
|
|
19
|
+
"name": "maxPlayers",
|
|
20
|
+
"control": "range",
|
|
21
|
+
"min": 2,
|
|
22
|
+
"max": 4
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# gen-api4 — ЗАМОРОЖЕНО
|
|
2
|
+
|
|
3
|
+
Снимок плагина 2026-08-29, поколение `engineApi: 4`.
|
|
4
|
+
Не править: правка фикстуры маскирует слом совместимости
|
|
5
|
+
(этап 1 плана `plan/plugin-forward-compat`). Новое поколение
|
|
6
|
+
добавляется копированием текущего `../../miniGame`, а не правкой
|
|
7
|
+
этого каталога.
|