vimp-engine 0.32.0 → 0.32.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/core/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vimp-engine-core"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  edition = "2024"
5
5
  description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
6
6
  license = "MIT"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,6 +43,13 @@ export default class SoundManager {
43
43
  // pannerAttr ставится один раз на экземпляр, а не каждый кадр
44
44
  this._equalPowerIds = new Set();
45
45
 
46
+ // экземпляры, у которых PannerNode УЖЕ создан. Howler создаёт узел
47
+ // лениво — на первом pos()/pannerAttr(id) — и заканчивает создание
48
+ // парой pause()/play() (setupPanner в howler.js), то есть щелчком.
49
+ // Пока источник стоит в точке слушателя, паннер не нужен вовсе:
50
+ // сигнал идёт прямо в gain, сохраняя стерео сэмпла
51
+ this._pannedIds = new Set();
52
+
46
53
  // позиция слушателя
47
54
  this._listenerX = 0;
48
55
  this._listenerY = 0;
@@ -164,7 +171,9 @@ export default class SoundManager {
164
171
  * @param {boolean} [data?.spatial=true] - Принадлежит ли звук миру.
165
172
  * `false` — источник игрока (двигатель и выстрел своего танка): он
166
173
  * стоит ровно на слушателе, и HRTF на нулевой дистанции сворачивается
167
- * в гребенчатую окраску («гул»), а не в тишину панорамы.
174
+ * в гребенчатую окраску («гул»), а не в тишину панорамы. Такому звуку
175
+ * PannerNode не создаётся вовсе — он идёт прямо в gain и остаётся
176
+ * стерео.
168
177
  * @param {function} [callback] - Функция, вызываемая по завершении.
169
178
  * @returns {symbol | null} Уникальный ID звука или null, если звук не найден.
170
179
  */
@@ -348,6 +357,7 @@ export default class SoundManager {
348
357
  sound.stop(soundId);
349
358
  this._activeInstances.delete(soundId);
350
359
  this._equalPowerIds.delete(soundId);
360
+ this._pannedIds.delete(soundId);
351
361
  continue;
352
362
  }
353
363
 
@@ -407,6 +417,7 @@ export default class SoundManager {
407
417
 
408
418
  this._activeInstances.delete(soundId);
409
419
  this._equalPowerIds.delete(soundId);
420
+ this._pannedIds.delete(soundId);
410
421
  },
411
422
  soundId,
412
423
  );
@@ -425,6 +436,7 @@ export default class SoundManager {
425
436
  instanceData.sound.stop(soundId);
426
437
  this._activeInstances.delete(soundId);
427
438
  this._equalPowerIds.delete(soundId);
439
+ this._pannedIds.delete(soundId);
428
440
  }
429
441
  }
430
442
 
@@ -445,11 +457,14 @@ export default class SoundManager {
445
457
 
446
458
  if (spatial === false) {
447
459
  // источник игрока: звук не принадлежит миру, он принадлежит игроку.
448
- // equalpower вместо HRTF — HRTF на нулевой дистанции даёт гребенчатую
449
- // окраску («гул»), а не тишину панорамы
450
- this._applyEqualPower(sound, soundId);
451
- sound.pos(0, 0, 0, soundId);
460
+ // Паннер ему не нужен ни в каком виде ни HRTF (на нулевой
461
+ // дистанции это не тишина панорамы, а фронтальная свёртка:
462
+ // гребенчатая окраска, из-за которой двигатель слышен как гул), ни
463
+ // equalpower (тот схлопывает стерео сэмпла в моно). Пока узла нет,
464
+ // pos() не зовётся вовсе: Howler создал бы паннер и щёлкнул
465
+ // pause()/play()
452
466
  sound.volume(volume, soundId);
467
+ this._recenterIfPanned(sound, soundId, true);
453
468
 
454
469
  return;
455
470
  }
@@ -467,9 +482,34 @@ export default class SoundManager {
467
482
  // если дистанция позволяет панорамировать звук
468
483
  if (distance > MIN_SPATIAL_DISTANCE) {
469
484
  sound.pos(x - this._listenerX, 0, y - this._listenerY, soundId);
485
+ this._pannedIds.add(soundId);
470
486
  } else {
471
- sound.pos(0, 0, 0, soundId); // отключение панорамирования
487
+ // внутри дед-зоны звук идёт по центру. Экземпляру, у которого паннер
488
+ // ещё не создан, здесь не место его заводить: узел появится на
489
+ // первом же выходе из дед-зоны
490
+ this._recenterIfPanned(sound, soundId);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * @private Возвращает в центр экземпляр, у которого PannerNode УЖЕ есть
496
+ * (источник успел побывать в стороне от слушателя). Экземпляр без узла
497
+ * не трогается: создание паннера — это лишний узел в цепочке, потеря
498
+ * стерео и щелчок от pause()/play() внутри setupPanner Howler'а.
499
+ * @param {boolean} [equalPower=false] - Перевести узел на equalpower:
500
+ * так делается только для источника игрока, миру HRTF сохраняется —
501
+ * он снова понадобится, как только источник выйдет из дед-зоны.
502
+ */
503
+ _recenterIfPanned(sound, soundId, equalPower = false) {
504
+ if (!this._pannedIds.has(soundId)) {
505
+ return;
506
+ }
507
+
508
+ if (equalPower) {
509
+ this._applyEqualPower(sound, soundId);
472
510
  }
511
+
512
+ sound.pos(0, 0, 0, soundId);
473
513
  }
474
514
 
475
515
  /**
@@ -521,6 +561,7 @@ export default class SoundManager {
521
561
  Howler.stop();
522
562
  this._activeInstances.clear();
523
563
  this._equalPowerIds.clear();
564
+ this._pannedIds.clear();
524
565
 
525
566
  // луп переживает reset: его владелец жив, и ближайший
526
567
  // processAudibility() запустит звук заново. Одноразовый — нет: