mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-26 01:28:33 +01:00
Begin payment logic and tests
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 18 KiB |
@@ -29,6 +29,7 @@
|
||||
|
||||
<!-- Other modules -->
|
||||
<script src="js/ui/ui.js"></script>
|
||||
<script src="js/ui/audioAlerts.js"></script>
|
||||
<script src="js/network/socketHandlers.js"></script>
|
||||
<script src="js/input/inputHandler.js"></script>
|
||||
</head>
|
||||
@@ -51,6 +52,11 @@
|
||||
<button id="animationToggleButton" style="background: #5c5c0a; color: #ff0; border: 2px solid #aa0; padding: 6px 12px; font-weight: bold; cursor: pointer; margin-top: 5px; margin-left: 10px; display: none;">
|
||||
🎬 DISABLE ANIMATION
|
||||
</button>
|
||||
<div style="margin-top:6px; font-size:11px; color:#ccc; display:flex; align-items:center; gap:4px;">
|
||||
<label style="cursor:pointer; display:flex; align-items:center; gap:4px;">
|
||||
<input type="checkbox" id="audioAlertsToggle" style="cursor:pointer;"> Audio Alerts
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment System UI -->
|
||||
@@ -160,6 +166,7 @@
|
||||
if (typeof SocketHandlers !== 'undefined') {
|
||||
console.log("Initializing SocketHandlers...");
|
||||
SocketHandlers.init();
|
||||
if (typeof AudioAlerts !== 'undefined') { AudioAlerts.init(); }
|
||||
} else {
|
||||
console.error("SocketHandlers object not found!");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ var Game = {
|
||||
_maxLogMessages: 5,
|
||||
_unconfirmedPayment: false,
|
||||
_unconfirmedPaymentInfo: null,
|
||||
_awaitingPayment: false,
|
||||
|
||||
init: function() {
|
||||
// Update screen dimensions from options
|
||||
@@ -178,7 +179,7 @@ var Game = {
|
||||
const baseY = Math.floor(ScreenManager._screenHeight / 2) + 5;
|
||||
ScreenManager.drawCenteredText(baseY, `Score: ${score}`);
|
||||
if (treasure) {
|
||||
ScreenManager.drawCenteredText(baseY + 1, 'You secured the treasure!');
|
||||
ScreenManager.drawCenteredText(baseY + 1, 'Treasure secured!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,11 +342,17 @@ var Game = {
|
||||
Game._pendingPaymentDetected = function(info) {
|
||||
this._unconfirmedPayment = true;
|
||||
this._unconfirmedPaymentInfo = info || {};
|
||||
this._awaitingPayment = false; // no longer just awaiting creation; we've seen a tx
|
||||
};
|
||||
|
||||
Game._pendingPaymentConfirmed = function() {
|
||||
this._unconfirmedPayment = false;
|
||||
this._unconfirmedPaymentInfo = null;
|
||||
this._awaitingPayment = false;
|
||||
};
|
||||
|
||||
Game._paymentRequested = function() {
|
||||
this._awaitingPayment = true;
|
||||
};
|
||||
|
||||
// Debug utility functions (keep as-is for compatibility)
|
||||
|
||||
@@ -202,26 +202,26 @@ var ScreenManager = {
|
||||
this.drawCenteredText(y, "CONGRATULATIONS!");
|
||||
|
||||
if (hasTreasure) {
|
||||
this.drawCenteredText(y + 2, "You escaped with the treasure!");
|
||||
this.drawCenteredText(y + 2, "You got the treasure!");
|
||||
} else {
|
||||
this.drawCenteredText(y + 2, "You escaped the dungeon!");
|
||||
this.drawCenteredText(y + 2, "You escaped alive!");
|
||||
}
|
||||
this.drawCenteredText(y + 3, "A true hero of Wownero!");
|
||||
this.drawCenteredText(y + 3, "Hero of Wownero!");
|
||||
},
|
||||
|
||||
drawLoseScreen: function(reason) {
|
||||
if (!DisplayManager.ensureDisplay()) return;
|
||||
DisplayManager.clearDisplay();
|
||||
|
||||
let y = Math.floor(this._screenHeight / 3);
|
||||
let y = Math.floor(this._screenHeight / 3);
|
||||
this.drawCenteredText(y, "YOU HAVE PERISHED");
|
||||
|
||||
if (reason === 'monster') {
|
||||
this.drawCenteredText(y + 2, "Slain by a fearsome beast.");
|
||||
this.drawCenteredText(y + 2, "Slain by a beast.");
|
||||
} else if (reason === 'timeout') {
|
||||
this.drawCenteredText(y + 2, "The dungeon claimed you before the next block.");
|
||||
this.drawCenteredText(y + 2, "Time ran out.");
|
||||
} else {
|
||||
this.drawCenteredText(y + 2, "Your adventure ends here.");
|
||||
this.drawCenteredText(y + 2, "Your adventure ends.");
|
||||
}
|
||||
this.drawCenteredText(y + 3, "Better luck next time.");
|
||||
},
|
||||
|
||||
@@ -76,7 +76,11 @@ var WaitingScreenAnimator = {
|
||||
|
||||
// Fixed-position animated text
|
||||
let y = Math.floor(screenHeight / 2) - 8;
|
||||
const baseText = "Awaiting next block";
|
||||
let baseText = "Awaiting next block";
|
||||
if (typeof Game !== 'undefined') {
|
||||
if (Game._awaitingPayment) baseText = 'Awaiting payment';
|
||||
else if (Game._unconfirmedPayment) baseText = 'Awaiting next block'; // mempool seen
|
||||
}
|
||||
const dots = ".".repeat((Math.floor(Date.now() / 500) % 4));
|
||||
const paddedDots = dots.padEnd(3, " ");
|
||||
drawCenteredTextFn(y, `${baseText}${paddedDots}`);
|
||||
@@ -364,15 +368,53 @@ var WaitingScreenAnimator = {
|
||||
},
|
||||
|
||||
resetAnimation: function() {
|
||||
// Track cycle count for variation
|
||||
if (typeof this._cycleCount === 'undefined') this._cycleCount = 0;
|
||||
this._cycleCount++;
|
||||
|
||||
// Random variant selection after first loop
|
||||
const variant = this._cycleCount === 1 ? 'classic' : (Math.random() < 0.33 ? 'reverse' : (Math.random() < 0.5 ? 'race' : 'ambush'));
|
||||
const basePlayerX = 8;
|
||||
const baseMonsterX = 5;
|
||||
let playerStart = basePlayerX;
|
||||
let monsterStart = baseMonsterX;
|
||||
let treasureX = 35;
|
||||
let initialPhase = 'enter';
|
||||
|
||||
switch(variant) {
|
||||
case 'reverse':
|
||||
// Swap sides: player closer to treasure, monster further back
|
||||
playerStart = 25 + Math.floor(Math.random()*4);
|
||||
monsterStart = playerStart - 10;
|
||||
treasureX = playerStart + 5 + Math.floor(Math.random()*3);
|
||||
break;
|
||||
case 'race':
|
||||
// Both start near entrance racing for treasure
|
||||
playerStart = 6;
|
||||
monsterStart = 4;
|
||||
treasureX = 40 + Math.floor(Math.random()*3);
|
||||
break;
|
||||
case 'ambush':
|
||||
// Monster waits near treasure
|
||||
playerStart = 8;
|
||||
monsterStart = 30 + Math.floor(Math.random()*3);
|
||||
treasureX = monsterStart + 3;
|
||||
initialPhase = 'chase'; // jump into chase faster
|
||||
break;
|
||||
default:
|
||||
// classic
|
||||
break;
|
||||
}
|
||||
|
||||
this._waitingAnimation = {
|
||||
frame: 0,
|
||||
playerX: 8, // Start near entrance
|
||||
playerX: playerStart, // variant start
|
||||
playerY: 12,
|
||||
monsterX: 5, // Start at entrance
|
||||
monsterX: monsterStart, // variant start
|
||||
monsterY: 12,
|
||||
treasureX: 35, // Treasure near exit
|
||||
treasureX: treasureX, // variant treasure
|
||||
treasureY: 12,
|
||||
phase: 'enter',
|
||||
phase: initialPhase,
|
||||
frameCount: 0,
|
||||
direction: 1
|
||||
};
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
*/
|
||||
const SocketHandlers = {
|
||||
_initialized: false,
|
||||
_didConnect: false,
|
||||
_setBannerStatus: function(text, color) {
|
||||
const el = $('#statusValue');
|
||||
if (!el.length) return;
|
||||
el.text(text);
|
||||
if (color) el.css('color', color);
|
||||
},
|
||||
|
||||
init: function() {
|
||||
if (this._initialized) {
|
||||
@@ -16,6 +23,11 @@ const SocketHandlers = {
|
||||
|
||||
this.registerEventHandlers();
|
||||
this._initialized = true;
|
||||
|
||||
// If the low-level socket connected before handlers were registered, emulate onConnect.
|
||||
if (window.socket && window.socket.connected) {
|
||||
this.onConnect();
|
||||
}
|
||||
},
|
||||
|
||||
registerEventHandlers: function() {
|
||||
@@ -39,17 +51,21 @@ const SocketHandlers = {
|
||||
socket.on('address_confirmed', this.onAddressConfirmed);
|
||||
socket.on('payment_created', this.onPaymentCreated);
|
||||
socket.on('payment_confirmed', this.onPaymentConfirmed);
|
||||
socket.on('payment_detected', this.onPaymentDetected);
|
||||
socket.on('payment_detected', this.onPaymentDetected);
|
||||
|
||||
// Block height handler
|
||||
socket.on('blockheight', this.onBlockHeight);
|
||||
},
|
||||
|
||||
onConnect: function() {
|
||||
if (SocketHandlers._didConnect) return; // prevent duplicate registration emission
|
||||
SocketHandlers._didConnect = true;
|
||||
socket.emit('register_client', {
|
||||
clientId: socket.id,
|
||||
userAgent: navigator.userAgent
|
||||
});
|
||||
// Update small status banner immediately
|
||||
SocketHandlers._setBannerStatus('Connected', '#0f0');
|
||||
},
|
||||
|
||||
onWelcome: function(msg) {
|
||||
@@ -71,6 +87,20 @@ const SocketHandlers = {
|
||||
|
||||
$('#messages').append($('<li class="' + statusClass + '" style="color: ' + statusColor + '; white-space: pre-line;">').text(data.message));
|
||||
UI.scrollChat();
|
||||
|
||||
// Mirror concise status in header banner for key lifecycle types
|
||||
if (data.type === 'connection') {
|
||||
SocketHandlers._setBannerStatus('Ready', '#0f0');
|
||||
} else if (data.type === 'queue') {
|
||||
SocketHandlers._setBannerStatus('Queued', '#0af');
|
||||
} else if (data.type === 'payment') {
|
||||
// Generic payment message (more granular handlers override later)
|
||||
SocketHandlers._setBannerStatus('Payment', '#0af');
|
||||
} else if (data.type === 'error') {
|
||||
SocketHandlers._setBannerStatus('Error', '#f00');
|
||||
} else if (data.type === 'success') {
|
||||
SocketHandlers._setBannerStatus('Success', '#0f0');
|
||||
}
|
||||
},
|
||||
|
||||
onChatBroadcast: function(data) {
|
||||
@@ -97,16 +127,25 @@ const SocketHandlers = {
|
||||
|
||||
onQueueJoined: function(data) {
|
||||
console.log('Queue joined:', data);
|
||||
|
||||
const currentBlockFallback = (typeof data.currentBlock === 'number' && data.currentBlock > 0)
|
||||
? data.currentBlock
|
||||
: (typeof UI !== 'undefined' && typeof UI._currentBlockHeight === 'number' && UI._currentBlockHeight > 0
|
||||
? UI._currentBlockHeight
|
||||
: '?');
|
||||
const nextBlockFallback = (typeof data.nextBlock === 'number' && data.nextBlock > 0)
|
||||
? data.nextBlock
|
||||
: (currentBlockFallback === '?' ? '?' : currentBlockFallback + 1);
|
||||
|
||||
$('#messages').append($('<li class="queue-info" style="color:#0f0;">').html(
|
||||
'�� <strong>Queue Joined!</strong> Position: ' + data.position + '<br>' +
|
||||
'📦 Current block: ' + data.currentBlock + ', Next: ' + data.nextBlock
|
||||
'⏳ <strong>Queue Joined!</strong> Position: ' + data.position + '<br>' +
|
||||
'📦 Current block: ' + currentBlockFallback + ', Next: ' + nextBlockFallback
|
||||
));
|
||||
UI.scrollChat();
|
||||
|
||||
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
|
||||
Game.drawWaitingScreen();
|
||||
}
|
||||
SocketHandlers._setBannerStatus('Queued', '#0af');
|
||||
},
|
||||
|
||||
onGameStart: function(data) {
|
||||
@@ -140,6 +179,7 @@ const SocketHandlers = {
|
||||
}
|
||||
|
||||
UI.scrollChat();
|
||||
SocketHandlers._setBannerStatus('In Game', '#0f0');
|
||||
},
|
||||
|
||||
onGameUpdate: function(data) {
|
||||
@@ -170,6 +210,12 @@ const SocketHandlers = {
|
||||
}
|
||||
|
||||
UI.scrollChat();
|
||||
const won = data && (data.status === 'won' || data.reason === 'escaped');
|
||||
if (won) {
|
||||
SocketHandlers._setBannerStatus('Won', '#0f0');
|
||||
} else {
|
||||
SocketHandlers._setBannerStatus('Lost', '#f00');
|
||||
}
|
||||
},
|
||||
|
||||
onQueueCancelled: function(data) {
|
||||
@@ -180,6 +226,7 @@ const SocketHandlers = {
|
||||
}
|
||||
|
||||
UI.scrollChat();
|
||||
SocketHandlers._setBannerStatus('Ready', '#0f0');
|
||||
},
|
||||
|
||||
onAddressDetected: function(data) {
|
||||
@@ -196,49 +243,167 @@ const SocketHandlers = {
|
||||
|
||||
onPaymentCreated: function(data) {
|
||||
console.log('Payment created:', data);
|
||||
|
||||
$('#messages').append($('<li class="payment-info">').html(
|
||||
'💳 <strong>Payment Required:</strong> ' + data.humanAmount + ' ' + data.cryptoType
|
||||
));
|
||||
const parts = [];
|
||||
parts.push('💳 <strong>Payment Required</strong>');
|
||||
parts.push('Amount: ' + data.humanAmount + ' ' + data.cryptoType);
|
||||
const shortAddr = data.address.substring(0, 10) + '…' + data.address.slice(-6);
|
||||
parts.push('Address: <span class="pay-address-full" style="cursor:pointer;" title="Click to toggle full address">' + shortAddr + '</span>' +
|
||||
' <button class="copy-pay-address" data-address="' + data.address + '" style="margin-left:4px;padding:1px 4px;font-size:11px;cursor:pointer;">Copy</button>');
|
||||
// (Removed inline QR image to avoid duplication; dedicated pinned sidebar QR is used instead.)
|
||||
const $li = $('<li class="payment-info" style="white-space:normal;">').html(parts.join('<br>'));
|
||||
$('#messages').append($li);
|
||||
|
||||
// Attach copy handler (delegated inside this LI to avoid duplicates)
|
||||
const fullAddress = data.address;
|
||||
$li.on('click', '.copy-pay-address', function(e) {
|
||||
e.preventDefault();
|
||||
const addr = $(this).data('address');
|
||||
const doCopy = async () => {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(addr);
|
||||
} else {
|
||||
// Fallback
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = addr; document.body.appendChild(ta); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
}
|
||||
$(this).text('Copied!');
|
||||
setTimeout(() => { $(this).text('Copy'); }, 2000);
|
||||
} catch (err) {
|
||||
console.error('Copy failed', err);
|
||||
$(this).text('Error');
|
||||
setTimeout(() => { $(this).text('Copy'); }, 2500);
|
||||
}
|
||||
};
|
||||
doCopy();
|
||||
});
|
||||
|
||||
// Toggle short/full address on span click
|
||||
let showingFull = false;
|
||||
$li.on('click', '.pay-address-full', function() {
|
||||
showingFull = !showingFull;
|
||||
$(this).text(showingFull ? fullAddress : shortAddr);
|
||||
});
|
||||
UI.scrollChat();
|
||||
|
||||
setTimeout(function() {
|
||||
$('#chatInput').focus();
|
||||
}, 100);
|
||||
SocketHandlers._setBannerStatus('Pay', '#0af');
|
||||
if (typeof Game !== 'undefined' && Game._paymentRequested) Game._paymentRequested();
|
||||
|
||||
// Sidebar QR (persistent) - create/update separate from chat scroll
|
||||
if (data.qr) {
|
||||
let qrHolder = document.getElementById('paymentQRContainer');
|
||||
if (!qrHolder) {
|
||||
const statusDiv = document.querySelector('.status');
|
||||
if (statusDiv) {
|
||||
qrHolder = document.createElement('div');
|
||||
qrHolder.id = 'paymentQRContainer';
|
||||
qrHolder.style.cssText = 'margin-top:6px;padding:4px;border:1px solid #0f0;background:#000;display:inline-block;width:150px;text-align:center;';
|
||||
statusDiv.appendChild(qrHolder);
|
||||
}
|
||||
}
|
||||
if (qrHolder) {
|
||||
qrHolder.innerHTML = '<img style="image-rendering:pixelated;width:140px;height:140px;display:block;margin:0 auto;" src="' + data.qr + '" alt="Payment QR" />';
|
||||
}
|
||||
} else {
|
||||
console.warn('Payment created but no QR data supplied by server.');
|
||||
let qrHolder = document.getElementById('paymentQRContainer');
|
||||
if (!qrHolder) {
|
||||
const statusDiv = document.querySelector('.status');
|
||||
if (statusDiv) {
|
||||
qrHolder = document.createElement('div');
|
||||
qrHolder.id = 'paymentQRContainer';
|
||||
qrHolder.style.cssText = 'margin-top:6px;padding:4px;border:1px solid #f80;background:#000;display:inline-block;color:#f80;font-size:11px;max-width:150px;';
|
||||
statusDiv.appendChild(qrHolder);
|
||||
}
|
||||
}
|
||||
if (qrHolder) {
|
||||
qrHolder.textContent = 'QR unavailable';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_lastDisplayedConfirmation: null,
|
||||
_confirmationTimestamps: {}, // paymentId -> ts
|
||||
onPaymentConfirmed: function(data) {
|
||||
console.log('Payment confirmed:', data);
|
||||
|
||||
console.log('Payment confirmed in block:', data);
|
||||
if (!data || !data.paymentId) return; // ignore malformed legacy event
|
||||
// Expire old confirmations (6h default) to prevent unbounded memory
|
||||
SocketHandlers._expireOldClientPaymentMarkers();
|
||||
if (SocketHandlers._lastDisplayedConfirmation === data.paymentId) return;
|
||||
SocketHandlers._lastDisplayedConfirmation = data.paymentId;
|
||||
SocketHandlers._confirmationTimestamps[data.paymentId] = Date.now();
|
||||
|
||||
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
|
||||
Game.drawWaitingScreen();
|
||||
}
|
||||
if (typeof Game !== 'undefined') {
|
||||
Game._pendingPaymentConfirmed();
|
||||
}
|
||||
|
||||
$('#messages').append($('<li class="payment-success">').html(
|
||||
'✅ <strong>Payment confirmed!</strong> You are in the game queue.'
|
||||
|
||||
$('#messages').append($('<li class="payment-success" style="color:#0f0;">').html(
|
||||
'✅ <strong>Payment confirmed in block.</strong> You are in the game queue.'
|
||||
));
|
||||
UI.scrollChat();
|
||||
SocketHandlers._setBannerStatus('Confirmed', '#0f0');
|
||||
},
|
||||
|
||||
_mempoolShownForPayment: new Set(),
|
||||
_mempoolTimestamps: {}, // paymentId -> ts
|
||||
onPaymentDetected: function(data) {
|
||||
console.log('Payment detected (mempool):', data);
|
||||
if (!data || !data.paymentId) return; // require paymentId for dedupe
|
||||
SocketHandlers._expireOldClientPaymentMarkers();
|
||||
if (SocketHandlers._mempoolShownForPayment.has(data.paymentId)) return;
|
||||
SocketHandlers._mempoolShownForPayment.add(data.paymentId);
|
||||
SocketHandlers._mempoolTimestamps[data.paymentId] = Date.now();
|
||||
if (typeof Game !== 'undefined') {
|
||||
Game._pendingPaymentDetected(data);
|
||||
if (Game.drawWaitingScreen) Game.drawWaitingScreen();
|
||||
}
|
||||
$('#messages').append($('<li class="payment-mempool" style="color:#0af;">').html(
|
||||
'🌀 <strong>Payment seen in mempool</strong> – awaiting block confirmation...'
|
||||
'🌀 <strong>Payment detected (mempool)</strong> – awaiting block confirmation...'
|
||||
));
|
||||
UI.scrollChat();
|
||||
SocketHandlers._setBannerStatus('Mempool', '#0af');
|
||||
},
|
||||
|
||||
// Internal: periodic cleanup of client-side payment marker sets (invoked opportunistically)
|
||||
_expireOldClientPaymentMarkers: function() {
|
||||
const TTL = 6 * 60 * 60 * 1000; // 6 hours
|
||||
const now = Date.now();
|
||||
// Mempool markers
|
||||
for (const pid of Array.from(SocketHandlers._mempoolShownForPayment)) {
|
||||
const ts = SocketHandlers._mempoolTimestamps[pid];
|
||||
if (!ts || now - ts > TTL) {
|
||||
SocketHandlers._mempoolShownForPayment.delete(pid);
|
||||
delete SocketHandlers._mempoolTimestamps[pid];
|
||||
}
|
||||
}
|
||||
// Confirmation marker (single id tracking + map of timestamps)
|
||||
if (SocketHandlers._lastDisplayedConfirmation) {
|
||||
const lastId = SocketHandlers._lastDisplayedConfirmation;
|
||||
const ts = SocketHandlers._confirmationTimestamps[lastId];
|
||||
if (ts && now - ts > TTL) {
|
||||
delete SocketHandlers._confirmationTimestamps[lastId];
|
||||
// Allow new confirmation message for same id after TTL (rare, but cleans memory)
|
||||
SocketHandlers._lastDisplayedConfirmation = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onBlockHeight: function(data) {
|
||||
if (typeof UI !== 'undefined' && UI.updateBlockHeight) {
|
||||
UI.updateBlockHeight(data.blockHeight);
|
||||
}
|
||||
// Fallback: if status banner still says Connecting..., upgrade it
|
||||
const current = $('#statusValue').text();
|
||||
if (current === 'Connecting...') {
|
||||
SocketHandlers._setBannerStatus('Ready', '#0f0');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* AudioAlerts: optional user-enabled audio notifications for key events
|
||||
* - Payment confirmed (game about to start soon / queued)
|
||||
* - Game start (focus attention)
|
||||
* Persists preference in localStorage and uses Web Audio or HTMLAudio fallback.
|
||||
*/
|
||||
const AudioAlerts = {
|
||||
_enabled: false,
|
||||
_initialized: false,
|
||||
_ctx: null,
|
||||
_gain: null,
|
||||
_volume: 0.4,
|
||||
_lastPlay: 0,
|
||||
_cooldownMs: 1500,
|
||||
|
||||
init() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
// Load preference
|
||||
try {
|
||||
const saved = localStorage.getItem('wow_audioAlerts');
|
||||
this._enabled = saved === '1';
|
||||
} catch(_) {}
|
||||
const toggle = document.getElementById('audioAlertsToggle');
|
||||
if (toggle) {
|
||||
toggle.checked = this._enabled;
|
||||
toggle.addEventListener('change', () => {
|
||||
this._enabled = !!toggle.checked;
|
||||
try { localStorage.setItem('wow_audioAlerts', this._enabled ? '1':'0'); } catch(_) {}
|
||||
if (this._enabled) this._lazyInitContext();
|
||||
});
|
||||
}
|
||||
|
||||
// Hook into socket events (if SocketHandlers already patched later)
|
||||
if (typeof SocketHandlers !== 'undefined') {
|
||||
this._patchSocketHandlers();
|
||||
}
|
||||
},
|
||||
|
||||
_patchSocketHandlers() {
|
||||
if (SocketHandlers._audioAlertsPatched) return; // idempotent
|
||||
SocketHandlers._audioAlertsPatched = true;
|
||||
const origPaymentConfirmed = SocketHandlers.onPaymentConfirmed;
|
||||
const origGameStart = SocketHandlers.onGameStart;
|
||||
SocketHandlers.onPaymentConfirmed = function(data) {
|
||||
if (origPaymentConfirmed) origPaymentConfirmed.call(SocketHandlers, data);
|
||||
AudioAlerts.playPattern('payment');
|
||||
};
|
||||
SocketHandlers.onGameStart = function(data) {
|
||||
if (origGameStart) origGameStart.call(SocketHandlers, data);
|
||||
AudioAlerts.playPattern('start');
|
||||
};
|
||||
},
|
||||
|
||||
_lazyInitContext() {
|
||||
if (this._ctx) return;
|
||||
try {
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioCtx) return;
|
||||
this._ctx = new AudioCtx();
|
||||
this._gain = this._ctx.createGain();
|
||||
this._gain.gain.value = this._volume;
|
||||
this._gain.connect(this._ctx.destination);
|
||||
} catch (e) {
|
||||
console.warn('Audio context init failed:', e);
|
||||
}
|
||||
},
|
||||
|
||||
playPattern(type) {
|
||||
if (!this._enabled) return;
|
||||
const now = Date.now();
|
||||
if (now - this._lastPlay < this._cooldownMs) return; // basic cooldown
|
||||
this._lastPlay = now;
|
||||
this._lazyInitContext();
|
||||
if (!this._ctx) return;
|
||||
|
||||
if (this._ctx.state === 'suspended') {
|
||||
// Attempt resume on user gesture contexts; safe to call
|
||||
this._ctx.resume().catch(()=>{});
|
||||
}
|
||||
|
||||
// Simple beep patterns
|
||||
const pattern = type === 'start' ? [440, 660, 880] : [440, 660];
|
||||
const dur = 0.18;
|
||||
let t = this._ctx.currentTime + 0.01;
|
||||
pattern.forEach((freq) => {
|
||||
const osc = this._ctx.createOscillator();
|
||||
const gainNode = this._ctx.createGain();
|
||||
osc.frequency.value = freq;
|
||||
osc.type = 'sine';
|
||||
gainNode.gain.setValueAtTime(0.0001, t);
|
||||
gainNode.gain.linearRampToValueAtTime(this._volume, t + 0.01);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||
osc.connect(gainNode).connect(this._gain || this._ctx.destination);
|
||||
osc.start(t);
|
||||
osc.stop(t + dur + 0.02);
|
||||
t += dur * 0.5; // slight overlap for chord-like effect
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = AudioAlerts;
|
||||
}
|
||||
Reference in New Issue
Block a user