vimp-engine 0.30.0 → 0.31.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/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.12.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
package/src/client/main.js
CHANGED
|
@@ -637,7 +637,7 @@ function applyMapData(data, { notifyHost = true } = {}) {
|
|
|
637
637
|
const staticData = {};
|
|
638
638
|
let staticIndex = 0;
|
|
639
639
|
|
|
640
|
-
const pushLayers = (levelLayers, levelMap, level, solid, floor) => {
|
|
640
|
+
const pushLayers = (levelLayers, levelMap, level, solid, floor, volumes) => {
|
|
641
641
|
for (const [layer, tiles] of Object.entries(levelLayers || {})) {
|
|
642
642
|
staticData[`s${staticIndex}`] = {
|
|
643
643
|
type: 'static',
|
|
@@ -649,6 +649,9 @@ function applyMapData(data, { notifyHost = true } = {}) {
|
|
|
649
649
|
level,
|
|
650
650
|
solid,
|
|
651
651
|
floor,
|
|
652
|
+
// визуальная высота слоя в уровнях: ядро её не знает, поле едет из
|
|
653
|
+
// карты прямо в парт. 0 — слой плоский
|
|
654
|
+
volume: Number(volumes?.[layer]) || 0,
|
|
652
655
|
// прежнее имя оставлено для парта, который его уже читает
|
|
653
656
|
physicsStatic,
|
|
654
657
|
scale,
|
|
@@ -658,7 +661,7 @@ function applyMapData(data, { notifyHost = true } = {}) {
|
|
|
658
661
|
}
|
|
659
662
|
};
|
|
660
663
|
|
|
661
|
-
pushLayers(layers, map, 0, physicsStatic, []);
|
|
664
|
+
pushLayers(layers, map, 0, physicsStatic, [], data.volumes);
|
|
662
665
|
|
|
663
666
|
for (const [key, levelData] of Object.entries(data.levels || {})) {
|
|
664
667
|
const level = Number(key);
|
|
@@ -669,6 +672,7 @@ function applyMapData(data, { notifyHost = true } = {}) {
|
|
|
669
672
|
level,
|
|
670
673
|
levelData.walls || [],
|
|
671
674
|
levelData.floor || [],
|
|
675
|
+
levelData.volumes,
|
|
672
676
|
);
|
|
673
677
|
}
|
|
674
678
|
|
package/src/client/style.css
CHANGED
|
@@ -442,7 +442,6 @@ body.hide-cursor {
|
|
|
442
442
|
text-shadow: 1px 1px 1px #000;
|
|
443
443
|
font-size: 12px;
|
|
444
444
|
line-height: 15px;
|
|
445
|
-
border-radius: 4px;
|
|
446
445
|
}
|
|
447
446
|
|
|
448
447
|
.card h2 {
|
|
@@ -546,21 +545,27 @@ body.hide-cursor {
|
|
|
546
545
|
#auth-link {
|
|
547
546
|
font-size: 9px;
|
|
548
547
|
margin: 0;
|
|
549
|
-
padding: 0
|
|
548
|
+
padding: 0 5px;
|
|
550
549
|
}
|
|
551
550
|
|
|
552
551
|
#lobby-footer {
|
|
553
|
-
width: calc(var(--lobby-width) + var(--lobby-panel-pad));
|
|
554
552
|
font-size: 10px;
|
|
555
553
|
padding: 0;
|
|
556
|
-
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#lobby-footer p {
|
|
557
|
+
margin: 0 5px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
#lobby-footer p:not(:last-child)::after {
|
|
561
|
+
content: '|';
|
|
562
|
+
padding-left: 10px;
|
|
557
563
|
}
|
|
558
564
|
|
|
559
565
|
/* три равные ячейки: середину держит не space-between, а сама равность —
|
|
560
566
|
любая из крайних бывает пустой (у пакета игры может не быть метаданных),
|
|
561
567
|
и при space-between версия съезжала бы с центра */
|
|
562
|
-
#auth-link p
|
|
563
|
-
#lobby-footer p {
|
|
568
|
+
#auth-link p {
|
|
564
569
|
margin: 0;
|
|
565
570
|
flex: 1;
|
|
566
571
|
}
|
|
@@ -888,7 +893,8 @@ body.hide-cursor {
|
|
|
888
893
|
колонку не нужно: с ним align-items: center увёл бы его влево на
|
|
889
894
|
половину поля */
|
|
890
895
|
#lobby-footer {
|
|
891
|
-
|
|
896
|
+
justify-content: flex-end;
|
|
897
|
+
width: calc(var(--lobby-width) + var(--lobby-panel-pad) * 2);
|
|
892
898
|
}
|
|
893
899
|
|
|
894
900
|
/* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
|
|
@@ -40,12 +40,25 @@ export default {
|
|
|
40
40
|
|
|
41
41
|
const RAMP_DIRS = ['north', 'south', 'west', 'east'];
|
|
42
42
|
|
|
43
|
+
// Зеркало `vimp_engine_core::map::MAX_LEVELS`: уровни занимают биты 0..7
|
|
44
|
+
// маски Rapier, бит 8 отдан статике. Константа живёт в Rust, поэтому здесь
|
|
45
|
+
// она — копия, и расходится она молча: сторожем стоит фикстура
|
|
46
|
+
// `bad-levels-too-many.json` общего корпуса.
|
|
47
|
+
const MAX_LEVELS = 8;
|
|
48
|
+
|
|
43
49
|
function checkMap(name, map, violations) {
|
|
44
50
|
const at = `map "${name}"`;
|
|
45
51
|
const levels = map.levels ?? {};
|
|
46
52
|
// уровень 0 живёт в map/physicsStatic/layers и в `levels` не значится
|
|
47
53
|
const levelCount = Object.keys(levels).length + 1;
|
|
48
54
|
|
|
55
|
+
// 0. потолок уровней: больше битов у маски нет
|
|
56
|
+
if (levelCount > MAX_LEVELS) {
|
|
57
|
+
violations.push(
|
|
58
|
+
`${at}: ${levelCount} levels, at most ${MAX_LEVELS} supported`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
// 1. ключи — целые >= 1, подряд от 1
|
|
50
63
|
const keys = Object.keys(levels).map(Number);
|
|
51
64
|
|
|
@@ -69,6 +82,9 @@ function checkMap(name, map, violations) {
|
|
|
69
82
|
}
|
|
70
83
|
});
|
|
71
84
|
|
|
85
|
+
// 11. высоты объёмов уровня 0 живут в корне карты, как и его слои
|
|
86
|
+
checkVolumes(at, '0', map.layers, map.volumes, violations);
|
|
87
|
+
|
|
72
88
|
const grid = map.map ?? [];
|
|
73
89
|
|
|
74
90
|
for (const [key, level] of Object.entries(levels)) {
|
|
@@ -106,6 +122,9 @@ function checkMap(name, map, violations) {
|
|
|
106
122
|
|
|
107
123
|
// 8. тайл рендер-слоя обязан существовать в тайл-листе по индексу
|
|
108
124
|
checkLayerFrames(at, `level ${key}`, level?.layers, map.spriteSheet, violations);
|
|
125
|
+
|
|
126
|
+
// 11. высоты объёмов уровня
|
|
127
|
+
checkVolumes(at, key, level?.layers, level?.volumes, violations);
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
for (const [index, ramp] of (map.ramps ?? []).entries()) {
|
|
@@ -154,6 +173,10 @@ function checkMap(name, map, violations) {
|
|
|
154
173
|
);
|
|
155
174
|
}
|
|
156
175
|
}
|
|
176
|
+
|
|
177
|
+
// 12. рампа через несколько уровней не имеет права прошивать плиту
|
|
178
|
+
// промежуточного: танк на ней въехал бы внутрь чужого моста
|
|
179
|
+
checkRampSlabs(at, map, index, ramp, from, to, fromGrid ?? [], violations);
|
|
157
180
|
}
|
|
158
181
|
|
|
159
182
|
// 10. край плиты без перил — обрыв; приземлиться с него нужно на
|
|
@@ -309,7 +332,12 @@ function checkLevelEdges(at, map, violations) {
|
|
|
309
332
|
continue;
|
|
310
333
|
}
|
|
311
334
|
|
|
312
|
-
|
|
335
|
+
// сосед вне плиты — обрыв: под ним обязана быть проходимая
|
|
336
|
+
// поверхность того уровня, на который тело приземлится (плита
|
|
337
|
+
// этажом ниже — законный обрыв, а не дыра)
|
|
338
|
+
const landing = landingLevel(map, Number(key), nx, ny);
|
|
339
|
+
|
|
340
|
+
if (walkable(map, landing, nx, ny)) {
|
|
313
341
|
continue;
|
|
314
342
|
}
|
|
315
343
|
|
|
@@ -324,6 +352,76 @@ function checkLevelEdges(at, map, violations) {
|
|
|
324
352
|
}
|
|
325
353
|
}
|
|
326
354
|
|
|
355
|
+
// зеркало `MapLevels::landing_level` (и `config_landing_level` валидатора
|
|
356
|
+
// ядра): ближайший уровень строго ниже `from` с плитой в этой клетке, иначе
|
|
357
|
+
// земля
|
|
358
|
+
function landingLevel(map, from, x, y) {
|
|
359
|
+
for (let level = from - 1; level >= 1; level -= 1) {
|
|
360
|
+
const cfg = (map.levels ?? {})[String(level)];
|
|
361
|
+
const tile = cellAt(cfg?.map ?? [], x, y);
|
|
362
|
+
|
|
363
|
+
if (tile !== undefined && (cfg?.floor ?? []).includes(tile)) {
|
|
364
|
+
return level;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// клетки прогона рампы не должны лежать под плитой промежуточного уровня:
|
|
372
|
+
// иначе подъём прошивает чужой мост
|
|
373
|
+
function checkRampSlabs(at, map, index, ramp, from, to, grid, violations) {
|
|
374
|
+
const low = Math.min(from, to);
|
|
375
|
+
const high = Math.max(from, to);
|
|
376
|
+
|
|
377
|
+
for (let level = low + 1; level < high; level += 1) {
|
|
378
|
+
const cfg = (map.levels ?? {})[String(level)];
|
|
379
|
+
|
|
380
|
+
if (!cfg) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
grid.forEach((row, y) => {
|
|
385
|
+
row.forEach((tile, x) => {
|
|
386
|
+
if (tile !== ramp?.tile) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const over = cellAt(cfg.map ?? [], x, y);
|
|
391
|
+
|
|
392
|
+
if (over !== undefined && (cfg.floor ?? []).includes(over)) {
|
|
393
|
+
violations.push(
|
|
394
|
+
`${at}: ramp ${index} climbs ${from}->${to} through level ` +
|
|
395
|
+
`${level} floor at (${x}, ${y})`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// высота рендер-слоя: ключ обязан быть ключом `layers` того же уровня,
|
|
404
|
+
// значение — конечная высота в уровнях. Ядро высоты не использует, поэтому
|
|
405
|
+
// опечатка в ключе даёт плоскую карту и ни одной строки в консоли
|
|
406
|
+
function checkVolumes(at, key, layers, volumes, violations) {
|
|
407
|
+
for (const [layer, height] of Object.entries(volumes ?? {})) {
|
|
408
|
+
if (!Object.hasOwn(layers ?? {}, layer)) {
|
|
409
|
+
violations.push(
|
|
410
|
+
`${at}: level ${key} volumes names layer ${layer}, which is not a ` +
|
|
411
|
+
'render layer',
|
|
412
|
+
);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!Number.isFinite(height) || height <= 0 || height > MAX_LEVELS) {
|
|
417
|
+
violations.push(
|
|
418
|
+
`${at}: level ${key} volumes layer ${layer} height ${height} is not ` +
|
|
419
|
+
`in (0, ${MAX_LEVELS}]`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
327
425
|
function checkLayerFrames(at, where, layers, spriteSheet, violations) {
|
|
328
426
|
const frames = spriteSheet?.frames;
|
|
329
427
|
|
package/src/lib/capabilities.js
CHANGED
|
@@ -27,6 +27,10 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
|
|
|
27
27
|
// слоёные карты: `levels`/`ramps` в формате карты, маски уровней в
|
|
28
28
|
// физике, уровень участника (`set_actor_level`), слоёный нав-граф
|
|
29
29
|
{ value: 'map.layers', since: '0.25.0' },
|
|
30
|
+
// многоуровневые карты: уровней до MAX_LEVELS = 8 (бит на уровень),
|
|
31
|
+
// рампы через несколько уровней, правила уровня и падение у тел карты,
|
|
32
|
+
// `z`/`level` в строке динамики, `volumes` (высота рендер-слоя)
|
|
33
|
+
{ value: 'map.levelsN', since: '0.31.0' },
|
|
30
34
|
]);
|
|
31
35
|
|
|
32
36
|
// плоский список имён — то, чем правило контракта B2 проверяет `requires`
|
package/src/lib/coreConfig.js
CHANGED
|
@@ -30,6 +30,7 @@ export const buildCoreConfig = (gameConfig, overrides = {}) => {
|
|
|
30
30
|
friendlyFire: gameConfig.parts.friendlyFire,
|
|
31
31
|
mapScale: gameConfig.mapScale,
|
|
32
32
|
mapSetId: gameConfig.mapSetId,
|
|
33
|
+
mapFallTime: gameConfig.mapFallTime,
|
|
33
34
|
models,
|
|
34
35
|
weapons,
|
|
35
36
|
playerKeys: gameConfig.playerKeys,
|
|
@@ -50,6 +51,9 @@ export const buildCoreConfig = (gameConfig, overrides = {}) => {
|
|
|
50
51
|
timeStep: flat.timeStep,
|
|
51
52
|
mapScale: flat.mapScale,
|
|
52
53
|
mapSetId: flat.mapSetId,
|
|
54
|
+
// движковый ключ, а не coreParams: траектория падения одна на танки и
|
|
55
|
+
// на тела карты, подменять её игровой половиной нельзя
|
|
56
|
+
mapFallTime: flat.mapFallTime,
|
|
53
57
|
snapshot: flat.snapshot,
|
|
54
58
|
seed: flat.seed,
|
|
55
59
|
},
|
|
@@ -28,6 +28,9 @@ const FIELDS = {
|
|
|
28
28
|
mapsInVote: { default: 1 },
|
|
29
29
|
mapScale: { default: 1 },
|
|
30
30
|
mapSetId: { default: null },
|
|
31
|
+
// секунды падения на уровень высоты (FallModel ядра): игра, не знающая о
|
|
32
|
+
// многоуровневых картах, получает движковое умолчание и не замечает поля
|
|
33
|
+
mapFallTime: { default: 0.35 },
|
|
31
34
|
|
|
32
35
|
// лимит комнаты: без него берётся движковый (hostDefaults.maxPlayers)
|
|
33
36
|
'roomDefaults.maxPlayers': { default: hostDefaults.maxPlayers },
|