vimp-engine 0.20.0 → 0.22.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.
@@ -26,9 +26,15 @@ class Chat {
26
26
  delete this._userList[gameId];
27
27
  }
28
28
 
29
- // добавляет сообщение
30
- push(message, name, teamId) {
31
- this._list.push([message, name, teamId]);
29
+ // добавляет сообщение. color ('#rrggbb') — цвет ника, который игра задала
30
+ // через ParticipantManager.setChatColor; без него четвёртого элемента в
31
+ // массиве нет вовсе, и провод игры, которая цвет не задаёт, не меняется
32
+ push(message, name, teamId, color) {
33
+ if (typeof color === 'string') {
34
+ this._list.push([message, name, teamId, color]);
35
+ } else {
36
+ this._list.push([message, name, teamId]);
37
+ }
32
38
  }
33
39
 
34
40
  // добавляет системное сообщение для всех
@@ -7,6 +7,12 @@ class Participant {
7
7
  this.team = team;
8
8
  this.teamId = teamId;
9
9
  this.status = 'spectator'; // 'spectator' | 'active' | 'dead'
10
+
11
+ // цвет ника в чате ('#rrggbb') или null — тогда ник красит класс команды.
12
+ // Ставит его игра (ParticipantManager.setChatColor): движок не знает, чем
13
+ // игра различает игроков визуально, а команда для этого годится не всегда
14
+ // — в игре на одну команду все ники одного цвета
15
+ this.chatColor = null;
10
16
  // индекс точки респауна, занятой этим участником, или null. Раздаёт и
11
17
  // освобождает его RoundManager: размер команды для этого не годится —
12
18
  // он меняется на входах и выходах, а между попаданием в команду и
@@ -149,6 +149,22 @@ class ParticipantManager {
149
149
  this._participants.delete(gameId);
150
150
  }
151
151
 
152
+ // задаёт цвет ника в чате: '#rgb'/'#rrggbb' или null (цвет команды).
153
+ // Единственный вход для игры — HostGame отдаёт это значение в chat.push,
154
+ // а клиентская view кладёт его инлайновой переменной на строку
155
+ setChatColor(gameId, color) {
156
+ const participant = this._participants.get(gameId);
157
+
158
+ if (!participant) {
159
+ return;
160
+ }
161
+
162
+ participant.chatColor =
163
+ typeof color === 'string' && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)
164
+ ? color
165
+ : null;
166
+ }
167
+
152
168
  get(gameId) {
153
169
  return this._participants.get(gameId);
154
170
  }
@@ -168,3 +168,50 @@ export const clampLimit = (value, fallback, max) => {
168
168
 
169
169
  return Number.isInteger(num) ? Math.min(Math.max(num, 1), max) : fallback;
170
170
  };
171
+
172
+ // Сколько завершённых игр движок может склеить в один запрос: окно склейки
173
+ // на его стороне — минута (lobbyConfig.playerData.minFlushInterval), столько
174
+ // игр в неё не влезает.
175
+ //
176
+ // Множитель СВЯЗАН с rank.maxPoints auth-сервиса (packages/auth/src/config/
177
+ // auth.js): мастер не должен пропускать то, что auth отклонит. Отклонённое
178
+ // тело для хоста неустранимо — PlayerDataSync повторял бы его до конца жизни
179
+ // комнаты, — поэтому при правке per-game maxGameScore проверяется и потолок
180
+ // суммы на той стороне.
181
+ const MERGED_GAMES_PER_WINDOW = 20;
182
+
183
+ // клампит результат игры (snakes-v3 этап 3.3): `best` — очки ОДНОЙ игры,
184
+ // `points` — сумма склеенных игр с прошлой синхронизации. Потолок одной
185
+ // игры объявляет сама игра (master:games[].maxGameScore), потолок суммы —
186
+ // это же значение на минутное окно синхронизации; всё, что больше, значит
187
+ // либо взломанного клиента, либо неверно настроенную игру, и мастер режет
188
+ // его перед проксированием, а не отклоняет запрос целиком: честная часть
189
+ // результата не должна пропадать из-за одной аномальной строки.
190
+ // Инвариант `best <= points` держится и после клампа — auth его требует
191
+ export const clampGameResult = (points, best, maxGameScore) => {
192
+ const maxPoints = maxGameScore * MERGED_GAMES_PER_WINDOW;
193
+ const toInt = value => {
194
+ const num = Math.trunc(Number(value));
195
+
196
+ return Number.isFinite(num) ? Math.max(num, 0) : 0;
197
+ };
198
+ const cappedBest = Math.min(toInt(best), maxGameScore);
199
+ const cappedPoints = Math.min(toInt(points), maxPoints);
200
+
201
+ return {
202
+ points: cappedPoints,
203
+ // best > points — битый клиент: сумма не может быть меньше своего
204
+ // максимума. Поднимать сумму до максимума значило бы дорисовать
205
+ // месячному рейтингу очки, которых не было, поэтому режется best
206
+ best: Math.min(cappedBest, cappedPoints),
207
+ // превышение логируется вызывающим: это сигнал о сломанной игре
208
+ clamped: cappedBest !== toInt(best) || cappedPoints !== toInt(points),
209
+ };
210
+ };
211
+
212
+ // Ключ сопоставления ника с глобальным топом. Уникальность users.nick в auth
213
+ // регистронезависимая (миграция 002), поэтому и сравнивать надо так же —
214
+ // иначе «Alice» и «alice» разъедутся. Правило одно, а мест, где по нику
215
+ // ищут, три (host/meta/modules/Accolades.js, client/components/model/Stat.js
216
+ // и сам auth), поэтому оно живёт здесь, а не переписывается в каждом.
217
+ export const nickKey = nick => String(nick ?? '').toLowerCase();