Fully containerized
This commit is contained in:
+89
-17
@@ -24,9 +24,16 @@ let widowSelected = [];
|
||||
let currentBidAmount = 85;
|
||||
let lastBidHandNumber = -1;
|
||||
|
||||
// Turn reminder state
|
||||
let turnReminderTimer = null;
|
||||
let turnReminderActive = false;
|
||||
// Turn reminder / escalation state
|
||||
let turnReminderTimer = null;
|
||||
let turnReminderActive = false;
|
||||
let turnEscalationLevel = 0;
|
||||
|
||||
// AFK state
|
||||
let afkBannerVoted = false;
|
||||
|
||||
// AI control state (which seats are AI-controlled)
|
||||
let aiControlledSeats = new Set();
|
||||
|
||||
// Unlock Web Audio on first user gesture (required by iOS)
|
||||
let _audioCtx = null;
|
||||
@@ -57,23 +64,36 @@ function playTurnChime() {
|
||||
} catch (e) { /* audio not available */ }
|
||||
}
|
||||
|
||||
function applyTurnEscalation(level) {
|
||||
const pm = $('phase-msg');
|
||||
if (!pm) return;
|
||||
pm.classList.remove('your-turn-lvl1', 'your-turn-lvl2', 'your-turn-lvl3');
|
||||
if (level > 0) pm.classList.add(`your-turn-lvl${level}`);
|
||||
}
|
||||
|
||||
function handleTurnReminder(isMyTurn) {
|
||||
if (isMyTurn && !turnReminderActive) {
|
||||
turnReminderActive = true;
|
||||
turnReminderActive = true;
|
||||
turnEscalationLevel = 0;
|
||||
$('my-area')?.classList.remove('turn-urgent');
|
||||
turnReminderTimer = setTimeout(() => {
|
||||
$('my-area')?.classList.add('turn-urgent');
|
||||
// Vibrate on Android; chime on iOS (vibrate not supported there)
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([300, 100, 300]);
|
||||
} else {
|
||||
playTurnChime();
|
||||
applyTurnEscalation(0);
|
||||
// Escalate every 5 seconds: level 1 → 2 → 3 (vibrate/chime at level 3)
|
||||
turnReminderTimer = setInterval(() => {
|
||||
turnEscalationLevel = Math.min(turnEscalationLevel + 1, 3);
|
||||
applyTurnEscalation(turnEscalationLevel);
|
||||
if (turnEscalationLevel >= 3) {
|
||||
$('my-area')?.classList.add('turn-urgent');
|
||||
if (navigator.vibrate) navigator.vibrate([300, 100, 300]);
|
||||
else playTurnChime();
|
||||
clearInterval(turnReminderTimer);
|
||||
turnReminderTimer = null;
|
||||
}
|
||||
}, 5000);
|
||||
} else if (!isMyTurn && (turnReminderActive || turnReminderTimer)) {
|
||||
clearTimeout(turnReminderTimer);
|
||||
turnReminderTimer = null;
|
||||
turnReminderActive = false;
|
||||
} else if (!isMyTurn && turnReminderActive) {
|
||||
if (turnReminderTimer) { clearInterval(turnReminderTimer); turnReminderTimer = null; }
|
||||
turnReminderActive = false;
|
||||
turnEscalationLevel = 0;
|
||||
applyTurnEscalation(0);
|
||||
$('my-area')?.classList.remove('turn-urgent');
|
||||
}
|
||||
}
|
||||
@@ -90,7 +110,8 @@ function loadPlayMode() {
|
||||
function savePlayMode(m) { localStorage.setItem('shelem_play_mode', m); }
|
||||
function loadHandMode() {
|
||||
const s = localStorage.getItem('shelem_hand_mode');
|
||||
return ['scroll','fan','playables'].includes(s) ? s : 'scroll';
|
||||
if (['scroll','fan','playables'].includes(s)) return s;
|
||||
return isTouchDevice() ? 'fan' : 'scroll';
|
||||
}
|
||||
function saveHandMode(m) { localStorage.setItem('shelem_hand_mode', m); }
|
||||
function loadBarBottom() { return localStorage.getItem('shelem_bar_bottom') === '1'; }
|
||||
@@ -512,6 +533,23 @@ function initSocketHandlers() {
|
||||
socket.on('handOver', onHandOver);
|
||||
socket.on('gameOver', onGameOver);
|
||||
|
||||
socket.on('afkWarning', ({ name } = {}) => {
|
||||
afkBannerVoted = false;
|
||||
$('afk-banner-msg').textContent = `${name} hasn't played for 1 minute.`;
|
||||
const btn = $('afk-vote-btn');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Let AI Play'; }
|
||||
show('afk-banner');
|
||||
});
|
||||
socket.on('afkResolved', () => {
|
||||
afkBannerVoted = false;
|
||||
hide('afk-banner');
|
||||
});
|
||||
socket.on('aiControl', ({ seat, active, name } = {}) => {
|
||||
if (active) aiControlledSeats.add(seat);
|
||||
else aiControlledSeats.delete(seat);
|
||||
updateAiControlBanner();
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {});
|
||||
}
|
||||
|
||||
@@ -641,7 +679,7 @@ function renderInfoBar(st) {
|
||||
const td = $('trump-display');
|
||||
const bd = $('bid-display');
|
||||
if (st.trump) {
|
||||
td.textContent = `Trump: ${suitSymbol(st.trump)} ${suitName(st.trump)}`;
|
||||
td.textContent = `Hokm:\n${suitSymbol(st.trump)}`;
|
||||
show('trump-display');
|
||||
} else {
|
||||
hide('trump-display');
|
||||
@@ -1255,6 +1293,30 @@ async function showLeaderboard() {
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function updateAiControlBanner() {
|
||||
if (aiControlledSeats.size === 0) {
|
||||
hide('ai-control-banner');
|
||||
return;
|
||||
}
|
||||
const isMe = aiControlledSeats.has(mySeat);
|
||||
let msg;
|
||||
if (isMe) {
|
||||
msg = '🤖 AI is playing for you! Play a card to take back control.';
|
||||
} else {
|
||||
const names = [...aiControlledSeats].map(s => lastState?.names[s] || `Seat ${s + 1}`);
|
||||
msg = `🤖 AI is playing for: ${names.join(', ')}`;
|
||||
}
|
||||
$('ai-control-msg').textContent = msg;
|
||||
$('ai-control-banner').classList.toggle('is-me', isMe);
|
||||
show('ai-control-banner');
|
||||
}
|
||||
|
||||
function clearGameState() {
|
||||
aiControlledSeats.clear();
|
||||
hide('ai-control-banner');
|
||||
hide('afk-banner');
|
||||
}
|
||||
|
||||
// ─── Event wiring ──────────────────────────────────────────────
|
||||
function wireGameEvents() {
|
||||
// Waiting room
|
||||
@@ -1337,6 +1399,7 @@ function wireGameEvents() {
|
||||
$('btn-exit-confirm-yes').addEventListener('click', () => {
|
||||
socket?.emit('leave', { roomId: myRoomId, seat: mySeat, token: myToken });
|
||||
clearSession();
|
||||
clearGameState();
|
||||
hide('overlay-exit-confirm');
|
||||
hide('overlay-hand');
|
||||
hide('overlay-gameover');
|
||||
@@ -1432,6 +1495,15 @@ function wireGameEvents() {
|
||||
} catch { $('change-pass-msg').textContent = 'Network error'; }
|
||||
});
|
||||
|
||||
$('afk-vote-btn')?.addEventListener('click', () => {
|
||||
if (afkBannerVoted) return;
|
||||
afkBannerVoted = true;
|
||||
socket?.emit('voteAITakeover', { roomId: myRoomId, seat: mySeat, token: myToken });
|
||||
const btn = $('afk-vote-btn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Voted ✓'; }
|
||||
});
|
||||
$('afk-dismiss-btn')?.addEventListener('click', () => hide('afk-banner'));
|
||||
|
||||
$('btn-toggle-signups').addEventListener('click', async () => {
|
||||
try {
|
||||
const r = await fetch('/api/admin/toggle-signups', {
|
||||
|
||||
Reference in New Issue
Block a user