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 +1 -1
- package/src/client/components/view/Lobby.js +6 -3
- package/src/client/lib/engineVersion.js +6 -0
- package/src/client/lib/footerLink.js +29 -0
- package/src/client/lib/formBuilder.js +25 -7
- package/src/client/main.js +12 -7
- package/src/client/style.css +96 -53
- package/src/client/views/gameShell.js +3 -12
- package/src/client/views/includes/auth.pug +4 -3
- package/src/client/views/includes/chat.pug +1 -1
- package/src/client/views/includes/lobby.pug +8 -4
- package/src/client/views/includes/lobbyAuth.pug +1 -1
- package/src/config/lobby.js +2 -1
- package/src/devtools/contract/rules/a7-package-repository.js +29 -0
- package/src/devtools/contract/rules/index.js +2 -1
- package/src/lib/packageLink.js +121 -0
package/package.json
CHANGED
|
@@ -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 =
|
|
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', () =>
|
|
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', () =>
|
|
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', () =>
|
|
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', () =>
|
|
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
|
|
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(
|
|
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 =
|
|
622
|
+
label.textContent =
|
|
623
|
+
(descriptor.label || descriptor.name) + buildLabelSuffix(descriptor);
|
|
606
624
|
|
|
607
625
|
if (field.labelFor) {
|
|
608
626
|
label.htmlFor = field.labelFor;
|
package/src/client/main.js
CHANGED
|
@@ -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
|
-
//
|
|
426
|
-
// elems приходят из authSchema.elems игрового плагина, и футер каркаса
|
|
427
|
-
// не его забота.
|
|
428
|
-
//
|
|
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
|
-
|
|
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
|
// хосту вообще без формы и обязан прийти к тем же значениям
|
package/src/client/style.css
CHANGED
|
@@ -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
|
-
/* кнопки: провайдеры логина,
|
|
402
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
477
|
-
|
|
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
|
|
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
|
-
/*
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
704
|
+
margin: 0 0 6px;
|
|
656
705
|
background-color: #363636;
|
|
657
|
-
border: 1px solid #
|
|
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:
|
|
788
|
+
gap: 10px;
|
|
738
789
|
justify-content: center;
|
|
739
790
|
}
|
|
740
791
|
|
|
741
792
|
#lobby-setup-panel {
|
|
742
|
-
width:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
20
|
-
//- версия пакета игры (manifest.packageVersion): проставляет main.js,
|
|
21
|
-
//- у игры, собранной до появления поля, строка остаётся пустой
|
|
22
|
+
a#auth-package-link
|
|
22
23
|
p#auth-version
|
|
23
24
|
p © #{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=
|
|
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
|
|
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
|
|
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 © #{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')
|
package/src/config/lobby.js
CHANGED
|
@@ -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
|
+
}
|