diff --git a/src/Zig-JS_Bridge.js b/src/Zig-JS_Bridge.js index c8d4761..80ba882 100644 --- a/src/Zig-JS_Bridge.js +++ b/src/Zig-JS_Bridge.js @@ -1,14 +1,14 @@ mergeInto(LibraryManager.library, { - WASMSave: function(pointer, length) { + WASMSave: function (pointer, length) { const settings = UTF8ToString(pointer, length); Settings.save(settings); }, - WASMLoad: function() { + WASMLoad: function () { const settings = Settings.get(); const ptr = allocateUTF8(settings); return ptr; }, - WASMLoaded: function(ptr) { + WASMLoaded: function (ptr) { Module._free(ptr); } }); \ No newline at end of file diff --git a/src/asteroids-website/src/app.d.ts b/src/asteroids-website/src/app.d.ts index cac20d7..d993e09 100644 --- a/src/asteroids-website/src/app.d.ts +++ b/src/asteroids-website/src/app.d.ts @@ -1,3 +1,5 @@ +import type { MiniAudio } from "./types/miniaudio"; + // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { @@ -7,7 +9,8 @@ declare global { // interface PageData {} // interface Platform {} } - declare interface Window { + interface Window extends Window { Settings: Settings; + miniaudio: MiniAudio | undefined = undefined; } } \ No newline at end of file diff --git a/src/asteroids-website/src/app.html b/src/asteroids-website/src/app.html index f9d0cbd..c051e18 100644 --- a/src/asteroids-website/src/app.html +++ b/src/asteroids-website/src/app.html @@ -1,14 +1,23 @@ - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - - + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + + \ No newline at end of file diff --git a/src/asteroids-website/src/app.postcss b/src/asteroids-website/src/app.postcss index 4aeb128..517b8e8 100644 --- a/src/asteroids-website/src/app.postcss +++ b/src/asteroids-website/src/app.postcss @@ -9,4 +9,14 @@ button, select, a { -webkit-tap-highlight-color: transparent; -} \ No newline at end of file +} + +html { + background-color: theme(colors.blue-dark); + background-repeat: repeat; + overflow: hidden; +} + +* { + @apply text-yellow-light; +} diff --git a/src/asteroids-website/src/lib/index.ts b/src/asteroids-website/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/asteroids-website/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/asteroids-website/src/lib/localizer.ts b/src/asteroids-website/src/lib/localizer.ts index 1ab5f20..769d2a0 100644 --- a/src/asteroids-website/src/lib/localizer.ts +++ b/src/asteroids-website/src/lib/localizer.ts @@ -1,10 +1,8 @@ import { _, getLocaleFromNavigator, - isLoading, register, init, - locale } from "svelte-i18n"; export enum Locales { @@ -25,8 +23,7 @@ class LocaleGroup { } private GetLocalePrefix(locale: Locales): string { - switch (locale) - { + switch (locale) { case Locales.english: return "en"; case Locales.spanish: @@ -46,7 +43,7 @@ export class Localizer { register("en", () => import("../locales/en.json")); register("es", () => import("../locales/es.json")); register("fr", () => import("../locales/fr.json")); - + init({ fallbackLocale: "en", initialLocale: getLocaleFromNavigator() @@ -55,8 +52,7 @@ export class Localizer { private static Locale: LocaleGroup | undefined = undefined; private static LoadLocale(): LocaleGroup | undefined { - if (Localizer.Locale == undefined) - { + if (Localizer.Locale == undefined) { const allLocales = Object.entries(Locales).map((_, locale) => new LocaleGroup(locale)); const sortedLocales = allLocales.filter(l => l.Index > -1).sort((a, b) => a.Index - b.Index); Localizer.Locale = sortedLocales.at(0); @@ -75,19 +71,5 @@ export class Localizer { if (locale == undefined) return "en"; return locale.Prefix; } - - public static GetLocalInitText(): string { - const locale = Localizer.LoadLocale(); - switch (locale?.Locale) { - case Locales.english: - case Locales.unknown: - default: - return "Starting... ⌛"; - case Locales.spanish: - return "Iniciando... ⌛"; - case Locales.french: - return "Démarrage en cours... ⌛"; - } - } } diff --git a/src/asteroids-website/src/lib/module.ts b/src/asteroids-website/src/lib/module.ts index 8a9921c..4f6b9cb 100644 --- a/src/asteroids-website/src/lib/module.ts +++ b/src/asteroids-website/src/lib/module.ts @@ -1,7 +1,7 @@ import { Localizer } from "./localizer" import { writable, get } from 'svelte/store'; -export interface CustomEmscriptenModule extends Module, EmscriptenModule {} +export interface CustomEmscriptenModule extends Module, EmscriptenModule { } export interface ICustomModule { requestFullscreen?: (lockPointer: boolean, resizeCanvas: boolean) => void; @@ -43,35 +43,33 @@ export class Module implements ICustomModule { private static wasmBinaryFile: string = new URL('../import/asteroids.wasm', import.meta.url).href; public static async Init(message: string): Promise { this.setStatus(message); - const wasmFile = await fetch(this.wasmBinaryFile, { + const wasmFile = await fetch(this.wasmBinaryFile, { cache: "default", }); console.log('wasm download finished'); return new Module(await wasmFile.arrayBuffer()); } - public onRuntimeInitialized(): void { + public onRuntimeInitialized(): void { document.getElementById("controls")?.classList.remove("hidden"); // Set Locale - if (this._updateWasmLocale) - { + if (this._updateWasmLocale) { this._updateWasmLocale(Localizer.GetLocale()); } } public instantiateWasm( - imports: WebAssembly.Imports, - successCallback: (module: WebAssembly.Instance) => void): WebAssembly.Exports - { + imports: WebAssembly.Imports, + successCallback: (module: WebAssembly.Instance) => void): WebAssembly.Exports { WebAssembly.instantiate(new Uint8Array(this.wasmBinary), imports) - .then((output) => { - console.log('wasm instantiation succeeded'); - successCallback(output.instance); - }).catch((e) => { - console.log('wasm instantiation failed! ' + e); - this.setStatus('wasm instantiation failed! ' + e); - }); + .then((output) => { + console.log('wasm instantiation succeeded'); + successCallback(output.instance); + }).catch((e) => { + console.log('wasm instantiation failed! ' + e); + this.setStatus('wasm instantiation failed! ' + e); + }); return {}; } @@ -83,11 +81,12 @@ export class Module implements ICustomModule { text = Array.prototype.slice.call(arguments).join(' '); globalThis.console.error(text); } - - public get canvas(): HTMLCanvasElement { - const e = document.getElementById("canvas"); - return e; - } + + public canvas: HTMLCanvasElement = (() => { + const c = document.createElement('canvas'); + c.classList.add("rounded-lg"); + return c; + })(); public get statusMessage(): string { return get(Module.statusMessage); @@ -99,8 +98,7 @@ export class Module implements ICustomModule { public static readonly statusMessage = writable("⏳"); public static setStatus(e: string): void { // "Running..." is from emscripten.js and isn't localized so just return" - if (e == "Running...") - { + if (e == "Running...") { return; } Module.statusMessage.set(e); diff --git a/src/asteroids-website/src/lib/settings.ts b/src/asteroids-website/src/lib/settings.ts index 317202d..664a738 100644 --- a/src/asteroids-website/src/lib/settings.ts +++ b/src/asteroids-website/src/lib/settings.ts @@ -20,8 +20,7 @@ export class Settings { const oldSettings = Settings.get(); if (window.location.search.length == 0) return false; - try - { + try { Module.setStatus("Updating Settings..."); const settings = JSON.parse(oldSettings); const settingsKeys = Object.keys(settings); @@ -34,19 +33,19 @@ export class Settings { return v; } }; - + Array.from(urlParams) .map(e => { return { key: e[0], value: e[1] } }) .filter((e) => settingsKeys.includes(e.key)) .forEach((e) => { settings[e.key] = getValue(e.value); }); - + const newSettings = JSON.stringify(settings); window.localStorage.setItem("settings", newSettings); return true; } - catch + catch { return false; } diff --git a/src/asteroids-website/src/locales/es.json b/src/asteroids-website/src/locales/es.json index 0af3ec7..c7ddcc4 100644 --- a/src/asteroids-website/src/locales/es.json +++ b/src/asteroids-website/src/locales/es.json @@ -16,4 +16,4 @@ "Right": "Derecha", "A": "A" } -} +} \ No newline at end of file diff --git a/src/asteroids-website/src/locales/fr.json b/src/asteroids-website/src/locales/fr.json index c89b26d..5f55a75 100644 --- a/src/asteroids-website/src/locales/fr.json +++ b/src/asteroids-website/src/locales/fr.json @@ -16,4 +16,4 @@ "Right": "Droite", "A": "A" } -} +} \ No newline at end of file diff --git a/src/asteroids-website/src/routes/+error.svelte b/src/asteroids-website/src/routes/+error.svelte index 0291fe3..1409784 100644 --- a/src/asteroids-website/src/routes/+error.svelte +++ b/src/asteroids-website/src/routes/+error.svelte @@ -1,34 +1,26 @@ - -
-
-
-

¯\_(ツ)_/¯
An error has occurred, sorry!

- {#if $page.error} -
-
- {#if $page?.status} -

Status: {$page?.status}

- {/if} - {#if $page?.error?.message} -

Error Message: {$page?.error?.message}

- {/if} -
- {/if} -
-
-
\ No newline at end of file + + +

¯\_(ツ)_/¯
An error has occurred, sorry!

+ {#if $page.error} +
+
+ {#if $page?.status} +

Status: {$page?.status}

+ {/if} + {#if $page?.error?.message} +

Error Message: {$page?.error?.message}

+ {/if} +
+ {/if} +
+
\ No newline at end of file diff --git a/src/asteroids-website/src/routes/+page.svelte b/src/asteroids-website/src/routes/+page.svelte index bb2cc3c..246aa2d 100644 --- a/src/asteroids-website/src/routes/+page.svelte +++ b/src/asteroids-website/src/routes/+page.svelte @@ -1,253 +1,314 @@ + import { Module, type CustomEmscriptenModule } from '$lib/module'; + import { onMount, onDestroy, tick } from 'svelte'; + import { BrowserDetector } from 'browser-dtector'; + import GameController from './controls.svelte'; + import StatusContainer from './status-container.svelte'; + import { Button } from '$lib/gameController'; + import { _, isLoading, waitLocale } from 'svelte-i18n'; + import { Localizer } from '$lib/localizer'; + import emscriptenModuleFactory from '../import/emscripten'; + import { Settings } from '$lib/settings'; + import { fade, blur, fly, slide, scale, crossfade } from 'svelte/transition'; + import { get } from 'svelte/store'; - + requestPause(); + + if (document.fullscreenElement) { + return; + } + + const updateSize = (): void => { + const updateWasmResolution = emscripten?._updateWasmResolution; + if (updateWasmResolution) { + const resolution = fitInto16x9AspectRatio( + override?.width ?? window.innerWidth, + override?.height ?? window.innerHeight + ); + updateWasmResolution(resolution.width, resolution.height); + } + }; + + // Update now if override is supplied + if (override) updateSize(); + else { + updateSizeTimeout = setTimeout(updateSize, 10); + } + } + + function fitInto16x9AspectRatio( + originalWidth: number, + originalHeight: number + ): { width: number; height: number } { + const targetAspectRatio = 16 / 9; + const currentAspectRatio = originalWidth / originalHeight; + + if (currentAspectRatio > targetAspectRatio) { + const newWidth = originalHeight * targetAspectRatio; + return { width: newWidth, height: originalHeight }; + } else { + const newHeight = originalWidth / targetAspectRatio; + return { width: originalWidth, height: newHeight }; + } + } + + function handleButtonPressed(b: Button): void { + const js_key_pressed = emscripten?._set_js_key; + if (js_key_pressed) { + js_key_pressed(b, true); + } + } + + function handleButtonReleased(b: Button): void { + const js_key_released = emscripten?._set_js_key; + if (js_key_released) { + js_key_released(b, false); + } + } + + function requestPause(): void { + handleButtonPressed(Button.Start); + setTimeout(() => handleButtonReleased(Button.Start), 100); + } + + function unlockAudio() { + // Unlock audio + ['touchstart', 'touchend', 'mousedown', 'keydown'].forEach((e) => + document.body.addEventListener( + e, + () => { + window.miniaudio?.unlock(); + }, + { + once: true + } + ) + ); + // Remove miniaudio object from window after unlocked + window.miniaudio?.devices.forEach((d, i) => + d.webaudio.addEventListener( + 'statechange', + (e) => { + if (d.webaudio.state === 'running') window.miniaudio?.untrack_device_by_index(i); + if (window.miniaudio?.devices.length == 0) { + muted = false; + delete window.miniaudio; + } + }, + { + once: true + } + ) + ); + muted = true; + } + + const isMobile: boolean = (() => { + const detector = new BrowserDetector(); + return detector.parseUserAgent().isMobile; + })(); + let isItchZone: boolean = false; + let fullscreenEnabled: boolean = true; + $: manifestJson = 'en.manifest.json'; + $: muted = false; + + let emscripten: CustomEmscriptenModule | undefined; + let starting = true; + onMount(async () => { + starting = false; + isItchZone = window.location?.host?.endsWith('itch.zone'); + fullscreenEnabled = document.fullscreenEnabled; + manifestJson = Localizer.GetLocalePrefix() + '.manifest.json'; + + if (Settings.updateFromQueryString()) { + window.location.search = ''; + } else { + await waitLocale(); + const module = await Module.Init($_('page.Downloading')); + emscripten = await (>emscriptenModuleFactory)(module); + await tick(); + unlockAudio(); + UpdateSize(null); + } + }); + + let status: string = ''; + const unsubscribeStatus = Module.statusMessage.subscribe((s) => (status = s)); + onDestroy(() => { + unsubscribeStatus(); + }); + + function onError(e: Event): void { + emscripten = undefined; + muted = false; + Module.setStatus($_('page.Exception')); + Module.setStatus = (e: any) => { + e && console.error('[post-exception status] ' + e); + }; + } + + function setCanvas(self: HTMLDivElement): void { + self.appendChild(emscripten!.canvas); + } + - {#if $isLoading} - - {:else} - - {/if} + {#if $isLoading} + + {:else} + + {/if} UpdateSize(e)} - on:resize={(e) => UpdateSize(e)} - on:blur={(e) => requestPause()}/> + on:error={(e) => onError(e)} + on:orientationchange={(e) => UpdateSize(e)} + on:resize={(e) => UpdateSize(e)} + on:blur={(e) => requestPause()} +/> {#if $isLoading} -
-
-
- {status} - -
-
-
+ {:else} -
- -
- e.preventDefault()} tabindex=-1> -
-
-
-
- {status} -
-
-
-
- -
-
-
- {#if isMobile} -
{$_('page.Rotate')} 🔄
- {:else} -
{$_('page.Resize')} ↔️
- {/if} -
-
-
-{/if} \ No newline at end of file +
+ +
+ {#if emscripten !== undefined} +
+ {/if} + {#if muted} +
+ 🔇 +
+
+ 🔇 +
+ {/if} +
+ {#key status} + + {/key} +
+ +
+ + + {#if isMobile} + {$_('page.Rotate')} 🔄 + {:else} + {$_('page.Resize')} ↔️ + {/if} + + +
+{/if} + + diff --git a/src/asteroids-website/src/routes/controls.svelte b/src/asteroids-website/src/routes/controls.svelte index f91a119..b7f0c56 100644 --- a/src/asteroids-website/src/routes/controls.svelte +++ b/src/asteroids-website/src/routes/controls.svelte @@ -1,182 +1,239 @@ +
+
touchMove(e)} + on:pointerdown={(e) => touchMove(e)} + on:pointerup={(e) => deselectDpad()} + on:pointerleave={(e) => deselectDpad()} + on:pointercancel={(e) => deselectDpad()} + on:lostpointercapture={(e) => deselectDpad()} + class="absolute bottom-10 left-4 z-10 m-auto p-1 grid grid-cols-3 grid-rows-3 w-fit h-fit items-center justify-items-center bg-green-light/[.5] rounded-full select-none touch-none" + > + + + + +
+
+ +
+ +
+ +
+ +
+
+ - -
-
touchMove(e)} - on:pointerdown={e => touchMove(e)} - on:pointerup={e => deselectDpad()} - on:pointerleave={e => deselectDpad()} - on:pointercancel={e => deselectDpad()} - on:lostpointercapture={e => deselectDpad()} - class="absolute bottom-10 left-4 z-10 m-auto p-1 grid grid-cols-3 grid-rows-3 w-fit h-fit items-center justify-items-center bg-slate-50/[.5] rounded-full select-none touch-none"> - - - - -
- - - - -
- -
- -
- -
- -
-
\ No newline at end of file diff --git a/src/asteroids-website/src/routes/status-container.svelte b/src/asteroids-website/src/routes/status-container.svelte new file mode 100644 index 0000000..eaad0f6 --- /dev/null +++ b/src/asteroids-website/src/routes/status-container.svelte @@ -0,0 +1,33 @@ + + +
+
+
+
+ + {#if status} + {status} + {/if} +
+ +
+
+
+ + diff --git a/src/asteroids-website/src/routes/status-container.ts b/src/asteroids-website/src/routes/status-container.ts new file mode 100644 index 0000000..c8cacf0 --- /dev/null +++ b/src/asteroids-website/src/routes/status-container.ts @@ -0,0 +1 @@ +export const prerender = true; \ No newline at end of file diff --git a/src/asteroids-website/src/types/emscripten.d.ts b/src/asteroids-website/src/types/emscripten.d.ts index 8705652..cd6f536 100644 --- a/src/asteroids-website/src/types/emscripten.d.ts +++ b/src/asteroids-website/src/types/emscripten.d.ts @@ -1,7 +1,7 @@ // Source - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e0e40a0b36b56c63ac89521ac8e8166ceded68d7/types/emscripten/index.d.ts declare namespace Emscripten { - interface FileSystemType {} + interface FileSystemType { } type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER"; type JSType = "number" | "string" | "array" | "boolean"; @@ -103,9 +103,9 @@ declare namespace FS { node: FSNode; } - interface FSStream {} - interface FSNode {} - interface ErrnoError {} + interface FSStream { } + interface FSNode { } + interface ErrnoError { } let ignorePermissions: boolean; let trackingDelegate: any; @@ -237,12 +237,12 @@ declare var IDBFS: Emscripten.FileSystemType; // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html type StringToType = R extends Emscripten.JSType ? { - number: number; - string: string; - array: number[] | string[] | boolean[] | Uint8Array | Int8Array; - boolean: boolean; - null: null; - }[R] + number: number; + string: string; + array: number[] | string[] | boolean[] | Uint8Array | Int8Array; + boolean: boolean; + null: null; +}[R] : never; type ArgsToType> = Extract< diff --git a/src/asteroids-website/src/types/miniaudio.d.ts b/src/asteroids-website/src/types/miniaudio.d.ts new file mode 100644 index 0000000..ee24a63 --- /dev/null +++ b/src/asteroids-website/src/types/miniaudio.d.ts @@ -0,0 +1,19 @@ +export interface MiniAudio { + referenceCount: number; + devices: Array; + get_device_by_index: (e: number) => MiniAudioDevice; + unlock: () => void; + unlock_event_types: Array; + track_device: (device: MiniAudioDevice) => number​; + untrack_device: (device: MiniAudioDevice) => void;​ + untrack_device_by_index: (deviceIndex: number) => void +} + +export interface MiniAudioDevice { + intermediaryBuffer: number; + intermediaryBufferSizeInBytes: number; + intermediaryBufferView: Float32Array; + scriptNode: ScriptProcessorNode; + state: 2; + webaudio: AudioContext; +} diff --git a/src/asteroids-website/tailwind.config.js b/src/asteroids-website/tailwind.config.js index 13207cc..e20b9c7 100644 --- a/src/asteroids-website/tailwind.config.js +++ b/src/asteroids-website/tailwind.config.js @@ -2,6 +2,21 @@ export default { content: ['./src/**/*.{html,js,svelte,ts}'], theme: { + colors: { + transparent: 'transparent', + 'blue-light': '#1b1e34', + 'blue-mid': '#201433', + 'blue-dark': '#201127', + 'green-light': '#94c5ac', + 'green-mid': '#6aaf9d', + 'green-dark': '#355d68', + 'yellow-light': '#ffeb99', + 'yellow-mid': '#ffc27a', + 'yellow-dark': '#ec9a6d', + 'red-light': '#d9626b', + 'red-mid': '#c24b6e', + 'red-dark': '#a73169', + }, extend: {}, }, plugins: [],