isle.pizza/app.js
2025-07-17 18:21:25 -07:00

437 lines
16 KiB
JavaScript

var Module = {
arguments: ['--ini', '/config/isle.ini'],
running: false,
preRun: function () {
Module["addRunDependency"]("isle");
Module.running = true;
},
canvas: (function () {
return document.getElementById('canvas');
})(),
onExit: function () {
window.location.reload();
}
};
document.addEventListener('DOMContentLoaded', function () {
// --- Elements ---
const video = document.getElementById('install-video');
const soundToggleEmoji = document.getElementById('sound-toggle-emoji');
const mainContainer = document.getElementById('main-container');
const topContent = document.getElementById('top-content');
const controlsWrapper = document.getElementById('controls-wrapper');
const footer = document.querySelector('.footer-disclaimer');
const allPages = document.querySelectorAll('.page-content');
const pageButtons = document.querySelectorAll('[data-target]');
const backButtons = document.querySelectorAll('.page-back-button');
const languageSelect = document.getElementById('language-select');
const installBtn = document.getElementById('install-btn');
const uninstallBtn = document.getElementById('uninstall-btn');
const controlsContainer = document.querySelector('.offline-play-controls');
// --- Sound Toggle ---
function updateSoundEmojiState() {
soundToggleEmoji.textContent = video.muted ? '🔇' : '🔊';
soundToggleEmoji.title = video.muted ? 'Unmute Audio' : 'Mute Audio';
}
if (video && soundToggleEmoji) {
updateSoundEmojiState();
soundToggleEmoji.addEventListener('click', function () {
video.muted = !video.muted;
updateSoundEmojiState();
});
video.addEventListener('volumechange', updateSoundEmojiState);
}
// --- Control Image Hover ---
const imageControls = document.querySelectorAll('.control-img');
imageControls.forEach(control => {
const hoverImage = new Image();
if (control.dataset.on) {
hoverImage.src = control.dataset.on;
}
control.addEventListener('mouseover', function () { if (this.dataset.on) { this.src = this.dataset.on; } });
control.addEventListener('mouseout', function () { if (this.dataset.off) { this.src = this.dataset.off; } });
});
// --- Emscripten Launch Logic ---
const runGameButton = document.getElementById('run-game-btn');
const emscriptenCanvas = document.getElementById('canvas');
const canvasWrapper = document.getElementById('canvas-wrapper');
const loadingGifOverlay = document.getElementById('loading-gif-overlay');
const statusMessageBar = document.getElementById('emscripten-status-message');
runGameButton.addEventListener('click', function () {
if (!Module.running) return;
video.muted = true;
updateSoundEmojiState();
this.src = this.dataset.on;
mainContainer.style.display = 'none';
canvasWrapper.style.display = 'grid';
document.documentElement.style.overflow = 'hidden';
document.documentElement.style.overscrollBehavior = 'none';
Module["disableOffscreenCanvases"] ||= document.getElementById('renderer-select').value == "0 0x682656f3 0x0 0x0 0x2000000";
console.log("disableOffscreenCanvases: " + Module["disableOffscreenCanvases"]);
Module["removeRunDependency"]("isle");
emscriptenCanvas.focus();
});
let progressUpdates = 0;
emscriptenCanvas.addEventListener('presenterProgress', function (event) {
// Intro animation is ready
if (event.detail.objectName == 'Lego_Smk' && event.detail.tickleState == 1) {
loadingGifOverlay.style.display = 'none';
emscriptenCanvas.style.setProperty('display', 'block', 'important');
}
else if (progressUpdates < 1003) {
progressUpdates++;
const percent = (progressUpdates / 1003 * 100).toFixed();
statusMessageBar.innerHTML = 'Loading LEGO® Island... please wait! <code>' + percent + '%</code>';
}
});
// --- Page Navigation Logic ---
function showPage(pageId, pushState = true) {
const page = document.querySelector(pageId);
if (!page) return;
// Hide main content
topContent.style.display = 'none';
controlsWrapper.style.display = 'none';
// Show selected page
page.style.display = 'flex';
window.scroll(0, 0);
if (pushState) {
const newPath = pageId.replace('-page', '');
history.pushState({ page: pageId }, '', newPath);
}
}
function showMainMenu() {
// Hide all pages
allPages.forEach(p => p.style.display = 'none');
// Show main content
topContent.style.display = 'flex';
controlsWrapper.style.display = 'flex';
}
pageButtons.forEach(button => {
button.addEventListener('click', (e) => {
const targetId = e.currentTarget.dataset.target;
showPage(targetId);
});
});
backButtons.forEach(button => {
button.addEventListener('click', () => {
history.back();
});
});
window.addEventListener('popstate', (e) => {
if (e.state && e.state.page && e.state.page !== 'main') {
showPage(e.state.page, false);
} else {
showMainMenu();
}
});
// --- OPFS Config Manager ---
const configManager = {
form: document.querySelector('.config-form'),
filePath: 'isle.ini',
async init() {
if (!this.form) return;
await this.loadConfig();
this.form.addEventListener('change', () => this.saveConfig());
},
async getFileHandle() {
try {
const root = await navigator.storage.getDirectory();
return await root.getFileHandle(this.filePath, { create: true });
} catch (e) {
console.error("OPFS not available or permission denied.", e);
document.getElementById('opfs-disabled').style.display = '';
document.getElementById('config-form').querySelectorAll('input, select').forEach(element => {
element.disabled = true;
});
return null;
}
},
async saveConfig() {
// This function now uses an inline Web Worker for maximum compatibility,
// especially with Safari, which does not support createWritable().
let iniContent = '[isle]\n';
const elements = this.form.elements;
for (const element of elements) {
if (!element.name || element.dataset.notIni == "true") continue;
let value;
switch (element.type) {
case 'checkbox':
value = element.checked ? 'YES' : 'NO';
iniContent += `${element.name}=${value}\n`;
break;
case 'radio':
if (element.checked) {
value = element.value;
iniContent += `${element.name}=${value}\n`;
}
break;
default:
value = element.value;
iniContent += `${element.name}=${value}\n`;
break;
}
}
const workerCode = `
self.onmessage = async (e) => {
if (e.data.action === 'save') {
try {
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(e.data.filePath, { create: true });
const accessHandle = await handle.createSyncAccessHandle();
const encoder = new TextEncoder();
const encodedData = encoder.encode(e.data.content);
accessHandle.truncate(0);
accessHandle.write(encodedData, { at: 0 });
accessHandle.flush();
accessHandle.close();
self.postMessage({ status: 'success', message: 'Config saved to ' + e.data.filePath });
} catch (err) {
self.postMessage({ status: 'error', message: 'Failed to save config: ' + err.message });
}
}
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);
worker.postMessage({
action: 'save',
content: iniContent,
filePath: this.filePath
});
worker.onmessage = (e) => {
console.log(e.data.message);
URL.revokeObjectURL(workerUrl); // Clean up the temporary URL
worker.terminate();
};
worker.onerror = (e) => {
console.error('An error occurred in the config-saving worker:', e.message);
URL.revokeObjectURL(workerUrl);
worker.terminate();
};
},
async loadConfig() {
const handle = await this.getFileHandle();
if (!handle) return;
const file = await handle.getFile();
const text = await file.text();
if (!text) {
console.log('No existing config file found, using defaults.');
await this.saveConfig();
return;
}
const config = {};
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('[') || !line.includes('=')) continue;
const [key, ...valueParts] = line.split('=');
const value = valueParts.join('=').trim();
config[key.trim()] = value;
}
this.applyConfigToForm(config);
console.log('Config loaded from', this.filePath);
},
applyConfigToForm(config) {
const elements = this.form.elements;
for (const key in config) {
const element = elements[key];
if (!element) continue;
const value = config[key];
if (element.type === 'checkbox') {
element.checked = (value === 'YES');
} else if (element.nodeName === 'RADIO') { // radio nodelist
for (const radio of element) {
if (radio.value === value) {
radio.checked = true;
break;
}
}
}
else {
element.value = value;
}
}
}
};
// Handle initial page load with a hash
const initialHash = window.location.hash;
if (initialHash) {
const initialPageId = initialHash + '-page';
if (document.querySelector(initialPageId)) {
const urlPath = window.location.pathname;
history.replaceState({ page: 'main' }, '', urlPath);
showPage(initialPageId, true);
}
} else {
history.replaceState({ page: 'main' }, '', window.location.pathname);
}
if (document.documentElement.requestFullscreen) {
const fullscreenElement = document.getElementById('window-fullscreen');
const windowedElement = document.getElementById('window-windowed');
fullscreenElement.addEventListener('change', () => {
if (fullscreenElement.checked) {
document.documentElement.requestFullscreen().catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
}
});
windowedElement.addEventListener('change', () => {
if (windowedElement.checked && document.fullscreenElement) {
document.exitFullscreen();
}
});
// Event listener for changes in fullscreen state (e.g., F11 or Esc key)
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
fullscreenElement.checked = true;
} else {
windowedElement.checked = true;
}
});
}
else {
document.getElementById('window-form').style.display = 'none';
}
if (!window.matchMedia('(any-pointer: coarse)').matches) {
document.getElementById('touch-section').style.display = 'none';
}
if ('serviceWorker' in navigator) {
Promise.all([
configManager.init(),
navigator.serviceWorker.register('/sw.js').then(() => navigator.serviceWorker.ready)
]).then(([configResult, swRegistration]) => {
checkInitialCacheStatus();
}).catch(error => {
console.error('Initialization failed:', error);
});
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
}
else {
configManager.init();
}
const progressCircular = document.createElement('div');
progressCircular.className = 'progress-circular';
controlsContainer.appendChild(progressCircular);
installBtn.addEventListener('click', () => {
if (navigator.serviceWorker.controller) {
const selectedLanguage = languageSelect.value;
installBtn.style.display = 'none';
uninstallBtn.style.display = 'none';
progressCircular.style.display = 'flex';
progressCircular.textContent = '0%';
progressCircular.style.background = 'conic-gradient(#FFD700 0deg, #333 0deg)';
navigator.serviceWorker.controller.postMessage({
action: 'install_language_pack',
language: selectedLanguage
});
}
});
uninstallBtn.addEventListener('click', () => {
const selectedLanguage = languageSelect.value;
navigator.serviceWorker.controller.postMessage({
action: 'uninstall_language_pack',
language: selectedLanguage
});
});
languageSelect.addEventListener('change', () => {
checkInitialCacheStatus();
});
function checkInitialCacheStatus() {
if (navigator.serviceWorker.controller) {
const selectedLanguage = languageSelect.value;
navigator.serviceWorker.controller.postMessage({
action: 'check_cache_status',
language: selectedLanguage
});
}
}
function updateInstallUI(isInstalled, inProgress = false) {
progressCircular.style.display = inProgress ? 'flex' : 'none';
installBtn.style.display = !isInstalled && !inProgress ? 'block' : 'none';
uninstallBtn.style.display = isInstalled && !inProgress ? 'block' : 'none';
}
function handleServiceWorkerMessage(event) {
const { action, language, progress, exists, success } = event.data;
if (language !== languageSelect.value) return;
switch (action) {
case 'cache_status':
updateInstallUI(exists);
break;
case 'install_progress':
updateInstallUI(false, true);
const angle = (progress / 100) * 360;
progressCircular.textContent = `${Math.round(progress)}%`;
progressCircular.style.background =
`radial-gradient(#181818 60%, transparent 61%), conic-gradient(#FFD700 ${angle}deg, #333 ${angle}deg)`;
break;
case 'install_complete':
updateInstallUI(success);
break;
case 'uninstall_complete':
updateInstallUI(!success);
break;
case 'install_failed':
alert('Download failed. Please check your internet connection and try again.');
updateInstallUI(false);
break;
}
}
});