mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-28 02:28:34 +01:00
launch features: win feed, public stats, share-a-win, live CTA
Four pre-launch additions to make the game feel alive and shareable:
- Live win feed: server broadcasts 'win_feed' on every escape; client shows a
transient toast ('X escaped with the bag') so the room feels active.
- Public stats: GET /api/stats (10s-cached) returns games + escapes in the last
24h and total paid out; a status-area strip polls it for social proof.
- Share-a-win: game-over screen gets 'Share to X' + 'Copy link' buttons pointing
at the /verify page, which now renders OpenGraph/Twitter-card meta (brand OG
image per instance) so shared links unfurl with a preview.
- Land-on-action: a dismissible 'N games live now — watch' CTA appears on connect
when games are in progress, opening the existing spectator panel. Non-intrusive:
hidden while playing/spectating and never hijacks the canvas.
This commit is contained in:
@@ -80,6 +80,7 @@
|
||||
<script src="js/ui/transactionHistory.js"></script>
|
||||
<script src="js/ui/leaderboard.js"></script>
|
||||
<script src="js/ui/helpModal.js"></script>
|
||||
<script src="js/ui/publicStats.js"></script>
|
||||
<script src="js/network/socketHandlers.js"></script>
|
||||
<script src="js/network/smirkAuth.js"></script>
|
||||
<script src="js/input/inputHandler.js"></script>
|
||||
@@ -98,6 +99,8 @@
|
||||
<div class="status">
|
||||
<div id="blockHeight">Block Height: <span id="heightValue">Loading...</span></div>
|
||||
<div id="connectionStatus">Status: <span id="statusValue">Connecting...</span></div>
|
||||
<div id="publicStats" style="display:none; margin-top:4px; font-size:11px; color:#9cf; font-family:monospace; line-height:1.4;"></div>
|
||||
<div id="liveCta" style="display:none; margin-top:6px; cursor:pointer; background:rgba(80,0,0,0.55); border:1px solid #f33; border-radius:4px; padding:5px 10px; font-size:12px; font-family:monospace; color:#ff8a8a; font-weight:bold;" title="Watch a live game"></div>
|
||||
<div id="creditsDisplay" style="display:none; margin-top:4px; font-size:13px; font-family:monospace; color:#0f0; background:#001a00; padding:4px 8px; border:1px solid #0a0; border-radius:4px; font-weight:bold;"></div>
|
||||
<button id="startButton" style="background: #0a5c0a; color: #ff0; border: 2px solid #0f0; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-top: 5px; display: none;">
|
||||
🎮 <span class="lbl-full">START GAME</span><span class="lbl-short">START</span>
|
||||
|
||||
@@ -158,6 +158,35 @@ const SocketHandlers = {
|
||||
socket.on('spectate_start', this.onSpectateStart);
|
||||
socket.on('spectator_update', this.onSpectatorUpdate);
|
||||
socket.on('spectate_ended', this.onSpectateEnded);
|
||||
|
||||
// Global win feed (someone escaped) -> floating toast
|
||||
socket.on('win_feed', this.onWinFeed);
|
||||
},
|
||||
|
||||
// Show a transient "someone just escaped" toast so the room feels alive. Toasts stack in a
|
||||
// fixed container (top-right), auto-dismiss, and never block the UI.
|
||||
onWinFeed: function(data) {
|
||||
if (!data) return;
|
||||
var name = String(data.name || 'Someone').slice(0, 24);
|
||||
var bag = data.treasure ? ' 💰 with the bag' : '';
|
||||
var paid = data.paid ? ' <span style="color:#fbbf24;">(payout!)</span>' : '';
|
||||
try {
|
||||
var $c = $('#win-feed');
|
||||
if (!$c.length) {
|
||||
$c = $('<div id="win-feed"></div>').appendTo('body');
|
||||
}
|
||||
var $t = $('<div class="win-toast"></div>').html(
|
||||
'🏆 <strong>' + escapeHtml(name) + '</strong> escaped' + bag + paid
|
||||
);
|
||||
$c.append($t);
|
||||
// Fade in, hold, fade out, remove.
|
||||
requestAnimationFrame(function () { $t.addClass('show'); });
|
||||
setTimeout(function () { $t.removeClass('show'); }, 5000);
|
||||
setTimeout(function () { $t.remove(); }, 5600);
|
||||
// Cap the number of simultaneous toasts.
|
||||
var $all = $c.children('.win-toast');
|
||||
if ($all.length > 4) { $all.first().remove(); }
|
||||
} catch (e) { /* non-critical */ }
|
||||
},
|
||||
|
||||
onConnect: function() {
|
||||
@@ -523,6 +552,12 @@ const SocketHandlers = {
|
||||
));
|
||||
}
|
||||
|
||||
// Offer a one-tap "Share to X" on wins (brag mechanic). The verify link unfurls with a
|
||||
// social card (OG meta on the /verify page), so a shared tweet shows a preview.
|
||||
if (won) {
|
||||
SocketHandlers._appendShareWin(data);
|
||||
}
|
||||
|
||||
UI.scrollChat();
|
||||
if (won) {
|
||||
SocketHandlers._setBannerStatus('Won', '#0f0');
|
||||
@@ -531,6 +566,43 @@ const SocketHandlers = {
|
||||
}
|
||||
},
|
||||
|
||||
// Build an absolute verify URL for this game (used by the share button).
|
||||
_verifyUrlFor: function(data) {
|
||||
var path = (data && data.proof && (data.proof.verificationUrl ||
|
||||
(data.proof.gameId ? '/verify/' + data.proof.gameId : null)));
|
||||
if (!path) return null;
|
||||
try { return new URL(path, window.location.origin).href; }
|
||||
catch (e) { return window.location.origin + path; }
|
||||
},
|
||||
|
||||
_appendShareWin: function(data) {
|
||||
var url = SocketHandlers._verifyUrlFor(data);
|
||||
if (!url) return; // no proof -> nothing to verify/share
|
||||
|
||||
var bag = data && data.treasure ? ' with the bag 💰' : '';
|
||||
var score = (data && typeof data.score === 'number') ? (' (score ' + data.score + ')') : '';
|
||||
var text = '🏆 I escaped the dungeon' + bag + score + ' in this provably-fair crypto roguelike!';
|
||||
var intent = 'https://twitter.com/intent/tweet?text=' +
|
||||
encodeURIComponent(text) + '&url=' + encodeURIComponent(url);
|
||||
|
||||
var $row = $('<li class="share-win" style="margin-top:6px; padding:8px; background:rgba(0,40,0,0.4); border-radius:4px; display:flex; gap:8px; align-items:center; flex-wrap:wrap;">');
|
||||
$row.append($('<strong style="color:#4ade80;">').text('Brag about it:'));
|
||||
|
||||
var $x = $('<button type="button" class="share-x-btn" style="cursor:pointer; background:#000; color:#fff; border:1px solid #555; border-radius:4px; padding:4px 10px; font-size:12px;">𝕏 Share to X</button>');
|
||||
$x.on('click', function () { window.open(intent, '_blank', 'noopener'); });
|
||||
|
||||
var $copy = $('<button type="button" class="share-copy-btn" style="cursor:pointer; background:#053655; color:#0af; border:1px solid #0af; border-radius:4px; padding:4px 10px; font-size:12px;">🔗 Copy link</button>');
|
||||
$copy.on('click', function () {
|
||||
var done = function () { $copy.text('✅ Copied!'); setTimeout(function () { $copy.text('🔗 Copy link'); }, 1500); };
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt('Copy this link:', url); });
|
||||
} else { window.prompt('Copy this link:', url); }
|
||||
});
|
||||
|
||||
$row.append($x).append($copy);
|
||||
$('#messages').append($row);
|
||||
},
|
||||
|
||||
onQueueCancelled: function(data) {
|
||||
// Clear queued state
|
||||
SocketHandlers._isQueued = false;
|
||||
@@ -1097,6 +1169,37 @@ const SocketHandlers = {
|
||||
if (!data || !data.games) return;
|
||||
SocketHandlers._activeGames = data.games;
|
||||
SocketHandlers._updateGamesListPanel(data);
|
||||
SocketHandlers._updateLiveCta(data);
|
||||
},
|
||||
|
||||
// "Land on action": when live games are in progress, surface a prominent, dismissible CTA
|
||||
// in the status area so a new visitor immediately sees the room is alive and can jump into
|
||||
// spectating with one click. Deliberately NON-intrusive — it never hijacks the canvas or
|
||||
// interrupts someone who came to play (hidden while in a game / spectating, and dismissible).
|
||||
_liveCtaDismissed: false,
|
||||
_updateLiveCta: function(data) {
|
||||
var el = document.getElementById('liveCta');
|
||||
if (!el) return;
|
||||
var liveCount = (data.games || []).length;
|
||||
var inGame = (typeof Game !== 'undefined' && (Game._gameActive || Game._isSpectating)) || SocketHandlers._spectatorMode || SocketHandlers._isQueued;
|
||||
var panelOpen = $('#gamesListPanel').is(':visible');
|
||||
|
||||
if (liveCount > 0 && !inGame && !panelOpen && !SocketHandlers._liveCtaDismissed) {
|
||||
el.innerHTML = '🔴 ' + liveCount + ' game' + (liveCount === 1 ? '' : 's') +
|
||||
' live right now — <span style="text-decoration:underline;">watch »</span>';
|
||||
el.style.display = 'block';
|
||||
if (!el._bound) {
|
||||
el._bound = true;
|
||||
el.addEventListener('click', function () {
|
||||
SocketHandlers._liveCtaDismissed = true; // don't nag after they engage
|
||||
el.style.display = 'none';
|
||||
if (window.socket) socket.emit('get_active_games', { page: 1, pageSize: 20 });
|
||||
SocketHandlers._showGamesPanel();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
onSpectateStart: function(data) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Public stats strip — light "social proof" shown in the status area: games and escapes in
|
||||
* the last 24h, and total paid out (only where payouts are enabled). Polls /api/stats, which
|
||||
* is cached server-side (10s), so a 30s poll is cheap. Online-player count is intentionally
|
||||
* NOT shown here — it already updates live via the 'user_count' socket event.
|
||||
*/
|
||||
(function () {
|
||||
var EL_ID = 'publicStats';
|
||||
|
||||
function fmtAmount(n) {
|
||||
if (!isFinite(n)) return '0';
|
||||
// Trim to a tidy magnitude: whole numbers plain, otherwise up to 4 dp without trailing zeros.
|
||||
if (n >= 1000) return Math.round(n).toLocaleString();
|
||||
var s = (Math.round(n * 10000) / 10000).toString();
|
||||
return s;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
var el = document.getElementById(EL_ID);
|
||||
if (!el || !data) return;
|
||||
var parts = [];
|
||||
parts.push('🎮 ' + (data.gamesToday || 0) + ' games (24h)');
|
||||
parts.push('🏆 ' + (data.escapesToday || 0) + ' escapes');
|
||||
if (data.payoutsEnabled && data.totalPaidOut > 0) {
|
||||
parts.push('💰 ' + fmtAmount(data.totalPaidOut) + ' ' + (data.currencyLabel || '') + ' paid out');
|
||||
}
|
||||
el.innerHTML = parts.join(' · ');
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function poll() {
|
||||
fetch('/api/stats')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(render)
|
||||
.catch(function () { /* ignore; transient */ });
|
||||
}
|
||||
|
||||
function start() {
|
||||
poll();
|
||||
setInterval(poll, 30000);
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') start();
|
||||
else document.addEventListener('DOMContentLoaded', start);
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -314,6 +314,37 @@ li.debug {
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================================
|
||||
Win feed toasts (global "someone escaped" announcements)
|
||||
========================================================================= */
|
||||
#win-feed {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
right: 12px;
|
||||
z-index: 1800;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
max-width: min(320px, 80vw);
|
||||
}
|
||||
.win-toast {
|
||||
background: rgba(0, 30, 0, 0.92);
|
||||
border: 1px solid #0f0;
|
||||
border-left: 4px solid #4ade80;
|
||||
border-radius: 6px;
|
||||
color: #cffccf;
|
||||
font-family: 'PixelOperator', monospace;
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 0 14px rgba(0, 255, 0, 0.18);
|
||||
opacity: 0;
|
||||
transform: translateX(40px);
|
||||
transition: opacity 0.35s ease, transform 0.35s ease;
|
||||
}
|
||||
.win-toast.show { opacity: 1; transform: translateX(0); }
|
||||
.win-toast strong { color: #4ade80; }
|
||||
|
||||
/* =========================================================================
|
||||
Mobile / touch support
|
||||
========================================================================= */
|
||||
|
||||
Reference in New Issue
Block a user