| 35 | import { createExampleComponent } from "../utils"; |
| 36 | |
| 37 | const createGame = () => { |
| 38 | // Stash teardown work assembled inside the loader callback so the |
| 39 | // outer `createGame` can return a single cleanup function from the |
| 40 | // async preload completion. |
| 41 | let pointerCleanup: (() => void) | null = null; |
| 42 | let domCleanup: (() => void) | null = null; |
| 43 | // NOTE: an `unmounted` guard around the preload callback would in |
| 44 | // principle fix the "user navigates away before preload finishes" |
| 45 | // race that Copilot flagged on review. It can't be added cleanly |
| 46 | // today — `examples/utils.tsx` runs `currentTeardown()` on every |
| 47 | // React useEffect cleanup (including StrictMode's dev double-mount |
| 48 | // cycle), but the same-example remount branch only reattaches the |
| 49 | // canvas without re-invoking `createGameFn`. An `unmounted` flag |
| 50 | // flipped in teardown therefore stays `true` across the StrictMode |
| 51 | // remount, the preload callback bails for the rest of the session, |
| 52 | // and the example never renders. Picks up cleanly once the |
| 53 | // utils.tsx remount path is fixed (separate review thread). |
| 54 | |
| 55 | // opt in to Camera3d at the Application level — every stage in this |
| 56 | // app gets a Camera3d as its default camera (the loader screen pins |
| 57 | // to Camera2d via its own constructor regardless). |
| 58 | const app = new Application(1024, 768, { |
| 59 | parent: "screen", |
| 60 | renderer: video.WEBGL, |
| 61 | scale: "auto", |
| 62 | cameraClass: Camera3dClass, |
| 63 | }); |
| 64 | |
| 65 | app.world.backgroundColor.parseCSS("#0a0a14"); |
| 66 | plugin.register(DebugPanelPlugin, "debugPanel"); |
| 67 | |
| 68 | loader.preload([{ name: "monster", type: "image", src: monsterImg }], () => { |
| 69 | // loader.preload internally transitions to state.LOADING (the |
| 70 | // DefaultLoadingScreen). Transition back to the default game |
| 71 | // stage so its Camera3d becomes the active viewport. |
| 72 | state.change(state.DEFAULT, true); |
| 73 | |
| 74 | // three monsters along the camera's forward axis at increasing |
| 75 | // depth. Same x, same y — only z differs. Perspective scales |
| 76 | // each one inversely to z. |
| 77 | const depths = [200, 400, 600]; |
| 78 | for (const z of depths) { |
| 79 | const sprite = new Sprite(0, 0, { image: "monster" }); |
| 80 | sprite.scale(0.5); |
| 81 | app.world.addChild(sprite); |
| 82 | // set depth AFTER addChild — Container.autoDepth (default |
| 83 | // true) would otherwise overwrite our intended z |
| 84 | sprite.depth = z; |
| 85 | } |
| 86 | |
| 87 | // the app's default camera is now a Camera3d (via cameraClass). |
| 88 | const camera = app.viewport as Camera3d; |
| 89 | |
| 90 | // orbit state: yaw / pitch / distance. Driven by drag + buttons. |
| 91 | let yaw = 0; |
| 92 | let pitch = 0; |
| 93 | let distance = 700; |
| 94 | |