vimp-engine 0.15.0 → 0.17.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.8.3"
3
+ version = "0.8.4"
4
4
  edition = "2024"
5
5
  description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
6
6
  license = "MIT"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,4 +1,6 @@
1
1
  import Publisher from '../../../lib/Publisher.js';
2
+ import { ENGINE_PACKAGE, ENGINE_VERSION } from '../../lib/engineVersion.js';
3
+ import { renderPackageLink } from '../../lib/footerLink.js';
2
4
 
3
5
  // Singleton LobbyView
4
6
 
@@ -34,6 +36,16 @@ export default class LobbyView {
34
36
  this._leaderboardTotal = document.getElementById(elems.leaderboardTotalId);
35
37
  this._myPlacement = document.getElementById(elems.myPlacementId);
36
38
 
39
+ // футер лобби: содержимое статично на всё время жизни вкладки, поэтому
40
+ // пишется здесь, а не через модель и publisher
41
+ const version = document.getElementById(elems.versionId);
42
+
43
+ if (version) {
44
+ version.textContent = ENGINE_VERSION;
45
+ }
46
+
47
+ renderPackageLink(document.getElementById(elems.linkId), ENGINE_PACKAGE);
48
+
37
49
  // заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
38
50
  // при выборе игры (сама модель не хранит title манифеста)
39
51
  this._gameTitle = '';
@@ -0,0 +1,19 @@
1
+ // Версия npm-пакета движка, запечённая в бандл на сборке: сети для неё нет
2
+ // (у мастера нет version-эндпоинта), а лобби обязано показывать версию того
3
+ // кода, который сейчас исполняется, а не того, что развёрнут на сервере.
4
+ //
5
+ // Модуль лежит в client/lib, а не в src/config: config/* импортирует мастер
6
+ // под Node, а сюда тянется package.json пакета — он нужен только браузерному
7
+ // бандлу (в npm-тарболле vimp-engine package.json есть всегда, поэтому
8
+ // standalone SDK собирается у игры тем же импортом).
9
+ import pkg from '../../../package.json' with { type: 'json' };
10
+ import { homepageOf } from '../../lib/packageLink.js';
11
+
12
+ export const ENGINE_VERSION = pkg.version;
13
+
14
+ // метаданные для ссылки в футере лобби — та же форма, что мастер отдаёт про
15
+ // пакет игры (GameCatalog), чтобы оба футера строил один resolvePackageLink
16
+ export const ENGINE_PACKAGE = {
17
+ name: pkg.name,
18
+ homepage: homepageOf(pkg),
19
+ };
@@ -0,0 +1,29 @@
1
+ import { resolvePackageLink } from '../../lib/packageLink.js';
2
+
3
+ // Ссылка в футере лобби и формы входа. Один рендер на оба экрана: лобби
4
+ // подставляет пакет движка, форма входа — пакет игры из манифеста.
5
+ //
6
+ // Данных нет (standalone-манифест без метаданных пакета) — якорь гасится, а не
7
+ // прячется: ячейка футера остаётся пустой, и раскладка space-between не едет.
8
+
9
+ /**
10
+ * @param {HTMLAnchorElement|null} anchor - якорь футера
11
+ * @param {{name?: string, homepage?: string}|null} pkg - метаданные пакета
12
+ */
13
+ export function renderPackageLink(anchor, pkg) {
14
+ if (!anchor) {
15
+ return;
16
+ }
17
+
18
+ const link = resolvePackageLink(pkg);
19
+
20
+ if (!link) {
21
+ anchor.textContent = '';
22
+ anchor.removeAttribute('href');
23
+
24
+ return;
25
+ }
26
+
27
+ anchor.textContent = link.label;
28
+ anchor.href = link.url;
29
+ }
@@ -28,6 +28,7 @@ import VoteView from './components/view/Vote.js';
28
28
  import VoteCtrl from './components/controller/Vote.js';
29
29
  import { buildForm, mergeRoomDefaults, bindLiveErrors } from './lib/formBuilder.js';
30
30
  import { normalizeAuthParams } from './lib/authParams.js';
31
+ import { renderPackageLink } from './lib/footerLink.js';
31
32
  import { createGameActivator } from './lib/gameActivator.js';
32
33
  import createAutostart from './lib/autostart.js';
33
34
  import { getBootConfig, resolveBootConfig } from './boot.js';
@@ -422,6 +423,22 @@ socketMethods[PS_AUTH_DATA] = data => {
422
423
 
423
424
  document.getElementById('logo').textContent = texts?.title || 'VIMP';
424
425
 
426
+ // футер формы входа: версия и ссылка пакета ИГРЫ. Здесь, а не в AuthView:
427
+ // его elems приходят из authSchema.elems игрового плагина, и футер каркаса
428
+ // — не его забота. Метаданные кладёт в манифест мастер (GameCatalog читает
429
+ // package.json пакета); в standalone-манифесте их может не быть — тогда
430
+ // ячейки пустые, раскладку space-between это не ломает
431
+ const authVersion = document.getElementById('auth-version');
432
+
433
+ if (authVersion) {
434
+ authVersion.textContent = activeGameManifest?.packageVersion ?? '';
435
+ }
436
+
437
+ renderPackageLink(document.getElementById('auth-package-link'), {
438
+ name: activeGameManifest?.packageName,
439
+ homepage: activeGameManifest?.packageHomepage,
440
+ });
441
+
425
442
  // память клиента (localStorage) + принудительное значение поля с
426
443
  // единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
427
444
  // хосту вообще без формы и обязан прийти к тем же значениям
@@ -473,7 +473,12 @@ body.hide-cursor {
473
473
  margin: 4px auto;
474
474
  }
475
475
 
476
- #auth-link {
476
+ /* футеры лобби и формы входа: одна и та же полоса из трёх ячеек —
477
+ ссылка на пакет, его версия, копирайт. Оба заполняет JS, поэтому вид
478
+ держится в одном месте (#lobby-footer лежит в секции lobby ниже только
479
+ разметкой) */
480
+ #auth-link,
481
+ #lobby-footer {
477
482
  font-size: 9px;
478
483
  height: 15px;
479
484
  margin: 0;
@@ -483,19 +488,31 @@ body.hide-cursor {
483
488
  justify-content: space-between;
484
489
  }
485
490
 
486
- #auth-link p {
491
+ #auth-link p,
492
+ #lobby-footer p {
487
493
  margin: 0;
488
494
  }
489
495
 
490
- #auth-link p a {
496
+ #auth-link p a,
497
+ #lobby-footer p a {
491
498
  color: #ff0;
492
499
  }
493
500
 
494
- #auth-link p:nth-child(1) {
501
+ /* позициями заведует space-between, а не nth-child: любая из ячеек бывает
502
+ пустой (у пакета игры может не быть метаданных), и порядковые правила
503
+ тогда съезжали бы */
504
+ #auth-link p:first-child,
505
+ #lobby-footer p:first-child {
495
506
  text-align: left;
496
507
  }
497
508
 
498
- #auth-link p:nth-child(2) {
509
+ #auth-version,
510
+ #lobby-version {
511
+ text-align: center;
512
+ }
513
+
514
+ #auth-link p:last-child,
515
+ #lobby-footer p:last-child {
499
516
  text-align: right;
500
517
  }
501
518
 
@@ -712,6 +729,14 @@ body.hide-cursor {
712
729
  color: #aaa;
713
730
  }
714
731
 
732
+ /* колоночная обёртка панелей и футера: #lobby центрирует содержимое как
733
+ флекс-строка, поэтому футер обязан лежать в одном столбце с .lobby-grid,
734
+ а не быть её соседом */
735
+ .lobby-column {
736
+ display: flex;
737
+ flex-direction: column;
738
+ }
739
+
715
740
  /* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
716
741
  Active Servers / Leaderboard справа; собственная ширина панелей
717
742
  переопределяет фиксированный .card { width:256px } (id сильнее класса);
@@ -768,6 +793,12 @@ body.hide-cursor {
768
793
  flex-shrink: 0;
769
794
  }
770
795
 
796
+ /* полосу футера рисуют общие с #auth-link правила в секции auth; здесь —
797
+ только отступ от панелей */
798
+ #lobby-footer {
799
+ margin: 6px 0 10px;
800
+ }
801
+
771
802
  @media (max-width: 800px) {
772
803
  .lobby-grid {
773
804
  flex-direction: column;
@@ -775,6 +806,12 @@ body.hide-cursor {
775
806
  gap: unset;
776
807
  }
777
808
 
809
+ /* в столбик обёртка тянется на всю ширину: иначе она сжалась бы по
810
+ содержимому и увела бы панели из центра */
811
+ .lobby-column {
812
+ width: 100%;
813
+ }
814
+
778
815
  /* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
779
816
  иначе absolute-слой схлопнулся бы в 0 */
780
817
  .lobby-browser-inner {
@@ -55,16 +55,10 @@ const SHELL = [
55
55
  tag: 'div',
56
56
  id: 'auth-link',
57
57
  children: [
58
- {
59
- tag: 'p',
60
- children: [
61
- {
62
- tag: 'a',
63
- attrs: { href: 'https://github.com/lgick/vimp' },
64
- text: 'GitHub',
65
- },
66
- ],
67
- },
58
+ // ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
59
+ // из метаданных пакета в манифесте
60
+ { tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
61
+ { tag: 'p', id: 'auth-version' },
68
62
  { tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
69
63
  ],
70
64
  },
@@ -15,6 +15,10 @@ div#auth
15
15
  div#auth-informs
16
16
 
17
17
  div#auth-link
18
+ //- ссылка и версия пакета ИГРЫ (мастер кладёт их в манифест, читая
19
+ //- package.json пакета) — проставляет main.js по AUTH_DATA; без данных
20
+ //- ячейки остаются пустыми
18
21
  p
19
- a(href='https://github.com/lgick/vimp') GitHub
22
+ a#auth-package-link
23
+ p#auth-version
20
24
  p &copy; #{new Date().getFullYear()} VIMP
@@ -1,54 +1,64 @@
1
1
  //- скрыт по умолчанию: открывается из main.js только после успешного логина
2
2
  //- в #lobby-auth (Этап B2, LobbyAuthCtrl 'authenticated')
3
3
  div#lobby(style='display:none')
4
- div.lobby-grid
5
- //- левая панель: профиль + форма создания комнаты (lobby-page-plan)
6
- div#lobby-setup-panel.card
7
- h2= 'VIMP'
8
- //- бейдж входа (Этап B2): управляется LobbyAuthView, показывается
9
- //- вместе с #lobby после успешной авторизации
10
- div#lobby-user
11
- div
12
- span= 'Signed in as '
13
- b#lobby-user-nick
14
- input#lobby-user-logout(type='button', value='Sign out')
15
- hr.lobby-divider
16
- h3.lobby-section-title= 'HOST A GAME'
17
- div#lobby-create
18
- //- селектор игры (lobby-page-plan): рабочий с первой же игрой в
19
- //- каталоге мастера, а не только когда их несколько
20
- div.form-row
21
- label.form-label(for='lobby-game')= 'Select Game'
22
- select#lobby-game.field-select
23
- div.form-row
24
- label.form-label(for='lobby-name')= 'Server name'
25
- input#lobby-name.field-text(type='text', maxlength='30')
26
- //- поля настроек комнаты генерируются по явной схеме roomForm
27
- //- манифеста активной игры (docs/en/plugin-api.md "Form schema")
28
- div#lobby-fields
29
- //- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
30
- //- плагина выбранной игры строкой в форме, а не в #tech-informer:
31
- //- тот кроет вкладку непрозрачным слоем и рассчитан на терминальные
32
- //- причины, а эти восстановимы (поправить поле, другая игра,
33
- //- повторный клик). Над кнопкой рядом с полями, к которым относятся
34
- div#lobby-error.form-error
35
- input#lobby-host(type='button', value='Create server')
36
- //- правая панель: вкладки Active Servers / Leaderboard (lobby-page-plan)
37
- div#lobby-browser-panel.card
38
- //- абсолютный слой: высоту .card задаёт форма слева (stretch), а
39
- //- список внутри скроллится, не раздувая карточку (lobby-page-plan)
40
- div.lobby-browser-inner
41
- div.lobby-tabs-header
42
- input#btn-show-servers.lobby-tab-btn.active(type='button', value='Active Servers')
43
- input#btn-show-leaderboard.lobby-tab-btn(type='button', value='Leaderboard')
44
- div#lobby-servers-content
45
- input#lobby-search.field-text(type='text', placeholder='Search servers…')
46
- ul#lobby-list
47
- p#lobby-empty= 'No servers available'
48
- input#lobby-more(type='button', value='Load more')
49
- div#lobby-leaderboard-content(style='display:none')
50
- div.lobby-leaderboard-header
51
- span#leaderboard-title
52
- span#leaderboard-total
53
- ol#lobby-leaderboard-list
54
- p#lobby-my-placement
4
+ //- колоночная обёртка: сам #lobby — флекс-строка, центрирующая содержимое,
5
+ //- и футер под панелями иначе встал бы третьей колонкой рядом с ними
6
+ div.lobby-column
7
+ div.lobby-grid
8
+ //- левая панель: профиль + форма создания комнаты (lobby-page-plan)
9
+ div#lobby-setup-panel.card
10
+ h2= 'VIMP'
11
+ //- бейдж входа (Этап B2): управляется LobbyAuthView, показывается
12
+ //- вместе с #lobby после успешной авторизации
13
+ div#lobby-user
14
+ div
15
+ span= 'Signed in as '
16
+ b#lobby-user-nick
17
+ input#lobby-user-logout(type='button', value='Sign out')
18
+ hr.lobby-divider
19
+ h3.lobby-section-title= 'HOST A GAME'
20
+ div#lobby-create
21
+ //- селектор игры (lobby-page-plan): рабочий с первой же игрой в
22
+ //- каталоге мастера, а не только когда их несколько
23
+ div.form-row
24
+ label.form-label(for='lobby-game')= 'Select Game'
25
+ select#lobby-game.field-select
26
+ div.form-row
27
+ label.form-label(for='lobby-name')= 'Server name'
28
+ input#lobby-name.field-text(type='text', maxlength='30')
29
+ //- поля настроек комнаты генерируются по явной схеме roomForm
30
+ //- манифеста активной игры (docs/en/plugin-api.md "Form schema")
31
+ div#lobby-fields
32
+ //- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
33
+ //- плагина выбранной игрыстрокой в форме, а не в #tech-informer:
34
+ //- тот кроет вкладку непрозрачным слоем и рассчитан на терминальные
35
+ //- причины, а эти восстановимы (поправить поле, другая игра,
36
+ //- повторный клик). Над кнопкой рядом с полями, к которым относятся
37
+ div#lobby-error.form-error
38
+ input#lobby-host(type='button', value='Create server')
39
+ //- правая панель: вкладки Active Servers / Leaderboard (lobby-page-plan)
40
+ div#lobby-browser-panel.card
41
+ //- абсолютный слой: высоту .card задаёт форма слева (stretch), а
42
+ //- список внутри скроллится, не раздувая карточку (lobby-page-plan)
43
+ div.lobby-browser-inner
44
+ div.lobby-tabs-header
45
+ input#btn-show-servers.lobby-tab-btn.active(type='button', value='Active Servers')
46
+ input#btn-show-leaderboard.lobby-tab-btn(type='button', value='Leaderboard')
47
+ div#lobby-servers-content
48
+ input#lobby-search.field-text(type='text', placeholder='Search servers…')
49
+ ul#lobby-list
50
+ p#lobby-empty= 'No servers available'
51
+ input#lobby-more(type='button', value='Load more')
52
+ div#lobby-leaderboard-content(style='display:none')
53
+ div.lobby-leaderboard-header
54
+ span#leaderboard-title
55
+ span#leaderboard-total
56
+ ol#lobby-leaderboard-list
57
+ p#lobby-my-placement
58
+ //- футер: ссылку и версию пакета движка проставляет LobbyView
59
+ //- (engineVersion.js + lib/packageLink.js)
60
+ div#lobby-footer
61
+ p
62
+ a#lobby-link
63
+ p#lobby-version
64
+ p &copy; #{new Date().getFullYear()} VIMP
@@ -96,6 +96,11 @@ export default {
96
96
  leaderboardTitleId: 'leaderboard-title',
97
97
  leaderboardTotalId: 'leaderboard-total',
98
98
  myPlacementId: 'lobby-my-placement',
99
+
100
+ // футер: версия npm-пакета движка, запечённая в бандл при сборке
101
+ // (client/lib/engineVersion.js), и ссылка на его страницу
102
+ versionId: 'lobby-version',
103
+ linkId: 'lobby-link',
99
104
  },
100
105
 
101
106
  // создание комнаты (хост в этой же вкладке); лимит игроков/время
@@ -0,0 +1,126 @@
1
+ // Ссылка на пакет для футеров лобби и формы входа: движок показывает рядом
2
+ // npm-версию, и клик по соседней ячейке должен вести на страницу этого пакета.
3
+ //
4
+ // Источник — `homepage`/`repository` его package.json, но объявлены они не у
5
+ // всех (у @vimp-games/snakes нет ни того, ни другого), а footer без ссылки
6
+ // выглядит поломкой. Поэтому фолбэк — страница пакета на npm: она выводится из
7
+ // одного лишь имени, то есть существует всегда, и сама ведёт на репозиторий,
8
+ // если он объявлен.
9
+ //
10
+ // Модуль изоморфный: мастер зовёт homepageOf для пакета игры (GameCatalog),
11
+ // клиент — для собственного package.json движка (client/lib/engineVersion.js).
12
+
13
+ const NPM_PACKAGE_BASE = 'https://www.npmjs.com/package/';
14
+
15
+ /**
16
+ * Сырой адрес проекта из package.json: homepage, иначе repository.
17
+ * @param {Object} pkg - разобранный package.json
18
+ * @returns {string|null} адрес как объявлен (нормализует resolvePackageLink)
19
+ */
20
+ export function homepageOf(pkg) {
21
+ if (!pkg || typeof pkg !== 'object') {
22
+ return null;
23
+ }
24
+
25
+ const { homepage, repository } = pkg;
26
+
27
+ if (typeof homepage === 'string' && homepage.trim()) {
28
+ return homepage;
29
+ }
30
+
31
+ // repository бывает и строкой ("github:user/repo"), и объектом {type, url}
32
+ if (typeof repository === 'string') {
33
+ return repository;
34
+ }
35
+
36
+ if (repository && typeof repository.url === 'string') {
37
+ return repository.url;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Ссылка футера по метаданным пакета.
45
+ * @param {{name?: string, homepage?: string}} [pkg] - имя пакета и сырой homepage
46
+ * @returns {{url: string, label: string}|null} null, если нет даже имени
47
+ */
48
+ export function resolvePackageLink(pkg) {
49
+ const { name, homepage } = pkg ?? {};
50
+ const declared = normalize(homepage);
51
+
52
+ if (declared) {
53
+ return { url: declared, label: labelFor(declared) };
54
+ }
55
+
56
+ if (typeof name === 'string' && name.trim()) {
57
+ return { url: `${NPM_PACKAGE_BASE}${name.trim()}`, label: 'npm' };
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ // приводит объявленный адрес к https-виду; всё, что после нормализации не
64
+ // http(s) (git@ без известной формы, file:, мусор), — не ссылка, а фолбэк
65
+ function normalize(raw) {
66
+ if (typeof raw !== 'string') {
67
+ return null;
68
+ }
69
+
70
+ let url = raw.trim();
71
+
72
+ if (!url) {
73
+ return null;
74
+ }
75
+
76
+ // npm-шорткаты: "github:user/repo", "gitlab:...", "bitbucket:..."
77
+ const shorthand = /^(github|gitlab|bitbucket):([\w.-]+\/[\w.-]+)$/.exec(url);
78
+
79
+ if (shorthand) {
80
+ const hosts = {
81
+ github: 'github.com',
82
+ gitlab: 'gitlab.com',
83
+ bitbucket: 'bitbucket.org',
84
+ };
85
+
86
+ return `https://${hosts[shorthand[1]]}/${shorthand[2]}`;
87
+ }
88
+
89
+ url = url.replace(/^git\+/, '');
90
+
91
+ // scp-форма git@host:user/repo — двоеточие здесь разделитель, не порт
92
+ const scp = /^git@([^:/]+):(.+)$/.exec(url);
93
+
94
+ if (scp) {
95
+ url = `https://${scp[1]}/${scp[2]}`;
96
+ }
97
+
98
+ url = url
99
+ .replace(/^ssh:\/\/git@/, 'https://')
100
+ .replace(/^ssh:\/\//, 'https://')
101
+ .replace(/^git:\/\//, 'https://');
102
+
103
+ if (!/^https?:\/\//.test(url)) {
104
+ return null;
105
+ }
106
+
107
+ return (
108
+ url
109
+ .replace(/\.git(?=$|[#?])/, '')
110
+ // npm сам дописывает "#readme" в homepage при генерации package.json
111
+ .replace(/#readme$/, '')
112
+ .replace(/\/+$/, '')
113
+ );
114
+ }
115
+
116
+ function labelFor(url) {
117
+ if (/^https?:\/\/(www\.)?github\.com\//.test(url)) {
118
+ return 'GitHub';
119
+ }
120
+
121
+ if (url.startsWith(NPM_PACKAGE_BASE)) {
122
+ return 'npm';
123
+ }
124
+
125
+ return 'Website';
126
+ }