Files
justice/www/html/Game/public/js/sound.js
T
2026-07-02 04:25:42 +00:00

175 lines
7.6 KiB
JavaScript

/* Justice — Sound Manager (BGM + SFX) · โหลดร่วมทุกหน้า (Main-Menu / room-lobby / play)
window.jdSound = { playBgm(scene), stopBgm(), playSfx(id[,scene]), setMusic(on), setSfx(on),
isMusicOn(), isSfxOn(), notifySpeaking(), duck(ms) }
- ประสาน UI ปุ่ม Music/SFX (profile popup) ผ่าน localStorage (jd_music_on / jd_sfx_on)
- ไม่ยุ่งกับ mic/WebRTC เลย — BGM/SFX เป็น HTMLAudio แยกจากเสียงพูด
- duck: หรี่ BGM อัตโนมัติตอนมีคนพูด (notifySpeaking) → เสียงคนไม่โดนกลบ
- volume master/music/sfx ปรับใน Admin ได้ (GET /Game/api/sound-settings) */
(function () {
'use strict';
if (window.jdSound) return;
var scriptBase = (function () {
try {
var s = (document.currentScript && document.currentScript.src) || '';
if (s) return s.replace(/\/js\/sound\.js.*$/, '');
} catch (e) { /* ignore */ }
/* fallback: หา <script src=".../js/sound.js"> */
try {
var els = document.getElementsByTagName('script');
for (var i = 0; i < els.length; i++) {
var src = els[i].src || '';
if (src.indexOf('/js/sound.js') >= 0) return src.replace(/\/js\/sound\.js.*$/, '');
}
} catch (e2) { /* ignore */ }
return '';
})();
function aurl(p) { return scriptBase + '/audio/' + p; }
var LS_MUSIC = 'jd_music_on', LS_SFX = 'jd_sfx_on';
function readBool(k, def) { try { var v = localStorage.getItem(k); return v == null ? def : (v === '1'); } catch (e) { return def; } }
function writeBool(k, v) { try { localStorage.setItem(k, v ? '1' : '0'); } catch (e) { /* ignore */ } }
var state = {
musicOn: readBool(LS_MUSIC, true),
sfxOn: readBool(LS_SFX, true),
masterVol: 1.0,
musicVol: 0.35,
sfxVol: 0.75,
curScene: null,
duckUntil: 0,
unlocked: false
};
/* ไฟล์ที่มีจริงต่อฉาก (กัน 404 — playSfx no-op ถ้าไม่มี) */
var MANIFEST = {
lobby: { bgm: 1 },
mg1: { bgm: 1, button: 1, correct: 1, endgame: 1, time: 1, wrong: 1 },
mg2: { bgm: 1, button: 1, gameover: 1, jump: 1, time: 1, winner: 1 },
mg3: { bgm: 1, button: 1, 'drop-clear': 1, 'drop-fail': 1, 'drop-hard': 1, endgame: 1, gameover: 1, time: 1, timeup: 1 },
mg4: { bgm: 1, button: 1, correct: 1, endgame: 1, gameover: 1, time: 1, timeup: 1, wrong: 1 },
mg5: { bgm: 1, button: 1, endgame: 1, gameover: 1, jump: 1, time: 1, timeup: 1 },
mg6: { bgm: 1, bomb: 1, button: 1, crash: 1, endgame: 1, gameover: 1, laser: 1, time: 1, timeup: 1 },
mg7: { bgm: 1, 'boss-elim': 1, button: 1, endgame: 1, gameover: 1, laser: 1, time: 1 }
};
var EXT = { button: 'wav' }; /* ที่เหลือ .mp3 */
function fileFor(scene, id) {
var ext = EXT[id] || 'mp3';
return scene + '/' + id + '.' + ext;
}
/* alias สำหรับ win/lose ให้เลือกไฟล์ที่มีในฉากนั้น */
function resolveSfx(scene, id) {
var m = MANIFEST[scene] || {};
if (id === 'win') { id = m.winner ? 'winner' : (m.endgame ? 'endgame' : null); }
else if (id === 'lose') { id = m.gameover ? 'gameover' : null; }
if (!id) return null;
if (m[id]) return fileFor(scene, id);
/* button ใช้ตัวกลางที่ audio root ถ้าฉากไม่มี */
if (id === 'button') return 'button.wav';
return null;
}
/* ===== BGM ===== */
var bgm = null;
function ensureBgm() {
if (!bgm) { bgm = new Audio(); bgm.loop = true; bgm.preload = 'auto'; bgm.setAttribute('playsinline', ''); }
return bgm;
}
function effBgmVol() {
var v = state.masterVol * state.musicVol;
if (Date.now() < state.duckUntil) v *= 0.22; /* หรี่ตอนมีคนพูด */
return Math.max(0, Math.min(1, v));
}
function applyBgmVol() { if (bgm) bgm.volume = state.musicOn ? effBgmVol() : 0; }
function playBgm(scene) {
if (!scene) return;
var man = MANIFEST[scene];
if (!man || !man.bgm) { state.curScene = scene; return; } /* ฉากไม่มี bgm (แต่จำ scene ไว้ให้ sfx) */
var a = ensureBgm();
var want = aurl(scene + '/bgm.mp3');
if (state.curScene !== scene || (a.src && a.src.indexOf(scene + '/bgm.mp3') < 0)) {
try { a.pause(); } catch (e) { /* ignore */ }
a.src = want;
try { a.currentTime = 0; } catch (e) { /* ignore */ }
}
state.curScene = scene;
applyBgmVol();
if (state.musicOn) { var p = a.play(); if (p && p.catch) p.catch(function () { /* รอ gesture ปลดล็อก */ }); }
}
function stopBgm() { if (bgm) { try { bgm.pause(); } catch (e) { /* ignore */ } } }
/* ===== SFX (pool กัน overlap) ===== */
var pool = [];
function grab() {
for (var i = 0; i < pool.length; i++) { if (pool[i].ended || pool[i].paused) return pool[i]; }
if (pool.length < 12) { var a = new Audio(); a.preload = 'auto'; a.setAttribute('playsinline', ''); pool.push(a); return a; }
return pool[0];
}
function playSfx(id, scene) {
if (!state.sfxOn || !id) return;
scene = scene || state.curScene || 'lobby';
var rel = resolveSfx(scene, id);
if (!rel) return;
try {
var a = grab();
a.src = aurl(rel);
a.volume = Math.max(0, Math.min(1, state.masterVol * state.sfxVol));
a.currentTime = 0;
var p = a.play(); if (p && p.catch) p.catch(function () { /* ignore */ });
} catch (e) { /* ignore */ }
}
/* ===== toggles ===== */
function setMusic(on) {
state.musicOn = !!on; writeBool(LS_MUSIC, state.musicOn);
if (!state.musicOn) { stopBgm(); }
else { applyBgmVol(); if (state.curScene) playBgm(state.curScene); }
return state.musicOn;
}
function setSfx(on) { state.sfxOn = !!on; writeBool(LS_SFX, state.sfxOn); return state.sfxOn; }
/* ===== duck (เสียงพูด) ===== */
function duck(ms) { state.duckUntil = Date.now() + (ms || 600); applyBgmVol(); }
function notifySpeaking() { duck(700); }
setInterval(applyBgmVol, 220); /* คลาย duck อัตโนมัติ */
/* ===== unlock audio ครั้งแรกที่ user gesture (autoplay policy) ===== */
function unlock() {
if (state.unlocked) return;
state.unlocked = true;
if (state.musicOn && state.curScene) playBgm(state.curScene);
}
['pointerdown', 'keydown', 'touchstart', 'click'].forEach(function (ev) {
window.addEventListener(ev, unlock, { once: false, passive: true });
});
/* ===== config จาก Admin (ไม่มีก็ใช้ default) ===== */
(function loadSettings() {
try {
fetch(scriptBase + '/api/sound-settings', { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d) return;
if (typeof d.masterVol === 'number') state.masterVol = Math.max(0, Math.min(1, d.masterVol));
if (typeof d.musicVol === 'number') state.musicVol = Math.max(0, Math.min(1, d.musicVol));
if (typeof d.sfxVol === 'number') state.sfxVol = Math.max(0, Math.min(1, d.sfxVol));
applyBgmVol();
}).catch(function () { /* ignore */ });
} catch (e) { /* ignore */ }
})();
window.jdSound = {
playBgm: playBgm,
stopBgm: stopBgm,
playSfx: playSfx,
setMusic: setMusic,
setSfx: setSfx,
isMusicOn: function () { return state.musicOn; },
isSfxOn: function () { return state.sfxOn; },
notifySpeaking: notifySpeaking,
duck: duck,
_state: state
};
})();