Begin payment logic and tests

This commit is contained in:
jwinterm
2025-09-27 08:45:22 -04:00
parent 4ad063f735
commit e3915dcde2
29 changed files with 564835 additions and 853 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+7
View File
@@ -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!");
}
+8 -1
View File
@@ -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)
+7 -7
View File
@@ -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.");
},
+47 -5
View File
@@ -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
};
+179 -14
View File
@@ -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');
}
}
};
+104
View File
@@ -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;
}