vimp-engine 0.17.0 → 0.18.1
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 +75 -40
- 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 +56 -45
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
|
}
|
|
@@ -459,7 +466,7 @@ body.hide-cursor {
|
|
|
459
466
|
color: #e00;
|
|
460
467
|
font-size: 12px;
|
|
461
468
|
line-height: 15px;
|
|
462
|
-
margin: 0 5px;
|
|
469
|
+
margin: 0 5px 10px;
|
|
463
470
|
}
|
|
464
471
|
|
|
465
472
|
/* пустой блок ошибок не должен занимать место: #lobby-error — флекс-элемент
|
|
@@ -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;
|
|
@@ -592,6 +617,13 @@ body.hide-cursor {
|
|
|
592
617
|
/*lobby*/
|
|
593
618
|
|
|
594
619
|
#lobby {
|
|
620
|
+
/* ширины панелей и их внутренние поля: по ним же считается ширина футера
|
|
621
|
+
(секция auth выше), поэтому числа живут в одном месте — иначе смена
|
|
622
|
+
ширины любой панели молча разъезжает вёрстку */
|
|
623
|
+
--lobby-setup-width: 410px;
|
|
624
|
+
--lobby-browser-width: 430px;
|
|
625
|
+
--lobby-panel-pad: 10px;
|
|
626
|
+
|
|
595
627
|
background-color: #222;
|
|
596
628
|
position: absolute;
|
|
597
629
|
width: 100%;
|
|
@@ -602,16 +634,20 @@ body.hide-cursor {
|
|
|
602
634
|
display: flex;
|
|
603
635
|
align-items: flex-start;
|
|
604
636
|
justify-content: center;
|
|
605
|
-
padding:
|
|
637
|
+
padding: 0;
|
|
606
638
|
box-sizing: border-box;
|
|
607
639
|
}
|
|
608
640
|
|
|
641
|
+
#lobby-user,
|
|
642
|
+
.lobby-section-title,
|
|
643
|
+
#lobby-create {
|
|
644
|
+
padding: 8px;
|
|
645
|
+
}
|
|
646
|
+
|
|
609
647
|
#lobby-create {
|
|
610
648
|
display: flex;
|
|
611
649
|
flex-direction: column;
|
|
612
|
-
gap:
|
|
613
|
-
margin-bottom: 12px;
|
|
614
|
-
padding: 8px;
|
|
650
|
+
gap: 10px;
|
|
615
651
|
}
|
|
616
652
|
|
|
617
653
|
/* поле имени сервера — строка формы, а не узкий числовой инпут
|
|
@@ -638,13 +674,10 @@ body.hide-cursor {
|
|
|
638
674
|
margin: 0;
|
|
639
675
|
}
|
|
640
676
|
|
|
641
|
-
#lobby-host {
|
|
642
|
-
margin: 16px auto 0;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
677
|
#lobby-search {
|
|
646
678
|
display: block;
|
|
647
679
|
width: 100%;
|
|
680
|
+
margin-bottom: 10px;
|
|
648
681
|
}
|
|
649
682
|
|
|
650
683
|
#lobby-list {
|
|
@@ -662,9 +695,10 @@ body.hide-cursor {
|
|
|
662
695
|
column-gap: 8px;
|
|
663
696
|
row-gap: 2px;
|
|
664
697
|
padding: 10px 12px;
|
|
665
|
-
margin
|
|
698
|
+
margin: 0 0 6px;
|
|
666
699
|
background-color: #363636;
|
|
667
|
-
border: 1px solid #
|
|
700
|
+
border: 1px solid #999;
|
|
701
|
+
border-radius: 4px;
|
|
668
702
|
cursor: pointer;
|
|
669
703
|
align-items: center;
|
|
670
704
|
}
|
|
@@ -735,6 +769,7 @@ body.hide-cursor {
|
|
|
735
769
|
.lobby-column {
|
|
736
770
|
display: flex;
|
|
737
771
|
flex-direction: column;
|
|
772
|
+
align-items: flex-end;
|
|
738
773
|
}
|
|
739
774
|
|
|
740
775
|
/* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
|
|
@@ -744,12 +779,12 @@ body.hide-cursor {
|
|
|
744
779
|
высоты более высокой панели */
|
|
745
780
|
.lobby-grid {
|
|
746
781
|
display: flex;
|
|
747
|
-
gap:
|
|
782
|
+
gap: 10px;
|
|
748
783
|
justify-content: center;
|
|
749
784
|
}
|
|
750
785
|
|
|
751
786
|
#lobby-setup-panel {
|
|
752
|
-
width:
|
|
787
|
+
width: var(--lobby-setup-width);
|
|
753
788
|
}
|
|
754
789
|
|
|
755
790
|
/* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
|
|
@@ -758,7 +793,7 @@ body.hide-cursor {
|
|
|
758
793
|
вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
|
|
759
794
|
расчёте высоты строки) */
|
|
760
795
|
#lobby-browser-panel {
|
|
761
|
-
width:
|
|
796
|
+
width: var(--lobby-browser-width);
|
|
762
797
|
position: relative;
|
|
763
798
|
padding: 0;
|
|
764
799
|
}
|
|
@@ -768,7 +803,7 @@ body.hide-cursor {
|
|
|
768
803
|
inset: 0;
|
|
769
804
|
display: flex;
|
|
770
805
|
flex-direction: column;
|
|
771
|
-
padding:
|
|
806
|
+
padding: var(--lobby-panel-pad);
|
|
772
807
|
box-sizing: border-box;
|
|
773
808
|
}
|
|
774
809
|
|
|
@@ -793,23 +828,23 @@ body.hide-cursor {
|
|
|
793
828
|
flex-shrink: 0;
|
|
794
829
|
}
|
|
795
830
|
|
|
796
|
-
/* полосу футера рисуют общие с #auth-link правила в секции auth; здесь —
|
|
797
|
-
только отступ от панелей */
|
|
798
|
-
#lobby-footer {
|
|
799
|
-
margin: 6px 0 10px;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
831
|
@media (max-width: 800px) {
|
|
803
832
|
.lobby-grid {
|
|
804
|
-
|
|
805
|
-
align-items: stretch;
|
|
806
|
-
gap: unset;
|
|
833
|
+
display: block;
|
|
807
834
|
}
|
|
808
835
|
|
|
809
836
|
/* в столбик обёртка тянется на всю ширину: иначе она сжалась бы по
|
|
810
837
|
содержимому и увела бы панели из центра */
|
|
811
838
|
.lobby-column {
|
|
812
839
|
width: 100%;
|
|
840
|
+
align-items: center;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* правое поле, прижимавшее футер к контенту правой панели, в одну
|
|
844
|
+
колонку не нужно: с ним align-items: center увёл бы его влево на
|
|
845
|
+
половину поля */
|
|
846
|
+
#lobby-footer {
|
|
847
|
+
margin: 0 auto;
|
|
813
848
|
}
|
|
814
849
|
|
|
815
850
|
/* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
|
|
@@ -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,73 @@
|
|
|
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;
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
// объявленный, но не приводящийся к http(s) repository (пустая строка,
|
|
36
|
+
// внутренний git-хост) не повод терять валидный homepage: поле проверяется
|
|
37
|
+
// по результату, а не по факту наличия
|
|
38
|
+
return normalizeRepository(declared) ?? normalize(homepage);
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
|
-
* Ссылка футера по
|
|
45
|
-
* @param {
|
|
46
|
-
* @returns {{url: string, label: string}|null} null, если нет
|
|
42
|
+
* Ссылка футера по готовому адресу проекта.
|
|
43
|
+
* @param {string|null} [url] - результат resolveProjectUrl
|
|
44
|
+
* @returns {{url: string, label: string}|null} null, если адреса нет
|
|
47
45
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
export function projectLink(url) {
|
|
47
|
+
// адрес перепроверяется, хотя свои вызовы приходят уже из
|
|
48
|
+
// resolveProjectUrl: функция — публичный экспорт ("./lib/*"), а результат
|
|
49
|
+
// уходит прямо в href якоря (client/lib/footerLink.js)
|
|
50
|
+
const value = typeof url === 'string' ? url.trim() : '';
|
|
51
51
|
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
52
|
+
if (!/^https?:\/\//.test(value)) {
|
|
53
|
+
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return { url: value, label: labelFor(value) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// шорткат "user/repo" — семантика поля repository (так его читает npm), в
|
|
60
|
+
// homepage такая строка означала бы относительный путь, а не адрес на GitHub
|
|
61
|
+
function normalizeRepository(raw) {
|
|
62
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
59
63
|
|
|
60
|
-
return
|
|
64
|
+
return /^[\w.-]+\/[\w.-]+$/.test(value)
|
|
65
|
+
? `https://github.com/${value}`
|
|
66
|
+
: normalize(raw);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
// приводит объявленный адрес к https-виду; всё, что после нормализации не
|
|
64
|
-
// http(s) (git@ без известной формы, file:, мусор),
|
|
70
|
+
// http(s) (git@ без известной формы, file:, мусор), адресом не считается
|
|
65
71
|
function normalize(raw) {
|
|
66
72
|
if (typeof raw !== 'string') {
|
|
67
73
|
return null;
|
|
@@ -106,6 +112,9 @@ function normalize(raw) {
|
|
|
106
112
|
|
|
107
113
|
return (
|
|
108
114
|
url
|
|
115
|
+
// логин (и тем более пароль) из ssh-формы адреса — не часть проекта, а
|
|
116
|
+
// в href он уехал бы в DOM и историю браузера
|
|
117
|
+
.replace(/^(https?:\/\/)[^/@]*@/, '$1')
|
|
109
118
|
.replace(/\.git(?=$|[#?])/, '')
|
|
110
119
|
// npm сам дописывает "#readme" в homepage при генерации package.json
|
|
111
120
|
.replace(/#readme$/, '')
|
|
@@ -113,14 +122,16 @@ function normalize(raw) {
|
|
|
113
122
|
);
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
// подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
|
|
126
|
+
// его хост, чтобы подпись не врала об источнике
|
|
116
127
|
function labelFor(url) {
|
|
117
|
-
|
|
118
|
-
return 'GitHub';
|
|
119
|
-
}
|
|
128
|
+
let host;
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
try {
|
|
131
|
+
host = new URL(url).hostname.replace(/^www\./, '');
|
|
132
|
+
} catch {
|
|
133
|
+
return 'Website';
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
return '
|
|
136
|
+
return host === 'github.com' ? 'GitHub' : host;
|
|
126
137
|
}
|