vimp-engine 0.7.2 → 0.8.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.
Files changed (73) hide show
  1. package/package.json +11 -4
  2. package/src/client/InputListener.js +36 -0
  3. package/src/client/SoundManager.js +498 -0
  4. package/src/client/boot.js +113 -0
  5. package/src/client/components/controller/Auth.js +44 -0
  6. package/src/client/components/controller/CanvasManager.js +26 -0
  7. package/src/client/components/controller/Chat.js +56 -0
  8. package/src/client/components/controller/Controls.js +51 -0
  9. package/src/client/components/controller/Game.js +37 -0
  10. package/src/client/components/controller/Lobby.js +81 -0
  11. package/src/client/components/controller/LobbyAuth.js +41 -0
  12. package/src/client/components/controller/Panel.js +23 -0
  13. package/src/client/components/controller/Stat.js +33 -0
  14. package/src/client/components/controller/Vote.js +55 -0
  15. package/src/client/components/model/Auth.js +92 -0
  16. package/src/client/components/model/CanvasManager.js +319 -0
  17. package/src/client/components/model/Chat.js +119 -0
  18. package/src/client/components/model/Controls.js +120 -0
  19. package/src/client/components/model/Game.js +172 -0
  20. package/src/client/components/model/Lobby.js +226 -0
  21. package/src/client/components/model/LobbyAuth.js +161 -0
  22. package/src/client/components/model/Panel.js +70 -0
  23. package/src/client/components/model/Stat.js +77 -0
  24. package/src/client/components/model/Vote.js +216 -0
  25. package/src/client/components/view/Auth.js +167 -0
  26. package/src/client/components/view/CanvasManager.js +44 -0
  27. package/src/client/components/view/Chat.js +90 -0
  28. package/src/client/components/view/Controls.js +30 -0
  29. package/src/client/components/view/Game.js +48 -0
  30. package/src/client/components/view/Lobby.js +303 -0
  31. package/src/client/components/view/LobbyAuth.js +111 -0
  32. package/src/client/components/view/Panel.js +314 -0
  33. package/src/client/components/view/Stat.js +238 -0
  34. package/src/client/components/view/Vote.js +108 -0
  35. package/src/client/debug.js +158 -0
  36. package/src/client/lib/autostart.js +61 -0
  37. package/src/client/lib/contextTracker.js +46 -0
  38. package/src/client/lib/formBuilder.js +280 -0
  39. package/src/client/lib/hostGate.js +16 -0
  40. package/src/client/main.js +2011 -0
  41. package/src/client/network/HostConnectionManager.js +196 -0
  42. package/src/client/network/HostController.js +422 -0
  43. package/src/client/network/InlineHostBridge.js +152 -0
  44. package/src/client/network/LoopbackTransport.js +51 -0
  45. package/src/client/network/SignalingClient.js +146 -0
  46. package/src/client/network/WebRtcManager.js +147 -0
  47. package/src/client/network/WebSocketTransport.js +78 -0
  48. package/src/client/network/policyClose.js +54 -0
  49. package/src/client/network/workerSupport.js +34 -0
  50. package/src/client/providers/BakingProvider.js +88 -0
  51. package/src/client/providers/DependencyProvider.js +41 -0
  52. package/src/client/style.css +898 -0
  53. package/src/client/views/gameShell.js +190 -0
  54. package/src/client/views/includes/auth.pug +20 -0
  55. package/src/client/views/includes/chat.pug +5 -0
  56. package/src/client/views/includes/informer.pug +2 -0
  57. package/src/client/views/includes/lobby.pug +48 -0
  58. package/src/client/views/includes/lobbyAuth.pug +19 -0
  59. package/src/client/views/includes/panel.pug +3 -0
  60. package/src/client/views/includes/stat.pug +2 -0
  61. package/src/client/views/index.pug +8 -0
  62. package/src/config/closeCodes.js +18 -0
  63. package/src/config/env.js +61 -0
  64. package/src/devtools/ScenarioRunner.js +3 -8
  65. package/src/devtools/pluginLoader.js +8 -93
  66. package/src/host/HostGame.js +41 -3
  67. package/src/host/PortMachine.js +294 -0
  68. package/src/host/host.worker.js +29 -246
  69. package/src/host/identity.js +100 -0
  70. package/src/lib/clientIp.js +39 -0
  71. package/src/lib/loadGamePackage.js +122 -0
  72. package/src/lib/offlinePlayerData.js +18 -0
  73. package/src/standalone/index.js +125 -0
@@ -0,0 +1,319 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+ import { lerp, clamp } from '../../../lib/math.js';
3
+
4
+ // Singleton CanvasManagerModel
5
+
6
+ let canvasManagerModel;
7
+
8
+ export default class CanvasManagerModel {
9
+ constructor(data) {
10
+ if (canvasManagerModel) {
11
+ return canvasManagerModel;
12
+ }
13
+
14
+ canvasManagerModel = this;
15
+
16
+ this._data = {};
17
+ this._coordX = 0; // текущая координата X игрока
18
+ this._coordY = 0; // текущая координата Y игрока
19
+
20
+ // текущие смещения камеры (для плавности)
21
+ this._camOffsetX = 0;
22
+ this._camOffsetY = 0;
23
+ this._camZoomModifier = 1;
24
+
25
+ // параметры тряски камеры
26
+ this._shakeIntensity = 0;
27
+ this._shakeDuration = 0;
28
+ this._shakeTotalDuration = 0;
29
+ this._lastShakeTime = 0;
30
+
31
+ // средние значения вектора движения и скорости
32
+ this._avgDx = 0;
33
+ this._avgDy = 0;
34
+ this._avgSpeed = 0;
35
+
36
+ // ширина экрана, при которой масштаб игры = 1.0 * baseScale
37
+ // если экран меньше этого значения,
38
+ // картинка canvas будет пропорционально уменьшаться
39
+ this._designWidth = 1920; // Full HD
40
+
41
+ // настройки камеры из конфига
42
+ const camConfig = data.dynamicCamera || {};
43
+
44
+ this._lookAheadFactor = camConfig.lookAheadFactor || 0;
45
+
46
+ // dead zone по правилу "0.5 пикселя"
47
+ const safeFactor = Math.max(this._lookAheadFactor, 1);
48
+
49
+ // если lookAheadFactor = 30, deadZone ≈ 0.016
50
+ // если lookAheadFactor = 0,deadZone = 0.5
51
+ this._deadZone = 0.5 / safeFactor;
52
+
53
+ this._zoomOutFactor = clamp(camConfig.zoomOutFactor ?? 0, 0, 1);
54
+ this._maxZoomOut = camConfig.maxZoomOut || 1;
55
+
56
+ // плавность изменений динамической камеры:
57
+ this._smoothnessPosition = clamp(camConfig.smoothnessPosition ?? 0.1, 0, 1);
58
+ this._smoothnessZoom = clamp(camConfig.smoothnessZoom ?? 0.005, 0, 1);
59
+ this._smoothnessVelocity = clamp(camConfig.smoothnessVelocity ?? 0.1, 0, 1);
60
+
61
+ const canvases = data.canvases || {};
62
+
63
+ for (const canvasName in canvases) {
64
+ if (Object.hasOwn(canvases, canvasName)) {
65
+ const canvasData = canvases[canvasName];
66
+ const [w, h] = (canvasData.baseScale || '1:1')
67
+ .split(':')
68
+ .map(value => Number(value));
69
+ const baseScale = Number((w / h).toFixed(2));
70
+
71
+ this._data[canvasName] = {
72
+ ...canvasData,
73
+ baseScale,
74
+ currentScale: baseScale,
75
+ dynamicCamera: !!canvasData.dynamicCamera,
76
+ shakeCamera: !!canvasData.shakeCamera,
77
+ };
78
+ }
79
+ }
80
+
81
+ this.publisher = new Publisher();
82
+ }
83
+
84
+ // рассчитывает размеры элементов с учетом пропорций
85
+ resize(data) {
86
+ const screenWidth = data.width;
87
+ const screenHeight = data.height;
88
+
89
+ // нулевой (свёрнутая вкладка/окно) resize уводил бы currentScale в 0, а
90
+ // renderer — в размер 0x0; scale сам не пересчитается до следующего
91
+ // настоящего resize, и полотно останется пустым
92
+ if (
93
+ !Number.isFinite(screenWidth) ||
94
+ screenWidth <= 0 ||
95
+ !Number.isFinite(screenHeight) ||
96
+ screenHeight <= 0
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ for (const canvasName in this._data) {
102
+ if (Object.hasOwn(this._data, canvasName)) {
103
+ const { fixSize, aspectRatio, baseScale } = this._data[canvasName];
104
+ let width, height;
105
+
106
+ // если есть фиксированный размер полотна
107
+ if (fixSize) {
108
+ const parts = fixSize.split(':');
109
+
110
+ width = +parts[0];
111
+ height = +parts[1] || +parts[0];
112
+ } else {
113
+ // если задано соотношение сторон
114
+ if (aspectRatio) {
115
+ const parts = aspectRatio.split(':');
116
+
117
+ // строку в число
118
+ const widthRatio = parseInt(parts[0], 10);
119
+ const heightRatio = parseInt(parts[1], 10);
120
+
121
+ width = screenWidth;
122
+ height = (width / widthRatio) * heightRatio;
123
+
124
+ // если фактическая высота больше полученной,
125
+ // то вычисления производятся относительно высоты
126
+ if (height > screenHeight) {
127
+ height = screenHeight;
128
+ width = (height / heightRatio) * widthRatio;
129
+ }
130
+ } else {
131
+ width = screenWidth;
132
+ height = screenHeight;
133
+ }
134
+
135
+ width = +width.toFixed();
136
+ height = +height.toFixed();
137
+
138
+ this._data[canvasName].currentScale =
139
+ (width / this._designWidth) * baseScale;
140
+ }
141
+
142
+ this.publisher.emit('resize', {
143
+ id: canvasName,
144
+ sizes: {
145
+ width: Math.max(1, width),
146
+ height: Math.max(1, height),
147
+ },
148
+ });
149
+ }
150
+ }
151
+
152
+ this.updateCoords(this._coordX, this._coordY);
153
+ }
154
+
155
+ // рассчитывает текущее смещение тряски
156
+ _calculateShakeOffset(shakeData) {
157
+ // обработка новых данных о тряске
158
+ if (typeof shakeData === 'string') {
159
+ const [intensity, duration] = shakeData.split(':').map(Number);
160
+ this._shakeIntensity = intensity;
161
+ this._shakeTotalDuration = duration;
162
+ this._shakeDuration = duration;
163
+ this._lastShakeTime = Date.now();
164
+ }
165
+
166
+ let shakeX = 0;
167
+ let shakeY = 0;
168
+
169
+ if (this._shakeDuration > 0) {
170
+ const now = Date.now();
171
+ const dt = now - (this._lastShakeTime || now);
172
+ this._lastShakeTime = now;
173
+
174
+ // уменьшение длительности
175
+ this._shakeDuration -= dt;
176
+
177
+ if (this._shakeDuration > 0) {
178
+ // расчет затухания
179
+ const currentIntensity =
180
+ this._shakeIntensity *
181
+ (this._shakeDuration / this._shakeTotalDuration);
182
+
183
+ // случайное смещение
184
+ shakeX = (Math.random() - 0.5) * 2 * currentIntensity;
185
+ shakeY = (Math.random() - 0.5) * 2 * currentIntensity;
186
+ } else {
187
+ this._shakeDuration = 0;
188
+ }
189
+ } else {
190
+ // обновление времени для следующего старта
191
+ this._lastShakeTime = Date.now();
192
+ }
193
+
194
+ return { shakeX, shakeY };
195
+ }
196
+
197
+ // вычисляет координаты для отображения
198
+ updateCoords(x, y, cameraReset, shakeData) {
199
+ // если сброс динамических данных камеры (телепорт/респаун/смена игрока)
200
+ if (cameraReset) {
201
+ this._coordX = x;
202
+ this._coordY = y;
203
+
204
+ // сброс инерции камеры (смещение и зум)
205
+ this._camOffsetX = 0;
206
+ this._camOffsetY = 0;
207
+ this._camZoomModifier = 1;
208
+
209
+ // сброс накопителя сглаживания скорости
210
+ this._avgDx = 0;
211
+ this._avgDy = 0;
212
+ this._avgSpeed = 0;
213
+
214
+ // сброс эффекта тряски
215
+ this._shakeDuration = 0;
216
+ }
217
+
218
+ // расчет смещения тряски
219
+ const { shakeX, shakeY } = this._calculateShakeOffset(shakeData);
220
+
221
+ // расчет движения
222
+ // если был сброс, dx/dy будут равны 0 (т.к. только что обновился _coordX/Y)
223
+ let dx = x - this._coordX;
224
+ let dy = y - this._coordY;
225
+
226
+ // скорость движения
227
+ let speed = Math.sqrt(dx * dx + dy * dy);
228
+
229
+ // если движение медленнее порога dead zone
230
+ if (speed < this._deadZone) {
231
+ dx = 0;
232
+ dy = 0;
233
+ speed = 0;
234
+ }
235
+
236
+ // фильтрация шума скорости (позволяет игнорировать сетевой джиттер)
237
+ this._avgDx = lerp(this._avgDx, dx, this._smoothnessVelocity);
238
+ this._avgDy = lerp(this._avgDy, dy, this._smoothnessVelocity);
239
+ this._avgSpeed = lerp(this._avgSpeed, speed, this._smoothnessVelocity);
240
+
241
+ // сдвиг камеры в сторону движения
242
+ const targetOffsetX = this._avgDx * this._lookAheadFactor;
243
+ const targetOffsetY = this._avgDy * this._lookAheadFactor;
244
+
245
+ // зум: чем быстрее, тем меньше масштаб (отдаление)
246
+ const targetZoomModifier = Math.max(
247
+ this._maxZoomOut,
248
+ 1 - this._avgSpeed * this._zoomOutFactor,
249
+ );
250
+
251
+ // сглаживание движения камеры
252
+ this._camOffsetX = lerp(
253
+ this._camOffsetX,
254
+ targetOffsetX,
255
+ this._smoothnessPosition,
256
+ );
257
+ this._camOffsetY = lerp(
258
+ this._camOffsetY,
259
+ targetOffsetY,
260
+ this._smoothnessPosition,
261
+ );
262
+
263
+ // сглаживание зума
264
+ this._camZoomModifier = lerp(
265
+ this._camZoomModifier,
266
+ targetZoomModifier,
267
+ this._smoothnessZoom,
268
+ );
269
+
270
+ this._coordX = x;
271
+ this._coordY = y;
272
+
273
+ for (const canvasName in this._data) {
274
+ if (Object.hasOwn(this._data, canvasName)) {
275
+ const { dynamicCamera, currentScale, shakeCamera } =
276
+ this._data[canvasName];
277
+
278
+ // если для полотна включена динамическая камера
279
+ if (dynamicCamera) {
280
+ let camX = x + this._camOffsetX;
281
+ let camY = y + this._camOffsetY;
282
+
283
+ // добавление эффекта тряски
284
+ if (shakeCamera) {
285
+ camX += shakeX;
286
+ camY += shakeY;
287
+ }
288
+
289
+ const finalScale = currentScale * this._camZoomModifier;
290
+
291
+ this.publisher.emit('updateCoords', {
292
+ id: canvasName,
293
+ coords: {
294
+ x: camX,
295
+ y: camY,
296
+ },
297
+ scale: finalScale,
298
+ });
299
+ } else {
300
+ // если динамическая камера выключена
301
+ let finalX = x;
302
+ let finalY = y;
303
+
304
+ // добавление эффекта тряски
305
+ if (shakeCamera) {
306
+ finalX += shakeX;
307
+ finalY += shakeY;
308
+ }
309
+
310
+ this.publisher.emit('updateCoords', {
311
+ id: canvasName,
312
+ coords: { x: finalX, y: finalY },
313
+ scale: currentScale,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
@@ -0,0 +1,119 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+
3
+ // Singleton ChatModel
4
+
5
+ let chatModel;
6
+
7
+ export default class ChatModel {
8
+ constructor(data) {
9
+ if (chatModel) {
10
+ return chatModel;
11
+ }
12
+
13
+ chatModel = this;
14
+
15
+ this._listLimit = data.listLimit || 5;
16
+ this._lineTime = data.lineTime || 15000;
17
+ this._cacheMin = data.cacheMin || 200;
18
+ this._cacheMax = data.cacheMax || 300;
19
+ this._messages = data.messages || {};
20
+
21
+ this._formatMessage = data.formatMessage;
22
+ this._sanitizeMessage = data.sanitizeMessage;
23
+
24
+ this._cache = []; // хранилище сообщений
25
+ this._list = []; // активный чат-лист
26
+ this._counter = 0; // id для сообщения чат-листа
27
+
28
+ this.publisher = new Publisher();
29
+ }
30
+
31
+ // открывает cmd
32
+ open() {
33
+ this.publisher.emit('open');
34
+ this.publisher.emit('mode', { name: 'chat', status: 'opened' });
35
+ }
36
+
37
+ // закрывает cmd
38
+ close(success) {
39
+ this.publisher.emit('close', success ? true : false);
40
+ this.publisher.emit('mode', { name: 'chat', status: 'closed' });
41
+ }
42
+
43
+ // отправляет сообщение на сервер
44
+ sendMessage(message) {
45
+ message = this._sanitizeMessage(message);
46
+
47
+ if (message) {
48
+ this.publisher.emit('socket', message);
49
+ }
50
+ }
51
+
52
+ // обновляет чат-лист. Данные могут быть 2-х видов:
53
+ // - в виде строки '<группа шаблонов>:<номер шаблона>:<параметры>'
54
+ // - в виде массива [<текст сообщения>,<имя автора>,<тип для класса>]
55
+ updateChat(arr) {
56
+ // если данные - строка
57
+ if (typeof arr === 'string') {
58
+ arr = arr.split(':');
59
+
60
+ // если сообщений не найдено
61
+ if (!this._messages[arr[0]] || !this._messages[arr[0]][arr[1]]) {
62
+ return;
63
+ }
64
+
65
+ let message = this._messages[arr[0]][arr[1]];
66
+ const params = arr[2];
67
+
68
+ // если есть параметры
69
+ if (params) {
70
+ message = this._formatMessage(message, params.split(','));
71
+ }
72
+
73
+ arr = [message];
74
+ }
75
+
76
+ // если количество сообщений в хранилище достигло предела -
77
+ // удалить лишние
78
+ if (this._cache.length === this._cacheMax) {
79
+ this._cache.splice(0, this._cache.length - this._cacheMin);
80
+ }
81
+
82
+ // добавить объект сообщения в хранилище
83
+ this._cache.push(arr);
84
+
85
+ // если количество выделенных линий исчерпано -
86
+ // удалить линию принудительно
87
+ if (this._list.length === this._listLimit) {
88
+ this.removeFromList(true);
89
+ }
90
+
91
+ this.publisher.emit('newLine', {
92
+ id: this._counter,
93
+ message: arr,
94
+ });
95
+
96
+ this.publisher.emit('newTimer', {
97
+ id: this._counter,
98
+ time: this._lineTime,
99
+ });
100
+
101
+ this._counter += 1;
102
+ }
103
+
104
+ // добавляет объект в чат-лист
105
+ addToList(data) {
106
+ this._list.push(data);
107
+ }
108
+
109
+ // удаляет объект из чат-листа
110
+ removeFromList(sync) {
111
+ const data = this._list.shift();
112
+
113
+ this.publisher.emit('oldLine', data.messageId);
114
+
115
+ if (sync) {
116
+ this.publisher.emit('oldTimer', data.timerId);
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,120 @@
1
+ import Publisher from '../../../lib/Publisher.js';
2
+
3
+ // Singleton ControlsModel
4
+
5
+ let controlsModel;
6
+
7
+ export default class ControlsModel {
8
+ constructor(data) {
9
+ if (controlsModel) {
10
+ return controlsModel;
11
+ }
12
+
13
+ controlsModel = this;
14
+
15
+ this._keySetList = data.keySetList;
16
+ this._modes = data.modes;
17
+ this._cmds = data.cmds;
18
+
19
+ this._currentKeySet = this._keySetList[0]; // текущий набор клавиш
20
+ this._currentModes = {}; // статусы режимов
21
+ this._pressedKeys = {}; // объект для хранения состояния зажатых клавиш
22
+ this._areKeysEnabled = false; // статус возможности нажатия клавиш
23
+
24
+ this.publisher = new Publisher();
25
+ }
26
+
27
+ // добавляет команду
28
+ addKey(event) {
29
+ const keyCode = event.keyCode;
30
+ const mode = this._modes[keyCode];
31
+ const cmd = this._cmds[keyCode];
32
+
33
+ // если запрет на ввод клавиш,
34
+ // то доступны только stat и chat
35
+ if (
36
+ this._areKeysEnabled === false &&
37
+ mode !== 'stat' &&
38
+ mode !== 'chat' &&
39
+ !this._currentModes.chat
40
+ ) {
41
+ return;
42
+ }
43
+
44
+ // если чат активен
45
+ if (this._currentModes.chat) {
46
+ if (cmd) {
47
+ this.publisher.emit('chat', cmd);
48
+ }
49
+
50
+ if (mode === 'stat') {
51
+ event.preventDefault();
52
+ this.publisher.emit('mode', mode);
53
+ }
54
+ } else {
55
+ // если клавиша ещё не зажата
56
+ if (!this._pressedKeys[keyCode]) {
57
+ const name = this._currentKeySet[keyCode];
58
+
59
+ if (name) {
60
+ this._pressedKeys[keyCode] = true;
61
+ this.publisher.emit('socket', `down:${name}`);
62
+ }
63
+ }
64
+
65
+ if (this._currentModes.vote) {
66
+ this.publisher.emit('vote', keyCode);
67
+ }
68
+
69
+ if (this._currentModes.stat) {
70
+ event.preventDefault();
71
+ }
72
+
73
+ if (mode) {
74
+ event.preventDefault();
75
+ this.publisher.emit('mode', mode);
76
+ }
77
+ }
78
+ }
79
+
80
+ // удаляет команду
81
+ removeKey(event) {
82
+ const keyCode = event.keyCode;
83
+ const mode = this._modes[keyCode];
84
+
85
+ // если клавиша была зажата
86
+ if (this._pressedKeys[keyCode]) {
87
+ const name = this._currentKeySet[keyCode];
88
+
89
+ if (name) {
90
+ this._pressedKeys[keyCode] = false;
91
+ this.publisher.emit('socket', `up:${name}`);
92
+ }
93
+ }
94
+
95
+ if (this._currentModes.stat && mode === 'stat') {
96
+ this.publisher.emit('stat');
97
+ }
98
+ }
99
+
100
+ // меняет состояние режима
101
+ setMode(mode, status) {
102
+ if (status === 'opened') {
103
+ this._currentModes[mode] = true;
104
+ } else if (status === 'closed') {
105
+ this._currentModes[mode] = false;
106
+ }
107
+ }
108
+
109
+ // меняет набор клавиш
110
+ changeKeySet(key) {
111
+ this._currentKeySet = this._keySetList[key];
112
+ this._pressedKeys = {};
113
+ }
114
+
115
+ // задаёт возможность нажатия клавиш
116
+ // (stat и chat доступны при любом значении)
117
+ setKeysEnabled(isEnabled) {
118
+ this._areKeysEnabled = isEnabled;
119
+ }
120
+ }