diff --git a/.gitignore b/.gitignore
index 4ec79a9..224c138 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+node_modules
+dist
isle.wasm
isle.wasm.map
isle.js
diff --git a/README.md b/README.md
index fb691ad..54cb525 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,75 @@
-# [isle.pizza](https://isle.pizza) frontend
+# [isle.pizza](https://isle.pizza) Frontend
-This is a custom frontend for the Emscripten port of [isle-portable](https://github.com/isledecomp/isle-portable). To use this, build the Emscripten version of the game and couple `isle.js` as well as `isle.wasm` with the files in this repository, and open `index.html`.
+A custom web frontend for the Emscripten port of [isle-portable](https://github.com/isledecomp/isle-portable), allowing LEGO Island to run directly in modern web browsers.
-[A Docker image that bundles the runtime with this frontend is also available](https://github.com/isledecomp/isle-portable/wiki/Installation#web-port-emscripten).
+## Requirements
+
+- [Node.js](https://nodejs.org/)
+
+## Quick Start
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/isledecomp/isle.pizza.git
+ cd isle.pizza
+ ```
+
+2. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+3. Obtain the game files (`isle.js` and `isle.wasm`) by building the Emscripten version of [isle-portable](https://github.com/isledecomp/isle-portable), then copy them to the project root.
+
+4. Start the development server:
+ ```bash
+ npm run dev
+ ```
+
+5. Open the URL shown in the terminal (usually `http://localhost:5173`).
+
+## Scripts
+
+| Command | Description |
+|---------|-------------|
+| `npm run dev` | Start development server with hot reload |
+| `npm run build` | Build for production (outputs to `dist/`) |
+| `npm run preview` | Preview the production build locally |
+
+## Project Structure
+
+```
+isle.pizza/
+├── src/
+│ ├── App.svelte # Main application component
+│ ├── app.css # Global styles
+│ ├── stores.js # Svelte stores for state management
+│ ├── core/ # Core modules (audio, OPFS, service worker, etc.)
+│ └── lib/ # UI components
+├── public/ # Static assets (images, fonts, PDFs)
+├── scripts/ # Build scripts
+├── src-sw/ # Service worker source
+├── index.html # HTML entry point
+├── isle.js # Emscripten JS (not in repo, build from isle-portable)
+├── isle.wasm # Emscripten WASM (not in repo, build from isle-portable)
+└── LEGO/ # Game data directory
+```
+
+## Building the Game Files
+
+The `isle.js` and `isle.wasm` files are not included in this repository. To obtain them:
+
+1. Follow the [isle-portable build instructions](https://github.com/isledecomp/isle-portable#building) for the Emscripten target
+2. Copy the resulting `isle.js` and `isle.wasm` to this project's root directory
+
+Alternatively, a [Docker image that bundles the runtime with this frontend](https://github.com/isledecomp/isle-portable/wiki/Installation#web-port-emscripten) is available.
+
+## Tech Stack
+
+- [Svelte 5](https://svelte.dev/) - UI framework
+- [Vite](https://vitejs.dev/) - Build tool and dev server
+- [Workbox](https://developer.chrome.com/docs/workbox/) - Service worker and offline support
+
+## License
+
+See [LICENSE](LICENSE) for details.
diff --git a/app.js b/app.js
deleted file mode 100644
index ae9f2aa..0000000
--- a/app.js
+++ /dev/null
@@ -1,1011 +0,0 @@
-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 audio = document.getElementById('install-audio');
- 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 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');
- const msaaSelect = document.querySelector('select[name="MSAA"]');
- const msaaGroup = msaaSelect.closest('.form-group');
- const afSelect = document.querySelector('select[name="Anisotropic"]');
- const afGroup = afSelect.closest('.form-group');
- const rendererSelect = document.getElementById('renderer-select');
- const hdTextures = document.getElementById('check-hd-textures');
- const hdMusic = document.getElementById('check-hd-music');
- const widescreenBgs = document.getElementById('check-widescreen-bgs');
- const outroFmv = document.getElementById('check-outro');
- const badEnding = document.getElementById('check-ending');
- const logo = document.getElementById('island-logo-img');
-
- // --- Debug Mode Activation (5 taps on logo) ---
- let debugTapCount = 0;
- let debugTapTimeout = null;
- let debugEnabled = false;
-
- // Pizza celebration animation for OGEL mode
- function celebratePizza(originElement) {
- const rect = originElement.getBoundingClientRect();
- const centerX = rect.left + rect.width / 2;
- const centerY = rect.top + rect.height / 2;
- const sliceCount = 12;
-
- for (let i = 0; i < sliceCount; i++) {
- const slice = document.createElement('div');
- slice.className = 'pizza-slice';
- slice.textContent = '🍕';
-
- // Calculate direction for this slice
- const angle = (i / sliceCount) * Math.PI * 2;
- const distance = 150 + Math.random() * 100;
- const tx = Math.cos(angle) * distance;
- const ty = Math.sin(angle) * distance;
- const rotation = (Math.random() - 0.5) * 720;
-
- slice.style.left = centerX + 'px';
- slice.style.top = centerY + 'px';
- slice.style.setProperty('--tx', tx + 'px');
- slice.style.setProperty('--ty', ty + 'px');
- slice.style.setProperty('--rot', rotation + 'deg');
- slice.style.animationDelay = (Math.random() * 0.2) + 's';
-
- document.body.appendChild(slice);
-
- // Remove after animation completes
- setTimeout(function() {
- slice.remove();
- }, 1700);
- }
- }
-
- logo.addEventListener('click', function() {
- if (debugEnabled) {
- // Replay pizza animation on subsequent clicks
- celebratePizza(logo);
- return;
- }
-
- debugTapCount++;
- clearTimeout(debugTapTimeout);
-
- if (debugTapCount >= 5) {
- // Enable debug mode
- debugEnabled = true;
- logo.src = 'ogel.webp';
- logo.alt = 'OGEL Mode Enabled';
-
- // Celebrate with pizza!
- celebratePizza(logo);
-
- // Dynamically load debug.js
- const script = document.createElement('script');
- script.src = 'debug.js';
- document.body.appendChild(script);
- } else {
- // Reset tap count after 1 second of no taps
- debugTapTimeout = setTimeout(function() {
- debugTapCount = 0;
- }, 1000);
- }
- });
-
- // --- Sound Toggle ---
- function updateSoundEmojiState() {
- soundToggleEmoji.textContent = audio.paused ? '🔇' : '🔊';
- soundToggleEmoji.title = audio.paused ? 'Play Audio' : 'Pause Audio';
- }
-
- if (audio && soundToggleEmoji) {
- updateSoundEmojiState();
- soundToggleEmoji.addEventListener('click', function () {
- if (audio.paused) {
- audio.currentTime = 0;
- audio.play();
- } else {
- audio.pause();
- }
- });
- audio.addEventListener('play', updateSoundEmojiState);
- audio.addEventListener('pause', 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;
- audio.pause();
- 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"] ||= rendererSelect.value == "0 0x682656f3 0x0 0x0 0x2000000";
- console.log("disableOffscreenCanvases: " + Module["disableOffscreenCanvases"]);
-
- Module["removeRunDependency"]("isle");
- emscriptenCanvas.focus();
- });
-
- let progressUpdates = 0;
- const debugUI = document.getElementById('debug-ui');
- let debugUIVisible = false;
-
- // MutationObserver to prevent Emscripten from hiding the debug UI
- const debugUIObserver = new MutationObserver(function(mutations) {
- if (debugUIVisible && debugUI.style.display === 'none') {
- debugUI.style.setProperty('display', 'block', 'important');
- }
- });
- debugUIObserver.observe(debugUI, { attributes: true, attributeFilter: ['style'] });
-
- 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');
- debugUIVisible = true;
- debugUI.style.setProperty('display', 'block', 'important');
- }
- else if (progressUpdates < 1003) {
- progressUpdates++;
- const percent = (progressUpdates / 1003 * 100).toFixed();
- statusMessageBar.innerHTML = 'Loading LEGO® Island... please wait! ' + percent + '%';
- }
- });
-
- emscriptenCanvas.addEventListener('extensionProgress', function (event) {
- statusMessageBar.innerHTML = 'Loading ' + event.detail.name + '... please wait! ' + event.detail.progress + '%';
- });
-
- // --- 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);
-
- // Reset Read Me tabs to About when entering
- if (pageId === '#read-me-page') {
- document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.classList.toggle('active', btn.dataset.tab === 'about');
- });
- document.querySelectorAll('.tab-panel').forEach(panel => {
- panel.classList.toggle('active', panel.id === 'tab-about');
- });
- }
-
- 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();
- });
- });
-
- // --- Read Me Tabs ---
- const tabButtons = document.querySelectorAll('.tab-btn');
- const tabPanels = document.querySelectorAll('.tab-panel');
-
- tabButtons.forEach(button => {
- button.addEventListener('click', () => {
- const targetTab = button.dataset.tab;
-
- // Update button states
- tabButtons.forEach(btn => btn.classList.remove('active'));
- button.classList.add('active');
-
- // Update panel visibility
- tabPanels.forEach(panel => {
- panel.classList.remove('active');
- if (panel.id === 'tab-' + targetTab) {
- panel.classList.add('active');
- }
- });
- });
- });
-
- // --- Configure Tabs ---
- const configTabButtons = document.querySelectorAll('.config-tab-btn');
- const configTabPanels = document.querySelectorAll('.config-tab-panel');
-
- configTabButtons.forEach(button => {
- button.addEventListener('click', () => {
- const targetTab = button.dataset.configTab;
- configTabButtons.forEach(btn => btn.classList.remove('active'));
- button.classList.add('active');
- configTabPanels.forEach(panel => {
- panel.classList.remove('active');
- if (panel.id === 'config-tab-' + targetTab) {
- panel.classList.add('active');
- // Open first section, close others
- const sections = panel.querySelectorAll('.config-section-card');
- sections.forEach((section, index) => {
- section.open = (index === 0);
- });
- }
- });
- });
- });
-
- // --- Accordion behavior for config sections ---
- // Note: We prevent default and manually toggle to avoid Safari's scroll jump bug
- const configSections = document.querySelectorAll('.config-section-card');
- configSections.forEach(section => {
- const summary = section.querySelector('summary');
- if (summary) {
- summary.addEventListener('click', (e) => {
- e.preventDefault();
-
- if (section.open) {
- // Closing this section
- section.open = false;
- } else {
- // Opening this section - close others first
- configSections.forEach(other => {
- if (other !== section && other.open) {
- other.open = false;
- }
- });
- section.open = true;
- }
- });
- }
- });
-
- // --- Tooltip handling for touch devices only ---
- const isTouchDevice = window.matchMedia('(pointer: coarse)').matches;
- if (isTouchDevice) {
- const tooltipTriggers = document.querySelectorAll('.tooltip-trigger');
- tooltipTriggers.forEach(trigger => {
- trigger.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- const wasActive = trigger.classList.contains('active');
-
- // Close all other tooltips
- tooltipTriggers.forEach(other => {
- other.classList.remove('active');
- });
-
- // Toggle this tooltip
- if (!wasActive) {
- trigger.classList.add('active');
- }
- });
- });
-
- // Close tooltips when clicking elsewhere
- document.addEventListener('click', (e) => {
- if (!e.target.closest('.tooltip-trigger')) {
- tooltipTriggers.forEach(trigger => {
- trigger.classList.remove('active');
- });
- }
- });
- }
-
- // --- Preset Buttons ---
- const presetClassic = document.getElementById('preset-classic');
- const presetModern = document.getElementById('preset-modern');
-
- function applyPreset(preset) {
- if (preset === 'classic') {
- // Game section defaults
- document.getElementById('language-select').value = 'en';
- document.getElementById('window-windowed').checked = true;
- document.getElementById('aspect-original').checked = true;
- document.getElementById('resolution-original').checked = true;
- // Detail section defaults
- document.getElementById('gfx-high').checked = true;
- document.getElementById('tex-high').checked = true;
- document.getElementById('max-lod').value = '3.6';
- document.getElementById('max-allowed-extras').value = '20';
- // Disable all extensions
- document.getElementById('check-hd-textures').checked = false;
- document.getElementById('check-hd-music').checked = false;
- document.getElementById('check-widescreen-bgs').checked = false;
- document.getElementById('check-outro').checked = false;
- document.getElementById('check-ending').checked = false;
- } else if (preset === 'modern') {
- document.getElementById('aspect-wide').checked = true;
- document.getElementById('resolution-wide').checked = true;
- document.getElementById('gfx-high').checked = true;
- document.getElementById('tex-high').checked = true;
- document.getElementById('max-lod').value = '6';
- document.getElementById('max-allowed-extras').value = '40';
- // Enable HD extensions
- document.getElementById('check-hd-textures').checked = true;
- document.getElementById('check-hd-music').checked = true;
- document.getElementById('check-widescreen-bgs').checked = true;
- }
- // Trigger change event to save config
- const configForm = document.getElementById('config-form');
- if (configForm) {
- configForm.dispatchEvent(new Event('change'));
- }
- // Refresh Offline Play button state
- if (typeof checkInitialCacheStatus === 'function') {
- checkInitialCacheStatus();
- }
- }
-
- if (presetClassic) {
- presetClassic.addEventListener('click', () => applyPreset('classic'));
- }
- if (presetModern) {
- presetModern.addEventListener('click', () => applyPreset('modern'));
- }
-
- // --- Config Toast ---
- const configToast = document.getElementById('config-toast');
- let toastTimeout = null;
-
- function showConfigToast() {
- if (configToast) {
- configToast.classList.add('show');
- if (toastTimeout) clearTimeout(toastTimeout);
- toastTimeout = setTimeout(() => {
- configToast.classList.remove('show');
- }, 2000);
- }
- }
-
- 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;
- });
- document.querySelectorAll('.preset-btn').forEach(btn => {
- btn.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;
- }
- }
-
- iniContent += "[extensions]\n";
-
- if (hdTextures) {
- value = hdTextures.checked ? 'YES' : 'NO';
- iniContent += `${hdTextures.name}=${value}\n`;
- }
-
- siFiles = getSiFiles();
- if (siFiles.length > 0 || outroFmv.checked) {
- iniContent += `SI Loader=YES\n`;
- iniContent += "[si loader]\n";
- }
-
- if (siFiles.length > 0) {
- iniContent += `files=${siFiles.join(',')}\n`;
- }
-
- let directives = [];
-
- if (outroFmv.checked) {
- directives = directives.concat([
- "FullScreenMovie:\\lego\\scripts\\intro:3",
- "Disable3d:\\lego\\scripts\\credits:499",
- "Prepend:\\lego\\scripts\\intro:3:\\lego\\scripts\\credits:499",
- "RemoveWith:\\lego\\scripts\\credits:499:\\lego\\scripts\\intro:3"
- ]);
- }
-
- if (directives.length > 0) {
- iniContent += `directives=${directives.join(",\\\n")}\n`;
- }
-
- 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();
- if (e.data.status === 'success') {
- showConfigToast();
- }
- };
-
- 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) {
- if (key == "files") {
- elements["HD Music"].checked = config[key].includes("hdmusic.si");
- elements["Widescreen Backgrounds"].checked = config[key].includes("widescreen.si");
- elements["Extended Bad Ending FMV"].checked = config[key].includes("badend.si");
- continue;
- }
-
- if (key == "directives") {
- elements["Outro FMV"].checked = config[key].includes("intro:3");
- continue;
- }
-
- 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';
- }
-
- const gl = document.createElement('canvas').getContext('webgl2');
- if (gl) {
- const samples = gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES);
- if (samples && samples.length > 0 && Math.max(...samples) > 1) {
- msaaSelect.innerHTML = '';
- const offOption = document.createElement('option');
- offOption.value = '1';
- offOption.textContent = 'Off';
- msaaSelect.appendChild(offOption);
- samples.sort().forEach(sampleCount => {
- if (sampleCount > 1) {
- const option = document.createElement('option');
- option.value = sampleCount;
- option.textContent = `${sampleCount}x`;
- msaaSelect.appendChild(option);
- }
- });
- } else {
- msaaGroup.style.display = 'none';
- }
-
- const ext = gl.getExtension('EXT_texture_filter_anisotropic');
- if (ext) {
- const maxAnisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
-
- afSelect.innerHTML = '';
-
- const offOption = document.createElement('option');
- offOption.value = '1';
- offOption.textContent = 'Off';
- afSelect.appendChild(offOption);
-
- const defaultAniso = Math.min(maxAnisotropy, 16);
-
- for (let i = 2; i <= maxAnisotropy; i *= 2) {
- const option = document.createElement('option');
- option.value = i;
- option.textContent = `${i}x`;
- option.selected = defaultAniso == i;
- afSelect.appendChild(option);
- }
- } else {
- afGroup.style.display = 'none';
- }
- } else {
- msaaGroup.style.display = 'none';
- afGroup.style.display = 'none';
- }
-
- let downloaderWorker = null;
- let missingGameFiles = [];
-
- // Update popup elements
- const updatePopup = document.getElementById('update-popup');
- const updateReloadBtn = document.getElementById('update-reload-btn');
- const updateDismissBtn = document.getElementById('update-dismiss-btn');
-
- function showUpdatePopup() {
- if (updatePopup) {
- updatePopup.style.display = 'flex';
- }
- }
-
- function hideUpdatePopup() {
- if (updatePopup) {
- updatePopup.style.display = 'none';
- }
- }
-
- if (updateReloadBtn) {
- updateReloadBtn.addEventListener('click', () => {
- window.location.reload();
- });
- }
-
- if (updateDismissBtn) {
- updateDismissBtn.addEventListener('click', () => {
- hideUpdatePopup();
- });
- }
-
- // Goodbye popup elements
- const goodbyePopup = document.getElementById('goodbye-popup');
- const goodbyeCancelBtn = document.getElementById('goodbye-cancel-btn');
- const goodbyeProgressBar = document.querySelector('.goodbye-progress-bar');
- const cancelBtn = document.getElementById('cancel-btn');
- let goodbyeTimeout = null;
- let goodbyeInterval = null;
-
- function showGoodbyePopup() {
- if (goodbyePopup && goodbyePopup.style.display !== 'flex') {
- goodbyePopup.style.display = 'flex';
- if (goodbyeProgressBar) {
- goodbyeProgressBar.style.width = '0%';
- }
- startGoodbyeCountdown();
- }
- }
-
- function hideGoodbyePopup() {
- if (goodbyePopup) {
- goodbyePopup.style.display = 'none';
- }
- if (goodbyeTimeout) {
- clearTimeout(goodbyeTimeout);
- goodbyeTimeout = null;
- }
- if (goodbyeInterval) {
- clearInterval(goodbyeInterval);
- goodbyeInterval = null;
- }
- }
-
- function startGoodbyeCountdown() {
- const duration = 4000;
- const startTime = Date.now();
-
- goodbyeInterval = setInterval(() => {
- const elapsed = Date.now() - startTime;
- const progress = Math.min(elapsed / duration, 1);
- if (goodbyeProgressBar) {
- goodbyeProgressBar.style.width = (progress * 100) + '%';
- }
- if (progress >= 1) {
- clearInterval(goodbyeInterval);
- }
- }, 50);
-
- goodbyeTimeout = setTimeout(() => {
- window.location.href = 'https://legoisland.org';
- }, duration);
- }
-
- if (cancelBtn) {
- cancelBtn.addEventListener('click', showGoodbyePopup);
- }
-
- if (goodbyeCancelBtn) {
- goodbyeCancelBtn.addEventListener('click', hideGoodbyePopup);
- }
-
- if ('serviceWorker' in navigator) {
- Promise.all([
- configManager.init(),
- navigator.serviceWorker.register('/sw.js').then(() => navigator.serviceWorker.ready)
- ]).then(([configResult, swRegistration]) => {
- checkInitialCacheStatus();
- showOrHideGraphicsOptions();
-
- // Check if there's already a waiting service worker (update ready)
- if (swRegistration.waiting) {
- showUpdatePopup();
- }
-
- // Listen for new service worker updates
- swRegistration.addEventListener('updatefound', () => {
- const newWorker = swRegistration.installing;
- if (newWorker) {
- newWorker.addEventListener('statechange', () => {
- // When the new worker is installed and waiting, show the update popup
- // Only show if there's an existing controller (this is an update, not first install)
- if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
- showUpdatePopup(newWorker);
- }
- });
- }
- });
- }).catch(error => {
- console.error('Initialization failed:', error);
- });
-
- navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
- navigator.serviceWorker.addEventListener('controllerchange', () => {
- checkInitialCacheStatus();
- });
- }
- else {
- configManager.init().then(() => {
- showOrHideGraphicsOptions();
- });
- }
-
- const progressCircular = document.createElement('div');
- progressCircular.className = 'progress-circular';
- controlsContainer.appendChild(progressCircular);
-
- installBtn.addEventListener('click', async () => {
- if (navigator.serviceWorker && navigator.serviceWorker.controller) {
- await requestPersistentStorage();
-
- if (downloaderWorker) downloaderWorker.terminate();
- downloaderWorker = new Worker('/downloader.js');
- downloaderWorker.onmessage = handleWorkerMessage;
-
- 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)';
-
- downloaderWorker.postMessage({
- action: 'install',
- missingFiles: missingGameFiles,
- language: selectedLanguage
- });
- }
- });
-
- uninstallBtn.addEventListener('click', () => {
- const selectedLanguage = languageSelect.value;
- navigator.serviceWorker.controller.postMessage({
- action: 'uninstall_language_pack',
- language: selectedLanguage
- });
- });
-
- languageSelect.addEventListener('change', () => {
- checkInitialCacheStatus();
- });
-
- hdTextures.addEventListener('change', () => {
- checkInitialCacheStatus();
- });
-
- hdMusic.addEventListener('change', () => {
- checkInitialCacheStatus();
- });
-
- widescreenBgs.addEventListener('change', () => {
- checkInitialCacheStatus();
- });
-
- badEnding.addEventListener('change', () => {
- checkInitialCacheStatus();
- });
-
- rendererSelect.addEventListener('change', () => {
- showOrHideGraphicsOptions();
- });
-
- async function requestPersistentStorage() {
- if (navigator.storage && navigator.storage.persist) {
- const isPersisted = await navigator.storage.persisted();
- if (!isPersisted) {
- const wasGranted = await navigator.storage.persist();
- if (wasGranted) {
- console.log('Persistent storage was granted.');
- } else {
- console.log('Persistent storage request was denied.');
- }
- }
- }
- }
-
- function getSiFiles() {
- siFiles = [];
- if (hdMusic && hdMusic.checked) {
- siFiles.push('/LEGO/extra/hdmusic.si');
- }
- if (widescreenBgs && widescreenBgs.checked) {
- siFiles.push('/LEGO/extra/widescreen.si');
- }
- if (badEnding && badEnding.checked) {
- siFiles.push('/LEGO/extra/badend.si');
- }
- return siFiles;
- }
-
- function checkInitialCacheStatus() {
- if (navigator.serviceWorker.controller) {
- navigator.serviceWorker.controller.postMessage({
- action: 'check_cache_status',
- language: languageSelect.value,
- hdTextures: hdTextures.checked,
- siFiles: getSiFiles(),
- });
- }
- }
-
- function showOrHideGraphicsOptions() {
- if (rendererSelect.value == "0 0x682656f3 0x0 0x0 0x2000000") {
- msaaGroup.style.display = 'none';
- afGroup.style.display = 'none';
- }
- else {
- msaaGroup.style.display = '';
- afGroup.style.display = '';
- }
- }
-
- 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, isInstalled, success } = event.data;
- if (language && language !== languageSelect.value) return;
-
- switch (action) {
- case 'cache_status':
- missingGameFiles = event.data.missingFiles;
- updateInstallUI(isInstalled);
- break;
- case 'uninstall_complete':
- updateInstallUI(!success);
- checkInitialCacheStatus();
- break;
- }
- }
-
- function handleWorkerMessage(event) {
- const { action, progress, success, error } = event.data;
-
- switch (action) {
- 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);
- if (downloaderWorker) downloaderWorker.terminate();
- break;
- case 'install_failed':
- alert(`Download failed: ${error}`);
- updateInstallUI(false);
- if (downloaderWorker) downloaderWorker.terminate();
- break;
- }
- }
-});
diff --git a/debug.html b/debug.html
deleted file mode 100644
index 0f5c100..0000000
--- a/debug.html
+++ /dev/null
@@ -1,533 +0,0 @@
-⚙
-
-
-
-
General
-
Pause/Resume
-
Return to Infocenter
-
Skip Animation
-
Save Game
-
-
-
Debug Mode (OGEL)
-
Enter Debug Mode
-
Toggle FPS
-
Toggle Music
-
Reset/Load Plants
-
-
-
Camera/View
-
Move Up
-
Move Down
-
-
-
LOD (Level of Detail)
-
LOD 0.0 (Lowest)
-
LOD 3.6 (Default)
-
LOD 5.0 (Highest)
-
-
-
Misc
-
Make Plants Dance
-
-
-
Switch Act
-
Act 2
-
Act 3
-
Good Ending
-
Bad Ending
-
-
-
Locations
-
- -- Select Location --
- LCAMBA1 (01)
- LCAMBA2 (02)
- LCAMBA3 (03)
- LCAMBA4 (04)
- LCAMCA1 (05)
- LCAMCA2 (06)
- LCAMCA3 (07)
- LCAMGS1 (08)
- LCAMGS2 (09)
- LCAMGS3 (10)
- LCAMHO1 (11)
- LCAMHO2 (12)
- LCAMHO3 (13)
- LCAMIS1 (14)
- LCAMIS2 (15)
- LCAMIS3 (16)
- LCAMIS4 (17)
- LCAMIS5 (18)
- LCAMJA1 (19)
- LCAMJA2 (20)
- LCAMPO1 (21)
- LCAMPO2 (22)
- LCAMPO3 (23)
- LCAMPZ1 (24)
- LCAMPZ2 (25)
- LCAMRA1 (26)
- LCAMRA2 (27)
- LCAMRA3 (28)
- LCAMRA4 (29)
- LCAMRT1 (30)
- LCAMRT2 (31)
- LCAMRT3 (32)
- LCAMRT4 (33)
- LCAMRT5 (34)
- LCAMRT6 (35)
- LCAMRT7 (36)
- LCAMRT8 (37)
- LCAMRT9 (38)
- LCAMRT10 (39)
- LCAMRT11 (40)
- LCAMRT12 (41)
- LCAMRT13 (42)
- LCAMRT14 (43)
- LCAMRT15 (44)
- LCAMRT16 (45)
- LCAMRT17 (46)
- LCAMRT18 (47)
- LCAMRT19 (48)
- LCAMRT20 (49)
- LCAMRT21 (50)
- LCAMRT22 (51)
- LCAMRT23 (52)
- LCAMRT24 (53)
- LCAMRT25 (54)
- LCAMRT26 (55)
- LCAMRT27 (56)
- LCAMRT28 (57)
- LCAMRT29 (58)
- LCAMRT30 (59)
- LCAMRT31 (60)
- LCAMRT32 (61)
- LCAMRT33 (62)
- LCAMRT34 (63)
- LCAMRT35 (64)
- LCAMRT36 (65)
- LCAMRT37 (66)
- LCAMRT38 (67)
- LCAMRT39 (68)
- LCAMRT40 (69)
-
-
Go to Location
-
-
-
Animations
-
Play all cam animations
-
- -- Select Animation --
-
- wns050p1 (400)
- wns049p1 (401)
- wns048p1 (402)
- wns057rd (403)
- pns123pr (404)
- wns045di (405)
- wns053pr (406)
- wns046mg (407)
- wns051bd (408)
- pnsx48pr (409)
- pnsx69pr (410)
- pns125ni (411)
- pns122pr (412)
- pns050p1 (413)
- pns069pr (414)
- pns066db (415)
- pns065rd (416)
- pns067gd (417)
- pns099pr (418)
- pns098pr (419)
- pns097pr (420)
- pns096pr (421)
- pns042bm (422)
- pns045p1 (423)
- pns048pr (424)
- pns043en (425)
- pns022pr (426)
- pns018rd (427)
- pns019pr (428)
- pns021dl (429)
-
-
- sba001bu (500)
- sba002bu (501)
- sba003bu (502)
- bns146rd (503)
- bns144rd (504)
- fns017la (505)
- bns005p1 (506)
- bns147rd (507)
- igs001na (508)
- sns003nu (509)
- sgs001na (510)
- sns001nu (511)
- sns002nu (512)
- sgs002na (513)
- sgs003na (514)
- fns001re (515)
- fns0x1re (516)
- fns007re (517)
- fns011re (518)
- sns001cl (519)
- sns002cl (520)
- sns003cl (521)
- bns191en (522)
- bho142en (523)
- bic143sy (524)
- sja004br (525)
- sja005br (526)
- sja006br (527)
- sja007br (528)
- sja008br (529)
- sja009br (530)
- sja010br (531)
- sja011br (532)
- sja012br (533)
- sja013br (534)
- sja014br (535)
- sja015br (536)
- sja016br (537)
- sja017br (538)
- sja018br (539)
- sja001br (540)
- sja002br (541)
- sja003br (542)
- ijs001sn (543)
- fjs148gd (544)
- fjs149va (545)
- sjs001va (546)
- sjs002va (547)
- sjs003va (548)
- sjs004va (549)
- fjs019rd (550)
- bjs009gd (551)
- sjs001sn (552)
- sjs002sn (553)
- sjs003sn (554)
- sjs004sn (555)
- sjs005sn (556)
- snsx31sh (557)
- bns007gd (558)
- fns001l1 (559)
- fns001l2 (560)
- fra157bm (561)
- bns145rd (562)
- ips001ro (563)
- sns010ni (564)
- sns003la (565)
- fps181ni (566)
- ipz001rd (567)
- spz004ma (568)
- spz005ma (569)
- spz006ma (570)
- spz004pa (571)
- spz013ma (572)
- spz006pa (573)
- spz014ma (574)
- spz005pa (575)
- spz015ma (576)
- spz007ma (577)
- spz013pa (578)
- spz008ma (579)
- spz014pa (580)
- spz009ma (581)
- spz015pa (582)
- spz007pa (583)
- spz011pe (584)
- spz008pa (585)
- spz009pa (586)
- spz010ma (587)
- spz010pa (588)
- spz011ma (589)
- spz011pa (590)
- spz012pa (591)
- spz001ma (592)
- spz002ma (593)
- spz003ma (594)
- spz003pa (595)
- fpz166p1 (596)
- fpz172rd (597)
- spz001pa (598)
- spz002pa (599)
-
-
- ppz086bs (600)
- ppz008rd (601)
- ppz009pg (602)
- ivo918in (603)
- spz004pe (604)
- spz005pe (605)
- srp006pe (606)
- spz013pe (607)
- sns001pe (608)
- fra192pe (609)
- fra163mg (610)
- fns185gd (611)
- irt001in (612)
- irtx01sl (613)
- frt135df (614)
- frt137df (615)
- frt139df (616)
- frt025rd (617)
- frt132rd (618)
- srt001rd (619)
- srt003bd (620)
- sst001mg (621)
- sns004la (622)
- sns005la (623)
- sns006la (624)
- sps004ni (625)
- sps005ni (626)
- sps006ni (627)
- sns007la (628)
- sns008la (629)
- sns009la (630)
- sns007ni (631)
- sns008ni (632)
- sns009ni (633)
- pns017ml (634)
- sns010la (635)
- sns010pe (636)
- sns011la (637)
- sns012la (638)
- sns007pe (639)
- sns008pe (640)
- sns013la (641)
- sns013ni (642)
- sns014la (643)
- sns014ni (644)
- sns015la (645)
- sns015ni (646)
- sns011ni (647)
- sns012ni (648)
- sns014pe (649)
- sns015pe (650)
- sns003pe (651)
- sns017ni (652)
- sps001ni (653)
- sps002ni (654)
- sps003ni (655)
- sns017la (656)
- sps001la (657)
- sps002la (658)
- bns005pg (659)
- sns001ml (660)
- sns002mg (661)
- sns002ml (662)
- sns002pe (663)
- sns003mg (664)
- sns004mg (665)
- sns004rd (666)
- sns006bd (667)
- sns006ro (668)
- sns011in (669)
- sps001ro (670)
- sps002ro (671)
- sps003ro (672)
- sps004ro (673)
- srt005pg (674)
- pns100ml (675)
- ppz029rd (676)
- sns007sy (677)
- cnsx12la (678)
- cnsx12ni (679)
- ijs006sn (680)
- igs008na (681)
- irt007in (682)
- ips002ro (683)
- hho142cl (684)
- hho143cl (685)
- hho144cl (686)
- hho027en (687)
- hps116bd (688)
- hps117bd (689)
- hps118re (690)
- hps120en (691)
- hps122en (692)
- hpz047pe (693)
- hpz048pe (694)
- hpz049bd (695)
- hpz050bd (696)
- hpz052ma (697)
- hpz053pa (698)
- hpz055pa (699)
-
-
- hpz057ma (700)
- hpza51gd (701)
- hpzb51gd (702)
- hpzc51gd (703)
- hpzf51gd (704)
- hpzw51gd (705)
- hpzx51gd (706)
- hpzy51gd (707)
- hpzz51gd (708)
- nic002pr (709)
- nic003pr (710)
- nic004pr (711)
- pps025ni (712)
- pps026ni (713)
- pps027ni (714)
- ppz001pe (715)
- ppz006pa (716)
- ppz007pa (717)
- ppz010pa (718)
- ppz011pa (719)
- ppz013pa (720)
- ppz014pe (721)
- ppz015pe (722)
- ppz016pe (723)
- pgs050nu (724)
- pgs051nu (725)
- pgs052nu (726)
- ppz031ma (727)
- ppz035pa (728)
- ppz036pa (729)
- ppz037ma (730)
- ppz038ma (731)
- ppz054ma (732)
- ppz055ma (733)
- ppz056ma (734)
- ppz059ma (735)
- ppz060ma (736)
- ppz061ma (737)
- ppz064ma (738)
- prt072sl (739)
- prt073sl (740)
- prt074sl (741)
- pho104re (742)
- pho105re (743)
- pho106re (744)
- ppz075pa (745)
- ppz082pa (746)
- ppz084pa (747)
- ppz088ma (748)
- ppz089ma (749)
- ppz090ma (750)
- ppz093pe (751)
- ppz094pe (752)
- ppz095pe (753)
- prp101pr (754)
- pja126br (755)
- pja127br (756)
- pja129br (757)
- pja130br (758)
- pja131br (759)
- pja132br (760)
- ppz107ma (761)
- ppz114pa (762)
- ppz117ma (763)
- ppz118ma (764)
- ppz119ma (765)
- ppz120pa (766)
- wgs083nu (767)
- wgs085nu (768)
- wgs086nu (769)
- wgs087nu (770)
- wgs088nu (771)
- wgs089nu (772)
- wgs090nu (773)
- wgs091nu (774)
- wgs092nu (775)
- wgs093nu (776)
- wgs094nu (777)
- wgs095nu (778)
- wgs096nu (779)
- wgs097nu (780)
- wgs098nu (781)
- wgs099nu (782)
- wgs100nu (783)
- wgs101nu (784)
- wgs102nu (785)
- wgs103nu (786)
- wrt060bm (787)
- wrt074sl (788)
- wrt075rh (789)
- wrt076df (790)
- wrt078ni (791)
- wrt079bm (792)
- npz001bd (793)
- npz002bd (794)
- npz003bd (795)
- npz004bd (796)
- npz005bd (797)
- npz006bd (798)
- npz007bd (799)
-
-
- nca001ca (800)
- nca002sk (801)
- nca003gh (802)
- nla001ha (803)
- nla002sd (804)
- npa001ns (805)
- npa002ns (806)
- npa003ns (807)
- npa004ns (808)
- npa005dl (809)
- npa007dl (810)
- npa009dl (811)
- npa010db (812)
- npa012db (813)
- npa014db (814)
- npa015ca (815)
- npa017ca (816)
- npa019ca (817)
- npa020p1 (818)
- npa022p1 (819)
- npa024p1 (820)
- npa025sh (821)
- npa027sh (822)
- npa029sh (823)
- npa030fl (824)
- npa031fl (825)
- npa032fl (826)
- npa034bh (827)
- npa035bh (828)
- npa036bh (829)
- npa038pn (830)
- npa039pn (831)
- npa040pn (832)
- npa042pm (833)
- npa043pm (834)
- npa044pm (835)
- npa046sr (836)
- npa047sr (837)
- npa048sr (838)
- npa050ba (839)
- npa051ba (840)
- npa052ba (841)
- npa054po (842)
- npa055po (843)
- npa056po (844)
- npa058r1 (845)
- npa059r1 (846)
- npa060r1 (847)
- npa061r3 (848)
- npa062r2 (849)
- npa062r3 (850)
- npa063r2 (851)
- npa063r3 (852)
- npa065r2 (853)
- nja001pr (854)
- nja002pr (855)
- sjs007in (856)
- sns005in (857)
- sns006in (858)
- sns008in (859)
- sjs012in (860)
- sjs013in (861)
- sjs014in (862)
- sjs015in (863)
- srt001in (864)
- srt002in (865)
- srt003in (866)
- srt004in (867)
- nrtflag0 (868)
-
-
-
Play Animation
-
-
diff --git a/debug.js b/debug.js
deleted file mode 100644
index 0cb3b37..0000000
--- a/debug.js
+++ /dev/null
@@ -1,214 +0,0 @@
-(async function() {
- const debugUI = document.getElementById('debug-ui');
- const canvas = document.getElementById('canvas');
-
- // Fetch and inject debug panel HTML
- try {
- const response = await fetch('debug.html');
- const html = await response.text();
- debugUI.innerHTML = html;
- } catch (error) {
- console.error('Failed to load debug panel:', error);
- return;
- }
-
- // Now get references to elements after they've been injected
- const debugToggle = document.getElementById('debug-toggle');
- const debugPanel = document.getElementById('debug-panel');
- const debugPasswordBtn = document.querySelector('.debug-password');
- const requiresDebugBtns = document.querySelectorAll('.requires-debug');
-
- let debugModeActive = false;
-
- // Key code mapping for special keys
- const keyCodeMap = {
- 'Pause': { key: 'Pause', code: 'Pause', keyCode: 19 },
- 'Escape': { key: 'Escape', code: 'Escape', keyCode: 27 },
- ' ': { key: ' ', code: 'Space', keyCode: 32 },
- 'Tab': { key: 'Tab', code: 'Tab', keyCode: 9 },
- 'F11': { key: 'F11', code: 'F11', keyCode: 122 },
- 'F12': { key: 'F12', code: 'F12', keyCode: 123 },
- '+': { key: '+', code: 'NumpadAdd', keyCode: 107 },
- '-kp': { key: '-', code: 'NumpadSubtract', keyCode: 109 },
- '*': { key: '*', code: 'NumpadMultiply', keyCode: 106 },
- '/': { key: '/', code: 'NumpadDivide', keyCode: 111 },
- // Digit keys
- '0': { key: '0', code: 'Digit0', keyCode: 48 },
- '1': { key: '1', code: 'Digit1', keyCode: 49 },
- '2': { key: '2', code: 'Digit2', keyCode: 50 },
- '3': { key: '3', code: 'Digit3', keyCode: 51 },
- '4': { key: '4', code: 'Digit4', keyCode: 52 },
- '5': { key: '5', code: 'Digit5', keyCode: 53 },
- '6': { key: '6', code: 'Digit6', keyCode: 54 },
- '7': { key: '7', code: 'Digit7', keyCode: 55 },
- '8': { key: '8', code: 'Digit8', keyCode: 56 },
- '9': { key: '9', code: 'Digit9', keyCode: 57 },
- };
-
- // Toggle debug panel
- debugToggle.addEventListener('click', function(e) {
- e.stopPropagation();
- debugPanel.classList.toggle('open');
- debugToggle.classList.toggle('active');
- });
-
- // Dispatch a keyboard event to the canvas
- function sendKey(key) {
- let keyInfo = keyCodeMap[key];
-
- if (!keyInfo) {
- // Regular character key (letters)
- const char = key.toLowerCase();
- const charCode = char.charCodeAt(0);
- keyInfo = {
- key: char,
- code: 'Key' + char.toUpperCase(),
- keyCode: charCode >= 97 && charCode <= 122 ? charCode - 32 : charCode
- };
- }
-
- const eventInit = {
- key: keyInfo.key,
- code: keyInfo.code,
- keyCode: keyInfo.keyCode,
- which: keyInfo.keyCode,
- bubbles: true,
- cancelable: true
- };
-
- canvas.dispatchEvent(new KeyboardEvent('keydown', eventInit));
- canvas.dispatchEvent(new KeyboardEvent('keyup', eventInit));
- }
-
- // Send a sequence of keys with delay (longer delay for multi-stage commands)
- function sendKeySequence(keys, delay = 100) {
- let index = 0;
- function sendNext() {
- if (index < keys.length) {
- sendKey(keys[index]);
- index++;
- setTimeout(sendNext, delay);
- } else {
- canvas.focus();
- }
- }
- sendNext();
- }
-
- // Update button states based on debug mode
- function updateDebugModeUI() {
- if (debugModeActive) {
- debugPasswordBtn.classList.add('active');
- debugPasswordBtn.textContent = 'Debug Mode Active';
- requiresDebugBtns.forEach(btn => btn.classList.add('enabled'));
- } else {
- debugPasswordBtn.classList.remove('active');
- debugPasswordBtn.textContent = 'Enter Debug Mode';
- requiresDebugBtns.forEach(btn => btn.classList.remove('enabled'));
- }
- }
-
- // Handle button clicks
- debugPanel.addEventListener('click', function(e) {
- const btn = e.target.closest('button');
- if (!btn || btn === debugToggle) return;
-
- const keys = btn.dataset.keys;
- if (!keys) return;
-
- e.preventDefault();
- e.stopPropagation();
-
- // Handle special cases
- if (keys === 'ogel') {
- // Enter debug password
- sendKeySequence(['o', 'g', 'e', 'l']);
- debugModeActive = true;
- updateDebugModeUI();
- return;
- }
-
- // For requires-debug buttons, ensure debug mode is active
- if (btn.classList.contains('requires-debug') && !debugModeActive) {
- // Auto-enter debug mode first
- sendKeySequence(['o', 'g', 'e', 'l']);
- debugModeActive = true;
- updateDebugModeUI();
- // Then send the actual keys after a delay
- setTimeout(() => {
- sendKeySequence(keys.split(''));
- }, 500);
- return;
- }
-
- // Handle multi-key sequences (like 'g1' for act switch or 'c00' for locations)
- if (keys.length > 1 && !keyCodeMap[keys]) {
- sendKeySequence(keys.split(''));
- } else {
- sendKey(keys);
- canvas.focus();
- }
- });
-
- // Handle location teleport
- const locationSelect = document.getElementById('debug-location-select');
- const gotoLocationBtn = document.getElementById('debug-goto-location');
-
- gotoLocationBtn.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- const locationValue = locationSelect.value;
- if (!locationValue) return;
-
- // Ensure debug mode is active
- if (!debugModeActive) {
- sendKeySequence(['o', 'g', 'e', 'l']);
- debugModeActive = true;
- updateDebugModeUI();
- // Then send location keys after a delay
- setTimeout(() => {
- sendKeySequence(locationValue.split(''));
- }, 500);
- return;
- }
-
- sendKeySequence(locationValue.split(''));
- });
-
- // Handle animation playback
- const animationSelect = document.getElementById('debug-animation-select');
- const playAnimationBtn = document.getElementById('debug-play-animation');
-
- playAnimationBtn.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- const animationId = animationSelect.value;
- if (!animationId) return;
-
- // Ensure debug mode is active
- if (!debugModeActive) {
- sendKeySequence(['o', 'g', 'e', 'l']);
- debugModeActive = true;
- updateDebugModeUI();
- // Then send animation keys after a delay
- setTimeout(() => {
- playAnimation(animationId);
- }, 500);
- return;
- }
-
- playAnimation(animationId);
- });
-
- function playAnimation(animationId) {
- // Animation command: 'v' + 3 digits (padded with leading zeros)
- const paddedId = animationId.toString().padStart(3, '0');
- const keys = ['v', ...paddedId.split('')];
- sendKeySequence(keys);
- }
-
- // Initialize UI
- updateDebugModeUI();
-})();
diff --git a/index.html b/index.html
index 333e4ce..e64cc0f 100644
--- a/index.html
+++ b/index.html
@@ -6,14 +6,6 @@
LEGO® Island
-
-
-
-
-
-
-
-
-
@@ -33,849 +24,19 @@
-
+
-
-
-
-
-
-
-
-
-
-
- Your browser does not support the audio tag.
-
-
🔇
-
-
-
-
-
-
-
-
← Back
-
-
Read Me
-
-
-
-
-
-
Welcome to the LEGO Island web port project! This is a recreation of the classic 1997 PC game,
- rebuilt to run in modern web browsers using Emscripten and WebAssembly.
-
This incredible project stands on the shoulders of giants. It was made possible by the original decompilation project , which achieved 100% decompilation of the
- original game. This was then adapted into a portable version that eliminated all Windows dependencies and
- replaced them with modern, cross-platform alternatives.
-
The technical work involved replacing Windows-specific systems with SDL for window management and input,
- migrating audio from DirectSound to the miniaudio library, converting Windows Registry configuration
- to INI files, and creating a modular graphics layer supporting multiple rendering backends including
- WebGL. This represents years of effort from many awesome contributors dedicated to preserving this
- piece of gaming history.
-
Thanks to this work, LEGO Island now runs on over 10 platforms including Windows, Linux, macOS, iOS,
- Android, Nintendo Switch, PlayStation Vita, and of course, web browsers. The web version uses the
- original, unmodified Interleaf streaming code, enabling progressive content loading just like the
- original CD-ROM.
-
Our goal is to make this classic accessible to everyone. The project is still in development, so you
- may encounter bugs. Your patience and feedback are greatly appreciated!
-
-
-
-
-
Supported Browsers
-
This game requires a modern browser with WebAssembly multi-threading support. The following browsers are supported:
-
- Chrome — version 95 or newer
- Firefox — version 92 or newer
- Edge — version 95 or newer
- Safari — version 15.4 or newer (iOS 18+ recommended)
-
-
For the best experience, keep your browser updated to the latest version.
-
-
-
-
Input Methods
-
The game supports multiple ways to play. Visit the Configure page to adjust your control preferences.
-
- Keyboard & Mouse — Traditional desktop controls using arrow keys or WASD
- Gamepad — Controller support with analog sticks and D-pad
- Touch Screen — Mobile-friendly controls with configurable schemes
-
-
-
-
-
Audio
-
Audio hardware is recommended for the full experience. If the game is silent, click the mute icon
- (🔇) on the animated intro to enable sound. Modern browsers require user interaction before playing audio.
-
-
-
-
Storage & Network
-
The game streams approximately 25MB of data on first load (more with extensions enabled).
- For offline play, you can install the full game (about 550MB ) via the Configure menu.
- A stable internet connection is recommended for initial loading.
-
-
-
-
Performance Tips
-
- Close other browser tabs to free up memory
- Use hardware acceleration (enabled by default in most browsers)
- On mobile, ensure your device isn't in low-power mode
- If experiencing lag, try reducing the resolution in Configure
-
-
-
-
-
-
- Is this the full, original game?
- This is a complete port of the original 1997 PC game. You can select from multiple languages,
- including both the 1.0 and 1.1 versions of English, from the "Configure" menu before
- starting.
-
-
- How does this differ from the original 1997 CD-ROM game?
- The core gameplay is identical, but this version has some great advantages! It runs in your
- browser with no installation needed and works on modern devices. It also includes
- enhancements like widescreen support, improved controls, many bug fixes from the
- decompilation project, and the ability to run at your display's maximum resolution (even
- 4K!).
- Check out the "Configure" page to see what's possible.
-
-
- Can I save my progress?
- Yes! The game automatically saves your progress. To ensure your game is saved, return to the
- Infocenter and use the exit door. This will bring you back to the main menu and lock in your
- save state. A "best effort" save is also attempted if you close the tab directly, but this
- method isn't always guaranteed.
-
-
- Does this run on mobile?
- Yes! The game is designed to work on a wide range of devices, including desktops, laptops,
- tablets, and phones. It has even been seen running on Tesla in-car browsers ! 🚗
-
-
- Which browsers are supported?
- This port runs best on recent versions of modern browsers, including Chrome, Firefox, and
- Safari. For an optimal experience on iOS devices, please ensure you are running iOS 18 or
- newer.
-
-
- What are the controls?
- You can play using a keyboard and mouse, a gamepad, or a touch screen. Gamepad support can
- vary depending on your browser. On mobile, you can select your preferred touch control
- scheme in the "Configure" menu.
-
-
- Can I play offline?
- You bet! In the "Configure" menu, scroll to the "Offline Play" section. You'll find an option
- there to install all necessary game files (about 550MB) for offline access.
-
-
- I don't hear any sound or music. How do I fix it?
- Most modern browsers block audio until you interact with the page. Click the mute icon (🔇)
- on the animated intro to enable sound.
-
-
- I think I found a bug! Where do I report it?
- As an active development project, some bugs are expected. If you find one, we'd be grateful
- if you'd report it on the isle-portable GitHub Issues page . Please include details about your
- browser, device, and what you were doing when the bug occurred.
-
-
- Is this project open-source?
- Yes, absolutely! This web port is built upon the incredible open-source LEGO Island (portable) project, and the code for this
- website is also available here .
-
-
-
-
-
- January 2026
-
- New: Debug menu for developers and power users. Tap the LEGO Island logo 5 times to unlock OGEL mode and access debug features like teleporting to locations, switching acts, and playing animations
- Improved: Configure page redesigned with tabbed navigation, collapsible sections, quick presets (Classic/Modern Mode), and modern toggle switches
- Improved: Read Me page reorganized into tabs (About, System, FAQ, Changelog, Manual) with the original instruction manual now viewable in-browser
-
-
-
- December 2025
-
- New: "Active in Background" option keeps the game running when the tab loses focus
- New: WASD navigation controls as an alternative to arrow keys
- Fixed: Act 3 helicopter ammo now correctly sticks to targets and finishes animations
- Fixed: Pick/click distance calculation for more accurate object selection
- Fixed: Maximum deltaTime capping prevents physics glitches in races
- Fixed: Touch controls now properly support widescreen aspect ratios
- Improved: Default anisotropic filtering increased to 16x for sharper textures
-
-
-
- November 2025
-
- Fixed: Dictionary loading failure no longer causes crashes
- Fixed: INI configuration now properly applies defaults when values are missing
-
-
-
- September 2025
-
- New: Additional widescreen background images
- Fixed: Jukebox state now correctly restored when using HD Music extension
- Fixed: Background audio no longer gets stuck when starting audio fails
- Improved: SI Loader actions now start at the correct time during world loading
-
-
-
- August 2025
-
- New: Extended Bad Ending FMV extension shows the uncut beta animation
- New: HD Music extension with high-quality audio
- New: Widescreen backgrounds extension eliminates 3D edges on wide displays
- New: SI Loader extension system for community content and modifications
- New: OpenGL ES 2.0/3.0 renderer for broader device compatibility
- Fixed: Purple edges no longer appear on scaled transparent 2D elements
- Fixed: Transparent pixels now render correctly with alpha channel support
-
-
-
- July 2025
-
- New: HD Textures extension with enhanced visuals
- New: MSAA anti-aliasing support for smoother edges
- New: Anisotropic filtering for sharper textures at angles
- New: Haptic feedback (vibration) support for gamepads and mobile devices
- New: Virtual Gamepad touch control scheme with sliding controls
- New: Gamepad/controller support with analog sticks and D-pad
- New: Full screen mode with in-game toggle
- New: Maximum LOD and Maximum Actors configuration options
- New: Configurable transition animations (Mosaic, Dissolve, Wipe, etc.)
- New: Extensions system allowing community-created content
- Fixed: WebGL driver compatibility issues resolved
- Fixed: Firefox Private browsing mode now works correctly
- Fixed: Virtual cursor transparency and positioning
- Fixed: Touch coordinate translation for proper viewport mapping
- Fixed: Memory leaks in ViewLODList
- Fixed: Screen transitions on software renderer and 32-bit displays
- Fixed: Tabbing in and out of fullscreen
- Fixed: Click spam prevention on touch screens
- Improved: Mosaic transition animation is faster and cleaner
- Improved: Loading UX for HD Textures with progress indicators
-
-
-
- June 2025 — Initial Release
-
- New: Emscripten web port — play LEGO Island directly in your browser!
- New: WebGL rendering for hardware-accelerated 3D graphics
- New: Software renderer fallback for devices without WebGL
- New: 32-bit color support for improved visual quality
- New: Full screen support
- New: Joystick/gamepad enabled by default
- New: Option to skip the startup delay
- New: Support for LEGO Island 1.0 version
- New: FPS display option
- New: Game runs without requiring an audio device
- Fixed: Infocenter to Act 2/Act 3 transition issues
- Fixed: Race initialization errors
- Fixed: Jetski race startup issues
- Fixed: Plant creation bug in LegoPlantManager
- Fixed: Late-game "sawtooth" audio glitches
- Fixed: Building variant switching (Pepper's buildings)
- Fixed: OpenGL rendering issues
- Fixed: Image serialization bugs
- Improved: Transparent objects now render correctly (sorted last)
- Improved: GPU mesh uploading via VBOs for better performance
- Improved: Backface culling enabled for faster rendering
- Improved: SIMD-optimized z-buffer clearing
- Improved: Edge-walking triangle rasterization
-
-
-
-
-
-
-
-
Reactions from the original LEGO Island development team:
-
-
- This is just fantastic! What an endeavor! It is a wonderful tribute to a team that was
- unparalleled in talent, and we should now include you and your team in that august group.
- I really wish Wes was around to see it. Keep us posted on updates...
-
-
-
- Wow; what a trip. My first trial was on my mac; which had problems displaying any of the
- bitmaps applied to the characters in the safari web browser. But it ran, with some
- navigation frustrations. But being delivered over the web means any fix you make goes out
- immediately. I want you all to know it was a joy to work on and how grateful I am to have
- been a part of the origin. I hope you are getting joy from working on it and keeping it alive.
-
-
-
- This is pretty neat. At least as responsive over the web as the game was on the target
- machines of the time! I hadn't heard of WebAssembly until now. What kind of changes to
- the source were needed to get it working under WebAssembly? I foresee many hours of my
- time being used up experimenting with this tool!
-
-
-
- Well done and such fun tapping back into such fond creative memories.
-
-
-
- That's awesome!
-
-
-
- Fantastic! Love it.
-
-
-
- Great stuff!
-
-
-
-
-
-
-
-
-
-
← Back
-
- OPFS is disabled in this browser. Default configuration will apply. If you are using a Firefox
- Private window, please change to a regular window instead to change configuration.
-
-
-
-
-
-
-
- Classic Mode
- Modern Mode
-
-
-
- Display
- Controls
- Audio
- Extras
-
-
-
-
-
-
Settings saved
-
-
-
-
← Back
-
-
-
-
-
-
-
- "In November of 2010, after all was said and done, I started getting emails from a few kids
- and some adults telling me how cool they thought LEGO Island was. Some people actually still
- play it. I was quite thrilled by these emails and actually quite honored."
- Wes Jenkins, Creative Director
-
-
-
- The Making of LEGO Island: A Documentary
- An in-depth documentary by MattKC that explores the fascinating and chaotic development story
- behind the classic game.
-
-
- LEGO Island Radio 24/7
- Enjoy the iconic, high-quality soundtrack of LEGO Island anytime with this continuous live
- stream, complete with the original DJ interludes.
-
-
- LEGO Island Wiki
- Your ultimate resource for all things LEGO Island. This fan-run wiki contains a wealth of
- information, research, and details about the game.
-
-
- LEGO Island Decompilation
- The core open-source project that reverse-engineered the original game, making this web port
- and other mods possible. Dive into the source code here.
-
-
- LEGO Island, Portable Version
- A portable, cross-platform version of the decompilation project which serves as the direct
- foundation for this web-based port.
-
-
- isle.pizza Frontend
- The source code for this website! A custom-built frontend for the Emscripten version of the
- portable decompilation project.
-
-
- LEGO Island Rebuilder
- A powerful launcher and tool for patching and modding the original 1997 PC version of LEGO
- Island. Essential for play and modding.
-
-
- SIEdit
- A suite of tools developed by the decompilation team for viewing and editing the ".si" script
- and resource files from the original game.
-
-
- The Making of LEGO Island, a memoir by Wes Jenkins
- Read the fascinating, incomplete memoir from Creative Director Wes Jenkins, detailing the
- development process and the team behind the game.
-
-
- LEGO Island: Free Poster
- Download a copy of the iconic poster that was originally included with the retail release of
- the game.
-
-
- Development Materials Archive
- Explore a collection of development materials, concept art, and other historical assets from
- the creation of LEGO Island.
-
-
- Video Game Flashback: An Interview with Wes Jenkins
- A detailed interview with LEGO Island's Creative Director, Wes Jenkins, offering unique
- insights into the game's production.
-
-
- LEGO® Island - Behind the Scenes
- Watch a rare promotional video created during the game's development, showcasing its progress
- and vision at the time.
-
-
- The Cutting Room Floor
- Discover unused assets, hidden data, and other secrets left in the retail version of the
- game. A fascinating look at what might have been.
-
-
- Project Island High Quality Music
- A complete, high-quality re-digitization of the LEGO Island soundtrack, restored by the
- game's main composer, Lorin Nelson.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
"Whoops! You have to put the CD in your computer"
-
- The Infomaniac (1997)
-
-
-
"Hello! Hola! Aloha! How ya doin'? YO!" It's your pal, the Infomaniac, with a 2025 update! No need to
- search for that CD case, my friend!
-
This amazing LEGO Island adventure is now streaming directly from... well, from a really, really big
- digital box of bricks! Keep an eye on the status below!
-
-
- Loading LEGO® Island... please wait! 0%
-
-
-
-
-
-
-
-
-
+
+
+