vimp-engine 0.17.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 +3 -3
- package/src/client/lib/engineVersion.js +5 -7
- package/src/client/lib/footerLink.js +6 -6
- package/src/client/lib/formBuilder.js +25 -7
- package/src/client/main.js +5 -5
- package/src/client/style.css +80 -39
- package/src/client/views/includes/chat.pug +1 -1
- package/src/client/views/includes/lobby.pug +3 -3
- package/src/client/views/includes/lobbyAuth.pug +1 -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 +41 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ENGINE_PROJECT_URL, ENGINE_VERSION } from '../../lib/engineVersion.js';
|
|
3
|
+
import { renderProjectLink } from '../../lib/footerLink.js';
|
|
4
4
|
|
|
5
5
|
// Singleton LobbyView
|
|
6
6
|
|
|
@@ -44,7 +44,7 @@ export default class LobbyView {
|
|
|
44
44
|
version.textContent = ENGINE_VERSION;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
renderProjectLink(document.getElementById(elems.linkId), ENGINE_PROJECT_URL);
|
|
48
48
|
|
|
49
49
|
// заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
|
|
50
50
|
// при выборе игры (сама модель не хранит title манифеста)
|
|
@@ -7,13 +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 {
|
|
10
|
+
import { resolveProjectUrl } from '../../lib/packageLink.js';
|
|
11
11
|
|
|
12
12
|
export const ENGINE_VERSION = pkg.version;
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
// пакет игры (GameCatalog), чтобы оба футера строил один
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
homepage: homepageOf(pkg),
|
|
19
|
-
};
|
|
14
|
+
// адрес проекта для ссылки в футере лобби — то же поле, что мастер отдаёт
|
|
15
|
+
// про пакет игры (GameCatalog.packageUrl), чтобы оба футера строил один
|
|
16
|
+
// projectLink
|
|
17
|
+
export const ENGINE_PROJECT_URL = resolveProjectUrl(pkg);
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { projectLink } from '../../lib/packageLink.js';
|
|
2
2
|
|
|
3
3
|
// Ссылка в футере лобби и формы входа. Один рендер на оба экрана: лобби
|
|
4
|
-
// подставляет
|
|
4
|
+
// подставляет адрес проекта движка, форма входа — адрес игры из манифеста.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// Адреса нет (пакет не объявил repository/homepage) — якорь гасится, а не
|
|
7
7
|
// прячется: ячейка футера остаётся пустой, и раскладка space-between не едет.
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @param {HTMLAnchorElement|null} anchor - якорь футера
|
|
11
|
-
* @param {
|
|
11
|
+
* @param {string|null} url - адрес проекта (resolveProjectUrl)
|
|
12
12
|
*/
|
|
13
|
-
export function
|
|
13
|
+
export function renderProjectLink(anchor, url) {
|
|
14
14
|
if (!anchor) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const link =
|
|
18
|
+
const link = projectLink(url);
|
|
19
19
|
|
|
20
20
|
if (!link) {
|
|
21
21
|
anchor.textContent = '';
|
|
@@ -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,7 +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 {
|
|
31
|
+
import { renderProjectLink } from './lib/footerLink.js';
|
|
32
32
|
import { createGameActivator } from './lib/gameActivator.js';
|
|
33
33
|
import createAutostart from './lib/autostart.js';
|
|
34
34
|
import { getBootConfig, resolveBootConfig } from './boot.js';
|
|
@@ -434,10 +434,10 @@ socketMethods[PS_AUTH_DATA] = data => {
|
|
|
434
434
|
authVersion.textContent = activeGameManifest?.packageVersion ?? '';
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
437
|
+
renderProjectLink(
|
|
438
|
+
document.getElementById('auth-package-link'),
|
|
439
|
+
activeGameManifest?.packageUrl,
|
|
440
|
+
);
|
|
441
441
|
|
|
442
442
|
// память клиента (localStorage) + принудительное значение поля с
|
|
443
443
|
// единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
|
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,7 +477,6 @@ body.hide-cursor {
|
|
|
470
477
|
|
|
471
478
|
#auth-enter {
|
|
472
479
|
display: block;
|
|
473
|
-
margin: 4px auto;
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
/* футеры лобби и формы входа: одна и та же полоса из трёх ячеек —
|
|
@@ -479,18 +485,35 @@ body.hide-cursor {
|
|
|
479
485
|
разметкой) */
|
|
480
486
|
#auth-link,
|
|
481
487
|
#lobby-footer {
|
|
482
|
-
font-size: 9px;
|
|
483
488
|
height: 15px;
|
|
484
|
-
margin: 0;
|
|
485
|
-
padding: 0 5px;
|
|
486
489
|
text-shadow: none;
|
|
487
490
|
display: flex;
|
|
488
|
-
justify-content: space-between;
|
|
489
491
|
}
|
|
490
492
|
|
|
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 версия съезжала бы с центра */
|
|
491
513
|
#auth-link p,
|
|
492
514
|
#lobby-footer p {
|
|
493
515
|
margin: 0;
|
|
516
|
+
flex: 1;
|
|
494
517
|
}
|
|
495
518
|
|
|
496
519
|
#auth-link p a,
|
|
@@ -498,9 +521,7 @@ body.hide-cursor {
|
|
|
498
521
|
color: #ff0;
|
|
499
522
|
}
|
|
500
523
|
|
|
501
|
-
/*
|
|
502
|
-
пустой (у пакета игры может не быть метаданных), и порядковые правила
|
|
503
|
-
тогда съезжали бы */
|
|
524
|
+
/* позиция текста внутри ячейки; сами ячейки уже разложены flex: 1 выше */
|
|
504
525
|
#auth-link p:first-child,
|
|
505
526
|
#lobby-footer p:first-child {
|
|
506
527
|
text-align: left;
|
|
@@ -516,7 +537,11 @@ body.hide-cursor {
|
|
|
516
537
|
text-align: right;
|
|
517
538
|
}
|
|
518
539
|
|
|
519
|
-
|
|
540
|
+
/* коробка контента: блок помощи формы входа и группа полей комнаты.
|
|
541
|
+
.lobby-card сюда не входит — ей нужен фон темнее, иначе он совпадёт с
|
|
542
|
+
её собственным hover'ом и наведение перестанет быть видно */
|
|
543
|
+
.auth-inform,
|
|
544
|
+
#lobby-fields {
|
|
520
545
|
background-color: #444;
|
|
521
546
|
padding: 7px 13px;
|
|
522
547
|
margin: 10px 0;
|
|
@@ -524,6 +549,12 @@ body.hide-cursor {
|
|
|
524
549
|
border: 1px solid #999;
|
|
525
550
|
}
|
|
526
551
|
|
|
552
|
+
/* вертикаль внутри #lobby-create держит его gap — собственный margin
|
|
553
|
+
коробки удвоил бы отступ до соседей */
|
|
554
|
+
#lobby-fields {
|
|
555
|
+
margin: 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
527
558
|
.auth-inform h4 {
|
|
528
559
|
margin: 0 0 4px 0;
|
|
529
560
|
text-align: center;
|
|
@@ -592,6 +623,13 @@ body.hide-cursor {
|
|
|
592
623
|
/*lobby*/
|
|
593
624
|
|
|
594
625
|
#lobby {
|
|
626
|
+
/* ширины панелей и их внутренние поля: по ним же считается ширина футера
|
|
627
|
+
(секция auth выше), поэтому числа живут в одном месте — иначе смена
|
|
628
|
+
ширины любой панели молча разъезжает вёрстку */
|
|
629
|
+
--lobby-setup-width: 410px;
|
|
630
|
+
--lobby-browser-width: 430px;
|
|
631
|
+
--lobby-panel-pad: 10px;
|
|
632
|
+
|
|
595
633
|
background-color: #222;
|
|
596
634
|
position: absolute;
|
|
597
635
|
width: 100%;
|
|
@@ -602,16 +640,20 @@ body.hide-cursor {
|
|
|
602
640
|
display: flex;
|
|
603
641
|
align-items: flex-start;
|
|
604
642
|
justify-content: center;
|
|
605
|
-
padding:
|
|
643
|
+
padding: 0;
|
|
606
644
|
box-sizing: border-box;
|
|
607
645
|
}
|
|
608
646
|
|
|
647
|
+
#lobby-user,
|
|
648
|
+
.lobby-section-title,
|
|
649
|
+
#lobby-create {
|
|
650
|
+
padding: 8px;
|
|
651
|
+
}
|
|
652
|
+
|
|
609
653
|
#lobby-create {
|
|
610
654
|
display: flex;
|
|
611
655
|
flex-direction: column;
|
|
612
|
-
gap:
|
|
613
|
-
margin-bottom: 12px;
|
|
614
|
-
padding: 8px;
|
|
656
|
+
gap: 10px;
|
|
615
657
|
}
|
|
616
658
|
|
|
617
659
|
/* поле имени сервера — строка формы, а не узкий числовой инпут
|
|
@@ -638,13 +680,10 @@ body.hide-cursor {
|
|
|
638
680
|
margin: 0;
|
|
639
681
|
}
|
|
640
682
|
|
|
641
|
-
#lobby-host {
|
|
642
|
-
margin: 16px auto 0;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
683
|
#lobby-search {
|
|
646
684
|
display: block;
|
|
647
685
|
width: 100%;
|
|
686
|
+
margin-bottom: 10px;
|
|
648
687
|
}
|
|
649
688
|
|
|
650
689
|
#lobby-list {
|
|
@@ -662,9 +701,10 @@ body.hide-cursor {
|
|
|
662
701
|
column-gap: 8px;
|
|
663
702
|
row-gap: 2px;
|
|
664
703
|
padding: 10px 12px;
|
|
665
|
-
margin
|
|
704
|
+
margin: 0 0 6px;
|
|
666
705
|
background-color: #363636;
|
|
667
|
-
border: 1px solid #
|
|
706
|
+
border: 1px solid #999;
|
|
707
|
+
border-radius: 4px;
|
|
668
708
|
cursor: pointer;
|
|
669
709
|
align-items: center;
|
|
670
710
|
}
|
|
@@ -735,6 +775,7 @@ body.hide-cursor {
|
|
|
735
775
|
.lobby-column {
|
|
736
776
|
display: flex;
|
|
737
777
|
flex-direction: column;
|
|
778
|
+
align-items: flex-end;
|
|
738
779
|
}
|
|
739
780
|
|
|
740
781
|
/* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
|
|
@@ -744,12 +785,12 @@ body.hide-cursor {
|
|
|
744
785
|
высоты более высокой панели */
|
|
745
786
|
.lobby-grid {
|
|
746
787
|
display: flex;
|
|
747
|
-
gap:
|
|
788
|
+
gap: 10px;
|
|
748
789
|
justify-content: center;
|
|
749
790
|
}
|
|
750
791
|
|
|
751
792
|
#lobby-setup-panel {
|
|
752
|
-
width:
|
|
793
|
+
width: var(--lobby-setup-width);
|
|
753
794
|
}
|
|
754
795
|
|
|
755
796
|
/* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
|
|
@@ -758,7 +799,7 @@ body.hide-cursor {
|
|
|
758
799
|
вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
|
|
759
800
|
расчёте высоты строки) */
|
|
760
801
|
#lobby-browser-panel {
|
|
761
|
-
width:
|
|
802
|
+
width: var(--lobby-browser-width);
|
|
762
803
|
position: relative;
|
|
763
804
|
padding: 0;
|
|
764
805
|
}
|
|
@@ -768,7 +809,7 @@ body.hide-cursor {
|
|
|
768
809
|
inset: 0;
|
|
769
810
|
display: flex;
|
|
770
811
|
flex-direction: column;
|
|
771
|
-
padding:
|
|
812
|
+
padding: var(--lobby-panel-pad);
|
|
772
813
|
box-sizing: border-box;
|
|
773
814
|
}
|
|
774
815
|
|
|
@@ -793,23 +834,23 @@ body.hide-cursor {
|
|
|
793
834
|
flex-shrink: 0;
|
|
794
835
|
}
|
|
795
836
|
|
|
796
|
-
/* полосу футера рисуют общие с #auth-link правила в секции auth; здесь —
|
|
797
|
-
только отступ от панелей */
|
|
798
|
-
#lobby-footer {
|
|
799
|
-
margin: 6px 0 10px;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
837
|
@media (max-width: 800px) {
|
|
803
838
|
.lobby-grid {
|
|
804
|
-
|
|
805
|
-
align-items: stretch;
|
|
806
|
-
gap: unset;
|
|
839
|
+
display: block;
|
|
807
840
|
}
|
|
808
841
|
|
|
809
842
|
/* в столбик обёртка тянется на всю ширину: иначе она сжалась бы по
|
|
810
843
|
содержимому и увела бы панели из центра */
|
|
811
844
|
.lobby-column {
|
|
812
845
|
width: 100%;
|
|
846
|
+
align-items: center;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* правое поле, прижимавшее футер к контенту правой панели, в одну
|
|
850
|
+
колонку не нужно: с ним align-items: center увёл бы его влево на
|
|
851
|
+
половину поля */
|
|
852
|
+
#lobby-footer {
|
|
853
|
+
margin: 0 auto;
|
|
813
854
|
}
|
|
814
855
|
|
|
815
856
|
/* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
|
|
@@ -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')
|
|
@@ -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')
|
|
@@ -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,
|
package/src/lib/packageLink.js
CHANGED
|
@@ -1,67 +1,55 @@
|
|
|
1
|
-
// Ссылка на
|
|
2
|
-
// npm-версию, и клик по соседней ячейке
|
|
1
|
+
// Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
|
|
2
|
+
// npm-версию, и клик по соседней ячейке ведёт в репозиторий этого пакета.
|
|
3
3
|
//
|
|
4
|
-
// Источник — `
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Источник — `repository` его package.json (иначе `homepage`). Фолбэка нет:
|
|
5
|
+
// пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
|
|
6
|
+
// футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
|
|
7
|
+
// в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
|
|
8
|
+
// маскировал недостающие метаданные вместо того, чтобы показать их отсутствие.
|
|
9
|
+
// О пропущенном поле предупреждает правило контракта A7.
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Хостинг не ограничен github.com: подпись выводится из хоста, поэтому чужой
|
|
12
|
+
// плагин на GitLab получает честную ссылку, а не пустую ячейку.
|
|
13
|
+
//
|
|
14
|
+
// Модуль изоморфный: мастер зовёт resolveProjectUrl для пакета игры
|
|
15
|
+
// (GameCatalog), клиент — для собственного package.json движка
|
|
16
|
+
// (client/lib/engineVersion.js).
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
19
|
+
* Адрес проекта из package.json, приведённый к https: repository, иначе
|
|
20
|
+
* homepage.
|
|
17
21
|
* @param {Object} pkg - разобранный package.json
|
|
18
|
-
* @returns {string|null}
|
|
22
|
+
* @returns {string|null} https-URL либо null, если поля нет или это не адрес
|
|
19
23
|
*/
|
|
20
|
-
export function
|
|
24
|
+
export function resolveProjectUrl(pkg) {
|
|
21
25
|
if (!pkg || typeof pkg !== 'object') {
|
|
22
26
|
return null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const { homepage, repository } = pkg;
|
|
26
30
|
|
|
27
|
-
if (typeof homepage === 'string' && homepage.trim()) {
|
|
28
|
-
return homepage;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
31
|
// repository бывает и строкой ("github:user/repo"), и объектом {type, url}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (repository && typeof repository.url === 'string') {
|
|
37
|
-
return repository.url;
|
|
38
|
-
}
|
|
32
|
+
const declared =
|
|
33
|
+
(typeof repository === 'string' ? repository : repository?.url) ?? homepage;
|
|
39
34
|
|
|
40
|
-
return
|
|
35
|
+
return normalize(declared);
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
/**
|
|
44
|
-
* Ссылка футера по
|
|
45
|
-
* @param {
|
|
46
|
-
* @returns {{url: string, label: string}|null} null, если нет
|
|
39
|
+
* Ссылка футера по готовому адресу проекта.
|
|
40
|
+
* @param {string|null} [url] - результат resolveProjectUrl
|
|
41
|
+
* @returns {{url: string, label: string}|null} null, если адреса нет
|
|
47
42
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
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' };
|
|
43
|
+
export function projectLink(url) {
|
|
44
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
45
|
+
return null;
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
return
|
|
48
|
+
return { url, label: labelFor(url) };
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
// приводит объявленный адрес к https-виду; всё, что после нормализации не
|
|
64
|
-
// http(s) (git@ без известной формы, file:, мусор),
|
|
52
|
+
// http(s) (git@ без известной формы, file:, мусор), адресом не считается
|
|
65
53
|
function normalize(raw) {
|
|
66
54
|
if (typeof raw !== 'string') {
|
|
67
55
|
return null;
|
|
@@ -73,7 +61,14 @@ function normalize(raw) {
|
|
|
73
61
|
return null;
|
|
74
62
|
}
|
|
75
63
|
|
|
76
|
-
// npm-шорткаты: "github:user/repo", "gitlab:...", "bitbucket:..."
|
|
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
|
+
|
|
77
72
|
const shorthand = /^(github|gitlab|bitbucket):([\w.-]+\/[\w.-]+)$/.exec(url);
|
|
78
73
|
|
|
79
74
|
if (shorthand) {
|
|
@@ -113,14 +108,14 @@ function normalize(raw) {
|
|
|
113
108
|
);
|
|
114
109
|
}
|
|
115
110
|
|
|
111
|
+
// подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
|
|
112
|
+
// его хост, чтобы подпись не врала об источнике
|
|
116
113
|
function labelFor(url) {
|
|
117
114
|
if (/^https?:\/\/(www\.)?github\.com\//.test(url)) {
|
|
118
115
|
return 'GitHub';
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
return 'npm';
|
|
123
|
-
}
|
|
118
|
+
const host = /^https?:\/\/(?:www\.)?([^/:]+)/.exec(url);
|
|
124
119
|
|
|
125
|
-
return 'Website';
|
|
120
|
+
return host ? host[1] : 'Website';
|
|
126
121
|
}
|