vimp-engine 0.19.0 → 0.20.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/controller/Lobby.js +42 -1
- package/src/client/components/view/Lobby.js +32 -1
- package/src/client/main.js +19 -7
- package/src/client/style.css +26 -0
- package/src/client/views/includes/lobby.pug +6 -0
- package/src/config/lobby.js +17 -0
- package/src/host/HostGame.js +13 -0
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ export default class LobbyCtrl {
|
|
|
27
27
|
this._model = model;
|
|
28
28
|
this._view = view;
|
|
29
29
|
this._clock = clock;
|
|
30
|
+
this._periods = [];
|
|
30
31
|
|
|
31
32
|
this.publisher = new Publisher();
|
|
32
33
|
|
|
@@ -37,6 +38,12 @@ export default class LobbyCtrl {
|
|
|
37
38
|
vp.on('visible', 'pingHost', this);
|
|
38
39
|
vp.on('join', 'join', this);
|
|
39
40
|
vp.on('show-tab', 'showTab', this);
|
|
41
|
+
vp.on('show-period', 'showPeriod', this);
|
|
42
|
+
|
|
43
|
+
// активная игра и открытый срез рейтинга (rank-periods): запрос за
|
|
44
|
+
// лидербордом всегда несёт обе координаты, а меняется каждый раз одна
|
|
45
|
+
this._gameId = null;
|
|
46
|
+
this._period = null;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
// показывает лобби и запрашивает первую страницу
|
|
@@ -69,13 +76,47 @@ export default class LobbyCtrl {
|
|
|
69
76
|
this._view.showTab(tab);
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
// срез рейтинга: подсветить кнопку и перезапросить список. Данных за день
|
|
80
|
+
// и за месяц у клиента нет — это другой ответ сервера, а не другая
|
|
81
|
+
// сортировка того же (rank-periods)
|
|
82
|
+
showPeriod(period) {
|
|
83
|
+
if (period === this._period) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._period = period;
|
|
88
|
+
this._view.setPeriod(period, this._titleOf(period));
|
|
89
|
+
|
|
90
|
+
if (this._gameId !== null) {
|
|
91
|
+
this.publisher.emit('leaderboard-needed', {
|
|
92
|
+
gameId: this._gameId,
|
|
93
|
+
period,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// подпись среза для заголовка списка; неизвестный id остаётся без подписи
|
|
99
|
+
_titleOf(period) {
|
|
100
|
+
return this._periods.find(item => item.id === period)?.title ?? '';
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
// вызывается main.js при смене #lobby-game (селектор игры вне этого MVC),
|
|
73
104
|
// а также один раз при открытии лобби для активной по умолчанию игры —
|
|
74
105
|
// Leaderboard всегда готов заранее, до того как пользователь откроет
|
|
75
106
|
// вкладку (см. класс-комментарий выше)
|
|
76
107
|
gameChanged(gameId, title) {
|
|
108
|
+
this._gameId = gameId;
|
|
77
109
|
this._view.setGameTitle(title);
|
|
78
110
|
|
|
79
|
-
this.publisher.emit('leaderboard-needed', gameId);
|
|
111
|
+
this.publisher.emit('leaderboard-needed', { gameId, period: this._period });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// срезы рейтинга и открытый по умолчанию (main.js передаёт их из
|
|
115
|
+
// lobbyConfig): контроллер не знает их наизусть, чтобы список срезов
|
|
116
|
+
// правился в конфиге, а не здесь
|
|
117
|
+
setPeriods(periods, defaultPeriod) {
|
|
118
|
+
this._periods = periods || [];
|
|
119
|
+
this._period = defaultPeriod;
|
|
120
|
+
this._view.setPeriod(defaultPeriod, this._titleOf(defaultPeriod));
|
|
80
121
|
}
|
|
81
122
|
}
|
|
@@ -36,6 +36,15 @@ export default class LobbyView {
|
|
|
36
36
|
this._leaderboardTotal = document.getElementById(elems.leaderboardTotalId);
|
|
37
37
|
this._myPlacement = document.getElementById(elems.myPlacementId);
|
|
38
38
|
|
|
39
|
+
// кнопки срезов рейтинга (rank-periods): id периода -> элемент. Кнопки
|
|
40
|
+
// объявлены конфигом, а не захардкожены здесь, чтобы срез можно было
|
|
41
|
+
// убрать или добавить в одном месте
|
|
42
|
+
this._periodBtns = new Map(
|
|
43
|
+
Object.entries(elems.periodBtnIds || {})
|
|
44
|
+
.map(([period, id]) => [period, document.getElementById(id)])
|
|
45
|
+
.filter(([, el]) => el),
|
|
46
|
+
);
|
|
47
|
+
|
|
39
48
|
// футер лобби: содержимое статично на всё время жизни вкладки, поэтому
|
|
40
49
|
// пишется здесь, а не через модель и publisher
|
|
41
50
|
const version = document.getElementById(elems.versionId);
|
|
@@ -56,6 +65,10 @@ export default class LobbyView {
|
|
|
56
65
|
// игроков, чем лимит)
|
|
57
66
|
this._leaderboardLimit = 0;
|
|
58
67
|
|
|
68
|
+
// заголовок открытого среза ("<TITLE> TOP-N — TODAY"): подпись, а не
|
|
69
|
+
// источник правды, срез хранит модель
|
|
70
|
+
this._periodTitle = '';
|
|
71
|
+
|
|
59
72
|
// ник вызывающего (lobby-page-plan, code review M4-остаток) — задаётся
|
|
60
73
|
// main.js один раз при открытии лобби; используется, чтобы решить,
|
|
61
74
|
// виден ли вызывающий уже в отрисованном топе (по нику, не по числу
|
|
@@ -86,6 +99,10 @@ export default class LobbyView {
|
|
|
86
99
|
this._tabServersBtn.onclick = () => this.publisher.emit('show-tab', 'servers');
|
|
87
100
|
this._tabLeaderboardBtn.onclick = () => this.publisher.emit('show-tab', 'leaderboard');
|
|
88
101
|
|
|
102
|
+
for (const [period, btn] of this._periodBtns) {
|
|
103
|
+
btn.onclick = () => this.publisher.emit('show-period', period);
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
this._mPublic = model.publisher;
|
|
90
107
|
this._mPublic.on('list', 'renderList', this);
|
|
91
108
|
this._mPublic.on('ping-update', 'updatePing', this);
|
|
@@ -118,6 +135,17 @@ export default class LobbyView {
|
|
|
118
135
|
this._selfNick = nick || '';
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
// подсвечивает открытый срез рейтинга и запоминает его подпись
|
|
139
|
+
// (rank-periods). Рисовать список здесь нечем: данные среза приезжают
|
|
140
|
+
// отдельным запросом, и до ответа список чистится моделью
|
|
141
|
+
setPeriod(period, title) {
|
|
142
|
+
this._periodTitle = title || '';
|
|
143
|
+
|
|
144
|
+
for (const [id, btn] of this._periodBtns) {
|
|
145
|
+
btn.classList.toggle('active', id === period);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
// переключает вкладки Active Servers / Leaderboard (lobby-page-plan)
|
|
122
150
|
showTab(tab) {
|
|
123
151
|
const showLeaderboard = tab === 'leaderboard';
|
|
@@ -130,7 +158,10 @@ export default class LobbyView {
|
|
|
130
158
|
|
|
131
159
|
// рендер топ-N + позиции вызывающего (lobby-page-plan)
|
|
132
160
|
renderLeaderboard({ leaderboard, total, myPlacement, loaded = true }) {
|
|
133
|
-
|
|
161
|
+
const period = this._periodTitle ? ` — ${this._periodTitle}` : '';
|
|
162
|
+
|
|
163
|
+
this._leaderboardTitle.textContent =
|
|
164
|
+
`${this._gameTitle.toUpperCase()} TOP-${this._leaderboardLimit}${period}`;
|
|
134
165
|
this._leaderboardTotal.textContent = `Total: ${total} players`;
|
|
135
166
|
|
|
136
167
|
this._leaderboardList.textContent = '';
|
package/src/client/main.js
CHANGED
|
@@ -1790,10 +1790,13 @@ async function fetchServers({ offset, limit, search }) {
|
|
|
1790
1790
|
|
|
1791
1791
|
// топ-N рейтинга игры (lobby-page-plan) — публичный эндпоинт, доступен и до
|
|
1792
1792
|
// логина, поэтому без Authorization
|
|
1793
|
-
async function fetchLeaderboard(gameId) {
|
|
1793
|
+
async function fetchLeaderboard(gameId, period) {
|
|
1794
1794
|
const params = new URLSearchParams({
|
|
1795
1795
|
game: gameId,
|
|
1796
1796
|
limit: lobbyConfig.leaderboardLimit,
|
|
1797
|
+
// rank-periods: срез времени. Мастер отвечает 400 на незнакомый —
|
|
1798
|
+
// значение всегда из lobbyConfig.leaderboardPeriods
|
|
1799
|
+
period,
|
|
1797
1800
|
});
|
|
1798
1801
|
|
|
1799
1802
|
try {
|
|
@@ -1807,7 +1810,7 @@ async function fetchLeaderboard(gameId) {
|
|
|
1807
1810
|
|
|
1808
1811
|
// позиция вызывающего в рейтинге игры (lobby-page-plan) — требует identity-
|
|
1809
1812
|
// токена, как и остальные /auth/* запросы игрока (rank/state)
|
|
1810
|
-
async function fetchPlacement(gameId) {
|
|
1813
|
+
async function fetchPlacement(gameId, period) {
|
|
1811
1814
|
const token = lobbyAuthModel.getToken();
|
|
1812
1815
|
|
|
1813
1816
|
if (!token) {
|
|
@@ -1816,7 +1819,9 @@ async function fetchPlacement(gameId) {
|
|
|
1816
1819
|
|
|
1817
1820
|
try {
|
|
1818
1821
|
const res = await fetch(
|
|
1819
|
-
|
|
1822
|
+
// тот же срез, что и у списка рядом: плашка позиции, посчитанная за
|
|
1823
|
+
// всё время под заголовком «сегодня», противоречила бы списку
|
|
1824
|
+
`${lobbyConfig.placementUrl}?${new URLSearchParams({ game: gameId, period })}`,
|
|
1820
1825
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
1821
1826
|
);
|
|
1822
1827
|
|
|
@@ -1954,6 +1959,13 @@ function initLobby() {
|
|
|
1954
1959
|
|
|
1955
1960
|
lobby = new LobbyCtrl(lobbyModel, lobbyView);
|
|
1956
1961
|
|
|
1962
|
+
// срезы рейтинга (rank-periods) — сразу после создания контроллера и до
|
|
1963
|
+
// первого gameChanged: тот уже несёт открытый срез в запросе
|
|
1964
|
+
lobby.setPeriods(
|
|
1965
|
+
lobbyConfig.leaderboardPeriods,
|
|
1966
|
+
lobbyConfig.defaultLeaderboardPeriod,
|
|
1967
|
+
);
|
|
1968
|
+
|
|
1957
1969
|
// список серверов — REST-запросом к мастеру
|
|
1958
1970
|
lobbyModel.publisher.on('fetch', async query => {
|
|
1959
1971
|
const list = await fetchServers(query);
|
|
@@ -1989,17 +2001,17 @@ function initLobby() {
|
|
|
1989
2001
|
// под заголовком новой, пока не пришёл ответ (и остаются навсегда при
|
|
1990
2002
|
// сетевом сбое); reqId отбрасывает ответ устаревшего запроса, если игру
|
|
1991
2003
|
// переключили быстрее, чем пришёл ответ (latest-wins)
|
|
1992
|
-
lobby.publisher.on('leaderboard-needed', async gameId => {
|
|
2004
|
+
lobby.publisher.on('leaderboard-needed', async ({ gameId, period }) => {
|
|
1993
2005
|
lobbyModel.clearLeaderboard();
|
|
1994
2006
|
|
|
1995
2007
|
const reqId = ++leaderboardReqId;
|
|
1996
2008
|
const [leaderboard, placement] = await Promise.all([
|
|
1997
|
-
fetchLeaderboard(gameId),
|
|
1998
|
-
fetchPlacement(gameId),
|
|
2009
|
+
fetchLeaderboard(gameId, period),
|
|
2010
|
+
fetchPlacement(gameId, period),
|
|
1999
2011
|
]);
|
|
2000
2012
|
|
|
2001
2013
|
if (reqId !== leaderboardReqId) {
|
|
2002
|
-
return; // игру уже переключили ещё раз —
|
|
2014
|
+
return; // игру или срез уже переключили ещё раз — ответ устарел
|
|
2003
2015
|
}
|
|
2004
2016
|
|
|
2005
2017
|
if (leaderboard) {
|
package/src/client/style.css
CHANGED
|
@@ -908,6 +908,32 @@ body.hide-cursor {
|
|
|
908
908
|
border-bottom: 2px solid #fff;
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
+
/* срезы рейтинга (rank-periods): день/месяц/всё время. Ряд под вкладками
|
|
912
|
+
правой панели — это выбор ДАННЫХ, а не вкладка, поэтому кнопки выглядят
|
|
913
|
+
как переключатель, а не как заголовок раздела */
|
|
914
|
+
.lobby-period-tabs {
|
|
915
|
+
display: flex;
|
|
916
|
+
gap: 4px;
|
|
917
|
+
margin-bottom: 10px;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.lobby-period-btn {
|
|
921
|
+
flex: 1 1 0;
|
|
922
|
+
background: rgb(255 255 255 / 6%);
|
|
923
|
+
border: none;
|
|
924
|
+
color: #888;
|
|
925
|
+
font-size: 12px;
|
|
926
|
+
font-weight: bold;
|
|
927
|
+
padding: 5px 4px;
|
|
928
|
+
margin: 0;
|
|
929
|
+
text-shadow: none;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.lobby-period-btn.active {
|
|
933
|
+
background: rgb(255 255 255 / 18%);
|
|
934
|
+
color: #fff;
|
|
935
|
+
}
|
|
936
|
+
|
|
911
937
|
.lobby-leaderboard-header {
|
|
912
938
|
display: flex;
|
|
913
939
|
justify-content: space-between;
|
|
@@ -50,6 +50,12 @@ div#lobby(style='display:none')
|
|
|
50
50
|
p#lobby-empty= 'No servers available'
|
|
51
51
|
input#lobby-more(type='button', value='Load more')
|
|
52
52
|
div#lobby-leaderboard-content(style='display:none')
|
|
53
|
+
//- срез рейтинга (rank-periods): день/месяц считаются из леджера
|
|
54
|
+
//- дельт, all-time — из кэша ratings
|
|
55
|
+
div.lobby-period-tabs
|
|
56
|
+
input#btn-period-day.lobby-period-btn(type='button', value='Daily')
|
|
57
|
+
input#btn-period-month.lobby-period-btn(type='button', value='Monthly')
|
|
58
|
+
input#btn-period-all.lobby-period-btn.active(type='button', value='All-Time')
|
|
53
59
|
div.lobby-leaderboard-header
|
|
54
60
|
span#leaderboard-title
|
|
55
61
|
span#leaderboard-total
|
package/src/config/lobby.js
CHANGED
|
@@ -50,6 +50,17 @@ export default {
|
|
|
50
50
|
placementUrl: '/auth/placement',
|
|
51
51
|
leaderboardLimit: 10,
|
|
52
52
|
|
|
53
|
+
// rank-periods: срезы рейтинга и тот, что открыт по умолчанию. Порядок
|
|
54
|
+
// здесь — порядок кнопок; `id` едет в auth как ?period=, `title` идёт в
|
|
55
|
+
// заголовок списка. Значения должны совпадать с RANK_PERIODS auth-сервиса:
|
|
56
|
+
// на всё прочее он отвечает 400
|
|
57
|
+
leaderboardPeriods: [
|
|
58
|
+
{ id: 'day', title: 'TODAY' },
|
|
59
|
+
{ id: 'month', title: 'THIS MONTH' },
|
|
60
|
+
{ id: 'all', title: 'ALL-TIME' },
|
|
61
|
+
],
|
|
62
|
+
defaultLeaderboardPeriod: 'all',
|
|
63
|
+
|
|
53
64
|
// переподключение сигнального WS хоста (комната без него выпадает из
|
|
54
65
|
// выдачи мастера): экспоненциальный бэкофф от baseDelay до maxDelay (мс)
|
|
55
66
|
reconnect: {
|
|
@@ -93,6 +104,12 @@ export default {
|
|
|
93
104
|
serversContentId: 'lobby-servers-content',
|
|
94
105
|
leaderboardContentId: 'lobby-leaderboard-content',
|
|
95
106
|
leaderboardListId: 'lobby-leaderboard-list',
|
|
107
|
+
// кнопки срезов (rank-periods): id периода -> id элемента
|
|
108
|
+
periodBtnIds: {
|
|
109
|
+
day: 'btn-period-day',
|
|
110
|
+
month: 'btn-period-month',
|
|
111
|
+
all: 'btn-period-all',
|
|
112
|
+
},
|
|
96
113
|
leaderboardTitleId: 'leaderboard-title',
|
|
97
114
|
leaderboardTotalId: 'leaderboard-total',
|
|
98
115
|
myPlacementId: 'lobby-my-placement',
|
package/src/host/HostGame.js
CHANGED
|
@@ -1063,6 +1063,19 @@ export default class HostGame {
|
|
|
1063
1063
|
this._playerDataSync.addRank(gameId, delta);
|
|
1064
1064
|
}
|
|
1065
1065
|
|
|
1066
|
+
// синхронизация профилей (rank/state) всех участников на мастер прямо
|
|
1067
|
+
// сейчас — для игр, у которых нет ни конца раунда, ни смены карты
|
|
1068
|
+
// (`endlessRound` + карта, пересобираемая через overrideMapData): обе
|
|
1069
|
+
// штатные границы flushAll живут в RoundManager, и такая игра не проходит
|
|
1070
|
+
// ни через одну из них. Без этого накопленный за матч ранг уезжает в auth
|
|
1071
|
+
// только на выходе участника, а закрытая вкладка хоста теряет его вовсе.
|
|
1072
|
+
//
|
|
1073
|
+
// Best-effort, как и весь PlayerDataSync: промис не отвергается, сбой
|
|
1074
|
+
// логируется в `[playerData]` и повторится следующим flush'ем.
|
|
1075
|
+
flushPlayerData() {
|
|
1076
|
+
return this._playerDataSync.flushAll();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1066
1079
|
// hostId + per-room секрет комнаты, подтверждённые мастером при
|
|
1067
1080
|
// register_host (кодревью №1) — не известны при создании HostGame (Worker
|
|
1068
1081
|
// стартует раньше ответа мастера); нужны PlayerDataSync для атрибуции
|