vimp-engine 0.16.0 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,6 @@
1
1
  import Publisher from '../../../lib/Publisher.js';
2
- import { ENGINE_VERSION } from '../../lib/engineVersion.js';
2
+ import { ENGINE_PROJECT_URL, ENGINE_VERSION } from '../../lib/engineVersion.js';
3
+ import { renderProjectLink } from '../../lib/footerLink.js';
3
4
 
4
5
  // Singleton LobbyView
5
6
 
@@ -35,14 +36,16 @@ export default class LobbyView {
35
36
  this._leaderboardTotal = document.getElementById(elems.leaderboardTotalId);
36
37
  this._myPlacement = document.getElementById(elems.myPlacementId);
37
38
 
38
- // футер лобби: строка статична на всё время жизни вкладки, поэтому
39
+ // футер лобби: содержимое статично на всё время жизни вкладки, поэтому
39
40
  // пишется здесь, а не через модель и publisher
40
41
  const version = document.getElementById(elems.versionId);
41
42
 
42
43
  if (version) {
43
- version.textContent = `vimp-engine ${ENGINE_VERSION}`;
44
+ version.textContent = ENGINE_VERSION;
44
45
  }
45
46
 
47
+ renderProjectLink(document.getElementById(elems.linkId), ENGINE_PROJECT_URL);
48
+
46
49
  // заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
47
50
  // при выборе игры (сама модель не хранит title манифеста)
48
51
  this._gameTitle = '';
@@ -7,5 +7,11 @@
7
7
  // бандлу (в npm-тарболле vimp-engine package.json есть всегда, поэтому
8
8
  // standalone SDK собирается у игры тем же импортом).
9
9
  import pkg from '../../../package.json' with { type: 'json' };
10
+ import { resolveProjectUrl } from '../../lib/packageLink.js';
10
11
 
11
12
  export const ENGINE_VERSION = pkg.version;
13
+
14
+ // адрес проекта для ссылки в футере лобби — то же поле, что мастер отдаёт
15
+ // про пакет игры (GameCatalog.packageUrl), чтобы оба футера строил один
16
+ // projectLink
17
+ export const ENGINE_PROJECT_URL = resolveProjectUrl(pkg);
@@ -0,0 +1,29 @@
1
+ import { projectLink } from '../../lib/packageLink.js';
2
+
3
+ // Ссылка в футере лобби и формы входа. Один рендер на оба экрана: лобби
4
+ // подставляет адрес проекта движка, форма входа — адрес игры из манифеста.
5
+ //
6
+ // Адреса нет (пакет не объявил repository/homepage) — якорь гасится, а не
7
+ // прячется: ячейка футера остаётся пустой, и раскладка space-between не едет.
8
+
9
+ /**
10
+ * @param {HTMLAnchorElement|null} anchor - якорь футера
11
+ * @param {string|null} url - адрес проекта (resolveProjectUrl)
12
+ */
13
+ export function renderProjectLink(anchor, url) {
14
+ if (!anchor) {
15
+ return;
16
+ }
17
+
18
+ const link = projectLink(url);
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
+ }
@@ -97,7 +97,9 @@ function buildSelect(descriptor, ctx) {
97
97
  el.value = value;
98
98
  },
99
99
  onChange(cb) {
100
- el.addEventListener('change', () => cb({ name: descriptor.name, value: el.value }));
100
+ el.addEventListener('change', () =>
101
+ cb({ name: descriptor.name, value: el.value }),
102
+ );
101
103
  },
102
104
  };
103
105
  }
@@ -110,6 +112,11 @@ function buildText(descriptor) {
110
112
  const numeric = isNumeric(descriptor);
111
113
 
112
114
  el.type = 'text';
115
+ // выпадашка прошлых значений/автозаполнения кроет форму, а подставить в
116
+ // игровое поле ей нечего. Только у текстового поля: у select такого списка
117
+ // нет, там autocomplete управлял бы лишь восстановлением значения при
118
+ // перезагрузке — а <option> создаёт JS, восстанавливать нечего
119
+ el.autocomplete = 'off';
113
120
  el.name = descriptor.name;
114
121
 
115
122
  // pattern/required — семантика контрола (a11y, автозаполнение), НЕ
@@ -157,7 +164,9 @@ function buildText(descriptor) {
157
164
  el.value = numeric ? toDisplay(descriptor, value) : (value ?? '');
158
165
  },
159
166
  onChange(cb) {
160
- el.addEventListener('change', () => cb({ name: descriptor.name, value: getValue() }));
167
+ el.addEventListener('change', () =>
168
+ cb({ name: descriptor.name, value: getValue() }),
169
+ );
161
170
  },
162
171
  };
163
172
  }
@@ -182,7 +191,9 @@ function buildCheckbox(descriptor) {
182
191
  el.checked = Boolean(value);
183
192
  },
184
193
  onChange(cb) {
185
- el.addEventListener('change', () => cb({ name: descriptor.name, value: el.checked }));
194
+ el.addEventListener('change', () =>
195
+ cb({ name: descriptor.name, value: el.checked }),
196
+ );
186
197
  },
187
198
  };
188
199
  }
@@ -239,7 +250,9 @@ function buildRadio(descriptor, ctx) {
239
250
  },
240
251
  onChange(cb) {
241
252
  inputs.forEach(input => {
242
- input.addEventListener('change', () => cb({ name: descriptor.name, value: getValue() }));
253
+ input.addEventListener('change', () =>
254
+ cb({ name: descriptor.name, value: getValue() }),
255
+ );
243
256
  });
244
257
  },
245
258
  };
@@ -458,7 +471,9 @@ export function renderFormErrors(container, errors) {
458
471
  // для них, как и раньше, остаётся имя поля
459
472
  const title = String(label || name).toUpperCase();
460
473
 
461
- line.textContent = error ? `${title}: ${error}` : `${title} is not correctly!`;
474
+ line.textContent = error
475
+ ? `${title}: ${error}`
476
+ : `${title} is not correctly!`;
462
477
 
463
478
  container.appendChild(line);
464
479
  });
@@ -556,7 +571,9 @@ export function buildForm(descriptors, container, ctx = {}, onChange) {
556
571
  } catch (e) {
557
572
  // одно поле с кривой/неизвестной схемой не должно ронять всю форму
558
573
  // (room-форма и особенно auth-форма — единственный путь к кнопке Start)
559
- console.error(`formBuilder: skipping field "${descriptor.name}" — ${e.message}`);
574
+ console.error(
575
+ `formBuilder: skipping field "${descriptor.name}" — ${e.message}`,
576
+ );
560
577
  return;
561
578
  }
562
579
 
@@ -602,7 +619,8 @@ export function buildForm(descriptors, container, ctx = {}, onChange) {
602
619
  const label = document.createElement(field.labelFor ? 'label' : 'span');
603
620
 
604
621
  label.className = 'form-label';
605
- label.textContent = (descriptor.label || descriptor.name) + buildLabelSuffix(descriptor);
622
+ label.textContent =
623
+ (descriptor.label || descriptor.name) + buildLabelSuffix(descriptor);
606
624
 
607
625
  if (field.labelFor) {
608
626
  label.htmlFor = field.labelFor;
@@ -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 { renderProjectLink } 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,18 +423,22 @@ socketMethods[PS_AUTH_DATA] = data => {
422
423
 
423
424
  document.getElementById('logo').textContent = texts?.title || 'VIMP';
424
425
 
425
- // версия пакета игры в футере формы входа. Здесь, а не в AuthView: его
426
- // elems приходят из authSchema.elems игрового плагина, и футер каркаса
427
- // не его забота. Игра, собранная до появления packageVersion, оставляет
428
- // строку пустой (#auth-link держит раскладку через space-between)
426
+ // футер формы входа: версия и ссылка пакета ИГРЫ. Здесь, а не в AuthView:
427
+ // его elems приходят из authSchema.elems игрового плагина, и футер каркаса
428
+ // не его забота. Метаданные кладёт в манифест мастер (GameCatalog читает
429
+ // package.json пакета); в standalone-манифесте их может не быть — тогда
430
+ // ячейки пустые, раскладку space-between это не ломает
429
431
  const authVersion = document.getElementById('auth-version');
430
432
 
431
433
  if (authVersion) {
432
- const packageVersion = activeGameManifest?.packageVersion;
433
-
434
- authVersion.textContent = packageVersion ? `v${packageVersion}` : '';
434
+ authVersion.textContent = activeGameManifest?.packageVersion ?? '';
435
435
  }
436
436
 
437
+ renderProjectLink(
438
+ document.getElementById('auth-package-link'),
439
+ activeGameManifest?.packageUrl,
440
+ );
441
+
437
442
  // память клиента (localStorage) + принудительное значение поля с
438
443
  // единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
439
444
  // хосту вообще без формы и обязан прийти к тем же значениям
@@ -385,10 +385,11 @@ body.hide-cursor {
385
385
  background-color: #333;
386
386
  border: 1px solid #eee;
387
387
  padding: 10px;
388
- margin: 10px auto;
388
+ margin: 10px auto 5px;
389
389
  text-shadow: 1px 1px 1px #000;
390
390
  font-size: 12px;
391
391
  line-height: 15px;
392
+ border-radius: 4px;
392
393
  }
393
394
 
394
395
  .card h2 {
@@ -396,17 +397,19 @@ body.hide-cursor {
396
397
  line-height: 30px;
397
398
  margin: 0 0 10px 0;
398
399
  text-align: center;
400
+ border-bottom: 1px solid #444;
401
+ padding: 5px 0 10px;
399
402
  }
400
403
 
401
- /* кнопки: провайдеры логина, Create server, Start, Load more, Sign out
402
- нативный вид input[type=button], без декоративных стилей, только размер */
404
+ /* кнопки: провайдеры логина, Choose nick, Start, Load more нативный вид
405
+ input[type=button], без декоративных стилей, только размер. Create server и
406
+ Sign out сюда не входят: они лежат во флекс-контейнерах с gap и
407
+ центрируются через align-self (секция lobby-auth ниже) */
403
408
  .lobby-auth-provider,
404
409
  #lobby-auth-nick-submit,
405
- #lobby-host,
406
- #lobby-user-logout,
407
410
  #lobby-more,
408
411
  #auth-enter {
409
- margin: 4px;
412
+ margin: 15px auto;
410
413
  }
411
414
 
412
415
  /* один вертикальный ритм для всех строк формы (room-форма и auth-форма) */
@@ -415,7 +418,7 @@ body.hide-cursor {
415
418
  align-items: center;
416
419
  justify-content: space-between;
417
420
  gap: 8px;
418
- margin: 8px 0;
421
+ margin: 0;
419
422
  }
420
423
 
421
424
  .form-row input[type='text'] {
@@ -423,6 +426,10 @@ body.hide-cursor {
423
426
  text-align: center;
424
427
  }
425
428
 
429
+ #lobby-fields .form-row {
430
+ margin: 6px 0;
431
+ }
432
+
426
433
  .form-label {
427
434
  color: #aaa;
428
435
  }
@@ -470,43 +477,71 @@ body.hide-cursor {
470
477
 
471
478
  #auth-enter {
472
479
  display: block;
473
- margin: 4px auto;
474
480
  }
475
481
 
476
- #auth-link {
477
- font-size: 9px;
482
+ /* футеры лобби и формы входа: одна и та же полоса из трёх ячеек —
483
+ ссылка на пакет, его версия, копирайт. Оба заполняет JS, поэтому вид
484
+ держится в одном месте (#lobby-footer лежит в секции lobby ниже только
485
+ разметкой) */
486
+ #auth-link,
487
+ #lobby-footer {
478
488
  height: 15px;
479
- margin: 0;
480
- padding: 0 5px;
481
489
  text-shadow: none;
482
490
  display: flex;
483
- justify-content: space-between;
484
491
  }
485
492
 
486
- #auth-link p {
493
+ #auth-link {
494
+ font-size: 9px;
495
+ margin: 0;
496
+ padding: 0 10px;
497
+ }
498
+
499
+ /* ширина — контентная область правой панели лобби (её ширина минус
500
+ собственные поля .lobby-browser-inner), под которой футер и стоит;
501
+ max-width, а не width: в одну колонку он обязан ужиматься по экрану */
502
+ #lobby-footer {
503
+ width: 100%;
504
+ max-width: calc(var(--lobby-browser-width) - 2 * var(--lobby-panel-pad));
505
+ font-size: 10px;
506
+ padding: 0;
507
+ margin: 0 var(--lobby-panel-pad) 0 0;
508
+ }
509
+
510
+ /* три равные ячейки: середину держит не space-between, а сама равность —
511
+ любая из крайних бывает пустой (у пакета игры может не быть метаданных),
512
+ и при space-between версия съезжала бы с центра */
513
+ #auth-link p,
514
+ #lobby-footer p {
487
515
  margin: 0;
516
+ flex: 1;
488
517
  }
489
518
 
490
- #auth-link p a {
519
+ #auth-link p a,
520
+ #lobby-footer p a {
491
521
  color: #ff0;
492
522
  }
493
523
 
494
- /* три колонки: GitHub версия игры копирайт. Позициями заведует
495
- space-between, а не nth-child: средний элемент бывает пустым (игра
496
- собрана без packageVersion), и порядковые правила тогда съезжали бы */
497
- #auth-link p:first-child {
524
+ /* позиция текста внутри ячейки; сами ячейки уже разложены flex: 1 выше */
525
+ #auth-link p:first-child,
526
+ #lobby-footer p:first-child {
498
527
  text-align: left;
499
528
  }
500
529
 
501
- #auth-version {
530
+ #auth-version,
531
+ #lobby-version {
502
532
  text-align: center;
503
533
  }
504
534
 
505
- #auth-link p:last-child {
535
+ #auth-link p:last-child,
536
+ #lobby-footer p:last-child {
506
537
  text-align: right;
507
538
  }
508
539
 
509
- .auth-inform {
540
+ /* коробка контента: блок помощи формы входа и группа полей комнаты.
541
+ .lobby-card сюда не входит — ей нужен фон темнее, иначе он совпадёт с
542
+ её собственным hover'ом и наведение перестанет быть видно */
543
+ .auth-inform,
544
+ #lobby-fields {
510
545
  background-color: #444;
511
546
  padding: 7px 13px;
512
547
  margin: 10px 0;
@@ -514,6 +549,12 @@ body.hide-cursor {
514
549
  border: 1px solid #999;
515
550
  }
516
551
 
552
+ /* вертикаль внутри #lobby-create держит его gap — собственный margin
553
+ коробки удвоил бы отступ до соседей */
554
+ #lobby-fields {
555
+ margin: 0;
556
+ }
557
+
517
558
  .auth-inform h4 {
518
559
  margin: 0 0 4px 0;
519
560
  text-align: center;
@@ -582,6 +623,13 @@ body.hide-cursor {
582
623
  /*lobby*/
583
624
 
584
625
  #lobby {
626
+ /* ширины панелей и их внутренние поля: по ним же считается ширина футера
627
+ (секция auth выше), поэтому числа живут в одном месте — иначе смена
628
+ ширины любой панели молча разъезжает вёрстку */
629
+ --lobby-setup-width: 410px;
630
+ --lobby-browser-width: 430px;
631
+ --lobby-panel-pad: 10px;
632
+
585
633
  background-color: #222;
586
634
  position: absolute;
587
635
  width: 100%;
@@ -592,16 +640,20 @@ body.hide-cursor {
592
640
  display: flex;
593
641
  align-items: flex-start;
594
642
  justify-content: center;
595
- padding: 10px 0 0;
643
+ padding: 0;
596
644
  box-sizing: border-box;
597
645
  }
598
646
 
647
+ #lobby-user,
648
+ .lobby-section-title,
649
+ #lobby-create {
650
+ padding: 8px;
651
+ }
652
+
599
653
  #lobby-create {
600
654
  display: flex;
601
655
  flex-direction: column;
602
- gap: 8px;
603
- margin-bottom: 12px;
604
- padding: 8px;
656
+ gap: 10px;
605
657
  }
606
658
 
607
659
  /* поле имени сервера — строка формы, а не узкий числовой инпут
@@ -628,13 +680,10 @@ body.hide-cursor {
628
680
  margin: 0;
629
681
  }
630
682
 
631
- #lobby-host {
632
- margin: 16px auto 0;
633
- }
634
-
635
683
  #lobby-search {
636
684
  display: block;
637
685
  width: 100%;
686
+ margin-bottom: 10px;
638
687
  }
639
688
 
640
689
  #lobby-list {
@@ -652,9 +701,10 @@ body.hide-cursor {
652
701
  column-gap: 8px;
653
702
  row-gap: 2px;
654
703
  padding: 10px 12px;
655
- margin-bottom: 6px;
704
+ margin: 0 0 6px;
656
705
  background-color: #363636;
657
- border: 1px solid #444;
706
+ border: 1px solid #999;
707
+ border-radius: 4px;
658
708
  cursor: pointer;
659
709
  align-items: center;
660
710
  }
@@ -725,6 +775,7 @@ body.hide-cursor {
725
775
  .lobby-column {
726
776
  display: flex;
727
777
  flex-direction: column;
778
+ align-items: flex-end;
728
779
  }
729
780
 
730
781
  /* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
@@ -734,12 +785,12 @@ body.hide-cursor {
734
785
  высоты более высокой панели */
735
786
  .lobby-grid {
736
787
  display: flex;
737
- gap: 20px;
788
+ gap: 10px;
738
789
  justify-content: center;
739
790
  }
740
791
 
741
792
  #lobby-setup-panel {
742
- width: 410px;
793
+ width: var(--lobby-setup-width);
743
794
  }
744
795
 
745
796
  /* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
@@ -748,7 +799,7 @@ body.hide-cursor {
748
799
  вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
749
800
  расчёте высоты строки) */
750
801
  #lobby-browser-panel {
751
- width: 430px;
802
+ width: var(--lobby-browser-width);
752
803
  position: relative;
753
804
  padding: 0;
754
805
  }
@@ -758,7 +809,7 @@ body.hide-cursor {
758
809
  inset: 0;
759
810
  display: flex;
760
811
  flex-direction: column;
761
- padding: 10px;
812
+ padding: var(--lobby-panel-pad);
762
813
  box-sizing: border-box;
763
814
  }
764
815
 
@@ -783,31 +834,23 @@ body.hide-cursor {
783
834
  flex-shrink: 0;
784
835
  }
785
836
 
786
- /* версия пакета движка под панелями — тем же мелким кеглем, что и футер
787
- формы входа (#auth-link) */
788
- #lobby-footer {
789
- font-size: 9px;
790
- padding: 0 5px;
791
- margin: 6px 0 10px;
792
- text-align: center;
793
- text-shadow: none;
794
- }
795
-
796
- #lobby-footer p {
797
- margin: 0;
798
- }
799
-
800
837
  @media (max-width: 800px) {
801
838
  .lobby-grid {
802
- flex-direction: column;
803
- align-items: stretch;
804
- gap: unset;
839
+ display: block;
805
840
  }
806
841
 
807
842
  /* в столбик обёртка тянется на всю ширину: иначе она сжалась бы по
808
843
  содержимому и увела бы панели из центра */
809
844
  .lobby-column {
810
845
  width: 100%;
846
+ align-items: center;
847
+ }
848
+
849
+ /* правое поле, прижимавшее футер к контенту правой панели, в одну
850
+ колонку не нужно: с ним align-items: center увёл бы его влево на
851
+ половину поля */
852
+ #lobby-footer {
853
+ margin: 0 auto;
811
854
  }
812
855
 
813
856
  /* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
@@ -55,18 +55,9 @@ 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
- },
68
- // версия пакета игры (manifest.packageVersion) — текст пишет
69
- // main.js по AUTH_DATA
58
+ // ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
59
+ // из метаданных пакета в манифесте
60
+ { tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
70
61
  { tag: 'p', id: 'auth-version' },
71
62
  { tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
72
63
  ],
@@ -15,9 +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
20
- //- версия пакета игры (manifest.packageVersion): проставляет main.js,
21
- //- у игры, собранной до появления поля, строка остаётся пустой
22
+ a#auth-package-link
22
23
  p#auth-version
23
24
  p &copy; #{new Date().getFullYear()} VIMP
@@ -2,4 +2,4 @@ div#chat
2
2
  div#chat-box
3
3
  //- maxlength — лишь UX-подсказка; авторитетный лимит на сервере
4
4
  //- (chatMaxLength в src/config/hostDefaults.js). Значения держать синхронными.
5
- input#cmd(type='text', maxlength='60' autocomplete="off")
5
+ input#cmd(type='text', maxlength='60', autocomplete='off')
@@ -21,11 +21,11 @@ div#lobby(style='display:none')
21
21
  //- селектор игры (lobby-page-plan): рабочий с первой же игрой в
22
22
  //- каталоге мастера, а не только когда их несколько
23
23
  div.form-row
24
- label.form-label(for='lobby-game')= 'Select Game'
24
+ label.form-label(for='lobby-game')= 'Select game'
25
25
  select#lobby-game.field-select
26
26
  div.form-row
27
27
  label.form-label(for='lobby-name')= 'Server name'
28
- input#lobby-name.field-text(type='text', maxlength='30')
28
+ input#lobby-name.field-text(type='text', maxlength='30', autocomplete='off')
29
29
  //- поля настроек комнаты генерируются по явной схеме roomForm
30
30
  //- манифеста активной игры (docs/en/plugin-api.md "Form schema")
31
31
  div#lobby-fields
@@ -45,7 +45,7 @@ div#lobby(style='display:none')
45
45
  input#btn-show-servers.lobby-tab-btn.active(type='button', value='Active Servers')
46
46
  input#btn-show-leaderboard.lobby-tab-btn(type='button', value='Leaderboard')
47
47
  div#lobby-servers-content
48
- input#lobby-search.field-text(type='text', placeholder='Search servers…')
48
+ input#lobby-search.field-text(type='text', placeholder='Search servers…', autocomplete='off')
49
49
  ul#lobby-list
50
50
  p#lobby-empty= 'No servers available'
51
51
  input#lobby-more(type='button', value='Load more')
@@ -55,6 +55,10 @@ div#lobby(style='display:none')
55
55
  span#leaderboard-total
56
56
  ol#lobby-leaderboard-list
57
57
  p#lobby-my-placement
58
- //- футер: версию пакета движка проставляет LobbyView (engineVersion.js)
58
+ //- футер: ссылку и версию пакета движка проставляет LobbyView
59
+ //- (engineVersion.js + lib/packageLink.js)
59
60
  div#lobby-footer
61
+ p
62
+ a#lobby-link
60
63
  p#lobby-version
64
+ p &copy; #{new Date().getFullYear()} VIMP
@@ -14,6 +14,6 @@ div#lobby-auth
14
14
 
15
15
  div#lobby-auth-nick.card(style='display:none')
16
16
  h2= 'Choose your nick'
17
- input#lobby-auth-nick-input.field-text(type='text', maxlength='14', placeholder='YourNick')
17
+ input#lobby-auth-nick-input.field-text(type='text', maxlength='14', placeholder='YourNick', autocomplete='off')
18
18
  div#lobby-auth-nick-error
19
19
  input#lobby-auth-nick-submit(type='button', value='Continue')
@@ -98,8 +98,9 @@ export default {
98
98
  myPlacementId: 'lobby-my-placement',
99
99
 
100
100
  // футер: версия npm-пакета движка, запечённая в бандл при сборке
101
- // (client/lib/engineVersion.js)
101
+ // (client/lib/engineVersion.js), и ссылка на его страницу
102
102
  versionId: 'lobby-version',
103
+ linkId: 'lobby-link',
103
104
  },
104
105
 
105
106
  // создание комнаты (хост в этой же вкладке); лимит игроков/время
@@ -0,0 +1,29 @@
1
+ import { WARN, skip, verdict } from '../result.js';
2
+ import { resolveProjectUrl } from '../../../lib/packageLink.js';
3
+
4
+ // Игра без объявленного репозитория собирается и играется — это не отказ.
5
+ // Но движок показывает ссылку на проект в футере формы входа, и берёт он её
6
+ // только отсюда (фолбэка нет, см. lib/packageLink.js): без поля ячейка
7
+ // футера у игроков останется пустой, а заметить это без подсказки негде.
8
+ // Отсюда warn — под --strict он становится ошибкой.
9
+ export default {
10
+ id: 'A7',
11
+ name: 'packageRepository',
12
+ level: WARN,
13
+ title: 'package.json declares repository (the #auth footer links to it)',
14
+
15
+ check(ctx) {
16
+ if (!ctx.pkg) {
17
+ return skip('no package.json');
18
+ }
19
+
20
+ if (resolveProjectUrl(ctx.pkg)) {
21
+ return verdict([]);
22
+ }
23
+
24
+ return verdict([
25
+ 'neither "repository" nor "homepage" resolves to a project URL — ' +
26
+ "the game's entry form will show no project link",
27
+ ]);
28
+ },
29
+ };
@@ -4,6 +4,7 @@ import a3 from './a3-entry-paths.js';
4
4
  import a4 from './a4-vite-config.js';
5
5
  import a5 from './a5-cargo-core.js';
6
6
  import a6 from './a6-manifest.js';
7
+ import a7 from './a7-package-repository.js';
7
8
  import b1 from './b1-host-shape.js';
8
9
  import b2 from './b2-engine-api.js';
9
10
  import b3 from './b3-game-config-shape.js';
@@ -34,7 +35,7 @@ import e3 from './e3-sound-registry.js';
34
35
  // Порядок групп — порядок отчёта: A пакет и сборка, B host, C client,
35
36
  // D снапшот, E ассеты (plan/create-vimp-game/stage_1.md).
36
37
  export const rules = [
37
- a1, a2, a3, a4, a5, a6,
38
+ a1, a2, a3, a4, a5, a6, a7,
38
39
  b1, b2, b3, b4, b5, b6, b7, b8, b9, b10,
39
40
  c1, c2, c3, c4, c5, c6, c7, c8, c9, c10,
40
41
  d1, d2, d3,
@@ -0,0 +1,121 @@
1
+ // Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
2
+ // npm-версию, и клик по соседней ячейке ведёт в репозиторий этого пакета.
3
+ //
4
+ // Источник — `repository` его package.json (иначе `homepage`). Фолбэка нет:
5
+ // пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
6
+ // футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
7
+ // в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
8
+ // маскировал недостающие метаданные вместо того, чтобы показать их отсутствие.
9
+ // О пропущенном поле предупреждает правило контракта A7.
10
+ //
11
+ // Хостинг не ограничен github.com: подпись выводится из хоста, поэтому чужой
12
+ // плагин на GitLab получает честную ссылку, а не пустую ячейку.
13
+ //
14
+ // Модуль изоморфный: мастер зовёт resolveProjectUrl для пакета игры
15
+ // (GameCatalog), клиент — для собственного package.json движка
16
+ // (client/lib/engineVersion.js).
17
+
18
+ /**
19
+ * Адрес проекта из package.json, приведённый к https: repository, иначе
20
+ * homepage.
21
+ * @param {Object} pkg - разобранный package.json
22
+ * @returns {string|null} https-URL либо null, если поля нет или это не адрес
23
+ */
24
+ export function resolveProjectUrl(pkg) {
25
+ if (!pkg || typeof pkg !== 'object') {
26
+ return null;
27
+ }
28
+
29
+ const { homepage, repository } = pkg;
30
+
31
+ // repository бывает и строкой ("github:user/repo"), и объектом {type, url}
32
+ const declared =
33
+ (typeof repository === 'string' ? repository : repository?.url) ?? homepage;
34
+
35
+ return normalize(declared);
36
+ }
37
+
38
+ /**
39
+ * Ссылка футера по готовому адресу проекта.
40
+ * @param {string|null} [url] - результат resolveProjectUrl
41
+ * @returns {{url: string, label: string}|null} null, если адреса нет
42
+ */
43
+ export function projectLink(url) {
44
+ if (typeof url !== 'string' || !url.trim()) {
45
+ return null;
46
+ }
47
+
48
+ return { url, label: labelFor(url) };
49
+ }
50
+
51
+ // приводит объявленный адрес к https-виду; всё, что после нормализации не
52
+ // http(s) (git@ без известной формы, file:, мусор), адресом не считается
53
+ function normalize(raw) {
54
+ if (typeof raw !== 'string') {
55
+ return null;
56
+ }
57
+
58
+ let url = raw.trim();
59
+
60
+ if (!url) {
61
+ return null;
62
+ }
63
+
64
+ // npm-шорткаты: "github:user/repo", "gitlab:...", "bitbucket:...", а также
65
+ // голое "user/repo" — его принимает флаг --repository скаффолдера
66
+ const bare = /^([\w.-]+\/[\w.-]+)$/.exec(url);
67
+
68
+ if (bare) {
69
+ return `https://github.com/${bare[1]}`;
70
+ }
71
+
72
+ const shorthand = /^(github|gitlab|bitbucket):([\w.-]+\/[\w.-]+)$/.exec(url);
73
+
74
+ if (shorthand) {
75
+ const hosts = {
76
+ github: 'github.com',
77
+ gitlab: 'gitlab.com',
78
+ bitbucket: 'bitbucket.org',
79
+ };
80
+
81
+ return `https://${hosts[shorthand[1]]}/${shorthand[2]}`;
82
+ }
83
+
84
+ url = url.replace(/^git\+/, '');
85
+
86
+ // scp-форма git@host:user/repo — двоеточие здесь разделитель, не порт
87
+ const scp = /^git@([^:/]+):(.+)$/.exec(url);
88
+
89
+ if (scp) {
90
+ url = `https://${scp[1]}/${scp[2]}`;
91
+ }
92
+
93
+ url = url
94
+ .replace(/^ssh:\/\/git@/, 'https://')
95
+ .replace(/^ssh:\/\//, 'https://')
96
+ .replace(/^git:\/\//, 'https://');
97
+
98
+ if (!/^https?:\/\//.test(url)) {
99
+ return null;
100
+ }
101
+
102
+ return (
103
+ url
104
+ .replace(/\.git(?=$|[#?])/, '')
105
+ // npm сам дописывает "#readme" в homepage при генерации package.json
106
+ .replace(/#readme$/, '')
107
+ .replace(/\/+$/, '')
108
+ );
109
+ }
110
+
111
+ // подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
112
+ // его хост, чтобы подпись не врала об источнике
113
+ function labelFor(url) {
114
+ if (/^https?:\/\/(www\.)?github\.com\//.test(url)) {
115
+ return 'GitHub';
116
+ }
117
+
118
+ const host = /^https?:\/\/(?:www\.)?([^/:]+)/.exec(url);
119
+
120
+ return host ? host[1] : 'Website';
121
+ }