Wire up decode puzzle chain and lock terminal on file removal
Connects the previously-disconnected /decode command to per-player messages: player 1 unlocks sudo for everyone, player 2 unlocks a scan command that reveals which files the AI virus has locked, player 3 learns those files should be removed. Also adds a retaliation mechanic: successfully removing a file locks the player's terminal. The AI virus locks it out in red immediately; the mainframe reveals a 12-character recovery code in green after 30 seconds, which can be submitted via /unlock to restore access early, otherwise the terminal auto-recovers after 45 seconds.
This commit is contained in:
+130
-7
@@ -77,6 +77,112 @@ function flashRed() {
|
||||
}, 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;
|
||||
msgEl.style.marginBottom = '10px';
|
||||
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 || {});
|
||||
@@ -133,6 +239,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
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);
|
||||
@@ -277,15 +386,20 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
console.log('[API][game1] message sent →', response);
|
||||
if (response && response.result && Array.isArray(response.result.result)) {
|
||||
response.result.result.forEach(text => {
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.className = 'message';
|
||||
msgEl.textContent = text;
|
||||
msgEl.style.marginBottom = '10px';
|
||||
messageContainer.appendChild(msgEl);
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -296,6 +410,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user