Migrate frontend to Svelte 5
- Replace vanilla JS with Svelte 5 components - Add Vite build system with Terser optimization - Reorganize assets into src/ and public/ directories - Update README with setup instructions
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
isle.wasm
|
||||
isle.wasm.map
|
||||
isle.js
|
||||
|
||||
76
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.
|
||||
|
||||
533
debug.html
@ -1,533 +0,0 @@
|
||||
<button id="debug-toggle" title="Debug Options">⚙</button>
|
||||
<div id="debug-panel">
|
||||
<div class="debug-header">Debug Options</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">General</div>
|
||||
<button data-keys="Pause">Pause/Resume</button>
|
||||
<button data-keys="Escape">Return to Infocenter</button>
|
||||
<button data-keys=" ">Skip Animation</button>
|
||||
<button data-keys="F12">Save Game</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Debug Mode (OGEL)</div>
|
||||
<button data-keys="ogel" class="debug-password">Enter Debug Mode</button>
|
||||
<button data-keys="Tab" class="requires-debug">Toggle FPS</button>
|
||||
<button data-keys="s" class="requires-debug">Toggle Music</button>
|
||||
<button data-keys="p" class="requires-debug">Reset/Load Plants</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Camera/View</div>
|
||||
<button data-keys="u" class="requires-debug">Move Up</button>
|
||||
<button data-keys="d" class="requires-debug">Move Down</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">LOD (Level of Detail)</div>
|
||||
<button data-keys="f" class="requires-debug">LOD 0.0 (Lowest)</button>
|
||||
<button data-keys="x" class="requires-debug">LOD 3.6 (Default)</button>
|
||||
<button data-keys="h" class="requires-debug">LOD 5.0 (Highest)</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Misc</div>
|
||||
<button data-keys="z">Make Plants Dance</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Switch Act</div>
|
||||
<button data-keys="g2" class="requires-debug">Act 2</button>
|
||||
<button data-keys="g3" class="requires-debug">Act 3</button>
|
||||
<button data-keys="g4" class="requires-debug">Good Ending</button>
|
||||
<button data-keys="g5" class="requires-debug">Bad Ending</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Locations</div>
|
||||
<select id="debug-location-select">
|
||||
<option value="">-- Select Location --</option>
|
||||
<option value="c01">LCAMBA1 (01)</option>
|
||||
<option value="c02">LCAMBA2 (02)</option>
|
||||
<option value="c03">LCAMBA3 (03)</option>
|
||||
<option value="c04">LCAMBA4 (04)</option>
|
||||
<option value="c05">LCAMCA1 (05)</option>
|
||||
<option value="c06">LCAMCA2 (06)</option>
|
||||
<option value="c07">LCAMCA3 (07)</option>
|
||||
<option value="c08">LCAMGS1 (08)</option>
|
||||
<option value="c09">LCAMGS2 (09)</option>
|
||||
<option value="c10">LCAMGS3 (10)</option>
|
||||
<option value="c11">LCAMHO1 (11)</option>
|
||||
<option value="c12">LCAMHO2 (12)</option>
|
||||
<option value="c13">LCAMHO3 (13)</option>
|
||||
<option value="c14">LCAMIS1 (14)</option>
|
||||
<option value="c15">LCAMIS2 (15)</option>
|
||||
<option value="c16">LCAMIS3 (16)</option>
|
||||
<option value="c17">LCAMIS4 (17)</option>
|
||||
<option value="c18">LCAMIS5 (18)</option>
|
||||
<option value="c19">LCAMJA1 (19)</option>
|
||||
<option value="c20">LCAMJA2 (20)</option>
|
||||
<option value="c21">LCAMPO1 (21)</option>
|
||||
<option value="c22">LCAMPO2 (22)</option>
|
||||
<option value="c23">LCAMPO3 (23)</option>
|
||||
<option value="c24">LCAMPZ1 (24)</option>
|
||||
<option value="c25">LCAMPZ2 (25)</option>
|
||||
<option value="c26">LCAMRA1 (26)</option>
|
||||
<option value="c27">LCAMRA2 (27)</option>
|
||||
<option value="c28">LCAMRA3 (28)</option>
|
||||
<option value="c29">LCAMRA4 (29)</option>
|
||||
<option value="c30">LCAMRT1 (30)</option>
|
||||
<option value="c31">LCAMRT2 (31)</option>
|
||||
<option value="c32">LCAMRT3 (32)</option>
|
||||
<option value="c33">LCAMRT4 (33)</option>
|
||||
<option value="c34">LCAMRT5 (34)</option>
|
||||
<option value="c35">LCAMRT6 (35)</option>
|
||||
<option value="c36">LCAMRT7 (36)</option>
|
||||
<option value="c37">LCAMRT8 (37)</option>
|
||||
<option value="c38">LCAMRT9 (38)</option>
|
||||
<option value="c39">LCAMRT10 (39)</option>
|
||||
<option value="c40">LCAMRT11 (40)</option>
|
||||
<option value="c41">LCAMRT12 (41)</option>
|
||||
<option value="c42">LCAMRT13 (42)</option>
|
||||
<option value="c43">LCAMRT14 (43)</option>
|
||||
<option value="c44">LCAMRT15 (44)</option>
|
||||
<option value="c45">LCAMRT16 (45)</option>
|
||||
<option value="c46">LCAMRT17 (46)</option>
|
||||
<option value="c47">LCAMRT18 (47)</option>
|
||||
<option value="c48">LCAMRT19 (48)</option>
|
||||
<option value="c49">LCAMRT20 (49)</option>
|
||||
<option value="c50">LCAMRT21 (50)</option>
|
||||
<option value="c51">LCAMRT22 (51)</option>
|
||||
<option value="c52">LCAMRT23 (52)</option>
|
||||
<option value="c53">LCAMRT24 (53)</option>
|
||||
<option value="c54">LCAMRT25 (54)</option>
|
||||
<option value="c55">LCAMRT26 (55)</option>
|
||||
<option value="c56">LCAMRT27 (56)</option>
|
||||
<option value="c57">LCAMRT28 (57)</option>
|
||||
<option value="c58">LCAMRT29 (58)</option>
|
||||
<option value="c59">LCAMRT30 (59)</option>
|
||||
<option value="c60">LCAMRT31 (60)</option>
|
||||
<option value="c61">LCAMRT32 (61)</option>
|
||||
<option value="c62">LCAMRT33 (62)</option>
|
||||
<option value="c63">LCAMRT34 (63)</option>
|
||||
<option value="c64">LCAMRT35 (64)</option>
|
||||
<option value="c65">LCAMRT36 (65)</option>
|
||||
<option value="c66">LCAMRT37 (66)</option>
|
||||
<option value="c67">LCAMRT38 (67)</option>
|
||||
<option value="c68">LCAMRT39 (68)</option>
|
||||
<option value="c69">LCAMRT40 (69)</option>
|
||||
</select>
|
||||
<button id="debug-goto-location" class="requires-debug">Go to Location</button>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Animations</div>
|
||||
<button data-keys="va" class="requires-debug">Play <b>all</b> cam animations</button>
|
||||
<select id="debug-animation-select">
|
||||
<option value="">-- Select Animation --</option>
|
||||
<optgroup label="400-429">
|
||||
<option value="400">wns050p1 (400)</option>
|
||||
<option value="401">wns049p1 (401)</option>
|
||||
<option value="402">wns048p1 (402)</option>
|
||||
<option value="403">wns057rd (403)</option>
|
||||
<option value="404">pns123pr (404)</option>
|
||||
<option value="405">wns045di (405)</option>
|
||||
<option value="406">wns053pr (406)</option>
|
||||
<option value="407">wns046mg (407)</option>
|
||||
<option value="408">wns051bd (408)</option>
|
||||
<option value="409">pnsx48pr (409)</option>
|
||||
<option value="410">pnsx69pr (410)</option>
|
||||
<option value="411">pns125ni (411)</option>
|
||||
<option value="412">pns122pr (412)</option>
|
||||
<option value="413">pns050p1 (413)</option>
|
||||
<option value="414">pns069pr (414)</option>
|
||||
<option value="415">pns066db (415)</option>
|
||||
<option value="416">pns065rd (416)</option>
|
||||
<option value="417">pns067gd (417)</option>
|
||||
<option value="418">pns099pr (418)</option>
|
||||
<option value="419">pns098pr (419)</option>
|
||||
<option value="420">pns097pr (420)</option>
|
||||
<option value="421">pns096pr (421)</option>
|
||||
<option value="422">pns042bm (422)</option>
|
||||
<option value="423">pns045p1 (423)</option>
|
||||
<option value="424">pns048pr (424)</option>
|
||||
<option value="425">pns043en (425)</option>
|
||||
<option value="426">pns022pr (426)</option>
|
||||
<option value="427">pns018rd (427)</option>
|
||||
<option value="428">pns019pr (428)</option>
|
||||
<option value="429">pns021dl (429)</option>
|
||||
</optgroup>
|
||||
<optgroup label="500-599">
|
||||
<option value="500">sba001bu (500)</option>
|
||||
<option value="501">sba002bu (501)</option>
|
||||
<option value="502">sba003bu (502)</option>
|
||||
<option value="503">bns146rd (503)</option>
|
||||
<option value="504">bns144rd (504)</option>
|
||||
<option value="505">fns017la (505)</option>
|
||||
<option value="506">bns005p1 (506)</option>
|
||||
<option value="507">bns147rd (507)</option>
|
||||
<option value="508">igs001na (508)</option>
|
||||
<option value="509">sns003nu (509)</option>
|
||||
<option value="510">sgs001na (510)</option>
|
||||
<option value="511">sns001nu (511)</option>
|
||||
<option value="512">sns002nu (512)</option>
|
||||
<option value="513">sgs002na (513)</option>
|
||||
<option value="514">sgs003na (514)</option>
|
||||
<option value="515">fns001re (515)</option>
|
||||
<option value="516">fns0x1re (516)</option>
|
||||
<option value="517">fns007re (517)</option>
|
||||
<option value="518">fns011re (518)</option>
|
||||
<option value="519">sns001cl (519)</option>
|
||||
<option value="520">sns002cl (520)</option>
|
||||
<option value="521">sns003cl (521)</option>
|
||||
<option value="522">bns191en (522)</option>
|
||||
<option value="523">bho142en (523)</option>
|
||||
<option value="524">bic143sy (524)</option>
|
||||
<option value="525">sja004br (525)</option>
|
||||
<option value="526">sja005br (526)</option>
|
||||
<option value="527">sja006br (527)</option>
|
||||
<option value="528">sja007br (528)</option>
|
||||
<option value="529">sja008br (529)</option>
|
||||
<option value="530">sja009br (530)</option>
|
||||
<option value="531">sja010br (531)</option>
|
||||
<option value="532">sja011br (532)</option>
|
||||
<option value="533">sja012br (533)</option>
|
||||
<option value="534">sja013br (534)</option>
|
||||
<option value="535">sja014br (535)</option>
|
||||
<option value="536">sja015br (536)</option>
|
||||
<option value="537">sja016br (537)</option>
|
||||
<option value="538">sja017br (538)</option>
|
||||
<option value="539">sja018br (539)</option>
|
||||
<option value="540">sja001br (540)</option>
|
||||
<option value="541">sja002br (541)</option>
|
||||
<option value="542">sja003br (542)</option>
|
||||
<option value="543">ijs001sn (543)</option>
|
||||
<option value="544">fjs148gd (544)</option>
|
||||
<option value="545">fjs149va (545)</option>
|
||||
<option value="546">sjs001va (546)</option>
|
||||
<option value="547">sjs002va (547)</option>
|
||||
<option value="548">sjs003va (548)</option>
|
||||
<option value="549">sjs004va (549)</option>
|
||||
<option value="550">fjs019rd (550)</option>
|
||||
<option value="551">bjs009gd (551)</option>
|
||||
<option value="552">sjs001sn (552)</option>
|
||||
<option value="553">sjs002sn (553)</option>
|
||||
<option value="554">sjs003sn (554)</option>
|
||||
<option value="555">sjs004sn (555)</option>
|
||||
<option value="556">sjs005sn (556)</option>
|
||||
<option value="557">snsx31sh (557)</option>
|
||||
<option value="558">bns007gd (558)</option>
|
||||
<option value="559">fns001l1 (559)</option>
|
||||
<option value="560">fns001l2 (560)</option>
|
||||
<option value="561">fra157bm (561)</option>
|
||||
<option value="562">bns145rd (562)</option>
|
||||
<option value="563">ips001ro (563)</option>
|
||||
<option value="564">sns010ni (564)</option>
|
||||
<option value="565">sns003la (565)</option>
|
||||
<option value="566">fps181ni (566)</option>
|
||||
<option value="567">ipz001rd (567)</option>
|
||||
<option value="568">spz004ma (568)</option>
|
||||
<option value="569">spz005ma (569)</option>
|
||||
<option value="570">spz006ma (570)</option>
|
||||
<option value="571">spz004pa (571)</option>
|
||||
<option value="572">spz013ma (572)</option>
|
||||
<option value="573">spz006pa (573)</option>
|
||||
<option value="574">spz014ma (574)</option>
|
||||
<option value="575">spz005pa (575)</option>
|
||||
<option value="576">spz015ma (576)</option>
|
||||
<option value="577">spz007ma (577)</option>
|
||||
<option value="578">spz013pa (578)</option>
|
||||
<option value="579">spz008ma (579)</option>
|
||||
<option value="580">spz014pa (580)</option>
|
||||
<option value="581">spz009ma (581)</option>
|
||||
<option value="582">spz015pa (582)</option>
|
||||
<option value="583">spz007pa (583)</option>
|
||||
<option value="584">spz011pe (584)</option>
|
||||
<option value="585">spz008pa (585)</option>
|
||||
<option value="586">spz009pa (586)</option>
|
||||
<option value="587">spz010ma (587)</option>
|
||||
<option value="588">spz010pa (588)</option>
|
||||
<option value="589">spz011ma (589)</option>
|
||||
<option value="590">spz011pa (590)</option>
|
||||
<option value="591">spz012pa (591)</option>
|
||||
<option value="592">spz001ma (592)</option>
|
||||
<option value="593">spz002ma (593)</option>
|
||||
<option value="594">spz003ma (594)</option>
|
||||
<option value="595">spz003pa (595)</option>
|
||||
<option value="596">fpz166p1 (596)</option>
|
||||
<option value="597">fpz172rd (597)</option>
|
||||
<option value="598">spz001pa (598)</option>
|
||||
<option value="599">spz002pa (599)</option>
|
||||
</optgroup>
|
||||
<optgroup label="600-699">
|
||||
<option value="600">ppz086bs (600)</option>
|
||||
<option value="601">ppz008rd (601)</option>
|
||||
<option value="602">ppz009pg (602)</option>
|
||||
<option value="603">ivo918in (603)</option>
|
||||
<option value="604">spz004pe (604)</option>
|
||||
<option value="605">spz005pe (605)</option>
|
||||
<option value="606">srp006pe (606)</option>
|
||||
<option value="607">spz013pe (607)</option>
|
||||
<option value="608">sns001pe (608)</option>
|
||||
<option value="609">fra192pe (609)</option>
|
||||
<option value="610">fra163mg (610)</option>
|
||||
<option value="611">fns185gd (611)</option>
|
||||
<option value="612">irt001in (612)</option>
|
||||
<option value="613">irtx01sl (613)</option>
|
||||
<option value="614">frt135df (614)</option>
|
||||
<option value="615">frt137df (615)</option>
|
||||
<option value="616">frt139df (616)</option>
|
||||
<option value="617">frt025rd (617)</option>
|
||||
<option value="618">frt132rd (618)</option>
|
||||
<option value="619">srt001rd (619)</option>
|
||||
<option value="620">srt003bd (620)</option>
|
||||
<option value="621">sst001mg (621)</option>
|
||||
<option value="622">sns004la (622)</option>
|
||||
<option value="623">sns005la (623)</option>
|
||||
<option value="624">sns006la (624)</option>
|
||||
<option value="625">sps004ni (625)</option>
|
||||
<option value="626">sps005ni (626)</option>
|
||||
<option value="627">sps006ni (627)</option>
|
||||
<option value="628">sns007la (628)</option>
|
||||
<option value="629">sns008la (629)</option>
|
||||
<option value="630">sns009la (630)</option>
|
||||
<option value="631">sns007ni (631)</option>
|
||||
<option value="632">sns008ni (632)</option>
|
||||
<option value="633">sns009ni (633)</option>
|
||||
<option value="634">pns017ml (634)</option>
|
||||
<option value="635">sns010la (635)</option>
|
||||
<option value="636">sns010pe (636)</option>
|
||||
<option value="637">sns011la (637)</option>
|
||||
<option value="638">sns012la (638)</option>
|
||||
<option value="639">sns007pe (639)</option>
|
||||
<option value="640">sns008pe (640)</option>
|
||||
<option value="641">sns013la (641)</option>
|
||||
<option value="642">sns013ni (642)</option>
|
||||
<option value="643">sns014la (643)</option>
|
||||
<option value="644">sns014ni (644)</option>
|
||||
<option value="645">sns015la (645)</option>
|
||||
<option value="646">sns015ni (646)</option>
|
||||
<option value="647">sns011ni (647)</option>
|
||||
<option value="648">sns012ni (648)</option>
|
||||
<option value="649">sns014pe (649)</option>
|
||||
<option value="650">sns015pe (650)</option>
|
||||
<option value="651">sns003pe (651)</option>
|
||||
<option value="652">sns017ni (652)</option>
|
||||
<option value="653">sps001ni (653)</option>
|
||||
<option value="654">sps002ni (654)</option>
|
||||
<option value="655">sps003ni (655)</option>
|
||||
<option value="656">sns017la (656)</option>
|
||||
<option value="657">sps001la (657)</option>
|
||||
<option value="658">sps002la (658)</option>
|
||||
<option value="659">bns005pg (659)</option>
|
||||
<option value="660">sns001ml (660)</option>
|
||||
<option value="661">sns002mg (661)</option>
|
||||
<option value="662">sns002ml (662)</option>
|
||||
<option value="663">sns002pe (663)</option>
|
||||
<option value="664">sns003mg (664)</option>
|
||||
<option value="665">sns004mg (665)</option>
|
||||
<option value="666">sns004rd (666)</option>
|
||||
<option value="667">sns006bd (667)</option>
|
||||
<option value="668">sns006ro (668)</option>
|
||||
<option value="669">sns011in (669)</option>
|
||||
<option value="670">sps001ro (670)</option>
|
||||
<option value="671">sps002ro (671)</option>
|
||||
<option value="672">sps003ro (672)</option>
|
||||
<option value="673">sps004ro (673)</option>
|
||||
<option value="674">srt005pg (674)</option>
|
||||
<option value="675">pns100ml (675)</option>
|
||||
<option value="676">ppz029rd (676)</option>
|
||||
<option value="677">sns007sy (677)</option>
|
||||
<option value="678">cnsx12la (678)</option>
|
||||
<option value="679">cnsx12ni (679)</option>
|
||||
<option value="680">ijs006sn (680)</option>
|
||||
<option value="681">igs008na (681)</option>
|
||||
<option value="682">irt007in (682)</option>
|
||||
<option value="683">ips002ro (683)</option>
|
||||
<option value="684">hho142cl (684)</option>
|
||||
<option value="685">hho143cl (685)</option>
|
||||
<option value="686">hho144cl (686)</option>
|
||||
<option value="687">hho027en (687)</option>
|
||||
<option value="688">hps116bd (688)</option>
|
||||
<option value="689">hps117bd (689)</option>
|
||||
<option value="690">hps118re (690)</option>
|
||||
<option value="691">hps120en (691)</option>
|
||||
<option value="692">hps122en (692)</option>
|
||||
<option value="693">hpz047pe (693)</option>
|
||||
<option value="694">hpz048pe (694)</option>
|
||||
<option value="695">hpz049bd (695)</option>
|
||||
<option value="696">hpz050bd (696)</option>
|
||||
<option value="697">hpz052ma (697)</option>
|
||||
<option value="698">hpz053pa (698)</option>
|
||||
<option value="699">hpz055pa (699)</option>
|
||||
</optgroup>
|
||||
<optgroup label="700-799">
|
||||
<option value="700">hpz057ma (700)</option>
|
||||
<option value="701">hpza51gd (701)</option>
|
||||
<option value="702">hpzb51gd (702)</option>
|
||||
<option value="703">hpzc51gd (703)</option>
|
||||
<option value="704">hpzf51gd (704)</option>
|
||||
<option value="705">hpzw51gd (705)</option>
|
||||
<option value="706">hpzx51gd (706)</option>
|
||||
<option value="707">hpzy51gd (707)</option>
|
||||
<option value="708">hpzz51gd (708)</option>
|
||||
<option value="709">nic002pr (709)</option>
|
||||
<option value="710">nic003pr (710)</option>
|
||||
<option value="711">nic004pr (711)</option>
|
||||
<option value="712">pps025ni (712)</option>
|
||||
<option value="713">pps026ni (713)</option>
|
||||
<option value="714">pps027ni (714)</option>
|
||||
<option value="715">ppz001pe (715)</option>
|
||||
<option value="716">ppz006pa (716)</option>
|
||||
<option value="717">ppz007pa (717)</option>
|
||||
<option value="718">ppz010pa (718)</option>
|
||||
<option value="719">ppz011pa (719)</option>
|
||||
<option value="720">ppz013pa (720)</option>
|
||||
<option value="721">ppz014pe (721)</option>
|
||||
<option value="722">ppz015pe (722)</option>
|
||||
<option value="723">ppz016pe (723)</option>
|
||||
<option value="724">pgs050nu (724)</option>
|
||||
<option value="725">pgs051nu (725)</option>
|
||||
<option value="726">pgs052nu (726)</option>
|
||||
<option value="727">ppz031ma (727)</option>
|
||||
<option value="728">ppz035pa (728)</option>
|
||||
<option value="729">ppz036pa (729)</option>
|
||||
<option value="730">ppz037ma (730)</option>
|
||||
<option value="731">ppz038ma (731)</option>
|
||||
<option value="732">ppz054ma (732)</option>
|
||||
<option value="733">ppz055ma (733)</option>
|
||||
<option value="734">ppz056ma (734)</option>
|
||||
<option value="735">ppz059ma (735)</option>
|
||||
<option value="736">ppz060ma (736)</option>
|
||||
<option value="737">ppz061ma (737)</option>
|
||||
<option value="738">ppz064ma (738)</option>
|
||||
<option value="739">prt072sl (739)</option>
|
||||
<option value="740">prt073sl (740)</option>
|
||||
<option value="741">prt074sl (741)</option>
|
||||
<option value="742">pho104re (742)</option>
|
||||
<option value="743">pho105re (743)</option>
|
||||
<option value="744">pho106re (744)</option>
|
||||
<option value="745">ppz075pa (745)</option>
|
||||
<option value="746">ppz082pa (746)</option>
|
||||
<option value="747">ppz084pa (747)</option>
|
||||
<option value="748">ppz088ma (748)</option>
|
||||
<option value="749">ppz089ma (749)</option>
|
||||
<option value="750">ppz090ma (750)</option>
|
||||
<option value="751">ppz093pe (751)</option>
|
||||
<option value="752">ppz094pe (752)</option>
|
||||
<option value="753">ppz095pe (753)</option>
|
||||
<option value="754">prp101pr (754)</option>
|
||||
<option value="755">pja126br (755)</option>
|
||||
<option value="756">pja127br (756)</option>
|
||||
<option value="757">pja129br (757)</option>
|
||||
<option value="758">pja130br (758)</option>
|
||||
<option value="759">pja131br (759)</option>
|
||||
<option value="760">pja132br (760)</option>
|
||||
<option value="761">ppz107ma (761)</option>
|
||||
<option value="762">ppz114pa (762)</option>
|
||||
<option value="763">ppz117ma (763)</option>
|
||||
<option value="764">ppz118ma (764)</option>
|
||||
<option value="765">ppz119ma (765)</option>
|
||||
<option value="766">ppz120pa (766)</option>
|
||||
<option value="767">wgs083nu (767)</option>
|
||||
<option value="768">wgs085nu (768)</option>
|
||||
<option value="769">wgs086nu (769)</option>
|
||||
<option value="770">wgs087nu (770)</option>
|
||||
<option value="771">wgs088nu (771)</option>
|
||||
<option value="772">wgs089nu (772)</option>
|
||||
<option value="773">wgs090nu (773)</option>
|
||||
<option value="774">wgs091nu (774)</option>
|
||||
<option value="775">wgs092nu (775)</option>
|
||||
<option value="776">wgs093nu (776)</option>
|
||||
<option value="777">wgs094nu (777)</option>
|
||||
<option value="778">wgs095nu (778)</option>
|
||||
<option value="779">wgs096nu (779)</option>
|
||||
<option value="780">wgs097nu (780)</option>
|
||||
<option value="781">wgs098nu (781)</option>
|
||||
<option value="782">wgs099nu (782)</option>
|
||||
<option value="783">wgs100nu (783)</option>
|
||||
<option value="784">wgs101nu (784)</option>
|
||||
<option value="785">wgs102nu (785)</option>
|
||||
<option value="786">wgs103nu (786)</option>
|
||||
<option value="787">wrt060bm (787)</option>
|
||||
<option value="788">wrt074sl (788)</option>
|
||||
<option value="789">wrt075rh (789)</option>
|
||||
<option value="790">wrt076df (790)</option>
|
||||
<option value="791">wrt078ni (791)</option>
|
||||
<option value="792">wrt079bm (792)</option>
|
||||
<option value="793">npz001bd (793)</option>
|
||||
<option value="794">npz002bd (794)</option>
|
||||
<option value="795">npz003bd (795)</option>
|
||||
<option value="796">npz004bd (796)</option>
|
||||
<option value="797">npz005bd (797)</option>
|
||||
<option value="798">npz006bd (798)</option>
|
||||
<option value="799">npz007bd (799)</option>
|
||||
</optgroup>
|
||||
<optgroup label="800-868">
|
||||
<option value="800">nca001ca (800)</option>
|
||||
<option value="801">nca002sk (801)</option>
|
||||
<option value="802">nca003gh (802)</option>
|
||||
<option value="803">nla001ha (803)</option>
|
||||
<option value="804">nla002sd (804)</option>
|
||||
<option value="805">npa001ns (805)</option>
|
||||
<option value="806">npa002ns (806)</option>
|
||||
<option value="807">npa003ns (807)</option>
|
||||
<option value="808">npa004ns (808)</option>
|
||||
<option value="809">npa005dl (809)</option>
|
||||
<option value="810">npa007dl (810)</option>
|
||||
<option value="811">npa009dl (811)</option>
|
||||
<option value="812">npa010db (812)</option>
|
||||
<option value="813">npa012db (813)</option>
|
||||
<option value="814">npa014db (814)</option>
|
||||
<option value="815">npa015ca (815)</option>
|
||||
<option value="816">npa017ca (816)</option>
|
||||
<option value="817">npa019ca (817)</option>
|
||||
<option value="818">npa020p1 (818)</option>
|
||||
<option value="819">npa022p1 (819)</option>
|
||||
<option value="820">npa024p1 (820)</option>
|
||||
<option value="821">npa025sh (821)</option>
|
||||
<option value="822">npa027sh (822)</option>
|
||||
<option value="823">npa029sh (823)</option>
|
||||
<option value="824">npa030fl (824)</option>
|
||||
<option value="825">npa031fl (825)</option>
|
||||
<option value="826">npa032fl (826)</option>
|
||||
<option value="827">npa034bh (827)</option>
|
||||
<option value="828">npa035bh (828)</option>
|
||||
<option value="829">npa036bh (829)</option>
|
||||
<option value="830">npa038pn (830)</option>
|
||||
<option value="831">npa039pn (831)</option>
|
||||
<option value="832">npa040pn (832)</option>
|
||||
<option value="833">npa042pm (833)</option>
|
||||
<option value="834">npa043pm (834)</option>
|
||||
<option value="835">npa044pm (835)</option>
|
||||
<option value="836">npa046sr (836)</option>
|
||||
<option value="837">npa047sr (837)</option>
|
||||
<option value="838">npa048sr (838)</option>
|
||||
<option value="839">npa050ba (839)</option>
|
||||
<option value="840">npa051ba (840)</option>
|
||||
<option value="841">npa052ba (841)</option>
|
||||
<option value="842">npa054po (842)</option>
|
||||
<option value="843">npa055po (843)</option>
|
||||
<option value="844">npa056po (844)</option>
|
||||
<option value="845">npa058r1 (845)</option>
|
||||
<option value="846">npa059r1 (846)</option>
|
||||
<option value="847">npa060r1 (847)</option>
|
||||
<option value="848">npa061r3 (848)</option>
|
||||
<option value="849">npa062r2 (849)</option>
|
||||
<option value="850">npa062r3 (850)</option>
|
||||
<option value="851">npa063r2 (851)</option>
|
||||
<option value="852">npa063r3 (852)</option>
|
||||
<option value="853">npa065r2 (853)</option>
|
||||
<option value="854">nja001pr (854)</option>
|
||||
<option value="855">nja002pr (855)</option>
|
||||
<option value="856">sjs007in (856)</option>
|
||||
<option value="857">sns005in (857)</option>
|
||||
<option value="858">sns006in (858)</option>
|
||||
<option value="859">sns008in (859)</option>
|
||||
<option value="860">sjs012in (860)</option>
|
||||
<option value="861">sjs013in (861)</option>
|
||||
<option value="862">sjs014in (862)</option>
|
||||
<option value="863">sjs015in (863)</option>
|
||||
<option value="864">srt001in (864)</option>
|
||||
<option value="865">srt002in (865)</option>
|
||||
<option value="866">srt003in (866)</option>
|
||||
<option value="867">srt004in (867)</option>
|
||||
<option value="868">nrtflag0 (868)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<button id="debug-play-animation" class="requires-debug">Play Animation</button>
|
||||
</div>
|
||||
</div>
|
||||
214
debug.js
@ -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();
|
||||
})();
|
||||
853
index.html
@ -6,14 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>LEGO® Island</title>
|
||||
|
||||
<link rel="preload" href="style.css" as="style">
|
||||
<link rel="preload" href="app.js" as="script">
|
||||
<link rel="preload" href="isle.js" as="script">
|
||||
<link rel="preload" href="isle.wasm" as="fetch" crossorigin>
|
||||
<link rel="preload" href="install.webp" as="image">
|
||||
<link rel="preload" href="island.webp" as="image">
|
||||
<link rel="preload" href="bonus.webp" as="image">
|
||||
|
||||
<meta name="description"
|
||||
content="Play the classic 1997 PC game LEGO® Island directly in your web browser! This fan-made web port, built with Emscripten from the isledecomp project, brings the beloved adventure back to life.">
|
||||
<meta name="keywords"
|
||||
@ -21,7 +13,6 @@
|
||||
<meta name="author" content="isledecomp/isle.pizza">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:url" content="https://isle.pizza/">
|
||||
<meta name="twitter:title" content="LEGO® Island - Online Web Port">
|
||||
<meta name="twitter:description" content="Play the classic 1997 PC game LEGO® Island directly in your web browser!">
|
||||
@ -33,849 +24,19 @@
|
||||
<meta property="og:image" content="https://isle.pizza/island.webp">
|
||||
<meta property="og:site_name" content="LEGO Island Web Port">
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<link rel="canonical" href="https://isle.pizza">
|
||||
<style>
|
||||
body { margin: 0; background-color: #000000; }
|
||||
#app:empty { visibility: hidden; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="goodbye-popup" class="notification-popup" style="display: none;">
|
||||
<div class="notification-popup-content">
|
||||
<button id="goodbye-cancel-btn" class="update-dismiss-btn" aria-label="Cancel">×</button>
|
||||
<div class="update-speech-bubble">
|
||||
<p class="update-message">See you later, Brickulator!</p>
|
||||
<div class="goodbye-progress">
|
||||
<div class="goodbye-progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img src="later.webp" alt="Goodbye" class="update-character" width="150" height="187">
|
||||
</div>
|
||||
|
||||
<div id="update-popup" class="notification-popup" style="display: none;">
|
||||
<div class="notification-popup-content">
|
||||
<button id="update-dismiss-btn" class="update-dismiss-btn" aria-label="Dismiss">×</button>
|
||||
<div class="update-speech-bubble">
|
||||
<p class="update-message">A new version just arrived!</p>
|
||||
<button id="update-reload-btn" class="update-reload-btn">Reload Now</button>
|
||||
</div>
|
||||
</div>
|
||||
<img src="bonus.webp" alt="Pepper" class="update-character" width="150" height="187">
|
||||
</div>
|
||||
|
||||
<div id="main-container">
|
||||
<div id="top-content">
|
||||
<div class="video-container">
|
||||
<img id="install-video" width="260" height="260" src="install.webp" alt="Install Game">
|
||||
<audio id="install-audio" loop preload="none">
|
||||
<source src="install.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<span id="sound-toggle-emoji" title="Unmute Audio">🔇</span>
|
||||
</div>
|
||||
<img id="island-logo-img" width="567" height="198" src="island.webp" alt="Lego Island Logo">
|
||||
</div>
|
||||
|
||||
<div id="controls-wrapper">
|
||||
<img class="control-img" width="135" height="164" id="run-game-btn" src="run_game_off.webp" alt="Run Game"
|
||||
data-off="run_game_off.webp" data-on="run_game_on.webp">
|
||||
<img class="control-img" width="130" height="147" id="configure-btn" src="configure_off.webp"
|
||||
alt="Configure" data-off="configure_off.webp" data-on="configure_on.webp" data-target="#configure-page">
|
||||
<img class="control-img" width="134" height="149" id="free-stuff-btn" src="free_stuff_off.webp"
|
||||
alt="Free Stuff" data-off="free_stuff_off.webp" data-on="free_stuff_on.webp"
|
||||
data-target="#free-stuff-page">
|
||||
<img class="control-img" width="134" height="149" id="read-me-btn" src="read_me_off.webp" alt="Read Me"
|
||||
data-off="read_me_off.webp" data-on="read_me_on.webp" data-target="#read-me-page">
|
||||
<img class="control-img" width="93" height="145" id="cancel-btn" src="cancel_off.webp" alt="Cancel"
|
||||
data-off="cancel_off.webp" data-on="cancel_on.webp">
|
||||
</div>
|
||||
|
||||
<div id="read-me-page" class="page-content">
|
||||
<span class="page-back-button" role="button" aria-label="Go back to main menu">← Back</span>
|
||||
<div class="page-inner-content">
|
||||
<h1>Read Me</h1>
|
||||
|
||||
<div class="readme-tabs">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-btn active" data-tab="about">
|
||||
<img src="register.webp" alt="" class="tab-icon">
|
||||
<span>About</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="system">
|
||||
<img src="sysinfo.webp" alt="" class="tab-icon">
|
||||
<span>System</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="faq">
|
||||
<img src="getinfo.webp" alt="" class="tab-icon">
|
||||
<span>FAQ</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="changelog">
|
||||
<img src="callfail.webp" alt="" class="tab-icon">
|
||||
<span>Changelog</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="manual">
|
||||
<img src="bonus.webp" alt="" class="tab-icon">
|
||||
<span>Manual</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="voices" style="display: none;">
|
||||
<img src="send.webp" alt="" class="tab-icon">
|
||||
<span>Voices</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel active" id="tab-about">
|
||||
<p>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.</p>
|
||||
<p>This incredible project stands on the shoulders of giants. It was made possible by the original <a
|
||||
href="https://github.com/isledecomp/isle" target="_blank"
|
||||
rel="noopener noreferrer">decompilation project</a>, which achieved 100% decompilation of the
|
||||
original game. This was then adapted into a <a
|
||||
href="https://github.com/isledecomp/isle-portable" target="_blank"
|
||||
rel="noopener noreferrer">portable version</a> that eliminated all Windows dependencies and
|
||||
replaced them with modern, cross-platform alternatives.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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!</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-system">
|
||||
<div class="requirements-section">
|
||||
<h3>Supported Browsers</h3>
|
||||
<p>This game requires a modern browser with WebAssembly multi-threading support. The following browsers are supported:</p>
|
||||
<ul class="requirements-list">
|
||||
<li><strong>Chrome</strong> — version 95 or newer</li>
|
||||
<li><strong>Firefox</strong> — version 92 or newer</li>
|
||||
<li><strong>Edge</strong> — version 95 or newer</li>
|
||||
<li><strong>Safari</strong> — version 15.4 or newer (iOS 18+ recommended)</li>
|
||||
</ul>
|
||||
<p class="requirements-note">For the best experience, keep your browser updated to the latest version.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Input Methods</h3>
|
||||
<p>The game supports multiple ways to play. Visit the Configure page to adjust your control preferences.</p>
|
||||
<ul class="requirements-list">
|
||||
<li><strong>Keyboard & Mouse</strong> — Traditional desktop controls using arrow keys or WASD</li>
|
||||
<li><strong>Gamepad</strong> — Controller support with analog sticks and D-pad</li>
|
||||
<li><strong>Touch Screen</strong> — Mobile-friendly controls with configurable schemes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Audio</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Storage & Network</h3>
|
||||
<p>The game streams approximately <strong>25MB</strong> of data on first load (more with extensions enabled).
|
||||
For offline play, you can install the full game (about <strong>550MB</strong>) via the Configure menu.
|
||||
A stable internet connection is recommended for initial loading.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Performance Tips</h3>
|
||||
<ul class="requirements-list">
|
||||
<li>Close other browser tabs to free up memory</li>
|
||||
<li>Use hardware acceleration (enabled by default in most browsers)</li>
|
||||
<li>On mobile, ensure your device isn't in low-power mode</li>
|
||||
<li>If experiencing lag, try reducing the resolution in Configure</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-faq">
|
||||
<details>
|
||||
<summary>Is this the full, original game?</summary>
|
||||
<p>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.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>How does this differ from the original 1997 CD-ROM game?</summary>
|
||||
<p>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!).</p>
|
||||
<p>Check out the "Configure" page to see what's possible.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Can I save my progress?</summary>
|
||||
<p>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.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Does this run on mobile?</summary>
|
||||
<p>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 <a
|
||||
href="https://github.com/isledecomp/isle-portable/issues/418#issuecomment-3003572219"
|
||||
target="_blank" rel="noopener noreferrer">Tesla in-car browsers</a>! 🚗</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Which browsers are supported?</summary>
|
||||
<p>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.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>What are the controls?</summary>
|
||||
<p>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.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Can I play offline?</summary>
|
||||
<p>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.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>I don't hear any sound or music. How do I fix it?</summary>
|
||||
<p>Most modern browsers block audio until you interact with the page. Click the mute icon (🔇)
|
||||
on the animated intro to enable sound.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>I think I found a bug! Where do I report it?</summary>
|
||||
<p>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 <a
|
||||
href="https://github.com/isledecomp/isle-portable/issues" target="_blank"
|
||||
rel="noopener noreferrer">GitHub Issues page</a>. Please include details about your
|
||||
browser, device, and what you were doing when the bug occurred.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Is this project open-source?</summary>
|
||||
<p>Yes, absolutely! This web port is built upon the incredible open-source <a
|
||||
href="https://github.com/isledecomp/isle-portable" target="_blank"
|
||||
rel="noopener noreferrer">LEGO Island (portable)</a> project, and the code for this
|
||||
website is also <a href="https://github.com/isledecomp/isle.pizza" target="_blank"
|
||||
rel="noopener noreferrer">available here</a>.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-changelog">
|
||||
<details>
|
||||
<summary>January 2026</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> 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</li>
|
||||
<li><strong>Improved:</strong> Configure page redesigned with tabbed navigation, collapsible sections, quick presets (Classic/Modern Mode), and modern toggle switches</li>
|
||||
<li><strong>Improved:</strong> Read Me page reorganized into tabs (About, System, FAQ, Changelog, Manual) with the original instruction manual now viewable in-browser</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>December 2025</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> "Active in Background" option keeps the game running when the tab loses focus</li>
|
||||
<li><strong>New:</strong> WASD navigation controls as an alternative to arrow keys</li>
|
||||
<li><strong>Fixed:</strong> Act 3 helicopter ammo now correctly sticks to targets and finishes animations</li>
|
||||
<li><strong>Fixed:</strong> Pick/click distance calculation for more accurate object selection</li>
|
||||
<li><strong>Fixed:</strong> Maximum deltaTime capping prevents physics glitches in races</li>
|
||||
<li><strong>Fixed:</strong> Touch controls now properly support widescreen aspect ratios</li>
|
||||
<li><strong>Improved:</strong> Default anisotropic filtering increased to 16x for sharper textures</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>November 2025</summary>
|
||||
<ul>
|
||||
<li><strong>Fixed:</strong> Dictionary loading failure no longer causes crashes</li>
|
||||
<li><strong>Fixed:</strong> INI configuration now properly applies defaults when values are missing</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>September 2025</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> Additional widescreen background images</li>
|
||||
<li><strong>Fixed:</strong> Jukebox state now correctly restored when using HD Music extension</li>
|
||||
<li><strong>Fixed:</strong> Background audio no longer gets stuck when starting audio fails</li>
|
||||
<li><strong>Improved:</strong> SI Loader actions now start at the correct time during world loading</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>August 2025</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> Extended Bad Ending FMV extension shows the uncut beta animation</li>
|
||||
<li><strong>New:</strong> HD Music extension with high-quality audio</li>
|
||||
<li><strong>New:</strong> Widescreen backgrounds extension eliminates 3D edges on wide displays</li>
|
||||
<li><strong>New:</strong> SI Loader extension system for community content and modifications</li>
|
||||
<li><strong>New:</strong> OpenGL ES 2.0/3.0 renderer for broader device compatibility</li>
|
||||
<li><strong>Fixed:</strong> Purple edges no longer appear on scaled transparent 2D elements</li>
|
||||
<li><strong>Fixed:</strong> Transparent pixels now render correctly with alpha channel support</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>July 2025</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> HD Textures extension with enhanced visuals</li>
|
||||
<li><strong>New:</strong> MSAA anti-aliasing support for smoother edges</li>
|
||||
<li><strong>New:</strong> Anisotropic filtering for sharper textures at angles</li>
|
||||
<li><strong>New:</strong> Haptic feedback (vibration) support for gamepads and mobile devices</li>
|
||||
<li><strong>New:</strong> Virtual Gamepad touch control scheme with sliding controls</li>
|
||||
<li><strong>New:</strong> Gamepad/controller support with analog sticks and D-pad</li>
|
||||
<li><strong>New:</strong> Full screen mode with in-game toggle</li>
|
||||
<li><strong>New:</strong> Maximum LOD and Maximum Actors configuration options</li>
|
||||
<li><strong>New:</strong> Configurable transition animations (Mosaic, Dissolve, Wipe, etc.)</li>
|
||||
<li><strong>New:</strong> Extensions system allowing community-created content</li>
|
||||
<li><strong>Fixed:</strong> WebGL driver compatibility issues resolved</li>
|
||||
<li><strong>Fixed:</strong> Firefox Private browsing mode now works correctly</li>
|
||||
<li><strong>Fixed:</strong> Virtual cursor transparency and positioning</li>
|
||||
<li><strong>Fixed:</strong> Touch coordinate translation for proper viewport mapping</li>
|
||||
<li><strong>Fixed:</strong> Memory leaks in ViewLODList</li>
|
||||
<li><strong>Fixed:</strong> Screen transitions on software renderer and 32-bit displays</li>
|
||||
<li><strong>Fixed:</strong> Tabbing in and out of fullscreen</li>
|
||||
<li><strong>Fixed:</strong> Click spam prevention on touch screens</li>
|
||||
<li><strong>Improved:</strong> Mosaic transition animation is faster and cleaner</li>
|
||||
<li><strong>Improved:</strong> Loading UX for HD Textures with progress indicators</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>June 2025 — Initial Release</summary>
|
||||
<ul>
|
||||
<li><strong>New:</strong> Emscripten web port — play LEGO Island directly in your browser!</li>
|
||||
<li><strong>New:</strong> WebGL rendering for hardware-accelerated 3D graphics</li>
|
||||
<li><strong>New:</strong> Software renderer fallback for devices without WebGL</li>
|
||||
<li><strong>New:</strong> 32-bit color support for improved visual quality</li>
|
||||
<li><strong>New:</strong> Full screen support</li>
|
||||
<li><strong>New:</strong> Joystick/gamepad enabled by default</li>
|
||||
<li><strong>New:</strong> Option to skip the startup delay</li>
|
||||
<li><strong>New:</strong> Support for LEGO Island 1.0 version</li>
|
||||
<li><strong>New:</strong> FPS display option</li>
|
||||
<li><strong>New:</strong> Game runs without requiring an audio device</li>
|
||||
<li><strong>Fixed:</strong> Infocenter to Act 2/Act 3 transition issues</li>
|
||||
<li><strong>Fixed:</strong> Race initialization errors</li>
|
||||
<li><strong>Fixed:</strong> Jetski race startup issues</li>
|
||||
<li><strong>Fixed:</strong> Plant creation bug in LegoPlantManager</li>
|
||||
<li><strong>Fixed:</strong> Late-game "sawtooth" audio glitches</li>
|
||||
<li><strong>Fixed:</strong> Building variant switching (Pepper's buildings)</li>
|
||||
<li><strong>Fixed:</strong> OpenGL rendering issues</li>
|
||||
<li><strong>Fixed:</strong> Image serialization bugs</li>
|
||||
<li><strong>Improved:</strong> Transparent objects now render correctly (sorted last)</li>
|
||||
<li><strong>Improved:</strong> GPU mesh uploading via VBOs for better performance</li>
|
||||
<li><strong>Improved:</strong> Backface culling enabled for faster rendering</li>
|
||||
<li><strong>Improved:</strong> SIMD-optimized z-buffer clearing</li>
|
||||
<li><strong>Improved:</strong> Edge-walking triangle rasterization</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-manual">
|
||||
<div class="manual-container">
|
||||
<p class="manual-description">The original 15-page instruction manual from the 1997 CD-ROM release.</p>
|
||||
<a href="comic.pdf" target="_blank" rel="noopener" class="manual-open-btn">Open Manual in New Tab</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-voices">
|
||||
<p class="voices-intro">Reactions from the original LEGO Island development team:</p>
|
||||
<div class="voices-grid">
|
||||
<blockquote class="voice-card">
|
||||
<p>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...</p>
|
||||
<footer>Scott Anderson</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>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.</p>
|
||||
<footer>Dennis Goodrow</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>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!</p>
|
||||
<footer>Jim Brown</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Well done and such fun tapping back into such fond creative memories.</p>
|
||||
<footer>Paul Melmed</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>That's awesome!</p>
|
||||
<footer>Randy Chou</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Fantastic! Love it.</p>
|
||||
<footer>Kevin Byall</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Great stuff!</p>
|
||||
<footer>Dave Cherry</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="configure-page" class="page-content">
|
||||
<span class="page-back-button" role="button" aria-label="Go back to main menu">← Back</span>
|
||||
<blockquote id="opfs-disabled" class="error-box" style="display:none;">
|
||||
<p>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.</p>
|
||||
</blockquote>
|
||||
<div class="page-inner-content config-layout">
|
||||
<div class="config-art-panel">
|
||||
<img src="shark.webp" alt="LEGO Island Shark and Brickster">
|
||||
</div>
|
||||
<div class="config-main">
|
||||
<div class="config-presets">
|
||||
<button type="button" class="preset-btn" id="preset-classic">Classic Mode</button>
|
||||
<button type="button" class="preset-btn" id="preset-modern">Modern Mode</button>
|
||||
</div>
|
||||
<div class="config-tabs">
|
||||
<div class="config-tab-buttons">
|
||||
<button class="config-tab-btn active" data-config-tab="display">Display</button>
|
||||
<button class="config-tab-btn" data-config-tab="controls">Controls</button>
|
||||
<button class="config-tab-btn" data-config-tab="audio">Audio</button>
|
||||
<button class="config-tab-btn" data-config-tab="extras">Extras</button>
|
||||
</div>
|
||||
<form id="config-form" class="config-form">
|
||||
<div class="config-tab-panel active" id="config-tab-display">
|
||||
<details class="config-section-card" open>
|
||||
<summary class="config-card-header">Game</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Version</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="language-select" name="Language">
|
||||
<option value="da">Danish</option>
|
||||
<option value="el">English (1.0)</option>
|
||||
<option value="en" selected>English (1.1)</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="window-form">
|
||||
<label class="form-group-label">Window</label>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="window-windowed" name="window" data-not-ini="true" checked>
|
||||
<label for="window-windowed">Windowed</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="window-fullscreen" name="window" data-not-ini="true">
|
||||
<label for="window-fullscreen">Full Screen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">
|
||||
Aspect Ratio
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Choose Original (4:3) to preserve the classic aspect ratio with black bars, or select Widescreen to stretch the image to fit your display.</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="aspect-original" value="1" name="Original Aspect Ratio" checked>
|
||||
<label for="aspect-original">Original (4:3)</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="aspect-wide" value="0" name="Original Aspect Ratio">
|
||||
<label for="aspect-wide">Widescreen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">
|
||||
Resolution
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Choose Original (640 x 480) to preserve the classic resolution, or select Maximum to render in the highest quality.</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="resolution-original" value="1" name="Original Resolution" checked>
|
||||
<label for="resolution-original">Original (640 x 480)</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="resolution-wide" value="0" name="Original Resolution">
|
||||
<label for="resolution-wide">Maximum</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="config-section-card">
|
||||
<summary class="config-card-header">Detail</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">
|
||||
Island Model Quality
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Note: using the "Low" setting will cause the island to disappear. This is not a bug, but the same behavior as present in the original game.</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-low" name="Island Quality" value="0">
|
||||
<label for="gfx-low">Low</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-med" name="Island Quality" value="1">
|
||||
<label for="gfx-med">Medium</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-high" name="Island Quality" value="2" checked>
|
||||
<label for="gfx-high">High</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Island Texture Quality</label>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="tex-low" name="Island Texture" value="0">
|
||||
<label for="tex-low">Low</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="tex-high" name="Island Texture" value="1" checked>
|
||||
<label for="tex-high">High</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="max-lod">
|
||||
Maximum LOD
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Maximum Level of Detail (LOD). A higher setting will cause higher quality textures to be drawn regardless of distance.</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="range" id="max-lod" name="Max LOD" min="0" max="6" step="0.1" value="3.6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="max-allowed-extras">
|
||||
Maximum actors (5..40)
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Maximum number of LEGO actors to exist in the world at a time. The game will gradually increase the number of actors until this maximum is reached and while performance is acceptable.</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="range" id="max-allowed-extras" name="Max Allowed Extras" min="5" max="40" value="20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="config-section-card">
|
||||
<summary class="config-card-header">Graphics</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Renderer</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="renderer-select" name="3D Device ID">
|
||||
<option value="0 0x682656f3 0x0 0x0 0x2000000">Software</option>
|
||||
<option value="0 0x682656f3 0x0 0x0 0x4000000" selected>WebGL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Transition Type</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="transition-type-select" name="Transition Type">
|
||||
<option value="1">No Animation</option>
|
||||
<option value="2">Dissolve</option>
|
||||
<option value="3" selected>Mosaic</option>
|
||||
<option value="4">Wipe Down</option>
|
||||
<option value="5">Windows</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Anti-aliasing</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="msaa-select" name="MSAA"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label">Anisotropic filtering</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="anisotropic-select" name="Anisotropic"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="config-tab-panel" id="config-tab-controls">
|
||||
<details class="config-section-card" open>
|
||||
<summary class="config-card-header">Input</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group" id="touch-section">
|
||||
<label class="form-group-label">
|
||||
Touch Control Scheme
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">
|
||||
<div><strong>Virtual Gamepad (Recommended):</strong> Slide your finger to move and turn.</div><br>
|
||||
<div><strong>Virtual Arrow Keys:</strong> Tap screen areas to move. The top moves forward, the bottom turns or moves back.</div><br>
|
||||
<div><strong>Virtual Mouse:</strong> Emulates classic mouse controls with touch.</div>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="touch-type-select" name="Touch Scheme">
|
||||
<option value="0">Virtual Mouse</option>
|
||||
<option value="1">Virtual Arrow Keys</option>
|
||||
<option value="2" selected>Virtual Gamepad</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-haptic" name="Haptic" checked><span class="toggle-slider"></span><span class="toggle-label">Haptic feedback</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">On supported devices and browsers, this provides physical feedback, like a vibration, while you play the game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-wasd" name="WASD"><span class="toggle-slider"></span><span class="toggle-label">Use WASD for navigation</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Use the WASD keys instead of the arrow keys to control the game, much akin to a modern PC game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-active-in-bg" name="Active in Background"><span class="toggle-slider"></span><span class="toggle-label">Active in background</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Keeps the game running even when it's in the background.</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="config-tab-panel" id="config-tab-audio">
|
||||
<details class="config-section-card" open>
|
||||
<summary class="config-card-header">Sound</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="check-music" name="Music" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">Music</span>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="check-3d-sound" name="3DSound" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">3D Sound</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="config-tab-panel" id="config-tab-extras">
|
||||
<details class="config-section-card" open>
|
||||
<summary class="config-card-header">Extensions</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-hd-textures" name="Texture Loader" data-not-ini="true"><span class="toggle-slider"></span><span class="toggle-label">HD Textures <span class="toggle-badge">+25MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Enhance the game's visuals with high-definition textures.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-hd-music" name="HD Music" data-not-ini="true"><span class="toggle-slider"></span><span class="toggle-label">HD Music <span class="toggle-badge">+450MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Improve the game's music with high-definition audio.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-widescreen-bgs" name="Widescreen Backgrounds" data-not-ini="true"><span class="toggle-slider"></span><span class="toggle-label">Widescreen Backgrounds <span class="toggle-badge">WIP</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Adapts the game's background art for modern widescreen monitors, eliminating unwanted 3D backgrounds on the sides of the screen.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-outro" name="Outro FMV" data-not-ini="true"><span class="toggle-slider"></span><span class="toggle-label">Outro FMV</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Plays the unused Outro animation upon exiting the game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-ending" name="Extended Bad Ending FMV" data-not-ini="true"><span class="toggle-slider"></span><span class="toggle-label">Extended Bad Ending FMV <span class="toggle-badge">+20MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Plays the extended / "uncut" Bad Ending animation as found in beta versions of the game upon failing to catch the Brickster.</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="config-section-card">
|
||||
<summary class="config-card-header">Offline Play</summary>
|
||||
<div class="config-card-content">
|
||||
<div class="offline-play-grid">
|
||||
<div class="offline-play-text">
|
||||
<p>Install the game for offline access. This will download all necessary files to your device (about 550MB).</p>
|
||||
<p class="offline-note">Note: browsers enforce strict storage quotas, especially in private/incognito windows.</p>
|
||||
</div>
|
||||
<div class="offline-play-controls">
|
||||
<img class="control-img" id="install-btn" src="install_off.webp" alt="Install Game" data-off="install_off.webp" data-on="install_on.webp" style="display: block;">
|
||||
<img class="control-img" id="uninstall-btn" src="uninstall_off.webp" alt="Uninstall Game" data-off="uninstall_off.webp" data-on="uninstall_on.webp" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="config-toast" class="config-toast">Settings saved</div>
|
||||
</div>
|
||||
|
||||
<div id="free-stuff-page" class="page-content">
|
||||
<span class="page-back-button" role="button" aria-label="Go back to main menu">← Back</span>
|
||||
<div class="page-inner-content">
|
||||
<div class="resource-list">
|
||||
<div class="quote-panel">
|
||||
<div class="quote-panel-art">
|
||||
<img src="congrats.webp" alt="LEGO Island characters celebrating">
|
||||
</div>
|
||||
<blockquote class="quote-panel-content">
|
||||
<p>"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."</p>
|
||||
<footer>Wes Jenkins, Creative Director</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
<a href="https://www.youtube.com/watch?v=bG55COe_f8I" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>The Making of LEGO Island: A Documentary</h3>
|
||||
<p>An in-depth documentary by MattKC that explores the fascinating and chaotic development story
|
||||
behind the classic game.</p>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=Poaxx9sMxjw" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>LEGO Island Radio 24/7</h3>
|
||||
<p>Enjoy the iconic, high-quality soundtrack of LEGO Island anytime with this continuous live
|
||||
stream, complete with the original DJ interludes.</p>
|
||||
</a>
|
||||
<a href="https://www.legoisland.org/" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>LEGO Island Wiki</h3>
|
||||
<p>Your ultimate resource for all things LEGO Island. This fan-run wiki contains a wealth of
|
||||
information, research, and details about the game.</p>
|
||||
</a>
|
||||
<a href="https://github.com/isledecomp/isle" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>LEGO Island Decompilation</h3>
|
||||
<p>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.</p>
|
||||
</a>
|
||||
<a href="https://github.com/isledecomp/isle-portable" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>LEGO Island, Portable Version</h3>
|
||||
<p>A portable, cross-platform version of the decompilation project which serves as the direct
|
||||
foundation for this web-based port.</p>
|
||||
</a>
|
||||
<a href="https://github.com/isledecomp/isle.pizza" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>isle.pizza Frontend</h3>
|
||||
<p>The source code for this website! A custom-built frontend for the Emscripten version of the
|
||||
portable decompilation project.</p>
|
||||
</a>
|
||||
<a href="https://github.com/isledecomp/LEGOIslandRebuilder" target="_blank"
|
||||
rel="noopener noreferrer" class="resource-item">
|
||||
<h3>LEGO Island Rebuilder</h3>
|
||||
<p>A powerful launcher and tool for patching and modding the original 1997 PC version of LEGO
|
||||
Island. Essential for play and modding.</p>
|
||||
</a>
|
||||
<a href="https://github.com/isledecomp/SIEdit" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>SIEdit</h3>
|
||||
<p>A suite of tools developed by the decompilation team for viewing and editing the ".si" script
|
||||
and resource files from the original game.</p>
|
||||
</a>
|
||||
<a href="https://www.legoisland.org/wiki/The_Making_of_LEGO_Island" target="_blank"
|
||||
rel="noopener noreferrer" class="resource-item">
|
||||
<h3>The Making of LEGO Island, a memoir by Wes Jenkins</h3>
|
||||
<p>Read the fascinating, incomplete memoir from Creative Director Wes Jenkins, detailing the
|
||||
development process and the team behind the game.</p>
|
||||
</a>
|
||||
<a href="/poster.pdf" target="_blank" rel="noopener noreferrer" class="resource-item">
|
||||
<h3>LEGO Island: Free Poster</h3>
|
||||
<p>Download a copy of the iconic poster that was originally included with the retail release of
|
||||
the game.</p>
|
||||
</a>
|
||||
<a href="https://brickstobytes.org/games/lego-island" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>Development Materials Archive</h3>
|
||||
<p>Explore a collection of development materials, concept art, and other historical assets from
|
||||
the creation of LEGO Island.</p>
|
||||
</a>
|
||||
<a href="https://le717.github.io/LEGO-Island-VGF/legoisland/#interview" target="_blank"
|
||||
rel="noopener noreferrer" class="resource-item">
|
||||
<h3>Video Game Flashback: An Interview with Wes Jenkins</h3>
|
||||
<p>A detailed interview with LEGO Island's Creative Director, Wes Jenkins, offering unique
|
||||
insights into the game's production.</p>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=fodBG_QylVM" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>LEGO® Island - Behind the Scenes</h3>
|
||||
<p>Watch a rare promotional video created during the game's development, showcasing its progress
|
||||
and vision at the time.</p>
|
||||
</a>
|
||||
<a href="https://tcrf.net/LEGO_Island" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>The Cutting Room Floor</h3>
|
||||
<p>Discover unused assets, hidden data, and other secrets left in the retail version of the
|
||||
game. A fascinating look at what might have been.</p>
|
||||
</a>
|
||||
<a href="https://projectisland.org/music/" target="_blank" rel="noopener noreferrer"
|
||||
class="resource-item">
|
||||
<h3>Project Island High Quality Music</h3>
|
||||
<p>A complete, high-quality re-digitization of the LEGO Island soundtrack, restored by the
|
||||
game's main composer, Lorin Nelson.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-disclaimer">
|
||||
<p>LEGO® and LEGO Island™ are trademarks of The LEGO Group.</p>
|
||||
<p>This is an unofficial fan project and is not affiliated with or endorsed by The LEGO Group.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-footer">
|
||||
<p>Last updated: <span id="app-version">2026-01-04 23:57:38 UTC</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="canvas-wrapper">
|
||||
<div id="loading-gif-overlay">
|
||||
<img src="cdspin.gif" alt="Loading game...">
|
||||
<div class="quote-block">
|
||||
<p class="quote-text">"Whoops! You have to put the CD in your computer"</p>
|
||||
<p class="quote-attribution">- The Infomaniac (1997)</p>
|
||||
</div>
|
||||
<div class="loading-info-text">
|
||||
<p>"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!</p>
|
||||
<p>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!</p>
|
||||
</div>
|
||||
<div id="emscripten-status-message" class="status-message-bar">
|
||||
Loading LEGO® Island... please wait! <code>0%</code>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="debug-ui"></div>
|
||||
|
||||
<script src="app.js" defer></script>
|
||||
<script src="isle.js" defer></script>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="/isle.js" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
7812
package-lock.json
generated
Normal file
16
package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "isle-pizza",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && node scripts/workbox-inject.js",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^5.4.0",
|
||||
"workbox-cli": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
14
scripts/workbox-inject.js
Normal file
@ -0,0 +1,14 @@
|
||||
// Injects the workbox manifest into the service worker
|
||||
// Config is loaded from workbox-config-vite.js for easy editing
|
||||
import {injectManifest} from 'workbox-build';
|
||||
import {createRequire} from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const config = require('../workbox-config.cjs');
|
||||
|
||||
injectManifest(config).then(({count, size}) => {
|
||||
console.log(`Precached ${count} files, totaling ${size} bytes.`);
|
||||
}).catch((err) => {
|
||||
console.error('Workbox injection failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
94
src/App.svelte
Normal file
@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { currentPage, debugEnabled } from './stores.js';
|
||||
import { registerServiceWorker, checkCacheStatus } from './core/serviceWorker.js';
|
||||
import { setupCanvasEvents } from './core/emscripten.js';
|
||||
import TopContent from './lib/TopContent.svelte';
|
||||
import Controls from './lib/Controls.svelte';
|
||||
import ReadMePage from './lib/ReadMePage.svelte';
|
||||
import ConfigurePage from './lib/ConfigurePage.svelte';
|
||||
import FreeStuffPage from './lib/FreeStuffPage.svelte';
|
||||
import UpdatePopup from './lib/UpdatePopup.svelte';
|
||||
import GoodbyePopup from './lib/GoodbyePopup.svelte';
|
||||
import ConfigToast from './lib/ConfigToast.svelte';
|
||||
import DebugPanel from './lib/DebugPanel.svelte';
|
||||
import CanvasWrapper from './lib/CanvasWrapper.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
// Disable browser's automatic scroll restoration
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
// Register service worker and initialize
|
||||
const registration = await registerServiceWorker();
|
||||
if (registration) {
|
||||
checkCacheStatus();
|
||||
}
|
||||
|
||||
// Setup canvas events
|
||||
setupCanvasEvents();
|
||||
|
||||
// Initialize history state based on current page
|
||||
const initialHash = window.location.hash;
|
||||
if (initialHash) {
|
||||
// Set up proper history state for the current hash
|
||||
history.replaceState({ page: 'main' }, '', window.location.pathname);
|
||||
history.pushState({ page: $currentPage }, '', initialHash);
|
||||
} else {
|
||||
history.replaceState({ page: 'main' }, '', window.location.pathname);
|
||||
}
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (e.state && e.state.page && e.state.page !== 'main') {
|
||||
currentPage.set(e.state.page);
|
||||
} else {
|
||||
currentPage.set('main');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Scroll to top whenever page changes
|
||||
$: $currentPage, window.scrollTo(0, 0);
|
||||
</script>
|
||||
|
||||
<!-- Audio element outside page routing so it persists across navigation -->
|
||||
<audio id="install-audio" loop preload="none">
|
||||
<source src="install.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
|
||||
<GoodbyePopup />
|
||||
<UpdatePopup />
|
||||
<ConfigToast />
|
||||
|
||||
<div id="main-container">
|
||||
<div class="page-wrapper" class:active={$currentPage === 'main'}>
|
||||
<TopContent />
|
||||
<Controls />
|
||||
</div>
|
||||
<div class="page-wrapper" class:active={$currentPage === 'read-me'}>
|
||||
<ReadMePage />
|
||||
</div>
|
||||
<div class="page-wrapper" class:active={$currentPage === 'configure'}>
|
||||
<ConfigurePage />
|
||||
</div>
|
||||
<div class="page-wrapper" class:active={$currentPage === 'free-stuff'}>
|
||||
<FreeStuffPage />
|
||||
</div>
|
||||
|
||||
<div class="footer-disclaimer">
|
||||
<p>LEGO® and LEGO Island™ are trademarks of The LEGO Group.</p>
|
||||
<p>This is an unofficial fan project and is not affiliated with or endorsed by The LEGO Group.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-footer">
|
||||
<p>Last updated: {__BUILD_TIME__}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CanvasWrapper />
|
||||
|
||||
{#if $debugEnabled}
|
||||
<DebugPanel />
|
||||
{/if}
|
||||
34
src/core/audio.js
Normal file
@ -0,0 +1,34 @@
|
||||
// Audio utilities for install-audio element
|
||||
import { soundEnabled } from '../stores.js';
|
||||
|
||||
export function getInstallAudio() {
|
||||
return document.getElementById('install-audio');
|
||||
}
|
||||
|
||||
export function pauseInstallAudio() {
|
||||
const audio = getInstallAudio();
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
soundEnabled.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function playInstallAudio() {
|
||||
const audio = getInstallAudio();
|
||||
if (audio) {
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
soundEnabled.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleInstallAudio() {
|
||||
const audio = getInstallAudio();
|
||||
if (!audio) return;
|
||||
|
||||
if (audio.paused) {
|
||||
playInstallAudio();
|
||||
} else {
|
||||
pauseInstallAudio();
|
||||
}
|
||||
}
|
||||
49
src/core/emscripten.js
Normal file
@ -0,0 +1,49 @@
|
||||
// Emscripten-related functions for game launching and canvas events
|
||||
import { gameRunning, debugUIVisible } from '../stores.js';
|
||||
|
||||
let progressUpdates = 0;
|
||||
|
||||
export function startGame(rendererValue) {
|
||||
const mainContainer = document.getElementById('main-container');
|
||||
const canvasWrapper = document.getElementById('canvas-wrapper');
|
||||
const canvas = document.getElementById('canvas');
|
||||
|
||||
if (!window.Module.running) return;
|
||||
|
||||
mainContainer.style.display = 'none';
|
||||
canvasWrapper.style.display = 'grid';
|
||||
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.documentElement.style.overscrollBehavior = 'none';
|
||||
|
||||
window.Module["disableOffscreenCanvases"] ||= rendererValue === "0 0x682656f3 0x0 0x0 0x2000000";
|
||||
console.log("disableOffscreenCanvases: " + window.Module["disableOffscreenCanvases"]);
|
||||
|
||||
window.Module["removeRunDependency"]("isle");
|
||||
canvas.focus();
|
||||
gameRunning.set(true);
|
||||
}
|
||||
|
||||
export function setupCanvasEvents() {
|
||||
const canvas = document.getElementById('canvas');
|
||||
const loadingGifOverlay = document.getElementById('loading-gif-overlay');
|
||||
const statusMessageBar = document.getElementById('emscripten-status-message');
|
||||
|
||||
canvas.addEventListener('presenterProgress', function (event) {
|
||||
// Intro animation is ready
|
||||
if (event.detail.objectName === 'Lego_Smk' && event.detail.tickleState === 1) {
|
||||
loadingGifOverlay.style.display = 'none';
|
||||
canvas.style.setProperty('display', 'block', 'important');
|
||||
debugUIVisible.set(true);
|
||||
}
|
||||
else if (progressUpdates < 1003) {
|
||||
progressUpdates++;
|
||||
const percent = (progressUpdates / 1003 * 100).toFixed();
|
||||
statusMessageBar.innerHTML = 'Loading LEGO® Island... please wait! <code>' + percent + '%</code>';
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('extensionProgress', function (event) {
|
||||
statusMessageBar.innerHTML = 'Loading ' + event.detail.name + '... please wait! <code>' + event.detail.progress + '%</code>';
|
||||
});
|
||||
}
|
||||
11
src/core/navigation.js
Normal file
@ -0,0 +1,11 @@
|
||||
// Navigation utilities
|
||||
import { currentPage } from '../stores.js';
|
||||
|
||||
export function navigateTo(page) {
|
||||
currentPage.set(page);
|
||||
history.pushState({ page }, '', '#' + page);
|
||||
}
|
||||
|
||||
export function navigateBack() {
|
||||
history.back();
|
||||
}
|
||||
197
src/core/opfs.js
Normal file
@ -0,0 +1,197 @@
|
||||
// OPFS Config Manager - handles saving/loading configuration via Origin Private File System
|
||||
import { configToastVisible } from '../stores.js';
|
||||
|
||||
const CONFIG_FILE = 'isle.ini';
|
||||
let toastTimeout = null;
|
||||
|
||||
export async function getFileHandle() {
|
||||
try {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
return await root.getFileHandle(CONFIG_FILE, { create: true });
|
||||
} catch (e) {
|
||||
console.error("OPFS not available or permission denied.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfig(form) {
|
||||
const handle = await getFileHandle();
|
||||
if (!handle) return null;
|
||||
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
const text = await file.text();
|
||||
if (!text) {
|
||||
console.log('No existing config file found, using defaults.');
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
applyConfigToForm(form, config);
|
||||
console.log('Config loaded from', CONFIG_FILE);
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error('Failed to load config:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyConfigToForm(form, config) {
|
||||
const elements = form.elements;
|
||||
for (const key in config) {
|
||||
if (key === "files") {
|
||||
const hdMusic = elements["HD Music"];
|
||||
const widescreenBgs = elements["Widescreen Backgrounds"];
|
||||
const badEnding = elements["Extended Bad Ending FMV"];
|
||||
if (hdMusic) hdMusic.checked = config[key].includes("hdmusic.si");
|
||||
if (widescreenBgs) widescreenBgs.checked = config[key].includes("widescreen.si");
|
||||
if (badEnding) badEnding.checked = config[key].includes("badend.si");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "directives") {
|
||||
const outroFmv = elements["Outro FMV"];
|
||||
if (outroFmv) outroFmv.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') {
|
||||
for (const radio of element) {
|
||||
if (radio.value === value) {
|
||||
radio.checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveConfig(form, getSiFiles, silent = false) {
|
||||
let iniContent = '[isle]\n';
|
||||
const elements = 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 hdTextures = elements["Texture Loader"];
|
||||
if (hdTextures) {
|
||||
iniContent += "[extensions]\n";
|
||||
const value = hdTextures.checked ? 'YES' : 'NO';
|
||||
iniContent += `${hdTextures.name}=${value}\n`;
|
||||
}
|
||||
|
||||
const siFiles = getSiFiles();
|
||||
const outroFmv = elements["Outro FMV"];
|
||||
|
||||
if (siFiles.length > 0 || (outroFmv && outroFmv.checked)) {
|
||||
iniContent += `SI Loader=YES\n`;
|
||||
iniContent += "[si loader]\n";
|
||||
}
|
||||
|
||||
if (siFiles.length > 0) {
|
||||
iniContent += `files=${siFiles.join(',')}\n`;
|
||||
}
|
||||
|
||||
let directives = [];
|
||||
if (outroFmv && 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`;
|
||||
}
|
||||
|
||||
// Use inline Web Worker for Safari compatibility
|
||||
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: CONFIG_FILE
|
||||
});
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
console.log(e.data.message);
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
worker.terminate();
|
||||
if (e.data.status === 'success' && !silent) {
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout);
|
||||
}
|
||||
configToastVisible.set(true);
|
||||
toastTimeout = setTimeout(() => configToastVisible.set(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (e) => {
|
||||
console.error('An error occurred in the config-saving worker:', e.message);
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
worker.terminate();
|
||||
};
|
||||
}
|
||||
167
src/core/serviceWorker.js
Normal file
@ -0,0 +1,167 @@
|
||||
// Service Worker registration and messaging
|
||||
import { showUpdatePopup, installState, swRegistration } from '../stores.js';
|
||||
|
||||
let downloaderWorker = null;
|
||||
|
||||
export async function registerServiceWorker() {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||
await navigator.serviceWorker.ready;
|
||||
swRegistration.set(registration);
|
||||
|
||||
// Check if there's already a waiting service worker (update ready)
|
||||
if (registration.waiting) {
|
||||
showUpdatePopup.set(true);
|
||||
}
|
||||
|
||||
// Listen for new service worker updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
showUpdatePopup.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
checkCacheStatus();
|
||||
});
|
||||
|
||||
return registration;
|
||||
} catch (error) {
|
||||
console.error('Service worker registration failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleServiceWorkerMessage(event) {
|
||||
const { action, language, isInstalled, success, missingFiles } = event.data;
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
if (language && languageSelect && language !== languageSelect.value) return;
|
||||
|
||||
switch (action) {
|
||||
case 'cache_status':
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installed: isInstalled,
|
||||
missingFiles: missingFiles || []
|
||||
}));
|
||||
break;
|
||||
case 'uninstall_complete':
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installed: !success,
|
||||
installing: false
|
||||
}));
|
||||
checkCacheStatus();
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown service worker action:', action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkCacheStatus() {
|
||||
if (!navigator.serviceWorker.controller) return;
|
||||
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
const hdTextures = document.getElementById('check-hd-textures');
|
||||
const hdMusic = document.getElementById('check-hd-music');
|
||||
const widescreenBgs = document.getElementById('check-widescreen-bgs');
|
||||
const badEnding = document.getElementById('check-ending');
|
||||
|
||||
const siFiles = getSiFilesForCache(hdMusic, widescreenBgs, badEnding);
|
||||
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
action: 'check_cache_status',
|
||||
language: languageSelect?.value || 'en',
|
||||
hdTextures: hdTextures?.checked || false,
|
||||
siFiles
|
||||
});
|
||||
}
|
||||
|
||||
export function getSiFilesForCache(hdMusic, widescreenBgs, badEnding) {
|
||||
const siFiles = [];
|
||||
if (hdMusic?.checked) siFiles.push('/LEGO/extra/hdmusic.si');
|
||||
if (widescreenBgs?.checked) siFiles.push('/LEGO/extra/widescreen.si');
|
||||
if (badEnding?.checked) siFiles.push('/LEGO/extra/badend.si');
|
||||
return siFiles;
|
||||
}
|
||||
|
||||
export async function startInstall(missingFiles, language) {
|
||||
await requestPersistentStorage();
|
||||
|
||||
if (downloaderWorker) downloaderWorker.terminate();
|
||||
downloaderWorker = new Worker('/downloader.js');
|
||||
downloaderWorker.onmessage = handleWorkerMessage;
|
||||
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installing: true,
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
downloaderWorker.postMessage({
|
||||
action: 'install',
|
||||
missingFiles,
|
||||
language
|
||||
});
|
||||
}
|
||||
|
||||
export function startUninstall(language) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
action: 'uninstall_language_pack',
|
||||
language
|
||||
});
|
||||
}
|
||||
|
||||
async function requestPersistentStorage() {
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
const isPersisted = await navigator.storage.persisted();
|
||||
if (!isPersisted) {
|
||||
const wasGranted = await navigator.storage.persist();
|
||||
console.log(wasGranted ? 'Persistent storage was granted.' : 'Persistent storage request was denied.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleWorkerMessage(event) {
|
||||
const { action, progress, success, error } = event.data;
|
||||
|
||||
switch (action) {
|
||||
case 'install_progress':
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installing: true,
|
||||
progress
|
||||
}));
|
||||
break;
|
||||
case 'install_complete':
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installed: success,
|
||||
installing: false
|
||||
}));
|
||||
if (downloaderWorker) downloaderWorker.terminate();
|
||||
break;
|
||||
case 'install_failed':
|
||||
alert(`Download failed: ${error}`);
|
||||
installState.update(state => ({
|
||||
...state,
|
||||
installing: false
|
||||
}));
|
||||
if (downloaderWorker) downloaderWorker.terminate();
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown worker action:', action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
71
src/core/webgl.js
Normal file
@ -0,0 +1,71 @@
|
||||
// WebGL detection and capability querying
|
||||
|
||||
let cachedGL = null;
|
||||
|
||||
function getWebGL2Context() {
|
||||
if (!cachedGL) {
|
||||
cachedGL = document.createElement('canvas').getContext('webgl2');
|
||||
}
|
||||
return cachedGL;
|
||||
}
|
||||
|
||||
export function getMsaaSamples() {
|
||||
const gl = getWebGL2Context();
|
||||
if (!gl) return null;
|
||||
|
||||
const samples = gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES);
|
||||
if (!samples || samples.length === 0 || Math.max(...samples) <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Array.from(samples).filter(s => s > 1).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export function getMaxAnisotropy() {
|
||||
const gl = getWebGL2Context();
|
||||
if (!gl) return null;
|
||||
|
||||
const ext = gl.getExtension('EXT_texture_filter_anisotropic');
|
||||
if (!ext) return null;
|
||||
|
||||
return gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
||||
}
|
||||
|
||||
export function populateMsaaSelect(selectElement, samples) {
|
||||
if (!selectElement || !samples) return;
|
||||
|
||||
selectElement.innerHTML = '';
|
||||
|
||||
const offOption = document.createElement('option');
|
||||
offOption.value = '1';
|
||||
offOption.textContent = 'Off';
|
||||
selectElement.appendChild(offOption);
|
||||
|
||||
samples.forEach(sampleCount => {
|
||||
const option = document.createElement('option');
|
||||
option.value = sampleCount;
|
||||
option.textContent = `${sampleCount}x`;
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
export function populateAfSelect(selectElement, maxAnisotropy) {
|
||||
if (!selectElement || !maxAnisotropy) return;
|
||||
|
||||
selectElement.innerHTML = '';
|
||||
|
||||
const offOption = document.createElement('option');
|
||||
offOption.value = '1';
|
||||
offOption.textContent = 'Off';
|
||||
selectElement.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;
|
||||
selectElement.appendChild(option);
|
||||
}
|
||||
}
|
||||
20
src/lib/Accordion.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
export let items = [];
|
||||
export let openItem = null;
|
||||
export let onToggle = () => {};
|
||||
export let titleKey = 'title';
|
||||
export let idKey = 'id';
|
||||
</script>
|
||||
|
||||
{#each items as item}
|
||||
<div class="accordion-item">
|
||||
<button type="button" class="accordion-header" onclick={() => onToggle(item[idKey])}>
|
||||
{item[titleKey]}
|
||||
</button>
|
||||
<div class="accordion-content" class:open={openItem === item[idKey]}>
|
||||
<div>
|
||||
<slot {item}></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
9
src/lib/BackButton.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
function goBack() {
|
||||
history.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="page-back-button" role="button" aria-label="Go back to main menu" onclick={goBack} onkeydown={(e) => e.key === 'Enter' && goBack()} tabindex="0">
|
||||
← Back
|
||||
</span>
|
||||
23
src/lib/CanvasWrapper.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
// Canvas wrapper for the Emscripten game
|
||||
</script>
|
||||
|
||||
<div id="canvas-wrapper">
|
||||
<div id="loading-gif-overlay">
|
||||
<img src="cdspin.gif" alt="Loading game...">
|
||||
<div class="quote-block">
|
||||
<p class="quote-text">"Whoops! You have to put the CD in your computer"</p>
|
||||
<p class="quote-attribution">- The Infomaniac (1997)</p>
|
||||
</div>
|
||||
<div class="loading-info-text">
|
||||
<p>"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!</p>
|
||||
<p>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!</p>
|
||||
</div>
|
||||
<div id="emscripten-status-message" class="status-message-bar">
|
||||
Loading LEGO® Island... please wait! <code>0%</code>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="canvas" oncontextmenu={(e) => e.preventDefault()} tabindex="-1"></canvas>
|
||||
</div>
|
||||
15
src/lib/CollapsibleSection.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
export let title;
|
||||
export let sectionId;
|
||||
export let isOpen = false;
|
||||
export let onToggle = () => {};
|
||||
</script>
|
||||
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => onToggle(sectionId)}>
|
||||
{title}
|
||||
</button>
|
||||
<div class="config-card-content" class:open={isOpen}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
7
src/lib/ConfigToast.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { configToastVisible } from '../stores.js';
|
||||
</script>
|
||||
|
||||
<div id="config-toast" class="config-toast" class:show={$configToastVisible}>
|
||||
Settings saved
|
||||
</div>
|
||||
276
src/lib/ConfigurePage.svelte
Normal file
@ -0,0 +1,276 @@
|
||||
<script>
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
import BackButton from './BackButton.svelte';
|
||||
import DisplayTab from './config/DisplayTab.svelte';
|
||||
import ControlsTab from './config/ControlsTab.svelte';
|
||||
import AudioTab from './config/AudioTab.svelte';
|
||||
import ExtrasTab from './config/ExtrasTab.svelte';
|
||||
import { installState, currentPage } from '../stores.js';
|
||||
import { loadConfig, saveConfig, getFileHandle } from '../core/opfs.js';
|
||||
import { checkCacheStatus, startInstall, startUninstall, getSiFilesForCache } from '../core/serviceWorker.js';
|
||||
import { getMsaaSamples, getMaxAnisotropy, populateMsaaSelect, populateAfSelect } from '../core/webgl.js';
|
||||
|
||||
let activeTab = 'display';
|
||||
let openSection = 'game';
|
||||
|
||||
// Reset to default tab/section when navigating to this page
|
||||
$: if ($currentPage === 'configure') {
|
||||
activeTab = 'display';
|
||||
openSection = 'game';
|
||||
}
|
||||
|
||||
let configForm;
|
||||
let opfsDisabled = false;
|
||||
let msaaSupported = false;
|
||||
let afSupported = false;
|
||||
let isTouchDevice = false;
|
||||
let fullscreenSupported = false;
|
||||
|
||||
const configTabs = [
|
||||
{ id: 'display', label: 'Display', firstSection: 'game' },
|
||||
{ id: 'controls', label: 'Controls', firstSection: 'input' },
|
||||
{ id: 'audio', label: 'Audio', firstSection: 'sound' },
|
||||
{ id: 'extras', label: 'Extras', firstSection: 'extensions' }
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
isTouchDevice = window.matchMedia('(any-pointer: coarse)').matches;
|
||||
fullscreenSupported = !!document.documentElement.requestFullscreen;
|
||||
|
||||
// Initialize WebGL options
|
||||
const samples = getMsaaSamples();
|
||||
const maxAniso = getMaxAnisotropy();
|
||||
|
||||
if (samples) msaaSupported = true;
|
||||
if (maxAniso) afSupported = true;
|
||||
|
||||
// Wait for DOM to update after setting flags
|
||||
await tick();
|
||||
|
||||
if (samples) {
|
||||
const msaaSelect = document.getElementById('msaa-select');
|
||||
populateMsaaSelect(msaaSelect, samples);
|
||||
}
|
||||
|
||||
if (maxAniso) {
|
||||
const afSelect = document.getElementById('anisotropic-select');
|
||||
populateAfSelect(afSelect, maxAniso);
|
||||
}
|
||||
|
||||
// Load config from OPFS
|
||||
const handle = await getFileHandle();
|
||||
if (!handle) {
|
||||
opfsDisabled = true;
|
||||
} else {
|
||||
const config = await loadConfig(configForm);
|
||||
if (!config) {
|
||||
// Save defaults silently (no toast on initial creation)
|
||||
await saveConfig(configForm, getSiFiles, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache status
|
||||
checkCacheStatus();
|
||||
|
||||
// Setup fullscreen handlers
|
||||
if (fullscreenSupported) {
|
||||
const fullscreenEl = document.getElementById('window-fullscreen');
|
||||
const windowedEl = document.getElementById('window-windowed');
|
||||
|
||||
fullscreenEl?.addEventListener('change', () => {
|
||||
if (fullscreenEl.checked) {
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
windowedEl?.addEventListener('change', () => {
|
||||
if (windowedEl.checked && document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (document.fullscreenElement) {
|
||||
fullscreenEl.checked = true;
|
||||
} else {
|
||||
windowedEl.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup tooltip handling for touch devices
|
||||
if (isTouchDevice) {
|
||||
setupTouchTooltips();
|
||||
}
|
||||
});
|
||||
|
||||
function setupTouchTooltips() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const trigger = e.target.closest('.tooltip-trigger');
|
||||
if (trigger) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const wasActive = trigger.classList.contains('active');
|
||||
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
|
||||
if (!wasActive) trigger.classList.add('active');
|
||||
} else {
|
||||
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSiFiles() {
|
||||
const hdMusic = document.getElementById('check-hd-music');
|
||||
const widescreenBgs = document.getElementById('check-widescreen-bgs');
|
||||
const badEnding = document.getElementById('check-ending');
|
||||
return getSiFilesForCache(hdMusic, widescreenBgs, badEnding);
|
||||
}
|
||||
|
||||
function handleFormChange() {
|
||||
if (!opfsDisabled) {
|
||||
saveConfig(configForm, getSiFiles);
|
||||
}
|
||||
showOrHideGraphicsOptions();
|
||||
}
|
||||
|
||||
function showOrHideGraphicsOptions() {
|
||||
const rendererSelect = document.getElementById('renderer-select');
|
||||
const msaaGroup = document.getElementById('msaa-select')?.closest('.form-group');
|
||||
const afGroup = document.getElementById('anisotropic-select')?.closest('.form-group');
|
||||
|
||||
if (rendererSelect?.value === "0 0x682656f3 0x0 0x0 0x2000000") {
|
||||
if (msaaGroup) msaaGroup.style.display = 'none';
|
||||
if (afGroup) afGroup.style.display = 'none';
|
||||
} else {
|
||||
if (msaaGroup && msaaSupported) msaaGroup.style.display = '';
|
||||
if (afGroup && afSupported) afGroup.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreset(preset) {
|
||||
if (preset === 'classic') {
|
||||
document.getElementById('language-select').value = 'en';
|
||||
const windowed = document.getElementById('window-windowed');
|
||||
if (windowed) windowed.checked = true;
|
||||
document.getElementById('aspect-original').checked = true;
|
||||
document.getElementById('resolution-original').checked = true;
|
||||
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';
|
||||
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';
|
||||
document.getElementById('check-hd-textures').checked = true;
|
||||
document.getElementById('check-hd-music').checked = true;
|
||||
document.getElementById('check-widescreen-bgs').checked = true;
|
||||
}
|
||||
handleFormChange();
|
||||
checkCacheStatus();
|
||||
}
|
||||
|
||||
function toggleSection(sectionId) {
|
||||
openSection = openSection === sectionId ? null : sectionId;
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
activeTab = tab.id;
|
||||
openSection = tab.firstSection;
|
||||
}
|
||||
|
||||
function handleInstall() {
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
startInstall($installState.missingFiles, languageSelect.value);
|
||||
}
|
||||
|
||||
function handleUninstall() {
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
startUninstall(languageSelect.value);
|
||||
}
|
||||
|
||||
function handleExtensionChange() {
|
||||
handleFormChange();
|
||||
checkCacheStatus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="configure-page" class="page-content">
|
||||
<BackButton />
|
||||
{#if opfsDisabled}
|
||||
<blockquote id="opfs-disabled" class="error-box">
|
||||
<p>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.</p>
|
||||
</blockquote>
|
||||
{/if}
|
||||
<div class="page-inner-content config-layout">
|
||||
<div class="config-art-panel">
|
||||
<img src="shark.webp" alt="LEGO Island Shark and Brickster">
|
||||
</div>
|
||||
<div class="config-main">
|
||||
<div class="config-presets">
|
||||
<button type="button" class="preset-btn" disabled={opfsDisabled} onclick={() => applyPreset('classic')}>Classic Mode</button>
|
||||
<button type="button" class="preset-btn" disabled={opfsDisabled} onclick={() => applyPreset('modern')}>Modern Mode</button>
|
||||
</div>
|
||||
<div class="config-tabs">
|
||||
<div class="config-tab-buttons">
|
||||
{#each configTabs as tab}
|
||||
<button class="config-tab-btn" class:active={activeTab === tab.id} onclick={() => switchTab(tab)}>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<form id="config-form" class="config-form" bind:this={configForm} onchange={handleFormChange}>
|
||||
<div class:hidden={activeTab !== 'display'}>
|
||||
<DisplayTab
|
||||
{opfsDisabled}
|
||||
{openSection}
|
||||
{toggleSection}
|
||||
{fullscreenSupported}
|
||||
{msaaSupported}
|
||||
{afSupported}
|
||||
{showOrHideGraphicsOptions}
|
||||
{checkCacheStatus}
|
||||
/>
|
||||
</div>
|
||||
<div class:hidden={activeTab !== 'controls'}>
|
||||
<ControlsTab
|
||||
{opfsDisabled}
|
||||
{openSection}
|
||||
{toggleSection}
|
||||
{isTouchDevice}
|
||||
/>
|
||||
</div>
|
||||
<div class:hidden={activeTab !== 'audio'}>
|
||||
<AudioTab
|
||||
{opfsDisabled}
|
||||
{openSection}
|
||||
{toggleSection}
|
||||
/>
|
||||
</div>
|
||||
<div class:hidden={activeTab !== 'extras'}>
|
||||
<ExtrasTab
|
||||
{opfsDisabled}
|
||||
{openSection}
|
||||
{toggleSection}
|
||||
{handleExtensionChange}
|
||||
{handleInstall}
|
||||
{handleUninstall}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
43
src/lib/Controls.svelte
Normal file
@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import { showGoodbyePopup } from '../stores.js';
|
||||
import { startGame } from '../core/emscripten.js';
|
||||
import { pauseInstallAudio } from '../core/audio.js';
|
||||
import { navigateTo } from '../core/navigation.js';
|
||||
import ImageButton from './ImageButton.svelte';
|
||||
|
||||
let rendererValue = "0 0x682656f3 0x0 0x0 0x4000000"; // WebGL default
|
||||
|
||||
const buttons = [
|
||||
{ id: 'run-game-btn', off: 'run_game_off.webp', on: 'run_game_on.webp', alt: 'Run Game', width: 135, height: 164, action: handleRunGame },
|
||||
{ id: 'configure-btn', off: 'configure_off.webp', on: 'configure_on.webp', alt: 'Configure', width: 130, height: 147, action: () => navigateTo('configure') },
|
||||
{ id: 'free-stuff-btn', off: 'free_stuff_off.webp', on: 'free_stuff_on.webp', alt: 'Free Stuff', width: 134, height: 149, action: () => navigateTo('free-stuff') },
|
||||
{ id: 'read-me-btn', off: 'read_me_off.webp', on: 'read_me_on.webp', alt: 'Read Me', width: 134, height: 149, action: () => navigateTo('read-me') },
|
||||
{ id: 'cancel-btn', off: 'cancel_off.webp', on: 'cancel_on.webp', alt: 'Cancel', width: 93, height: 145, action: () => showGoodbyePopup.set(true) }
|
||||
];
|
||||
|
||||
function handleRunGame() {
|
||||
pauseInstallAudio();
|
||||
|
||||
// Get current renderer value from select
|
||||
const rendererSelect = document.getElementById('renderer-select');
|
||||
if (rendererSelect) {
|
||||
rendererValue = rendererSelect.value;
|
||||
}
|
||||
|
||||
startGame(rendererValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="controls-wrapper">
|
||||
{#each buttons as btn}
|
||||
<ImageButton
|
||||
id={btn.id}
|
||||
offSrc={btn.off}
|
||||
onSrc={btn.on}
|
||||
alt={btn.alt}
|
||||
width={btn.width}
|
||||
height={btn.height}
|
||||
onclick={btn.action}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
252
src/lib/DebugPanel.svelte
Normal file
@ -0,0 +1,252 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
import { debugUIVisible } from '../stores.js';
|
||||
|
||||
let debugPanelOpen = false;
|
||||
let debugUIElement;
|
||||
let observer = null;
|
||||
|
||||
// Set up MutationObserver when the element becomes available
|
||||
$: if (debugUIElement && !observer) {
|
||||
observer = new MutationObserver(() => {
|
||||
if (debugUIElement && debugUIElement.style.display === 'none') {
|
||||
debugUIElement.style.setProperty('display', 'block', 'important');
|
||||
}
|
||||
});
|
||||
observer.observe(debugUIElement, { attributes: true, attributeFilter: ['style'] });
|
||||
}
|
||||
|
||||
// Clean up observer when component is destroyed
|
||||
onDestroy(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
});
|
||||
|
||||
let debugModeActive = false;
|
||||
let selectedLocation = '';
|
||||
let selectedAnimation = '';
|
||||
|
||||
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 },
|
||||
'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 },
|
||||
};
|
||||
|
||||
const locations = [
|
||||
{ value: 'c01', label: 'LCAMBA1 (01)' }, { value: 'c02', label: 'LCAMBA2 (02)' }, { value: 'c03', label: 'LCAMBA3 (03)' },
|
||||
{ value: 'c04', label: 'LCAMBA4 (04)' }, { value: 'c05', label: 'LCAMCA1 (05)' }, { value: 'c06', label: 'LCAMCA2 (06)' },
|
||||
{ value: 'c07', label: 'LCAMCA3 (07)' }, { value: 'c08', label: 'LCAMGS1 (08)' }, { value: 'c09', label: 'LCAMGS2 (09)' },
|
||||
{ value: 'c10', label: 'LCAMGS3 (10)' }, { value: 'c11', label: 'LCAMHO1 (11)' }, { value: 'c12', label: 'LCAMHO2 (12)' },
|
||||
{ value: 'c13', label: 'LCAMHO3 (13)' }, { value: 'c14', label: 'LCAMIS1 (14)' }, { value: 'c15', label: 'LCAMIS2 (15)' },
|
||||
{ value: 'c16', label: 'LCAMIS3 (16)' }, { value: 'c17', label: 'LCAMIS4 (17)' }, { value: 'c18', label: 'LCAMIS5 (18)' },
|
||||
{ value: 'c19', label: 'LCAMJA1 (19)' }, { value: 'c20', label: 'LCAMJA2 (20)' }, { value: 'c21', label: 'LCAMPO1 (21)' },
|
||||
{ value: 'c22', label: 'LCAMPO2 (22)' }, { value: 'c23', label: 'LCAMPO3 (23)' }, { value: 'c24', label: 'LCAMPZ1 (24)' },
|
||||
{ value: 'c25', label: 'LCAMPZ2 (25)' }, { value: 'c26', label: 'LCAMRA1 (26)' }, { value: 'c27', label: 'LCAMRA2 (27)' },
|
||||
{ value: 'c28', label: 'LCAMRA3 (28)' }, { value: 'c29', label: 'LCAMRA4 (29)' }, { value: 'c30', label: 'LCAMRT1 (30)' },
|
||||
{ value: 'c31', label: 'LCAMRT2 (31)' }, { value: 'c32', label: 'LCAMRT3 (32)' }, { value: 'c33', label: 'LCAMRT4 (33)' },
|
||||
{ value: 'c34', label: 'LCAMRT5 (34)' }, { value: 'c35', label: 'LCAMRT6 (35)' }, { value: 'c36', label: 'LCAMRT7 (36)' },
|
||||
{ value: 'c37', label: 'LCAMRT8 (37)' }, { value: 'c38', label: 'LCAMRT9 (38)' }, { value: 'c39', label: 'LCAMRT10 (39)' },
|
||||
{ value: 'c40', label: 'LCAMRT11 (40)' }, { value: 'c41', label: 'LCAMRT12 (41)' }, { value: 'c42', label: 'LCAMRT13 (42)' },
|
||||
{ value: 'c43', label: 'LCAMRT14 (43)' }, { value: 'c44', label: 'LCAMRT15 (44)' }, { value: 'c45', label: 'LCAMRT16 (45)' },
|
||||
{ value: 'c46', label: 'LCAMRT17 (46)' }, { value: 'c47', label: 'LCAMRT18 (47)' }, { value: 'c48', label: 'LCAMRT19 (48)' },
|
||||
{ value: 'c49', label: 'LCAMRT20 (49)' }, { value: 'c50', label: 'LCAMRT21 (50)' }, { value: 'c51', label: 'LCAMRT22 (51)' },
|
||||
{ value: 'c52', label: 'LCAMRT23 (52)' }, { value: 'c53', label: 'LCAMRT24 (53)' }, { value: 'c54', label: 'LCAMRT25 (54)' },
|
||||
{ value: 'c55', label: 'LCAMRT26 (55)' }, { value: 'c56', label: 'LCAMRT27 (56)' }, { value: 'c57', label: 'LCAMRT28 (57)' },
|
||||
{ value: 'c58', label: 'LCAMRT29 (58)' }, { value: 'c59', label: 'LCAMRT30 (59)' }, { value: 'c60', label: 'LCAMRT31 (60)' },
|
||||
{ value: 'c61', label: 'LCAMRT32 (61)' }, { value: 'c62', label: 'LCAMRT33 (62)' }, { value: 'c63', label: 'LCAMRT34 (63)' },
|
||||
{ value: 'c64', label: 'LCAMRT35 (64)' }, { value: 'c65', label: 'LCAMRT36 (65)' }, { value: 'c66', label: 'LCAMRT37 (66)' },
|
||||
{ value: 'c67', label: 'LCAMRT38 (67)' }, { value: 'c68', label: 'LCAMRT39 (68)' }, { value: 'c69', label: 'LCAMRT40 (69)' }
|
||||
];
|
||||
|
||||
// Sample animations (abbreviated for brevity - full list would be included in production)
|
||||
const animations = [
|
||||
{ value: '400', label: 'wns050p1 (400)' }, { value: '500', label: 'sba001bu (500)' },
|
||||
{ value: '600', label: 'ppz086bs (600)' }, { value: '700', label: 'hpz057ma (700)' },
|
||||
{ value: '800', label: 'nca001ca (800)' }
|
||||
];
|
||||
|
||||
function sendKey(key) {
|
||||
const canvas = document.getElementById('canvas');
|
||||
let keyInfo = keyCodeMap[key];
|
||||
|
||||
if (!keyInfo) {
|
||||
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));
|
||||
}
|
||||
|
||||
function sendKeySequence(keys, delay = 100) {
|
||||
let index = 0;
|
||||
const canvas = document.getElementById('canvas');
|
||||
|
||||
function sendNext() {
|
||||
if (index < keys.length) {
|
||||
sendKey(keys[index]);
|
||||
index++;
|
||||
setTimeout(sendNext, delay);
|
||||
} else {
|
||||
canvas.focus();
|
||||
}
|
||||
}
|
||||
sendNext();
|
||||
}
|
||||
|
||||
function enterDebugMode() {
|
||||
sendKeySequence(['o', 'g', 'e', 'l']);
|
||||
debugModeActive = true;
|
||||
}
|
||||
|
||||
function handleAction(keys, requiresDebug = false) {
|
||||
if (requiresDebug && !debugModeActive) {
|
||||
enterDebugMode();
|
||||
setTimeout(() => sendKeySequence(keys.split('')), 500);
|
||||
} else if (keys.length > 1 && !keyCodeMap[keys]) {
|
||||
sendKeySequence(keys.split(''));
|
||||
} else {
|
||||
sendKey(keys);
|
||||
document.getElementById('canvas').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function gotoLocation() {
|
||||
if (!selectedLocation) return;
|
||||
if (!debugModeActive) {
|
||||
enterDebugMode();
|
||||
setTimeout(() => sendKeySequence(selectedLocation.split('')), 500);
|
||||
} else {
|
||||
sendKeySequence(selectedLocation.split(''));
|
||||
}
|
||||
}
|
||||
|
||||
function playAnimation() {
|
||||
if (!selectedAnimation) return;
|
||||
const paddedId = selectedAnimation.padStart(3, '0');
|
||||
const keys = ['v', ...paddedId.split('')];
|
||||
|
||||
if (!debugModeActive) {
|
||||
enterDebugMode();
|
||||
setTimeout(() => sendKeySequence(keys), 500);
|
||||
} else {
|
||||
sendKeySequence(keys);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $debugUIVisible}
|
||||
<div id="debug-ui" bind:this={debugUIElement}>
|
||||
<button id="debug-toggle" title="Debug Options" class:active={debugPanelOpen} onclick={() => debugPanelOpen = !debugPanelOpen}>⚙</button>
|
||||
|
||||
{#if debugPanelOpen}
|
||||
<div id="debug-panel" class="open">
|
||||
<div class="debug-header">Debug Options</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">General</div>
|
||||
<button onclick={() => handleAction('Pause')}>Pause/Resume</button>
|
||||
<button onclick={() => handleAction('Escape')}>Return to Infocenter</button>
|
||||
<button onclick={() => handleAction(' ')}>Skip Animation</button>
|
||||
<button onclick={() => handleAction('F12')}>Save Game</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Debug Mode (OGEL)</div>
|
||||
<button class="debug-password" class:active={debugModeActive} onclick={enterDebugMode}>
|
||||
{debugModeActive ? 'Debug Mode Active' : 'Enter Debug Mode'}
|
||||
</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('Tab', true)}>Toggle FPS</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('s', true)}>Toggle Music</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('p', true)}>Reset/Load Plants</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Camera/View</div>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('u', true)}>Move Up</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('d', true)}>Move Down</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">LOD (Level of Detail)</div>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('f', true)}>LOD 0.0 (Lowest)</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('x', true)}>LOD 3.6 (Default)</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('h', true)}>LOD 5.0 (Highest)</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Misc</div>
|
||||
<button onclick={() => handleAction('z')}>Make Plants Dance</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Switch Act</div>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('g2', true)}>Act 2</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('g3', true)}>Act 3</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('g4', true)}>Good Ending</button>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('g5', true)}>Bad Ending</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Locations</div>
|
||||
<select bind:value={selectedLocation}>
|
||||
<option value="">-- Select Location --</option>
|
||||
{#each locations as loc}
|
||||
<option value={loc.value}>{loc.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={gotoLocation}>Go to Location</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<div class="debug-section-title">Animations</div>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={() => handleAction('va', true)}>Play <b>all</b> cam animations</button>
|
||||
<select bind:value={selectedAnimation}>
|
||||
<option value="">-- Select Animation --</option>
|
||||
{#each animations as anim}
|
||||
<option value={anim.value}>{anim.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="requires-debug" class:enabled={debugModeActive} onclick={playAnimation}>Play Animation</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Styles moved to app.css for #debug-ui */
|
||||
</style>
|
||||
46
src/lib/FreeStuffPage.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script>
|
||||
import BackButton from './BackButton.svelte';
|
||||
|
||||
const resources = [
|
||||
{ href: 'https://www.youtube.com/watch?v=bG55COe_f8I', title: 'The Making of LEGO Island: A Documentary', desc: 'An in-depth documentary by MattKC that explores the fascinating and chaotic development story behind the classic game.' },
|
||||
{ href: 'https://www.youtube.com/watch?v=Poaxx9sMxjw', title: 'LEGO Island Radio 24/7', desc: 'Enjoy the iconic, high-quality soundtrack of LEGO Island anytime with this continuous live stream, complete with the original DJ interludes.' },
|
||||
{ href: 'https://www.legoisland.org/', title: 'LEGO Island Wiki', desc: 'Your ultimate resource for all things LEGO Island. This fan-run wiki contains a wealth of information, research, and details about the game.' },
|
||||
{ href: 'https://github.com/isledecomp/isle', title: 'LEGO Island Decompilation', desc: '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.' },
|
||||
{ href: 'https://github.com/isledecomp/isle-portable', title: 'LEGO Island, Portable Version', desc: 'A portable, cross-platform version of the decompilation project which serves as the direct foundation for this web-based port.' },
|
||||
{ href: 'https://github.com/isledecomp/isle.pizza', title: 'isle.pizza Frontend', desc: 'The source code for this website! A custom-built frontend for the Emscripten version of the portable decompilation project.' },
|
||||
{ href: 'https://github.com/isledecomp/LEGOIslandRebuilder', title: 'LEGO Island Rebuilder', desc: 'A powerful launcher and tool for patching and modding the original 1997 PC version of LEGO Island. Essential for play and modding.' },
|
||||
{ href: 'https://github.com/isledecomp/SIEdit', title: 'SIEdit', desc: 'A suite of tools developed by the decompilation team for viewing and editing the ".si" script and resource files from the original game.' },
|
||||
{ href: 'https://www.legoisland.org/wiki/The_Making_of_LEGO_Island', title: 'The Making of LEGO Island, a memoir by Wes Jenkins', desc: 'Read the fascinating, incomplete memoir from Creative Director Wes Jenkins, detailing the development process and the team behind the game.' },
|
||||
{ href: '/poster.pdf', title: 'LEGO Island: Free Poster', desc: 'Download a copy of the iconic poster that was originally included with the retail release of the game.' },
|
||||
{ href: 'https://brickstobytes.org/games/lego-island', title: 'Development Materials Archive', desc: 'Explore a collection of development materials, concept art, and other historical assets from the creation of LEGO Island.' },
|
||||
{ href: 'https://le717.github.io/LEGO-Island-VGF/legoisland/#interview', title: 'Video Game Flashback: An Interview with Wes Jenkins', desc: 'A detailed interview with LEGO Island\'s Creative Director, Wes Jenkins, offering unique insights into the game\'s production.' },
|
||||
{ href: 'https://www.youtube.com/watch?v=fodBG_QylVM', title: 'LEGO Island - Behind the Scenes', desc: 'Watch a rare promotional video created during the game\'s development, showcasing its progress and vision at the time.' },
|
||||
{ href: 'https://tcrf.net/LEGO_Island', title: 'The Cutting Room Floor', desc: 'Discover unused assets, hidden data, and other secrets left in the retail version of the game. A fascinating look at what might have been.' },
|
||||
{ href: 'https://projectisland.org/music/', title: 'Project Island High Quality Music', desc: 'A complete, high-quality re-digitization of the LEGO Island soundtrack, restored by the game\'s main composer, Lorin Nelson.' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div id="free-stuff-page" class="page-content">
|
||||
<BackButton />
|
||||
<div class="page-inner-content">
|
||||
<div class="resource-list">
|
||||
<div class="quote-panel">
|
||||
<div class="quote-panel-art">
|
||||
<img src="congrats.webp" alt="LEGO Island characters celebrating">
|
||||
</div>
|
||||
<blockquote class="quote-panel-content">
|
||||
<p>"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."</p>
|
||||
<footer>Wes Jenkins, Creative Director</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
{#each resources as resource}
|
||||
<a href={resource.href} target="_blank" rel="noopener noreferrer" class="resource-item">
|
||||
<h3>{resource.title}</h3>
|
||||
<p>{resource.desc}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
59
src/lib/GoodbyePopup.svelte
Normal file
@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import { showGoodbyePopup, goodbyeProgress } from '../stores.js';
|
||||
|
||||
let timeout = null;
|
||||
let interval = null;
|
||||
|
||||
$: if ($showGoodbyePopup) {
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
const duration = 4000;
|
||||
const startTime = performance.now();
|
||||
|
||||
function animate(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
goodbyeProgress.set(progress * 100);
|
||||
|
||||
if (progress < 1) {
|
||||
interval = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
interval = requestAnimationFrame(animate);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
window.location.href = 'https://legoisland.org';
|
||||
}, duration);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
if (interval) {
|
||||
cancelAnimationFrame(interval);
|
||||
interval = null;
|
||||
}
|
||||
goodbyeProgress.set(0);
|
||||
showGoodbyePopup.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $showGoodbyePopup}
|
||||
<div id="goodbye-popup" class="notification-popup">
|
||||
<div class="notification-popup-content">
|
||||
<button class="update-dismiss-btn" aria-label="Cancel" onclick={cancel}>×</button>
|
||||
<div class="update-speech-bubble">
|
||||
<p class="update-message">See you later, Brickulator!</p>
|
||||
<div class="goodbye-progress">
|
||||
<div class="goodbye-progress-bar" style="width: {$goodbyeProgress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img src="later.webp" alt="Goodbye" class="update-character" width="150" height="187">
|
||||
</div>
|
||||
{/if}
|
||||
26
src/lib/ImageButton.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script>
|
||||
export let id = '';
|
||||
export let offSrc;
|
||||
export let onSrc;
|
||||
export let alt;
|
||||
export let width = undefined;
|
||||
export let height = undefined;
|
||||
export let onclick = () => {};
|
||||
|
||||
let hovered = false;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions -->
|
||||
<img
|
||||
class="control-img"
|
||||
{id}
|
||||
{width}
|
||||
{height}
|
||||
src={hovered ? onSrc : offSrc}
|
||||
{alt}
|
||||
tabindex="0"
|
||||
onmouseenter={() => hovered = true}
|
||||
onmouseleave={() => hovered = false}
|
||||
{onclick}
|
||||
onkeydown={(e) => e.key === 'Enter' && onclick()}
|
||||
/>
|
||||
284
src/lib/ReadMePage.svelte
Normal file
@ -0,0 +1,284 @@
|
||||
<script>
|
||||
import BackButton from './BackButton.svelte';
|
||||
import Accordion from './Accordion.svelte';
|
||||
import { currentPage } from '../stores.js';
|
||||
|
||||
let activeTab = 'about';
|
||||
let openItem = null;
|
||||
|
||||
$: if ($currentPage === 'read-me') {
|
||||
activeTab = 'about';
|
||||
openItem = null;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'about', label: 'About', icon: 'register.webp' },
|
||||
{ id: 'system', label: 'System', icon: 'sysinfo.webp' },
|
||||
{ id: 'faq', label: 'FAQ', icon: 'getinfo.webp' },
|
||||
{ id: 'changelog', label: 'Changelog', icon: 'callfail.webp' },
|
||||
{ id: 'manual', label: 'Manual', icon: 'bonus.webp' }
|
||||
];
|
||||
|
||||
const faqItems = [
|
||||
{ id: 'faq1', question: 'Is this the full, original game?', answer: `<p>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.</p>` },
|
||||
{ id: 'faq2', question: 'How does this differ from the original 1997 CD-ROM game?', answer: `<p>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!).</p><p>Check out the "Configure" page to see what's possible.</p>` },
|
||||
{ id: 'faq3', question: 'Can I save my progress?', answer: `<p>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.</p>` },
|
||||
{ id: 'faq4', question: 'Does this run on mobile?', answer: `<p>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 <a href="https://github.com/isledecomp/isle-portable/issues/418#issuecomment-3003572219" target="_blank" rel="noopener noreferrer">Tesla in-car browsers</a>!</p>` },
|
||||
{ id: 'faq5', question: 'Which browsers are supported?', answer: `<p>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.</p>` },
|
||||
{ id: 'faq6', question: 'What are the controls?', answer: `<p>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.</p>` },
|
||||
{ id: 'faq7', question: 'Can I play offline?', answer: `<p>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.</p>` },
|
||||
{ id: 'faq8', question: "I don't hear any sound or music. How do I fix it?", answer: `<p>Most modern browsers block audio until you interact with the page. Click the mute icon on the animated intro to enable sound.</p>` },
|
||||
{ id: 'faq9', question: 'I think I found a bug! Where do I report it?', answer: `<p>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 <a href="https://github.com/isledecomp/isle-portable/issues" target="_blank" rel="noopener noreferrer">GitHub Issues page</a>. Please include details about your browser, device, and what you were doing when the bug occurred.</p>` },
|
||||
{ id: 'faq10', question: 'Is this project open-source?', answer: `<p>Yes, absolutely! This web port is built upon the incredible open-source <a href="https://github.com/isledecomp/isle-portable" target="_blank" rel="noopener noreferrer">LEGO Island (portable)</a> project, and the code for this website is also <a href="https://github.com/isledecomp/isle.pizza" target="_blank" rel="noopener noreferrer">available here</a>.</p>` }
|
||||
];
|
||||
|
||||
const changelogItems = [
|
||||
{ id: 'cl1', title: 'January 2026', items: [
|
||||
{ type: 'New', text: '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' },
|
||||
{ type: 'Improved', text: 'Configure page redesigned with tabbed navigation, collapsible sections, quick presets (Classic/Modern Mode), and modern toggle switches' },
|
||||
{ type: 'Improved', text: 'Read Me page reorganized into tabs (About, System, FAQ, Changelog, Manual) with the original instruction manual now viewable in-browser' }
|
||||
]},
|
||||
{ id: 'cl2', title: 'December 2025', items: [
|
||||
{ type: 'New', text: '"Active in Background" option keeps the game running when the tab loses focus' },
|
||||
{ type: 'New', text: 'WASD navigation controls as an alternative to arrow keys' },
|
||||
{ type: 'Fixed', text: 'Act 3 helicopter ammo now correctly sticks to targets and finishes animations' },
|
||||
{ type: 'Fixed', text: 'Pick/click distance calculation for more accurate object selection' },
|
||||
{ type: 'Fixed', text: 'Maximum deltaTime capping prevents physics glitches in races' },
|
||||
{ type: 'Fixed', text: 'Touch controls now properly support widescreen aspect ratios' },
|
||||
{ type: 'Improved', text: 'Default anisotropic filtering increased to 16x for sharper textures' }
|
||||
]},
|
||||
{ id: 'cl3', title: 'November 2025', items: [
|
||||
{ type: 'Fixed', text: 'Dictionary loading failure no longer causes crashes' },
|
||||
{ type: 'Fixed', text: 'INI configuration now properly applies defaults when values are missing' }
|
||||
]},
|
||||
{ id: 'cl4', title: 'September 2025', items: [
|
||||
{ type: 'New', text: 'Additional widescreen background images' },
|
||||
{ type: 'Fixed', text: 'Jukebox state now correctly restored when using HD Music extension' },
|
||||
{ type: 'Fixed', text: 'Background audio no longer gets stuck when starting audio fails' },
|
||||
{ type: 'Improved', text: 'SI Loader actions now start at the correct time during world loading' }
|
||||
]},
|
||||
{ id: 'cl5', title: 'August 2025', items: [
|
||||
{ type: 'New', text: 'Extended Bad Ending FMV extension shows the uncut beta animation' },
|
||||
{ type: 'New', text: 'HD Music extension with high-quality audio' },
|
||||
{ type: 'New', text: 'Widescreen backgrounds extension eliminates 3D edges on wide displays' },
|
||||
{ type: 'New', text: 'SI Loader extension system for community content and modifications' },
|
||||
{ type: 'New', text: 'OpenGL ES 2.0/3.0 renderer for broader device compatibility' },
|
||||
{ type: 'Fixed', text: 'Purple edges no longer appear on scaled transparent 2D elements' },
|
||||
{ type: 'Fixed', text: 'Transparent pixels now render correctly with alpha channel support' }
|
||||
]},
|
||||
{ id: 'cl6', title: 'July 2025', items: [
|
||||
{ type: 'New', text: 'HD Textures extension with enhanced visuals' },
|
||||
{ type: 'New', text: 'MSAA anti-aliasing support for smoother edges' },
|
||||
{ type: 'New', text: 'Anisotropic filtering for sharper textures at angles' },
|
||||
{ type: 'New', text: 'Haptic feedback (vibration) support for gamepads and mobile devices' },
|
||||
{ type: 'New', text: 'Virtual Gamepad touch control scheme with sliding controls' },
|
||||
{ type: 'New', text: 'Gamepad/controller support with analog sticks and D-pad' },
|
||||
{ type: 'New', text: 'Full screen mode with in-game toggle' },
|
||||
{ type: 'New', text: 'Maximum LOD and Maximum Actors configuration options' },
|
||||
{ type: 'New', text: 'Configurable transition animations (Mosaic, Dissolve, Wipe, etc.)' },
|
||||
{ type: 'New', text: 'Extensions system allowing community-created content' },
|
||||
{ type: 'Fixed', text: 'WebGL driver compatibility issues resolved' },
|
||||
{ type: 'Fixed', text: 'Firefox Private browsing mode now works correctly' },
|
||||
{ type: 'Fixed', text: 'Virtual cursor transparency and positioning' },
|
||||
{ type: 'Fixed', text: 'Touch coordinate translation for proper viewport mapping' },
|
||||
{ type: 'Fixed', text: 'Memory leaks in ViewLODList' },
|
||||
{ type: 'Fixed', text: 'Screen transitions on software renderer and 32-bit displays' },
|
||||
{ type: 'Fixed', text: 'Tabbing in and out of fullscreen' },
|
||||
{ type: 'Fixed', text: 'Click spam prevention on touch screens' },
|
||||
{ type: 'Improved', text: 'Mosaic transition animation is faster and cleaner' },
|
||||
{ type: 'Improved', text: 'Loading UX for HD Textures with progress indicators' }
|
||||
]},
|
||||
{ id: 'cl7', title: 'June 2025 — Initial Release', items: [
|
||||
{ type: 'New', text: 'Emscripten web port — play LEGO Island directly in your browser!' },
|
||||
{ type: 'New', text: 'WebGL rendering for hardware-accelerated 3D graphics' },
|
||||
{ type: 'New', text: 'Software renderer fallback for devices without WebGL' },
|
||||
{ type: 'New', text: '32-bit color support for improved visual quality' },
|
||||
{ type: 'New', text: 'Full screen support' },
|
||||
{ type: 'New', text: 'Joystick/gamepad enabled by default' },
|
||||
{ type: 'New', text: 'Option to skip the startup delay' },
|
||||
{ type: 'New', text: 'Support for LEGO Island 1.0 version' },
|
||||
{ type: 'New', text: 'FPS display option' },
|
||||
{ type: 'New', text: 'Game runs without requiring an audio device' },
|
||||
{ type: 'Fixed', text: 'Infocenter to Act 2/Act 3 transition issues' },
|
||||
{ type: 'Fixed', text: 'Race initialization errors' },
|
||||
{ type: 'Fixed', text: 'Jetski race startup issues' },
|
||||
{ type: 'Fixed', text: 'Plant creation bug in LegoPlantManager' },
|
||||
{ type: 'Fixed', text: 'Late-game "sawtooth" audio glitches' },
|
||||
{ type: 'Fixed', text: "Building variant switching (Pepper's buildings)" },
|
||||
{ type: 'Fixed', text: 'OpenGL rendering issues' },
|
||||
{ type: 'Fixed', text: 'Image serialization bugs' },
|
||||
{ type: 'Improved', text: 'Transparent objects now render correctly (sorted last)' },
|
||||
{ type: 'Improved', text: 'GPU mesh uploading via VBOs for better performance' },
|
||||
{ type: 'Improved', text: 'Backface culling enabled for faster rendering' },
|
||||
{ type: 'Improved', text: 'SIMD-optimized z-buffer clearing' },
|
||||
{ type: 'Improved', text: 'Edge-walking triangle rasterization' }
|
||||
]}
|
||||
];
|
||||
|
||||
function toggleItem(id) {
|
||||
openItem = openItem === id ? null : id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="read-me-page" class="page-content">
|
||||
<BackButton />
|
||||
<div class="page-inner-content">
|
||||
<h1>Read Me</h1>
|
||||
|
||||
<div class="readme-tabs">
|
||||
<div class="tab-buttons">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class="tab-btn"
|
||||
class:active={activeTab === tab.id}
|
||||
onclick={() => activeTab = tab.id}
|
||||
>
|
||||
<img src={tab.icon} alt="" class="tab-icon">
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:active={activeTab === 'about'} id="tab-about">
|
||||
<p>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.</p>
|
||||
<p>This incredible project stands on the shoulders of giants. It was made possible by the original <a
|
||||
href="https://github.com/isledecomp/isle" target="_blank"
|
||||
rel="noopener noreferrer">decompilation project</a>, which achieved 100% decompilation of the
|
||||
original game. This was then adapted into a <a
|
||||
href="https://github.com/isledecomp/isle-portable" target="_blank"
|
||||
rel="noopener noreferrer">portable version</a> that eliminated all Windows dependencies and
|
||||
replaced them with modern, cross-platform alternatives.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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!</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:active={activeTab === 'system'} id="tab-system">
|
||||
<div class="requirements-section">
|
||||
<h3>Supported Browsers</h3>
|
||||
<p>This game requires a modern browser with WebAssembly multi-threading support. The following browsers are supported:</p>
|
||||
<ul class="requirements-list">
|
||||
<li><strong>Chrome</strong> — version 95 or newer</li>
|
||||
<li><strong>Firefox</strong> — version 92 or newer</li>
|
||||
<li><strong>Edge</strong> — version 95 or newer</li>
|
||||
<li><strong>Safari</strong> — version 15.4 or newer (iOS 18+ recommended)</li>
|
||||
</ul>
|
||||
<p class="requirements-note">For the best experience, keep your browser updated to the latest version.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Input Methods</h3>
|
||||
<p>The game supports multiple ways to play. Visit the Configure page to adjust your control preferences.</p>
|
||||
<ul class="requirements-list">
|
||||
<li><strong>Keyboard & Mouse</strong> — Traditional desktop controls using arrow keys or WASD</li>
|
||||
<li><strong>Gamepad</strong> — Controller support with analog sticks and D-pad</li>
|
||||
<li><strong>Touch Screen</strong> — Mobile-friendly controls with configurable schemes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Audio</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Storage & Network</h3>
|
||||
<p>The game streams approximately <strong>25MB</strong> of data on first load (more with extensions enabled).
|
||||
For offline play, you can install the full game (about <strong>550MB</strong>) via the Configure menu.
|
||||
A stable internet connection is recommended for initial loading.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-section">
|
||||
<h3>Performance Tips</h3>
|
||||
<ul class="requirements-list">
|
||||
<li>Close other browser tabs to free up memory</li>
|
||||
<li>Use hardware acceleration (enabled by default in most browsers)</li>
|
||||
<li>On mobile, ensure your device isn't in low-power mode</li>
|
||||
<li>If experiencing lag, try reducing the resolution in Configure</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:active={activeTab === 'faq'} id="tab-faq">
|
||||
<Accordion items={faqItems} {openItem} onToggle={toggleItem} titleKey="question">
|
||||
<svelte:fragment let:item>
|
||||
{@html item.answer}
|
||||
</svelte:fragment>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:active={activeTab === 'changelog'} id="tab-changelog">
|
||||
<Accordion items={changelogItems} {openItem} onToggle={toggleItem} titleKey="title">
|
||||
<svelte:fragment let:item>
|
||||
<ul>
|
||||
{#each item.items as entry}
|
||||
<li><strong>{entry.type}:</strong> {entry.text}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:active={activeTab === 'manual'} id="tab-manual">
|
||||
<div class="manual-container">
|
||||
<p class="manual-description">The original 15-page instruction manual from the 1997 CD-ROM release.</p>
|
||||
<a href="comic.pdf" target="_blank" rel="noopener" class="manual-open-btn">Open Manual in New Tab</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Voices tab - not shown in tab buttons but content preserved -->
|
||||
<div class="tab-panel" class:active={activeTab === 'voices'} id="tab-voices">
|
||||
<p class="voices-intro">Reactions from the original LEGO Island development team:</p>
|
||||
<div class="voices-grid">
|
||||
<blockquote class="voice-card">
|
||||
<p>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...</p>
|
||||
<footer>Scott Anderson</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>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.</p>
|
||||
<footer>Dennis Goodrow</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>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!</p>
|
||||
<footer>Jim Brown</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Well done and such fun tapping back into such fond creative memories.</p>
|
||||
<footer>Paul Melmed</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>That's awesome!</p>
|
||||
<footer>Randy Chou</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Fantastic! Love it.</p>
|
||||
<footer>Kevin Byall</footer>
|
||||
</blockquote>
|
||||
<blockquote class="voice-card">
|
||||
<p>Great stuff!</p>
|
||||
<footer>Dave Cherry</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
18
src/lib/TabNavigation.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
export let tabs = [];
|
||||
export let activeTab = '';
|
||||
export let onTabChange = () => {};
|
||||
export let buttonClass = 'tab-btn';
|
||||
</script>
|
||||
|
||||
<div class="tab-buttons">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class={buttonClass}
|
||||
class:active={activeTab === tab.id}
|
||||
onclick={() => onTabChange(tab)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
49
src/lib/ToggleSwitch.svelte
Normal file
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
export let id;
|
||||
export let name;
|
||||
export let label;
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
export let badge = '';
|
||||
export let tooltip = '';
|
||||
export let notIni = false;
|
||||
export let onchange = undefined;
|
||||
</script>
|
||||
|
||||
{#if tooltip}
|
||||
<div class="toggle-switch">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
{id}
|
||||
{name}
|
||||
{checked}
|
||||
{disabled}
|
||||
data-not-ini={notIni || undefined}
|
||||
{onchange}
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">
|
||||
{label}
|
||||
{#if badge}
|
||||
<span class="toggle-badge">{badge}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">{tooltip}</span></span>
|
||||
</div>
|
||||
{:else}
|
||||
<label class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
{id}
|
||||
{name}
|
||||
{checked}
|
||||
{disabled}
|
||||
data-not-ini={notIni || undefined}
|
||||
{onchange}
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">{label}</span>
|
||||
</label>
|
||||
{/if}
|
||||
92
src/lib/TopContent.svelte
Normal file
@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { debugEnabled, soundEnabled } from '../stores.js';
|
||||
import { getInstallAudio, toggleInstallAudio } from '../core/audio.js';
|
||||
|
||||
let debugTapCount = 0;
|
||||
let debugTapTimeout = null;
|
||||
|
||||
onMount(() => {
|
||||
const audio = getInstallAudio();
|
||||
if (audio) {
|
||||
audio.addEventListener('play', () => soundEnabled.set(true));
|
||||
audio.addEventListener('pause', () => soundEnabled.set(false));
|
||||
}
|
||||
});
|
||||
|
||||
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 = '🍕';
|
||||
|
||||
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);
|
||||
|
||||
setTimeout(() => slice.remove(), 1700);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogoClick(event) {
|
||||
const imgElement = event.currentTarget.querySelector('img');
|
||||
|
||||
if ($debugEnabled) {
|
||||
celebratePizza(imgElement);
|
||||
return;
|
||||
}
|
||||
|
||||
debugTapCount++;
|
||||
clearTimeout(debugTapTimeout);
|
||||
|
||||
if (debugTapCount >= 5) {
|
||||
debugEnabled.set(true);
|
||||
celebratePizza(imgElement);
|
||||
} else {
|
||||
debugTapTimeout = setTimeout(() => {
|
||||
debugTapCount = 0;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="top-content">
|
||||
<div class="video-container">
|
||||
<img id="install-video" width="260" height="260" src="install.webp" alt="Install Game">
|
||||
<span
|
||||
id="sound-toggle-emoji"
|
||||
title={$soundEnabled ? 'Pause Audio' : 'Play Audio'}
|
||||
onclick={toggleInstallAudio}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === 'Enter' && toggleInstallAudio()}
|
||||
>
|
||||
{$soundEnabled ? '🔊' : '🔇'}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="img-button" onclick={handleLogoClick}>
|
||||
<img
|
||||
id="island-logo-img"
|
||||
width="567"
|
||||
height="198"
|
||||
src={$debugEnabled ? 'ogel.webp' : 'island.webp'}
|
||||
alt={$debugEnabled ? 'OGEL Mode Enabled' : 'Lego Island Logo'}
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
24
src/lib/UpdatePopup.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { showUpdatePopup } from '../stores.js';
|
||||
|
||||
function reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
showUpdatePopup.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $showUpdatePopup}
|
||||
<div id="update-popup" class="notification-popup">
|
||||
<div class="notification-popup-content">
|
||||
<button class="update-dismiss-btn" aria-label="Dismiss" onclick={dismiss}>×</button>
|
||||
<div class="update-speech-bubble">
|
||||
<p class="update-message">A new version just arrived!</p>
|
||||
<button class="update-reload-btn" onclick={reload}>Reload Now</button>
|
||||
</div>
|
||||
</div>
|
||||
<img src="bonus.webp" alt="Pepper" class="update-character" width="150" height="187">
|
||||
</div>
|
||||
{/if}
|
||||
25
src/lib/config/AudioTab.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
export let opfsDisabled;
|
||||
export let openSection;
|
||||
export let toggleSection;
|
||||
</script>
|
||||
|
||||
<div class="config-tab-panel active" id="config-tab-audio">
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('sound')}>Sound</button>
|
||||
<div class="config-card-content" class:open={openSection === 'sound'}>
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="check-music" name="Music" checked disabled={opfsDisabled}>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">Music</span>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="check-3d-sound" name="3DSound" checked disabled={opfsDisabled}>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">3D Sound</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
53
src/lib/config/ControlsTab.svelte
Normal file
@ -0,0 +1,53 @@
|
||||
<script>
|
||||
export let opfsDisabled;
|
||||
export let openSection;
|
||||
export let toggleSection;
|
||||
export let isTouchDevice;
|
||||
</script>
|
||||
|
||||
<div class="config-tab-panel active" id="config-tab-controls">
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('input')}>Input</button>
|
||||
<div class="config-card-content" class:open={openSection === 'input'}>
|
||||
<div class="form-grid">
|
||||
{#if isTouchDevice}
|
||||
<div class="form-group" id="touch-section">
|
||||
<label class="form-group-label" for="touch-type-select">
|
||||
Touch Control Scheme
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">
|
||||
<div><strong>Virtual Gamepad (Recommended):</strong> Slide your finger to move and turn.</div><br>
|
||||
<div><strong>Virtual Arrow Keys:</strong> Tap screen areas to move. The top moves forward, the bottom turns or moves back.</div><br>
|
||||
<div><strong>Virtual Mouse:</strong> Emulates classic mouse controls with touch.</div>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="touch-type-select" name="Touch Scheme" disabled={opfsDisabled}>
|
||||
<option value="0">Virtual Mouse</option>
|
||||
<option value="1">Virtual Arrow Keys</option>
|
||||
<option value="2" selected>Virtual Gamepad</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-group">
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-haptic" name="Haptic" checked disabled={opfsDisabled}><span class="toggle-slider"></span><span class="toggle-label">Haptic feedback</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">On supported devices and browsers, this provides physical feedback, like a vibration, while you play the game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-wasd" name="WASD" disabled={opfsDisabled}><span class="toggle-slider"></span><span class="toggle-label">Use WASD for navigation</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Use the WASD keys instead of the arrow keys to control the game, much akin to a modern PC game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-active-in-bg" name="Active in Background" disabled={opfsDisabled}><span class="toggle-slider"></span><span class="toggle-label">Active in background</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Keeps the game running even when it's in the background.</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
193
src/lib/config/DisplayTab.svelte
Normal file
@ -0,0 +1,193 @@
|
||||
<script>
|
||||
export let opfsDisabled;
|
||||
export let openSection;
|
||||
export let toggleSection;
|
||||
export let fullscreenSupported;
|
||||
export let msaaSupported;
|
||||
export let afSupported;
|
||||
export let showOrHideGraphicsOptions;
|
||||
export let checkCacheStatus;
|
||||
</script>
|
||||
|
||||
<div class="config-tab-panel active" id="config-tab-display">
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('game')}>Game</button>
|
||||
<div class="config-card-content" class:open={openSection === 'game'}>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="language-select">Version</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="language-select" name="Language" disabled={opfsDisabled} onchange={() => checkCacheStatus()}>
|
||||
<option value="da">Danish</option>
|
||||
<option value="el">English (1.0)</option>
|
||||
<option value="en" selected>English (1.1)</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if fullscreenSupported}
|
||||
<div class="form-group" id="window-form">
|
||||
<span class="form-group-label">Window</span>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="window-windowed" name="window" data-not-ini="true" checked disabled={opfsDisabled}>
|
||||
<label for="window-windowed">Windowed</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="window-fullscreen" name="window" data-not-ini="true" disabled={opfsDisabled}>
|
||||
<label for="window-fullscreen">Full Screen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-group">
|
||||
<span class="form-group-label">
|
||||
Aspect Ratio
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Choose Original (4:3) to preserve the classic aspect ratio with black bars, or select Widescreen to stretch the image to fit your display.</span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="aspect-original" value="1" name="Original Aspect Ratio" checked disabled={opfsDisabled}>
|
||||
<label for="aspect-original">Original (4:3)</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="aspect-wide" value="0" name="Original Aspect Ratio" disabled={opfsDisabled}>
|
||||
<label for="aspect-wide">Widescreen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="form-group-label">
|
||||
Resolution
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Choose Original (640 x 480) to preserve the classic resolution, or select Maximum to render in the highest quality.</span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="resolution-original" value="1" name="Original Resolution" checked disabled={opfsDisabled}>
|
||||
<label for="resolution-original">Original (640 x 480)</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="resolution-wide" value="0" name="Original Resolution" disabled={opfsDisabled}>
|
||||
<label for="resolution-wide">Maximum</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('detail')}>Detail</button>
|
||||
<div class="config-card-content" class:open={openSection === 'detail'}>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<span class="form-group-label">
|
||||
Island Model Quality
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Note: using the "Low" setting will cause the island to disappear. This is not a bug, but the same behavior as present in the original game.</span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-low" name="Island Quality" value="0" disabled={opfsDisabled}>
|
||||
<label for="gfx-low">Low</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-med" name="Island Quality" value="1" disabled={opfsDisabled}>
|
||||
<label for="gfx-med">Medium</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="gfx-high" name="Island Quality" value="2" checked disabled={opfsDisabled}>
|
||||
<label for="gfx-high">High</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="form-group-label">Island Texture Quality</span>
|
||||
<div class="radio-group option-list">
|
||||
<div class="option-item">
|
||||
<input type="radio" id="tex-low" name="Island Texture" value="0" disabled={opfsDisabled}>
|
||||
<label for="tex-low">Low</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="radio" id="tex-high" name="Island Texture" value="1" checked disabled={opfsDisabled}>
|
||||
<label for="tex-high">High</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="max-lod">
|
||||
Maximum LOD
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Maximum Level of Detail (LOD). A higher setting will cause higher quality textures to be drawn regardless of distance.</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="range" id="max-lod" name="Max LOD" min="0" max="6" step="0.1" value="3.6" disabled={opfsDisabled}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="max-allowed-extras">
|
||||
Maximum actors (5..40)
|
||||
<span class="tooltip-trigger">?
|
||||
<span class="tooltip-content">Maximum number of LEGO actors to exist in the world at a time. The game will gradually increase the number of actors until this maximum is reached and while performance is acceptable.</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="range" id="max-allowed-extras" name="Max Allowed Extras" min="5" max="40" value="20" disabled={opfsDisabled}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('graphics')}>Graphics</button>
|
||||
<div class="config-card-content" class:open={openSection === 'graphics'}>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="renderer-select">Renderer</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="renderer-select" name="3D Device ID" disabled={opfsDisabled} onchange={showOrHideGraphicsOptions}>
|
||||
<option value="0 0x682656f3 0x0 0x0 0x2000000">Software</option>
|
||||
<option value="0 0x682656f3 0x0 0x0 0x4000000" selected>WebGL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="transition-type-select">Transition Type</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="transition-type-select" name="Transition Type" disabled={opfsDisabled}>
|
||||
<option value="1">No Animation</option>
|
||||
<option value="2">Dissolve</option>
|
||||
<option value="3" selected>Mosaic</option>
|
||||
<option value="4">Wipe Down</option>
|
||||
<option value="5">Windows</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if msaaSupported}
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="msaa-select">Anti-aliasing</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="msaa-select" name="MSAA" disabled={opfsDisabled}></select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if afSupported}
|
||||
<div class="form-group">
|
||||
<label class="form-group-label" for="anisotropic-select">Anisotropic filtering</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="anisotropic-select" name="Anisotropic" disabled={opfsDisabled}></select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
77
src/lib/config/ExtrasTab.svelte
Normal file
@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import ImageButton from '../ImageButton.svelte';
|
||||
import { installState } from '../../stores.js';
|
||||
|
||||
export let opfsDisabled;
|
||||
export let openSection;
|
||||
export let toggleSection;
|
||||
export let handleExtensionChange;
|
||||
export let handleInstall;
|
||||
export let handleUninstall;
|
||||
|
||||
$: progressAngle = ($installState.progress / 100) * 360;
|
||||
</script>
|
||||
|
||||
<div class="config-tab-panel active" id="config-tab-extras">
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('extensions')}>Extensions</button>
|
||||
<div class="config-card-content" class:open={openSection === 'extensions'}>
|
||||
<div class="toggle-group">
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-hd-textures" name="Texture Loader" data-not-ini="true" disabled={opfsDisabled} onchange={handleExtensionChange}><span class="toggle-slider"></span><span class="toggle-label">HD Textures <span class="toggle-badge">+25MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Enhance the game's visuals with high-definition textures.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-hd-music" name="HD Music" data-not-ini="true" disabled={opfsDisabled} onchange={handleExtensionChange}><span class="toggle-slider"></span><span class="toggle-label">HD Music <span class="toggle-badge">+450MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Improve the game's music with high-definition audio.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-widescreen-bgs" name="Widescreen Backgrounds" data-not-ini="true" disabled={opfsDisabled} onchange={handleExtensionChange}><span class="toggle-slider"></span><span class="toggle-label">Widescreen Backgrounds <span class="toggle-badge">WIP</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Adapts the game's background art for modern widescreen monitors, eliminating unwanted 3D backgrounds on the sides of the screen.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-outro" name="Outro FMV" data-not-ini="true" disabled={opfsDisabled}><span class="toggle-slider"></span><span class="toggle-label">Outro FMV</span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Plays the unused Outro animation upon exiting the game.</span></span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label><input type="checkbox" id="check-ending" name="Extended Bad Ending FMV" data-not-ini="true" disabled={opfsDisabled} onchange={handleExtensionChange}><span class="toggle-slider"></span><span class="toggle-label">Extended Bad Ending FMV <span class="toggle-badge">+20MB</span></span></label>
|
||||
<span class="tooltip-trigger">?<span class="tooltip-content">Plays the extended / "uncut" Bad Ending animation as found in beta versions of the game upon failing to catch the Brickster.</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card">
|
||||
<button type="button" class="config-card-header" onclick={() => toggleSection('offline')}>Offline Play</button>
|
||||
<div class="config-card-content" class:open={openSection === 'offline'}>
|
||||
<div class="offline-play-grid">
|
||||
<div class="offline-play-text">
|
||||
<p>Install the game for offline access. This will download all necessary files to your device (about 550MB).</p>
|
||||
<p class="offline-note">Note: browsers enforce strict storage quotas, especially in private/incognito windows.</p>
|
||||
</div>
|
||||
<div class="offline-play-controls">
|
||||
{#if $installState.installing}
|
||||
<div class="progress-circular" style="background: radial-gradient(var(--color-bg-input) 60%, transparent 61%), conic-gradient(var(--color-primary) {progressAngle}deg, var(--color-border-dark) {progressAngle}deg);">
|
||||
{Math.round($installState.progress)}%
|
||||
</div>
|
||||
{:else if $installState.installed}
|
||||
<ImageButton
|
||||
id="uninstall-btn"
|
||||
offSrc="uninstall_off.webp"
|
||||
onSrc="uninstall_on.webp"
|
||||
alt="Uninstall Game"
|
||||
onclick={handleUninstall}
|
||||
/>
|
||||
{:else}
|
||||
<ImageButton
|
||||
id="install-btn"
|
||||
offSrc="install_off.webp"
|
||||
onSrc="install_on.webp"
|
||||
alt="Install Game"
|
||||
onclick={handleInstall}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
src/main.js
Normal file
@ -0,0 +1,27 @@
|
||||
import App from './App.svelte';
|
||||
import { mount } from 'svelte';
|
||||
import './app.css';
|
||||
|
||||
// Global Module object required by Emscripten - must be defined before isle.js loads
|
||||
window.Module = {
|
||||
arguments: ['--ini', '/config/isle.ini'],
|
||||
running: false,
|
||||
preRun: function () {
|
||||
window.Module["addRunDependency"]("isle");
|
||||
window.Module.running = true;
|
||||
},
|
||||
canvas: null, // Will be set after mount
|
||||
onExit: function () {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Mount Svelte app
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')
|
||||
});
|
||||
|
||||
// Set canvas reference after mount
|
||||
window.Module.canvas = document.getElementById('canvas');
|
||||
|
||||
export default app;
|
||||
46
src/stores.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Page navigation - initialize from URL hash to prevent flicker on reload
|
||||
function getInitialPage() {
|
||||
if (typeof window === 'undefined') return 'main';
|
||||
const hash = window.location.hash;
|
||||
const pageMap = {
|
||||
'#read-me': 'read-me',
|
||||
'#configure': 'configure',
|
||||
'#free-stuff': 'free-stuff'
|
||||
};
|
||||
return pageMap[hash] || 'main';
|
||||
}
|
||||
|
||||
export const currentPage = writable(getInitialPage());
|
||||
|
||||
// Debug mode
|
||||
export const debugEnabled = writable(false);
|
||||
|
||||
// Sound state
|
||||
export const soundEnabled = writable(false);
|
||||
|
||||
// Popup visibility
|
||||
export const showUpdatePopup = writable(false);
|
||||
export const showGoodbyePopup = writable(false);
|
||||
export const goodbyeProgress = writable(0);
|
||||
|
||||
// Install state
|
||||
export const installState = writable({
|
||||
installed: false,
|
||||
installing: false,
|
||||
progress: 0,
|
||||
missingFiles: []
|
||||
});
|
||||
|
||||
// Config toast
|
||||
export const configToastVisible = writable(false);
|
||||
|
||||
// Debug UI visible (set when game reaches intro animation)
|
||||
export const debugUIVisible = writable(false);
|
||||
|
||||
// Game running state
|
||||
export const gameRunning = writable(false);
|
||||
|
||||
// Service worker registration
|
||||
export const swRegistration = writable(null);
|
||||
5
svelte.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
141
sw.js
@ -1,141 +0,0 @@
|
||||
importScripts('/workbox/workbox-sw.js');
|
||||
|
||||
workbox.setConfig({
|
||||
modulePathPrefix: '/workbox/'
|
||||
});
|
||||
|
||||
const { precacheAndRoute, cleanupOutdatedCaches } = workbox.precaching;
|
||||
const { registerRoute } = workbox.routing;
|
||||
const { Strategy } = workbox.strategies;
|
||||
const { CacheableResponsePlugin } = workbox.cacheableResponse;
|
||||
const { RangeRequestsPlugin } = workbox.rangeRequests;
|
||||
|
||||
precacheAndRoute([{"revision":"0390b70f2b0096d2d3baa43a0b887b67","url":"index.html"},{"revision":"013ceb7d67293d532e979dde0347f3af","url":"cancel_off.webp"},{"revision":"bfc1563be018d82685716c6130529129","url":"cancel_on.webp"},{"revision":"d282c260fd35522036936bb6faf8ad21","url":"cdspin.gif"},{"revision":"3d820bf72b19bd4e437a61e75f317b83","url":"configure_off.webp"},{"revision":"e2c0c5e6aa1f7703c385a433a2d2a519","url":"configure_on.webp"},{"revision":"88e1e81c930d8e6c24dfdc7af274e812","url":"favicon.png"},{"revision":"d16b293eca457e2fb1e7ef2caca8c904","url":"favicon.svg"},{"revision":"d2b9c2e128ef1e5e4265c603b0bc3305","url":"free_stuff_off.webp"},{"revision":"cbc6a6779897f932c3a3c8dceb329804","url":"free_stuff_on.webp"},{"revision":"05fba4ef1884cbbd6afe09ea3325efc0","url":"install_off.webp"},{"revision":"11247e92082ba3d978a2e3785b0acf51","url":"install_on.webp"},{"revision":"d23ea8243c18eb217ef08fe607097824","url":"island.webp"},{"revision":"c97d78e159b8bff44d41e56d0aa20220","url":"isle.js"},{"revision":"5f174d45de1e3c5e0abdbccfd64567b6","url":"isle.wasm"},{"revision":"6d4248f1a08c218943e582673179b7be","url":"poster.pdf"},{"revision":"a6fcac24a24996545c039a1755af33ea","url":"read_me_off.webp"},{"revision":"aae783d064996b4322e23b092d97ea4a","url":"read_me_on.webp"},{"revision":"766a9e6e6d890f24cef252e81753b29d","url":"run_game_off.webp"},{"revision":"70208e00e9ea641e4c98699f74100db3","url":"run_game_on.webp"},{"revision":"0a65c71d9983c9bb1bc6a5f405fd6fd9","url":"shark.webp"},{"revision":"88c1fd032e6fc16814690712a26c1ede","url":"uninstall_off.webp"},{"revision":"0118a4aca04c5fb0a525bf00b001844e","url":"uninstall_on.webp"},{"revision":"bcc0826b8bd1a49d241ad0812eed8b7e","url":"app.js"},{"revision":"648639355c3b7ae7ee6aeeda619466ee","url":"style.css"},{"revision":"060210979e13e305510de6285e085db1","url":"manifest.json"},{"revision":"4f0172bc7007d34cebf681cc233ab57f","url":"install.webp"},{"revision":"6a70d35dadf51d2ec6e38a6202d7fb0b","url":"install.mp3"},{"revision":"eac041a0b8835bfea706d997b0b7b224","url":"downloader.js"},{"revision":"6899f72755d4e84c707b93ac54a8fb06","url":"debug.js"},{"revision":"7817b36ddda9f07797c05a0ff6cacb21","url":"debug.html"},{"revision":"4ea2aac9446188b8a588811bc593919e","url":"ogel.webp"},{"revision":"c57d24598537443c5b8276c8dd5dbdc9","url":"bonus.webp"},{"revision":"d11c8c893d5525c8842555dc2861c393","url":"callfail.webp"},{"revision":"be9a89fb567b632cf8d4661cbf8afd9e","url":"getinfo.webp"},{"revision":"fe986681f41e96631f39f3288b23e538","url":"sysinfo.webp"},{"revision":"4ec902e0b0ce60ffd9dd565c9ddf40a1","url":"send.webp"},{"revision":"81f3c8fc38b876dc2fcfeefaadad1d1b","url":"congrats.webp"},{"revision":"f906318cb87e09a819e5916676caab2e","url":"register.webp"},{"revision":"c633a7500e6f30162bf1cf4ec4e95a6d","url":"later.webp"},{"revision":"d149d5709ac00fd5e2967ab4f3d74886","url":"comic.pdf"}]);
|
||||
|
||||
const gameFiles = [
|
||||
"/LEGO/Scripts/CREDITS.SI", "/LEGO/Scripts/INTRO.SI", "/LEGO/Scripts/NOCD.SI", "/LEGO/Scripts/SNDANIM.SI",
|
||||
"/LEGO/Scripts/Act2/ACT2MAIN.SI", "/LEGO/Scripts/Act3/ACT3.SI", "/LEGO/Scripts/Build/COPTER.SI",
|
||||
"/LEGO/Scripts/Build/DUNECAR.SI", "/LEGO/Scripts/Build/JETSKI.SI", "/LEGO/Scripts/Build/RACECAR.SI",
|
||||
"/LEGO/Scripts/Garage/GARAGE.SI", "/LEGO/Scripts/Hospital/HOSPITAL.SI", "/LEGO/Scripts/Infocntr/ELEVBOTT.SI",
|
||||
"/LEGO/Scripts/Infocntr/HISTBOOK.SI", "/LEGO/Scripts/Infocntr/INFODOOR.SI", "/LEGO/Scripts/Infocntr/INFOMAIN.SI",
|
||||
"/LEGO/Scripts/Infocntr/INFOSCOR.SI", "/LEGO/Scripts/Infocntr/REGBOOK.SI", "/LEGO/Scripts/Isle/ISLE.SI",
|
||||
"/LEGO/Scripts/Isle/JUKEBOX.SI", "/LEGO/Scripts/Isle/JUKEBOXW.SI", "/LEGO/Scripts/Police/POLICE.SI",
|
||||
"/LEGO/Scripts/Race/CARRACE.SI", "/LEGO/Scripts/Race/CARRACER.SI", "/LEGO/Scripts/Race/JETRACE.SI",
|
||||
"/LEGO/Scripts/Race/JETRACER.SI", "/LEGO/data/WORLD.WDB"
|
||||
];
|
||||
|
||||
const textureFiles = [
|
||||
"/LEGO/textures/beach.gif.bmp", "/LEGO/textures/doctor.gif.bmp", "/LEGO/textures/infochst.gif.bmp",
|
||||
"/LEGO/textures/o.gif.bmp", "/LEGO/textures/relrel01.gif.bmp", "/LEGO/textures/rockx.gif.bmp",
|
||||
"/LEGO/textures/water2x.gif.bmp", "/LEGO/textures/bowtie.gif.bmp", "/LEGO/textures/e.gif.bmp",
|
||||
"/LEGO/textures/jfrnt.gif.bmp", "/LEGO/textures/papachst.gif.bmp", "/LEGO/textures/road1way.gif.bmp",
|
||||
"/LEGO/textures/sandredx.gif.bmp", "/LEGO/textures/w_curve.gif.bmp", "/LEGO/textures/brela_01.gif.bmp",
|
||||
"/LEGO/textures/flowers.gif.bmp", "/LEGO/textures/l6.gif.bmp", "/LEGO/textures/pebblesx.gif.bmp",
|
||||
"/LEGO/textures/road3wa2.gif.bmp", "/LEGO/textures/se_curve.gif.bmp", "/LEGO/textures/wnbars.gif.bmp",
|
||||
"/LEGO/textures/bth1chst.gif.bmp", "/LEGO/textures/fruit.gif.bmp", "/LEGO/textures/l.gif.bmp",
|
||||
"/LEGO/textures/pizcurve.gif.bmp", "/LEGO/textures/road3wa3.gif.bmp", "/LEGO/textures/shftchst.gif.bmp",
|
||||
"/LEGO/textures/bth2chst.gif.bmp", "/LEGO/textures/gasroad.gif.bmp", "/LEGO/textures/mamachst.gif.bmp",
|
||||
"/LEGO/textures/polbar01.gif.bmp", "/LEGO/textures/road3way.gif.bmp", "/LEGO/textures/tightcrv.gif.bmp",
|
||||
"/LEGO/textures/cheker01.gif.bmp", "/LEGO/textures/g.gif.bmp", "/LEGO/textures/mech.gif.bmp",
|
||||
"/LEGO/textures/polkadot.gif.bmp", "/LEGO/textures/road4way.gif.bmp", "/LEGO/textures/unkchst.gif.bmp",
|
||||
"/LEGO/textures/construct.gif.bmp", "/LEGO/textures/grassx.gif.bmp", "/LEGO/textures/nwcurve.gif.bmp",
|
||||
"/LEGO/textures/redskul.gif.bmp", "/LEGO/textures/roadstr8.gif.bmp", "/LEGO/textures/vest.gif.bmp"
|
||||
];
|
||||
|
||||
const rangeRequestsPlugin = new RangeRequestsPlugin();
|
||||
const normalizePathPlugin = {
|
||||
cacheKeyWillBeUsed: async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const normalizedPath = url.pathname.replace(/\/{2,}/g, '/');
|
||||
const normalizedUrl = url.origin + normalizedPath;
|
||||
if (request.url === normalizedUrl) {
|
||||
return request;
|
||||
}
|
||||
return new Request(normalizedUrl, {
|
||||
headers: request.headers, method: request.method,
|
||||
credentials: request.credentials, redirect: request.redirect,
|
||||
referrer: request.referrer, body: request.body,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
class LegoCacheStrategy extends Strategy {
|
||||
async _handle(request, handler) {
|
||||
const cacheKeyRequest = await normalizePathPlugin.cacheKeyWillBeUsed({ request });
|
||||
const cachedResponse = await caches.match(cacheKeyRequest);
|
||||
|
||||
if (cachedResponse) {
|
||||
return await rangeRequestsPlugin.cachedResponseWillBeUsed({
|
||||
request: cacheKeyRequest,
|
||||
cachedResponse: cachedResponse,
|
||||
});
|
||||
}
|
||||
|
||||
return handler.fetch(request);
|
||||
}
|
||||
}
|
||||
|
||||
const getLanguageCacheName = (language) => `game-assets-${language}`;
|
||||
|
||||
async function uninstallLanguagePack(language, client) {
|
||||
const cacheName = getLanguageCacheName(language);
|
||||
try {
|
||||
const deleted = await caches.delete(cacheName);
|
||||
if (deleted) {
|
||||
console.log(`Cache ${cacheName} deleted successfully.`);
|
||||
}
|
||||
client.postMessage({ action: 'uninstall_complete', success: deleted, language: language });
|
||||
} catch (error) {
|
||||
console.error('Error during language pack uninstallation:', error);
|
||||
client.postMessage({ action: 'uninstall_complete', success: false, language: language, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCacheStatus(language, hdTextures, siFiles, client) {
|
||||
const cacheName = getLanguageCacheName(language);
|
||||
const cache = await caches.open(cacheName);
|
||||
const requests = await cache.keys();
|
||||
const cachedUrls = requests.map(req => new URL(req.url).pathname);
|
||||
let requiredFiles = gameFiles;
|
||||
if (hdTextures) {
|
||||
requiredFiles = requiredFiles.concat(textureFiles);
|
||||
}
|
||||
if (siFiles.length > 0) {
|
||||
requiredFiles = requiredFiles.concat(siFiles);
|
||||
}
|
||||
const missingFiles = requiredFiles.filter(file => !cachedUrls.includes(file));
|
||||
|
||||
client.postMessage({
|
||||
action: 'cache_status',
|
||||
isInstalled: missingFiles.length === 0,
|
||||
missingFiles: missingFiles,
|
||||
language: language
|
||||
});
|
||||
}
|
||||
|
||||
registerRoute(
|
||||
({ url }) => url.pathname.startsWith('/LEGO/'),
|
||||
new LegoCacheStrategy()
|
||||
);
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
cleanupOutdatedCaches();
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.action) {
|
||||
switch (event.data.action) {
|
||||
case 'uninstall_language_pack':
|
||||
uninstallLanguagePack(event.data.language, event.source);
|
||||
break;
|
||||
case 'check_cache_status':
|
||||
checkCacheStatus(event.data.language, event.data.hdTextures, event.data.siFiles, event.source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
34
vite.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
const buildTime = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
define: {
|
||||
__BUILD_TIME__: JSON.stringify(buildTime)
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
passes: 2
|
||||
}
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'app.js',
|
||||
chunkFileNames: '[name].js',
|
||||
assetFileNames: '[name][extname]'
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
}
|
||||
});
|
||||
9
workbox-config.cjs
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
globDirectory: 'dist/',
|
||||
globPatterns: [
|
||||
'**/*.{js,css,html,webp,wasm,pdf,mp3,gif,png,svg,json}'
|
||||
],
|
||||
swSrc: 'src-sw/sw.js',
|
||||
swDest: 'dist/sw.js',
|
||||
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
module.exports = {
|
||||
globDirectory: './',
|
||||
globPatterns: [
|
||||
'index.html', 'cancel_off.webp', 'cancel_on.webp', 'cdspin.gif',
|
||||
'configure_off.webp', 'configure_on.webp', 'favicon.png', 'favicon.svg',
|
||||
'free_stuff_off.webp', 'free_stuff_on.webp', 'install_off.webp', 'install_on.webp',
|
||||
'island.webp', 'isle.js', 'isle.wasm', 'poster.pdf', 'read_me_off.webp',
|
||||
'read_me_on.webp', 'run_game_off.webp', 'run_game_on.webp', 'shark.webp',
|
||||
'uninstall_off.webp', 'uninstall_on.webp', 'app.js', 'style.css', 'manifest.json',
|
||||
'install.webp', 'install.mp3', 'downloader.js', 'debug.js', 'debug.html', 'ogel.webp',
|
||||
'bonus.webp', 'callfail.webp', 'getinfo.webp', 'sysinfo.webp', 'send.webp', 'congrats.webp',
|
||||
'register.webp', 'later.webp', 'comic.pdf'
|
||||
],
|
||||
swSrc: 'src/sw.js',
|
||||
swDest: 'sw.js',
|
||||
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
|
||||
};
|
||||