Back to Wizard Duel
SOURCE / PINNED RELEASE

Made of little things.

Wizard Duel

Release
48b4ed933a32…
Author-recorded commit
3ed8fcbbf62c…
License
LICENSE
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-86005d75eb4

Archive hash verified: 4196bfb69e536a86…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/main.ts
import { CONFIG_DOMAIN } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { startSettings } from './napplet-settings.js';
import { Duel, GAME_KEYS, VIEW_H, VIEW_W } from './game/game.js';
import { setSoundEnabled } from './game/sound.js';
import { createGamepadInput } from './game/gamepad.js';
import { PadRouter, wizardPadBindings } from './game/gamepad-input.js';
import './styles.css';

// Wizard Duel keeps its own night-castle colours; it never follows the host theme.

const stage = requireElement<HTMLDivElement>('#stage');
const canvas = requireElement<HTMLCanvasElement>('#game');
const tooSmall = requireElement<HTMLDivElement>('#tooSmall');

function requireElement<T extends HTMLElement>(selector: string): T {
  const element = document.querySelector<T>(selector);
  if (!element) throw new Error(`Missing required element: ${selector}`);
  return element;
}

const duel = new Duel(canvas);
duel.touchOnly = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
duel.screenShake = !matchMedia('(prefers-reduced-motion: reduce)').matches;

/** Scale the 320×180 pixel canvas to fit the frame, using whole-pixel steps when possible. */
function fitCanvas(): void {
  const width = stage.clientWidth;
  const height = stage.clientHeight;
  const fit = Math.min(width / VIEW_W, height / VIEW_H);
  const scale = fit >= 1 ? Math.floor(fit) : fit;
  canvas.style.width = `${Math.floor(VIEW_W * scale)}px`;
  canvas.style.height = `${Math.floor(VIEW_H * scale)}px`;
  tooSmall.hidden = width >= 300 && height >= 160;
}

new ResizeObserver(fitCanvas).observe(stage);
fitCanvas();

window.addEventListener('keydown', (event) => {
  if (!GAME_KEYS.has(event.code)) return;
  event.preventDefault();
  duel.keyDown(event.code, event.repeat);
});
window.addEventListener('keyup', (event) => {
  if (!GAME_KEYS.has(event.code)) return;
  event.preventDefault();
  duel.keyUp(event.code);
});
window.addEventListener('blur', () => duel.releaseAll());
document.addEventListener('visibilitychange', () => {
  if (document.hidden) duel.releaseAll();
});
stage.addEventListener('pointerdown', () => {
  window.focus();
  duel.pointerStart();
});

// Game controllers (optional): if the browser blocks them, the keyboard still works.
const pads = createGamepadInput(wizardPadBindings);
const padRouter = new PadRouter(duel);
duel.beforeUpdate = () => padRouter.update(pads.poll());

// Optional settings (sound and screen shake). Without the config domain the defaults stay on.
if (runtimeHasDomain(CONFIG_DOMAIN)) {
  startSettings((values) => {
    setSoundEnabled(values.sound !== false);
    duel.screenShake = values.screenShake !== false && !matchMedia('(prefers-reduced-motion: reduce)').matches;
  });
}

duel.start();