Snarky\'s
LOADING
Mouse / Touch dragAim Norman's castClick / TapCast spell (also starts the run, and restarts after the run ends)SpaceResurrect: raise every corpse on the floor into a skeleton allyPPause (keyboard-only; no touch button -- use the host's pause control)MMute (keyboard-only; no touch button -- use the host's mute control if offered)Arrows Up / DownIn the between-floor shop: move the selectionEnterIn the shop: buy the selected ritual, or continue to the next floor
rpg#roguelike#survivors-like#pixel-art#procgen-runs#js13k

Hold the doorway. Raise the dead. Outlast the king.

Norman stands his ground in a crumbling corridor, flinging bone magic at waves of villagers, archers and knights. Every kill drops a corpse you can raise into a skeleton ally with Resurrect, and the souls you collect buy rituals between floors that twist the run in your favor. Ten floors deep, the King is waiting.

0 plays0 players0 remixesPublished 3d ago
Open source

Made by Dan Prince. Released under the Unlicense license — original source.

Changes for this platform: Switched index.ts's keyboard handling from the legacy numeric event.which to event.code, since the platform's touch shim synthesizes KeyboardEvents that only set .key/.code, not the legacy fields (same category of fix as hexgl-racer's ShipControls.js change). Replaced Player()'s onDeath handler in objects.ts -- upstream hard-reset the whole page on death via `window.location = window.location`, an instant, feedback-free restart that bypassed the game's own (declared but previously unused) LOSE state -- with code that sets game.state = LOSE and shows a 'Norman has fallen...' message; a tap on that screen now reloads the frame instead, same end result, with a visible beat first. Added a small window.__norman bridge hook in index.ts exposing pause()/resume() (closured `paused` state arcade-bridge.js can't otherwise reach) and status() returning [state, level, souls] as a plain array -- deliberately not a named object, since terser's production property-mangling (vite.config.ts) renames unusual property names like `souls` but happens to spare ones that collide with built-in DOM properties (state, level, pause, resume), which isn't safe to depend on. Added KeyM mute, which upstream never had, wired through a new exported setMuted() in sounds.ts that drives the existing masterGain node. Guarded the LOSE/WIN states in onpointerup so a tap there means 'restart' instead of firing another spell. Added arcade-bridge.js (new file, not a patch to the original game): calls ArcadeSDK.ready() immediately since the title dialogue renders on the game's own canvas from the first frame, polls window.__norman.status() once a second to report score (souls) and fire gameOver()/submitScore() exactly once when the run ends, reloads the frame on a post-game tap, and forwards ArcadeSDK.onPause/onResume plus a visibilitychange fallback into the game's pause flag. Wrapped the Vite single-file build output (which Vite/the project's own 'vite:single-file' plugin deliberately emits without <html>/<head>/</body> tags, as a js13k byte-golfing trick) in a real <html><head>...</head> document with a charset, title, viewport meta and an html,body{margin:0;height:100%;overflow:hidden;background:#000} base style, all required by docs/GAME_RUNTIME.md's hard rules and absent from the raw js13k entry; none of the inlined game script itself was touched by this. vite.config.ts already built with base: './', so the Vite output never had the absolute '/assets/...' path problem other Vite imports can hit; verified by grepping the built index.html for '="/' (no matches).

Comments 0

Log in to comment.

  • Loading…