Removing one of the 3 AI-virus-protected files now starts a 60-second window (tracked session-wide via LockedFilesRemovalDeadline). If the other locked files aren't also removed before it expires, the virus restores whichever ones were deleted and broadcasts a red warning to the whole session, forcing players to coordinate the removal instead of picking them off one at a time. Also extends the Mercure broadcast payload with an optional 3rd "messageType" element so pushed messages can render red (virus) or green (mainframe) instead of always defaulting to green.
421 lines
14 KiB
JavaScript
421 lines
14 KiB
JavaScript
/* Game1 entry point built with Webpack Encore */
|
|
import './styles/game1.css';
|
|
|
|
let sequenceFinished = false;
|
|
let stillPlayingSound = true;
|
|
|
|
function subscribeToMercure(mercurePublicUrl, topic, myScreen) {
|
|
try {
|
|
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
|
|
const es = new EventSource(url);
|
|
|
|
es.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
console.log('[Mercure][game1] Update:', data);
|
|
|
|
// data is [sendTo, message, messageType?] - messageType defaults to 'mainframe' (green)
|
|
if (Array.isArray(data) && data.length >= 2) {
|
|
const sendTo = parseInt(data[0]);
|
|
// Filter: 0 means everyone, otherwise must match myScreen
|
|
if (sendTo !== 0 && sendTo !== parseInt(myScreen)) {
|
|
console.log('[Mercure][game1] Message not for this player, skipping.');
|
|
return;
|
|
}
|
|
|
|
const messageContainer = document.getElementById('message-container');
|
|
if (messageContainer) {
|
|
appendResultMessage(messageContainer, data[1], data[2] || 'mainframe');
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
if(stillPlayingSound)
|
|
playSound();
|
|
console.log('[Mercure][game1] sequenceFinished status:', sequenceFinished);
|
|
if (sequenceFinished) {
|
|
flashRed();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('[Mercure][game1] Raw event:', event.data);
|
|
}
|
|
};
|
|
|
|
es.onerror = (err) => {
|
|
console.warn('[Mercure][game1] EventSource error:', err);
|
|
};
|
|
|
|
console.log('[Mercure][game1] Subscribed to', url);
|
|
} catch (e) {
|
|
console.error('[Mercure][game1] Failed to subscribe:', e);
|
|
}
|
|
}
|
|
|
|
function playSound() {
|
|
const sound = document.getElementById('message-sound');
|
|
if (sound) {
|
|
sound.currentTime = 0;
|
|
sound.play().catch(e => console.warn('[Audio] Playback failed:', e));
|
|
}
|
|
}
|
|
|
|
function flashRed() {
|
|
console.log('[Game1] Triggering flashRed');
|
|
const body = document.body;
|
|
body.classList.remove('flash-red');
|
|
void body.offsetWidth; // Trigger reflow to restart animation
|
|
body.classList.add('flash-red');
|
|
|
|
// Also remove it after animation finishes so it's clean for inspection
|
|
setTimeout(() => {
|
|
body.classList.remove('flash-red');
|
|
console.log('[Game1] Removed flash-red class');
|
|
}, 150);
|
|
}
|
|
|
|
let lockRevealTimer = null;
|
|
let lockExpireTimer = null;
|
|
let lockCountdownTimer = null;
|
|
let currentLockedAt = null;
|
|
|
|
function lockMessageClass(messageType) {
|
|
if (messageType === 'virus') return 'message-virus';
|
|
if (messageType === 'mainframe') return 'message-mainframe';
|
|
return '';
|
|
}
|
|
|
|
function appendResultMessage(container, text, messageType) {
|
|
const msgEl = document.createElement('div');
|
|
msgEl.className = ('message ' + lockMessageClass(messageType)).trim();
|
|
msgEl.textContent = text;
|
|
container.appendChild(msgEl);
|
|
}
|
|
|
|
function setInputDisabled(disabled) {
|
|
const inputField = document.getElementById('input-message');
|
|
if (inputField) {
|
|
inputField.disabled = disabled;
|
|
}
|
|
}
|
|
|
|
function clearLockTimers() {
|
|
if (lockRevealTimer) { clearTimeout(lockRevealTimer); lockRevealTimer = null; }
|
|
if (lockExpireTimer) { clearTimeout(lockExpireTimer); lockExpireTimer = null; }
|
|
if (lockCountdownTimer) { clearInterval(lockCountdownTimer); lockCountdownTimer = null; }
|
|
}
|
|
|
|
function clearLock() {
|
|
clearLockTimers();
|
|
currentLockedAt = null;
|
|
document.body.classList.remove('locked');
|
|
const banner = document.getElementById('lock-banner');
|
|
if (banner) banner.style.display = 'none';
|
|
setInputDisabled(false);
|
|
}
|
|
|
|
function updateLockCountdown(unlockAtMs) {
|
|
const countdownEl = document.getElementById('lock-countdown');
|
|
if (!countdownEl) return;
|
|
const remaining = Math.max(0, Math.ceil((unlockAtMs - Date.now()) / 1000));
|
|
countdownEl.textContent = remaining + 's';
|
|
}
|
|
|
|
async function fetchLockReveal(apiEchoUrl, messageContainer) {
|
|
if (!apiEchoUrl) return;
|
|
try {
|
|
const response = await fetchJson(apiEchoUrl, {
|
|
method: 'POST',
|
|
body: { message: '', ts: new Date().toISOString() },
|
|
});
|
|
const result = response && response.result;
|
|
if (result && Array.isArray(result.result)) {
|
|
result.result.forEach(text => appendResultMessage(messageContainer, text, result.messageType));
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
}
|
|
if (result && result.locked === false) {
|
|
clearLock();
|
|
return;
|
|
}
|
|
// Code has been revealed (or already was), let the player try /unlock
|
|
setInputDisabled(false);
|
|
} catch (e) {
|
|
console.error('[Game1] Failed to fetch lock reveal:', e);
|
|
}
|
|
}
|
|
|
|
function applyLock(lockData, apiEchoUrl, messageContainer) {
|
|
if (currentLockedAt === lockData.lockedAt) {
|
|
return; // already tracking this lock, avoid re-fetching/duplicating messages
|
|
}
|
|
currentLockedAt = lockData.lockedAt;
|
|
|
|
clearLockTimers();
|
|
|
|
const banner = document.getElementById('lock-banner');
|
|
if (banner) banner.style.display = 'flex';
|
|
document.body.classList.add('locked');
|
|
|
|
const revealAtMs = lockData.revealAt * 1000;
|
|
const unlockAtMs = lockData.unlockAt * 1000;
|
|
const now = Date.now();
|
|
|
|
if (now < revealAtMs) {
|
|
setInputDisabled(true);
|
|
lockRevealTimer = setTimeout(() => fetchLockReveal(apiEchoUrl, messageContainer), revealAtMs - now);
|
|
} else {
|
|
fetchLockReveal(apiEchoUrl, messageContainer);
|
|
}
|
|
|
|
lockExpireTimer = setTimeout(() => clearLock(), Math.max(0, unlockAtMs - now));
|
|
|
|
updateLockCountdown(unlockAtMs);
|
|
lockCountdownTimer = setInterval(() => {
|
|
updateLockCountdown(unlockAtMs);
|
|
if (Date.now() >= unlockAtMs) {
|
|
clearInterval(lockCountdownTimer);
|
|
lockCountdownTimer = null;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
async function fetchJson(url, options = {}) {
|
|
const opts = { ...options };
|
|
const headers = new Headers(opts.headers || {});
|
|
headers.set('Accept', 'application/json');
|
|
if (opts.body !== undefined && typeof opts.body !== 'string') {
|
|
headers.set('Content-Type', 'application/json');
|
|
opts.body = JSON.stringify(opts.body);
|
|
}
|
|
// Useful convention for server-side checks
|
|
if (!headers.has('X-Requested-With')) {
|
|
headers.set('X-Requested-With', 'XMLHttpRequest');
|
|
}
|
|
opts.headers = headers;
|
|
const res = await fetch(url, opts);
|
|
const text = await res.text();
|
|
let data;
|
|
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
|
|
if (!res.ok) {
|
|
const err = new Error('HTTP ' + res.status + ' ' + res.statusText);
|
|
console.error('[API][game1]', err, data);
|
|
throw err;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Simple boot log so you can verify it in the browser console
|
|
// and confirm this specific bundle is loaded on the Game Hub page.
|
|
console.log('Game1 bundle loaded');
|
|
|
|
// Example: add a CSS class to <body> so page-specific styles can apply
|
|
document.body.classList.add('game1-page');
|
|
|
|
// Look for config injected by Twig in the page
|
|
const cfgEl = document.getElementById('mercure-config');
|
|
|
|
// Prevent/warn on page reload
|
|
window.addEventListener('beforeunload', (event) => {
|
|
// Standard way to trigger the browser's confirmation dialog
|
|
event.preventDefault();
|
|
// Included for compatibility with older browsers
|
|
event.returnValue = '';
|
|
});
|
|
|
|
if (!cfgEl) {
|
|
console.warn('[Mercure][game1] #mercure-config element not found on page');
|
|
return;
|
|
}
|
|
|
|
const mercurePublicUrl = cfgEl.dataset.mercurePublicUrl;
|
|
const topic = cfgEl.dataset.topic;
|
|
const screen = cfgEl.dataset.screen;
|
|
const apiPingUrl = cfgEl.dataset.apiPingUrl;
|
|
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
|
|
const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl;
|
|
const lostUrl = cfgEl.dataset.lostUrl;
|
|
const lockLockedAt = cfgEl.dataset.lockLockedAt;
|
|
const lockRevealAt = cfgEl.dataset.lockRevealAt;
|
|
const lockUnlockAt = cfgEl.dataset.lockUnlockAt;
|
|
|
|
if (mercurePublicUrl && topic) {
|
|
subscribeToMercure(mercurePublicUrl, topic, screen);
|
|
} else {
|
|
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
|
|
}
|
|
|
|
// Timer logic
|
|
const timerEl = document.getElementById('game-timer');
|
|
if (timerEl && timerEl.dataset.endTime) {
|
|
const endTime = parseInt(timerEl.dataset.endTime) * 1000;
|
|
|
|
const updateTimer = async () => {
|
|
const now = Date.now();
|
|
const diff = endTime - now;
|
|
|
|
if (diff <= 0) {
|
|
timerEl.textContent = '00:00:00';
|
|
|
|
// Timer reached zero, check with server
|
|
if (apiCheckFinishedUrl && lostUrl) {
|
|
try {
|
|
const response = await fetchJson(apiCheckFinishedUrl, { method: 'POST' });
|
|
if (response && response.finished) {
|
|
window.location.href = lostUrl;
|
|
return; // Stop the timer loop
|
|
}
|
|
} catch (e) {
|
|
console.error('[API][game1] Failed to check finished status:', e);
|
|
}
|
|
}
|
|
|
|
// Even if check failed or not finished, stop the loop if diff <= 0
|
|
// (though technically if the server says not finished, we might want to keep checking,
|
|
// but 00:00:00 usually means it's over).
|
|
return;
|
|
}
|
|
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
|
|
const hDisplay = hours.toString().padStart(2, '0');
|
|
const mDisplay = minutes.toString().padStart(2, '0');
|
|
const sDisplay = seconds.toString().padStart(2, '0');
|
|
|
|
timerEl.textContent = `${hDisplay}:${mDisplay}:${sDisplay}`;
|
|
setTimeout(updateTimer, 1000);
|
|
};
|
|
|
|
updateTimer();
|
|
}
|
|
|
|
// Demo API calls
|
|
try {
|
|
if (apiPingUrl) {
|
|
const ping = await fetchJson(apiPingUrl);
|
|
console.log('[API][game1] ping →', ping);
|
|
} else {
|
|
console.warn('[API][game1] data-api-ping-url missing');
|
|
}
|
|
|
|
if (apiEchoUrl) {
|
|
const echo = await fetchJson(apiEchoUrl, {
|
|
method: 'POST',
|
|
body: { message: 'from game1.js', ts: new Date().toISOString() },
|
|
});
|
|
console.log('[API][game1] echo →', echo);
|
|
} else {
|
|
console.warn('[API][game1] data-api-echo-url missing');
|
|
}
|
|
} catch (e) {
|
|
console.error('[API][game1] Request failed:', e);
|
|
}
|
|
|
|
// Add messages to message-container
|
|
const messageContainer = document.getElementById('message-container');
|
|
if (messageContainer) {
|
|
let messages = [
|
|
['System initializing...', 500],
|
|
['Connection established.', 200],
|
|
['Welcome agent to the mainframe.', 1000],
|
|
['Scanning...', 3000],
|
|
['Virus detected.', 500],
|
|
['Starting Mainframe help modus...', 2000],
|
|
['Help modus activated.', 500],
|
|
['Blocking virus activated', 0]
|
|
];
|
|
|
|
let currentMessageIndex = 0;
|
|
|
|
const printNextMessage = () => {
|
|
if (currentMessageIndex < messages.length) {
|
|
|
|
const msg = messages[currentMessageIndex];
|
|
const msgEl = document.createElement('div');
|
|
|
|
let extraClass = '';
|
|
if(msg[2])
|
|
extraClass = msg[2];
|
|
|
|
msgEl.className = 'message ' + extraClass;
|
|
msgEl.textContent = msg[0];
|
|
messageContainer.appendChild(msgEl);
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
|
|
playSound();
|
|
|
|
currentMessageIndex++;
|
|
setTimeout(printNextMessage, msg[1]);
|
|
if (sequenceFinished) {
|
|
flashRed();
|
|
}
|
|
} else {
|
|
// After it has printed a set of messages, it has to start a timer of 2 seconds
|
|
console.log('[Game1] All messages printed. Starting 2s timer to expand message-container height...');
|
|
setTimeout(() => {
|
|
messageContainer.style.height = '400vh';
|
|
const inputField = document.getElementById('input-message');
|
|
inputField.disabled = false;
|
|
|
|
// Add event listener for Enter key
|
|
inputField.addEventListener('keypress', async (e) => {
|
|
if (e.key === 'Enter') {
|
|
stillPlayingSound = false;
|
|
sequenceFinished = false;
|
|
const message = inputField.value.trim();
|
|
|
|
const msgEl = document.createElement('div');
|
|
msgEl.className = 'message';
|
|
msgEl.textContent = message;
|
|
messageContainer.appendChild(msgEl);
|
|
|
|
if (message && apiEchoUrl) {
|
|
inputField.value = '';
|
|
try {
|
|
const response = await fetchJson(apiEchoUrl, {
|
|
method: 'POST',
|
|
body: { message, ts: new Date().toISOString() },
|
|
});
|
|
console.log('[API][game1] message sent →', response);
|
|
if (response && response.result && Array.isArray(response.result.result)) {
|
|
response.result.result.forEach(text => appendResultMessage(messageContainer, text, response.result.messageType));
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
}
|
|
if (response && response.result) {
|
|
if (response.result.locked === true) {
|
|
applyLock({
|
|
lockedAt: response.result.lockedAt,
|
|
revealAt: response.result.revealAt,
|
|
unlockAt: response.result.unlockAt,
|
|
}, apiEchoUrl, messageContainer);
|
|
} else if (response.result.locked === false) {
|
|
clearLock();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[API][game1] Failed to send message:', err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('[Game1] message-container height changed to 400vh and input enabled');
|
|
sequenceFinished = true;
|
|
console.log('[Game1] sequenceFinished is now TRUE');
|
|
|
|
// Restore an in-progress lock after a page refresh
|
|
if (lockUnlockAt && parseInt(lockUnlockAt, 10) * 1000 > Date.now()) {
|
|
applyLock({
|
|
lockedAt: parseInt(lockLockedAt, 10),
|
|
revealAt: parseInt(lockRevealAt, 10),
|
|
unlockAt: parseInt(lockUnlockAt, 10),
|
|
}, apiEchoUrl, messageContainer);
|
|
}
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
printNextMessage();
|
|
}
|
|
});
|