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
This commit is contained in:
Christian Semmler 2026-01-11 19:10:16 -07:00
parent 4a55a478e2
commit 804a87e687
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
92 changed files with 10530 additions and 2945 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules
dist
isle.wasm
isle.wasm.map
isle.js

View File

@ -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.

1011
app.js

File diff suppressed because it is too large Load Diff

View File

@ -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
View File

@ -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();
})();

View File

@ -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 &amp; 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 &amp; 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

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View 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"
}
}

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 413 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

14
scripts/workbox-inject.js Normal file
View 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
View 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}

File diff suppressed because it is too large Load Diff

34
src/core/audio.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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}

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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>

View 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>

View 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}

View 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
View 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 &amp; 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 &amp; 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>

View 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>

View 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
View 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>

View 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}

View 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>

View 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>

View 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>

View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
};

141
sw.js
View File

@ -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
View 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
View 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,
};

View File

@ -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,
};