@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 +192 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -25
- package/dist/index.d.ts +43 -25
- package/dist/index.js +192 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1418
|
+
// ─── Music ───────────────────────────────────────────
|
|
1419
|
+
/** Play background music with optional crossfade */
|
|
1355
1420
|
playMusic(alias, options = {}) {
|
|
1356
|
-
if (this.currentMusicAlias
|
|
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
|
-
|
|
1365
|
-
this.
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
if (
|
|
1375
|
-
this.
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
import_sound.sound.pauseAll();
|
|
1463
|
+
get currentMusicName() {
|
|
1464
|
+
return this.currentMusicAlias;
|
|
1396
1465
|
}
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1474
|
+
// ─── Volume ──────────────────────────────────────────
|
|
1402
1475
|
setVolume(channel, value) {
|
|
1403
1476
|
this.volumes[channel] = Math.max(0, Math.min(1, value));
|
|
1404
|
-
this.
|
|
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
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
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
|
|
1511
|
+
return this._muted;
|
|
1431
1512
|
}
|
|
1432
|
-
|
|
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
|
|