/** * Castle Run — a polished 2D adventure PLATFORMER on Incanto. Run a medieval * knight through a castle: A/D (or arrows) to run, Space/W to jump — tap again in * the air to DOUBLE-JUMP, and the controller forgives you with coyote-time + * jump-buffering. STOMP goblins from above (and chain-bounce), ride the moving * platform and the lift, grab coins + gems, dodge the spikes and the pit, touch * the Checkpoint flag so a fall sends you back there, and reach the gold flag to * clear the castle. Three hearts per life, three lives. * * The level is JSON: StaticBody2D platforms, Patrol/Oscillate moving platforms + * goblins, Pickup coins/gems, ScoreKeeper score/lives/win-lose, AudioPlayer SFX — * all wired in `game.scene.json`. The custom TypeScript is the game FEEL the * built-ins leave open: PlayerController (coyote/buffer/double-jump/variable * height/stomp/knockback/checkpoint-respawn/platform-carry), GoblinSkin, * FollowCam (follow + screen-shake), ParallaxLayer, HudUpdater. */ import { createGame2D, showBootFailure } from 'incanto/2d'; import goblinUrl from 'incanto/assets/characters/goblin.png'; import knightUrl from 'incanto/assets/characters/medieval-knight.png'; import coinUrl from 'incanto/assets/items/coin.png'; import gemUrl from 'incanto/assets/items/gem.png'; import { useEffect, useRef } from 'react'; import { FollowCam, GoblinSkin, HudUpdater, ParallaxLayer, PlayerController } from './behaviors'; import sceneJson from './game.scene.json'; export function App() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; // StrictMode runs this effect twice against the SAME canvas element, and a // second engine on one canvas is two renderers fighting for one context. // The flag lives on the element, so it lasts exactly as long as the thing // it guards — a page-level game like this one never unmounts otherwise. if (!canvas || (canvas as { _incanto?: true })._incanto) return; (canvas as { _incanto?: true })._incanto = true; void (async () => { // Built-in sprite art ships in the package — import the URLs and inject them into // the scene's asset placeholders before boot (the engine clones the scene). // OPTIONAL looping music (large tracks aren't bundled). Point at any URL to loop. const MUSIC_URL = ''; const behaviors = { PlayerController, GoblinSkin, FollowCam, ParallaxLayer, HudUpdater }; const scene = structuredClone(sceneJson) as typeof sceneJson & { assets: { knight: { url: string }; goblin: { url: string }; coin: { url: string }; gem: { url: string }; }; }; scene.assets.knight.url = knightUrl; scene.assets.goblin.url = goblinUrl; scene.assets.coin.url = coinUrl; scene.assets.gem.url = gemUrl; /** Dev-only surfaces: the ☰ debug overlay, and the editor behind it. */ const DEBUG = import.meta.env.VITE_INCANTO_DEBUG === '1'; const game = await createGame2D({ debug: DEBUG, // The editor's 📚 buttons, served by `incantoLibrary()` in vite.config.ts. // BOTH halves are the opt-in, and this half was missing from every shipped // template: the plugin alone left a vite config whose own comment promised // "the agent8 asset catalog behind the 📚 buttons" and no button anywhere. // Gated on the same flag, because `editor` DEFAULTS to `debug` and an object // here would turn the editor on in a production build. editor: DEBUG && { library: true }, canvas, scene, behaviors, }).catch((e) => { // Without this the player watches the loading overlay sit at 100% forever: // `#loading` is removed on the line below, so anything that rejects here (no // WebGL context, a scene that will not load) leaves the bar up and the reason // in a console nobody opens. showBootFailure(e); throw e; }); // The console handle FIRST, before any wiring of your own can throw. // `game.stats()`, `game.assetErrors()`, `game.frame()` and `game.engine.log` // are the entire in-page diagnostic surface, and a static build has no other. // Assigned after the wiring, one mistake below took all of them with it. (window as unknown as { game: typeof game }).game = game; // And the loading overlay comes down LAST. Everything between here and there is // your code; a throw in it used to leave a level that renders perfectly with no // player, no error UI, and no way to ask the page what happened. try { if (MUSIC_URL) { game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 }); game.engine.audio.music = 0.3; } // No hand-rolled touch button here: the scene declares `"touch": "button"` on // `jump` and `"touch": "joystick"` on `move`, so the ENGINE draws both on a // coarse pointer — safe-area aware, off the home-indicator band, and sized to // its own rules. This template used to draw an 88 px disc of its own at a flat // `bottom: 2rem`, which covered 78% of the engine's jump button and put its // lower edge inside the band iOS reserves for the leave-the-app swipe. } catch (e) { showBootFailure(e); throw e; } document.querySelector('#loading')?.remove(); })(); }, []); return ; }