Add certificate template, shield logo, audio tracks, and enhance game UI/UX
This commit is contained in:
+259
@@ -25,6 +25,22 @@ class SoundEngine {
|
||||
this.activeGrinderNode = null;
|
||||
this.activeLiquidNode = null;
|
||||
|
||||
// Dedicated Countdown Tick Sound Clip (User uploaded custom audio)
|
||||
try {
|
||||
this.countdownTickAudio = new Audio('audio/countdown_tick.mp3');
|
||||
this.countdownTickAudio.preload = 'auto';
|
||||
} catch (e) {
|
||||
this.countdownTickAudio = null;
|
||||
}
|
||||
|
||||
// Dedicated Top Timer Last 10-Second Warning Sound Clip (User uploaded custom audio)
|
||||
try {
|
||||
this.timerLast10sAudio = new Audio('audio/timer_last_10s.wav');
|
||||
this.timerLast10sAudio.preload = 'auto';
|
||||
} catch (e) {
|
||||
this.timerLast10sAudio = null;
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
@@ -91,6 +107,12 @@ class SoundEngine {
|
||||
if (this.masterGain && this.ctx) {
|
||||
this.masterGain.gain.setTargetAtTime(this.isMuted ? 0 : 1.0, this.ctx.currentTime, 0.05);
|
||||
}
|
||||
if (this.countdownTickAudio) {
|
||||
this.countdownTickAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
if (this.timerLast10sAudio) {
|
||||
this.timerLast10sAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
}
|
||||
|
||||
setMusicVolume(val) {
|
||||
@@ -105,6 +127,94 @@ class SoundEngine {
|
||||
if (this.sfxGain && this.ctx) {
|
||||
this.sfxGain.gain.setTargetAtTime(this.sfxVolume, this.ctx.currentTime, 0.05);
|
||||
}
|
||||
if (this.countdownTickAudio) {
|
||||
this.countdownTickAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
if (this.timerLast10sAudio) {
|
||||
this.timerLast10sAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
}
|
||||
|
||||
playCountdownTickAudio() {
|
||||
this.ensureContext();
|
||||
if (this.isMuted) return;
|
||||
|
||||
if (this.countdownTickAudio) {
|
||||
try {
|
||||
const sound = this.countdownTickAudio.cloneNode();
|
||||
sound.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
sound.play().catch(err => {
|
||||
console.warn("Countdown tick audio playback note:", err);
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
this.countdownTickAudio.currentTime = 0;
|
||||
this.countdownTickAudio.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
this.countdownTickAudio.play().catch(() => {});
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal Clock Tick for the first 20 seconds (30s down to 11s)
|
||||
*/
|
||||
playTimerTick() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
// Crisp high-tech clock tick matching 1st beat
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(1100, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(320, t + 0.035);
|
||||
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1200, t);
|
||||
filter.Q.setValueAtTime(3.5, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.045);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated Last 10 Seconds Audio Track (User uploaded custom audio)
|
||||
*/
|
||||
playTimerLast10sAudio() {
|
||||
this.ensureContext();
|
||||
if (this.isMuted) return;
|
||||
|
||||
if (this.timerLast10sAudio) {
|
||||
try {
|
||||
this.timerLast10sAudio.currentTime = 0;
|
||||
this.timerLast10sAudio.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
this.timerLast10sAudio.play().catch(err => {
|
||||
console.warn("Timer last 10s audio playback note:", err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Error playing timer last 10s audio:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopTimerLast10sAudio() {
|
||||
if (this.timerLast10sAudio) {
|
||||
try {
|
||||
this.timerLast10sAudio.pause();
|
||||
this.timerLast10sAudio.currentTime = 0;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
setAmbientEnabled(enabled) {
|
||||
@@ -520,6 +630,155 @@ class SoundEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer Warning Tick & Urgent Countdown Beep SFX
|
||||
*/
|
||||
playTimerWarningTick(isUrgent = false) {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
if (isUrgent) {
|
||||
// Rapid, tense dual-tone hazard emergency beep
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(880, t);
|
||||
osc.frequency.setValueAtTime(700, t + 0.06);
|
||||
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1400, t);
|
||||
filter.Q.setValueAtTime(4.0, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.14);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.15);
|
||||
} else {
|
||||
// High-tech warning clock tick
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(560, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(420, t + 0.05);
|
||||
|
||||
gain.gain.setValueAtTime(0.12, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.09);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Industrial Emergency Hazard Alarm Sound (Time Expired / Hazard Warning)
|
||||
*/
|
||||
playTimerHazardAlarm() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
// Industrial emergency hazard klaxon / buzzer burst
|
||||
[0, 0.18, 0.36].forEach(offset => {
|
||||
const time = t + offset;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(620, time);
|
||||
osc.frequency.linearRampToValueAtTime(380, time + 0.14);
|
||||
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(2000, time);
|
||||
|
||||
gain.gain.setValueAtTime(0.26, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Screen Start Countdown Beep (3, 2, 1)
|
||||
*/
|
||||
playCountdownBeep(num) {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const osc2 = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
// Pitch rises: 3 (520Hz) -> 2 (680Hz) -> 1 (880Hz)
|
||||
const baseFreq = num === 3 ? 520 : (num === 2 ? 680 : 880);
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(baseFreq, t);
|
||||
|
||||
osc2.type = 'sine';
|
||||
osc2.frequency.setValueAtTime(baseFreq * 2, t);
|
||||
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(2400, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.24, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.28);
|
||||
|
||||
osc.connect(filter);
|
||||
osc2.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc2.start(t);
|
||||
osc.stop(t + 0.3);
|
||||
osc2.stop(t + 0.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Screen Countdown Launch / GO! Chime
|
||||
*/
|
||||
playCountdownGo() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
// Energetic ascending triad (C5 -> E5 -> G5)
|
||||
[523.25, 659.25, 783.99, 1046.50].forEach((freq, idx) => {
|
||||
const time = t + (idx * 0.05);
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, time);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.45);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
playCriticalStopAlert() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
Reference in New Issue
Block a user