diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8f93744 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + pull_request: + branches: + - main + - dev + workflow_dispatch: + +env: + EM_CACHE_FOLDER: "emsdk-cache" + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: "Setup node" + uses: actions/setup-node@v2 + with: + node-version: "23.6.0" + + - name: "Setup bun" + uses: oven-sh/setup-bun@v2 + + - name: "Install dependencies" + run: bun install + + - name: "Run tests" + run: bun lerna run test:unit + + - name: "Run linter" + run: bun lerna run lint diff --git a/apps/web/src/assets/logo.png b/apps/web/src/assets/logo.png new file mode 100644 index 0000000..35b9d5b Binary files /dev/null and b/apps/web/src/assets/logo.png differ diff --git a/apps/web/src/cache/cache.ts b/apps/web/src/cache/cache.ts index 21337bc..fa11b0b 100644 --- a/apps/web/src/cache/cache.ts +++ b/apps/web/src/cache/cache.ts @@ -9,7 +9,7 @@ import { } from "../types/manifest.type"; import { Logger } from "../utils/logger.utils"; import { setVersion } from "../version"; -import { setLoadingStatus } from "../window"; +import { setLoadingStatus, setLoadingTotalFiles } from "../window"; export class GameCache { private readonly logger: Logger = new Logger("Cache"); @@ -36,15 +36,15 @@ export class GameCache { private async _updateCacheFiles(files: IManifestFile[]): Promise { const res = []; - for (const file of files) { + setLoadingTotalFiles(files.length); + for (const [i, file] of files.entries()) { + setLoadingStatus(`Download: ${file.path.replace(/^\/+/, "")}`, i); res.push(await this._updateCacheFile(file)); } return res; } private async _updateCacheFile(fileManifest: IManifestFile): Promise { - setLoadingStatus(`Fetching ${fileManifest.path}`); - const res = await fetch(`${env.PUBLIC_BASE_SERVER_URL}/game/${fileManifest.path}`); const file = await this.fs.getFile(fileManifest.path); diff --git a/apps/web/src/ids.ts b/apps/web/src/ids.ts index 22dc405..bea4561 100644 --- a/apps/web/src/ids.ts +++ b/apps/web/src/ids.ts @@ -1,5 +1,11 @@ export const IDS = { canvas: "nanoforge-canvas", loader: "nanoforge-loader", - loaderStatus: "nanoforge-loader-status", + + loadingStatus: "loading-status", + loadingStep: "loading-step", + loadingBar: "loading-bar-fill", + + loaderError: "loader-error", + loaderErrorMessage: "loader-error-message", }; diff --git a/apps/web/src/index.html b/apps/web/src/index.html index 16f4b35..bc65100 100644 --- a/apps/web/src/index.html +++ b/apps/web/src/index.html @@ -2,13 +2,25 @@ + Title
-
Loading...
-
+ +
+ +
+
+
+
+
+
+
{ logger.info("Starting loading game"); + const manifest = await getManifest(); const cache = new GameCache(); const extendedManifest = await cache.updateCache(manifest, true); const [files, mainModule] = await loadGameFiles(extendedManifest); + setLoadingStatus("Starting game"); runGame(mainModule, { files }); }; @@ -20,5 +23,6 @@ runLoad() logger.info("Game loaded !"); }) .catch((e) => { + setError(e); logger.error(`Failed to load game : ${e}`); }); diff --git a/apps/web/src/loader/loader.ts b/apps/web/src/loader/loader.ts index 937d220..d8efecb 100644 --- a/apps/web/src/loader/loader.ts +++ b/apps/web/src/loader/loader.ts @@ -10,7 +10,8 @@ export const loadGameFiles = async ( ): Promise<[IGameOptions["files"], any]> => { const files = { assets: new Map(), - scripts: new Map(), + wasm: new Map(), + wgsl: new Map(), }; let mainModule = undefined; logger.info("Starting load game files from cache"); @@ -21,7 +22,9 @@ export const loadGameFiles = async ( continue; } if (file.path.endsWith(".wasm")) { - files.scripts.set(file.path, file.localPath); + files.wasm.set(file.path, file.localPath); + } else if (file.path.endsWith(".wgsl")) { + files.wgsl.set(file.path, file.localPath); } else { files.assets.set(file.path, file.localPath); } diff --git a/apps/web/src/style.css b/apps/web/src/style.css new file mode 100644 index 0000000..3b45b39 --- /dev/null +++ b/apps/web/src/style.css @@ -0,0 +1,76 @@ +#nanoforge-loader { + height: 100%; + width: 100%; + left: 0; + top: 0; + background-color: oklch(0.21 0.034 264.665); + font-weight: bold; + color: white; + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; +} + +#loader-logo { + height: 125px; + width: 125px; +} + +#loading-status { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +#loading-status[hidden] { + display: none !important; +} + +#loading-bar { + height: 12px; + width: 500px; + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.2); + overflow: hidden; + position: relative; +} + +#loading-bar-fill { + height: 100%; + width: 0; + background-color: #973fff; + transition: width 0.3s ease-in-out; +} + +#loader-error { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +#loader-error[hidden] { + display: none !important; +} + +#back-home { + background-color: rgba(151, 63, 255, 0.8); + text-decoration: none; + border-radius: 5px; + font-weight: bold; + font-size: large; + color: white; + padding: 10px; +} + +.fade-out { + opacity: 0; + transition: opacity 1s ease-out; + pointer-events: none; +} diff --git a/apps/web/src/types/game.type.ts b/apps/web/src/types/game.type.ts index 757c453..2cb5425 100644 --- a/apps/web/src/types/game.type.ts +++ b/apps/web/src/types/game.type.ts @@ -2,6 +2,7 @@ export interface IGameOptions { canvas: HTMLCanvasElement; files: { assets: Map; - scripts: Map; + wasm: Map; + wgsl: Map; }; } diff --git a/apps/web/src/utils/delay.utils.ts b/apps/web/src/utils/delay.utils.ts new file mode 100644 index 0000000..4557adc --- /dev/null +++ b/apps/web/src/utils/delay.utils.ts @@ -0,0 +1 @@ +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/apps/web/src/window.ts b/apps/web/src/window.ts index 722a81a..9a2b0cc 100644 --- a/apps/web/src/window.ts +++ b/apps/web/src/window.ts @@ -1,22 +1,48 @@ import { IDS } from "./ids"; -import { getElementById, setHiddenStatusOnId } from "./utils/document.utils"; +import { delay } from "./utils/delay.utils"; +import { setHiddenStatusOnId } from "./utils/document.utils"; import { Logger } from "./utils/logger.utils"; const logger: Logger = new Logger("Window"); +let totalFiles = 0; -export const changeWindowToGame = () => { +export const changeWindowToGame = async () => { setHiddenStatusOnId(IDS.loader, true); setHiddenStatusOnId(IDS.canvas, false); + await delay(500); + const loader = document.getElementById(IDS.loader); + if (loader) loader.classList.add("fade-out"); logger.info("Change window to game"); }; -export const changeWindowToLoader = () => { +export const changeWindowToLoader = async () => { setHiddenStatusOnId(IDS.canvas, true); setHiddenStatusOnId(IDS.loader, false); logger.info("Change window to loader"); }; -export const setLoadingStatus = (text: string) => { - const el = getElementById(IDS.loaderStatus); - el.innerText = text; +export const setLoadingStatus = (filename: string, index?: number | null) => { + const loaderFilename = document.getElementById(IDS.loadingStep); + const loadingBarFill = document.getElementById(IDS.loadingBar); + + if (loaderFilename) loaderFilename.innerText = filename; + + if (loadingBarFill && index && totalFiles > 0) { + const progress = ((index + 1) / totalFiles) * 100; + loadingBarFill.style.width = `${progress}%`; + } +}; + +export const setLoadingTotalFiles = (total: number) => { + totalFiles = total; +}; + +export const setError = (error: string) => { + const loaderErrorMessage = document.getElementById(IDS.loaderErrorMessage); + + if (!loaderErrorMessage) return; + loaderErrorMessage.innerText = error; + + setHiddenStatusOnId(IDS.loadingStatus, true); + setHiddenStatusOnId(IDS.loaderError, false); };