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.
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();
