@duyquangnvx/pixi-game-engine 0.1.6 → 0.1.7

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/dist/index.cjs CHANGED
@@ -1327,111 +1327,252 @@ var SceneManager = class {
1327
1327
 
1328
1328
  // src/sound/sound-manager.ts
1329
1329
  var import_sound = require("@pixi/sound");
1330
+ var STORAGE_KEY = "sound_settings";
1330
1331
  var SoundManager = class {
1331
1332
  volumes = {
1332
1333
  master: 1,
1333
1334
  music: 1,
1334
1335
  sfx: 1
1335
1336
  };
1337
+ _muted = false;
1336
1338
  currentMusic = null;
1337
1339
  currentMusicAlias = null;
1338
- playSfx(alias, options = {}) {
1339
- this.play(alias, options);
1340
+ currentMusicInstance = null;
1341
+ /** Track active SFX instances for pause/resume/stop */
1342
+ activeSfx = /* @__PURE__ */ new Map();
1343
+ /** Whether the AudioContext has been unlocked by user interaction */
1344
+ _unlocked = false;
1345
+ constructor() {
1346
+ this.loadSettings();
1347
+ this.setupUnlockListener();
1340
1348
  }
1341
- /** Play a sound effect */
1342
- play(alias, options = {}) {
1349
+ // ─── AudioContext unlock ──────────────────────────────
1350
+ /**
1351
+ * Browsers block audio until first user interaction.
1352
+ * Call this early or let the auto-listener handle it.
1353
+ */
1354
+ setupUnlockListener() {
1355
+ const unlock = () => {
1356
+ if (this._unlocked) return;
1357
+ const ctx = import_sound.sound.context;
1358
+ if (ctx.audioContext.state === "suspended") {
1359
+ ctx.audioContext.resume().then(() => {
1360
+ this._unlocked = true;
1361
+ Logger.info("SoundManager", "AudioContext unlocked");
1362
+ });
1363
+ } else {
1364
+ this._unlocked = true;
1365
+ }
1366
+ if (this._unlocked) {
1367
+ document.removeEventListener("pointerdown", unlock);
1368
+ document.removeEventListener("keydown", unlock);
1369
+ }
1370
+ };
1371
+ document.addEventListener("pointerdown", unlock, { once: false });
1372
+ document.addEventListener("keydown", unlock, { once: false });
1373
+ }
1374
+ get isUnlocked() {
1375
+ return this._unlocked;
1376
+ }
1377
+ // ─── SFX ─────────────────────────────────────────────
1378
+ /** Play a sound effect. Returns instance for fine-grained control. */
1379
+ playSfx(alias, options = {}) {
1343
1380
  const snd = import_sound.sound.find(alias);
1344
1381
  if (!snd) {
1345
1382
  Logger.warn("SoundManager", `Sound "${alias}" not found`);
1346
- return;
1383
+ return null;
1347
1384
  }
1348
- snd.play({
1349
- volume: (options.volume ?? 1) * this.volumes.master * this.volumes.sfx,
1385
+ const effectiveVolume = (options.volume ?? 1) * this.volumes.master * this.volumes.sfx;
1386
+ const instance = snd.play({
1387
+ volume: options.fadeIn ? 0 : effectiveVolume,
1350
1388
  loop: options.loop ?? false,
1351
1389
  speed: options.speed ?? 1
1352
1390
  });
1391
+ if (!this.activeSfx.has(alias)) {
1392
+ this.activeSfx.set(alias, /* @__PURE__ */ new Set());
1393
+ }
1394
+ this.activeSfx.get(alias).add(instance);
1395
+ instance.on("end", () => {
1396
+ this.activeSfx.get(alias)?.delete(instance);
1397
+ });
1398
+ if (options.fadeIn && options.fadeIn > 0) {
1399
+ this.fadeInstance(instance, 0, effectiveVolume, options.fadeIn);
1400
+ }
1401
+ return instance;
1402
+ }
1403
+ /** Stop a specific SFX by alias (all instances) */
1404
+ stopSfx(alias, fadeOut) {
1405
+ const instances = this.activeSfx.get(alias);
1406
+ if (!instances) return;
1407
+ for (const inst of instances) {
1408
+ if (fadeOut && fadeOut > 0) {
1409
+ this.fadeInstance(inst, inst.volume, 0, fadeOut, () => {
1410
+ inst.stop();
1411
+ });
1412
+ } else {
1413
+ inst.stop();
1414
+ }
1415
+ }
1416
+ this.activeSfx.delete(alias);
1353
1417
  }
1354
- /** Play background music (only one at a time) */
1418
+ // ─── Music ───────────────────────────────────────────
1419
+ /** Play background music with optional crossfade */
1355
1420
  playMusic(alias, options = {}) {
1356
- if (this.currentMusicAlias !== alias) {
1357
- this.stopMusic();
1358
- }
1421
+ if (this.currentMusicAlias === alias) return;
1359
1422
  const snd = import_sound.sound.find(alias);
1360
1423
  if (!snd) {
1361
1424
  Logger.warn("SoundManager", `Music "${alias}" not found`);
1362
1425
  return;
1363
1426
  }
1364
- this.currentMusic = snd;
1365
- this.currentMusicAlias = alias;
1366
- snd.play({
1367
- volume: (options.volume ?? 1) * this.volumes.master * this.volumes.music,
1427
+ const crossfade = options.crossfadeDuration ?? 0;
1428
+ const targetVolume = (options.volume ?? 1) * this.volumes.master * this.volumes.music;
1429
+ if (this.currentMusicInstance && crossfade > 0) {
1430
+ const oldInstance = this.currentMusicInstance;
1431
+ this.fadeInstance(oldInstance, oldInstance.volume, 0, crossfade, () => {
1432
+ oldInstance.stop();
1433
+ });
1434
+ } else {
1435
+ this.stopMusic();
1436
+ }
1437
+ const instance = snd.play({
1438
+ volume: crossfade > 0 ? 0 : targetVolume,
1368
1439
  loop: options.loop ?? true,
1369
1440
  speed: options.speed ?? 1
1370
1441
  });
1371
- }
1372
- /** Stop the current music */
1373
- stopMusic() {
1374
- if (this.currentMusic) {
1375
- this.currentMusic.stop();
1376
- this.currentMusic = null;
1377
- this.currentMusicAlias = null;
1442
+ this.currentMusic = snd;
1443
+ this.currentMusicAlias = alias;
1444
+ this.currentMusicInstance = instance;
1445
+ if (crossfade > 0) {
1446
+ this.fadeInstance(instance, 0, targetVolume, crossfade);
1378
1447
  }
1379
1448
  }
1380
- /** Stop a specific sound */
1381
- stop(alias) {
1382
- const snd = import_sound.sound.find(alias);
1383
- if (snd) {
1384
- snd.stop();
1449
+ stopMusic(fadeOut) {
1450
+ if (!this.currentMusicInstance) return;
1451
+ if (fadeOut && fadeOut > 0) {
1452
+ const inst = this.currentMusicInstance;
1453
+ this.fadeInstance(inst, inst.volume, 0, fadeOut, () => {
1454
+ inst.stop();
1455
+ });
1456
+ } else {
1457
+ this.currentMusicInstance.stop();
1385
1458
  }
1386
- }
1387
- /** Stop all sounds */
1388
- stopAll() {
1389
- import_sound.sound.stopAll();
1390
1459
  this.currentMusic = null;
1391
1460
  this.currentMusicAlias = null;
1461
+ this.currentMusicInstance = null;
1392
1462
  }
1393
- /** Pause all sounds */
1394
- pauseAll() {
1395
- import_sound.sound.pauseAll();
1463
+ get currentMusicName() {
1464
+ return this.currentMusicAlias;
1396
1465
  }
1397
- /** Resume all sounds */
1398
- resumeAll() {
1399
- import_sound.sound.resumeAll();
1466
+ /** Pause only music */
1467
+ pauseMusic() {
1468
+ this.currentMusicInstance?.set("paused", true);
1469
+ }
1470
+ /** Resume only music */
1471
+ resumeMusic() {
1472
+ this.currentMusicInstance?.set("paused", false);
1400
1473
  }
1401
- /** Set volume for a channel */
1474
+ // ─── Volume ──────────────────────────────────────────
1402
1475
  setVolume(channel, value) {
1403
1476
  this.volumes[channel] = Math.max(0, Math.min(1, value));
1404
- this.updateMusicVolume();
1477
+ this.applyVolumes();
1478
+ this.saveSettings();
1405
1479
  }
1406
- /** Get volume for a channel */
1407
1480
  getVolume(channel) {
1408
1481
  return this.volumes[channel];
1409
1482
  }
1410
- updateMusicVolume() {
1411
- if (this.currentMusic) {
1412
- this.currentMusic.volume = this.volumes.master * this.volumes.music;
1483
+ /** Re-apply volume to all active sounds after a channel change */
1484
+ applyVolumes() {
1485
+ if (this.currentMusicInstance) {
1486
+ this.currentMusicInstance.volume = this.volumes.master * this.volumes.music;
1487
+ }
1488
+ for (const instances of this.activeSfx.values()) {
1489
+ for (const inst of instances) {
1490
+ inst.volume = this.volumes.master * this.volumes.sfx;
1491
+ }
1413
1492
  }
1414
1493
  }
1415
- /** Mute all sounds */
1494
+ // ─── Mute ────────────────────────────────────────────
1416
1495
  mute() {
1496
+ this._muted = true;
1417
1497
  import_sound.sound.muteAll();
1498
+ this.saveSettings();
1418
1499
  }
1419
- /** Unmute all sounds */
1420
1500
  unmute() {
1501
+ this._muted = false;
1421
1502
  import_sound.sound.unmuteAll();
1503
+ this.saveSettings();
1422
1504
  }
1423
- /** Toggle mute */
1424
1505
  toggleMute() {
1425
- import_sound.sound.toggleMuteAll();
1426
- return import_sound.sound.context.muted;
1506
+ if (this._muted) this.unmute();
1507
+ else this.mute();
1508
+ return this._muted;
1427
1509
  }
1428
- /** Check if muted */
1429
1510
  get isMuted() {
1430
- return import_sound.sound.context.muted;
1511
+ return this._muted;
1431
1512
  }
1432
- /** Clean up */
1513
+ // ─── Global controls ─────────────────────────────────
1514
+ stopAll() {
1515
+ import_sound.sound.stopAll();
1516
+ this.currentMusic = null;
1517
+ this.currentMusicAlias = null;
1518
+ this.currentMusicInstance = null;
1519
+ this.activeSfx.clear();
1520
+ }
1521
+ pauseAll() {
1522
+ import_sound.sound.pauseAll();
1523
+ }
1524
+ resumeAll() {
1525
+ import_sound.sound.resumeAll();
1526
+ }
1527
+ // ─── Persistence ─────────────────────────────────────
1528
+ saveSettings() {
1529
+ try {
1530
+ const data = {
1531
+ volumes: this.volumes,
1532
+ muted: this._muted
1533
+ };
1534
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1535
+ } catch {
1536
+ }
1537
+ }
1538
+ loadSettings() {
1539
+ try {
1540
+ const raw = localStorage.getItem(STORAGE_KEY);
1541
+ if (!raw) return;
1542
+ const data = JSON.parse(raw);
1543
+ if (data.volumes) {
1544
+ this.volumes.master = data.volumes.master ?? 1;
1545
+ this.volumes.music = data.volumes.music ?? 1;
1546
+ this.volumes.sfx = data.volumes.sfx ?? 1;
1547
+ }
1548
+ if (data.muted) {
1549
+ this._muted = true;
1550
+ import_sound.sound.muteAll();
1551
+ }
1552
+ } catch {
1553
+ }
1554
+ }
1555
+ // ─── Fade utility ────────────────────────────────────
1556
+ fadeInstance(instance, from, to, duration, onComplete) {
1557
+ const steps = 30;
1558
+ const interval = duration * 1e3 / steps;
1559
+ const delta = (to - from) / steps;
1560
+ let step = 0;
1561
+ const timer = setInterval(() => {
1562
+ step++;
1563
+ instance.volume = from + delta * step;
1564
+ if (step >= steps) {
1565
+ clearInterval(timer);
1566
+ instance.volume = to;
1567
+ onComplete?.();
1568
+ }
1569
+ }, interval);
1570
+ }
1571
+ // ─── Cleanup ─────────────────────────────────────────
1433
1572
  destroy() {
1434
1573
  this.stopAll();
1574
+ document.removeEventListener("pointerdown", this.setupUnlockListener);
1575
+ document.removeEventListener("keydown", this.setupUnlockListener);
1435
1576
  }
1436
1577
  };
1437
1578