SOURCE / PINNED RELEASE
Made of little things.
Stick Brawl
- Release
- 732bb70ea856…
- Author-recorded commit
- 918970bb749a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-554b329db93
Archive hash verified: 67f16c1148706bbd…. 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 { Game, VIEW_H, VIEW_W } from './game/game.js';
import './styles.css';
// Stick Brawl keeps its own night-time colours; it never follows the host theme.
const app = requireElement<HTMLElement>('#app');
const stage = requireElement<HTMLDivElement>('#stage');
const screen = requireElement<HTMLDivElement>('#screen');
const canvas = requireElement<HTMLCanvasElement>('#game');
const menu = requireElement<HTMLDivElement>('#menu');
const menuButton = requireElement<HTMLButtonElement>('#menuButton');
const playButton = requireElement<HTMLButtonElement>('#playButton');
const restartButton = requireElement<HTMLButtonElement>('#restartButton');
const touch = requireElement<HTMLDivElement>('#touch');
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 game = new Game(canvas);
// ---------- touch buttons ----------
const touchScreen = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
let alwaysShowTouch = false;
function updateTouchMode(): void {
const show = touchScreen || alwaysShowTouch;
touch.hidden = !show;
app.classList.toggle('touch-mode', show);
fitScreen();
}
/** A touch button that works like holding a key, and optionally like pressing one. */
function touchButton(selector: string, held: 'left' | 'right' | 'down' | 'block' | null, press: 'jump' | 'attack' | 'kick' | 'roll' | 'shoot' | 'sword' | null): void {
const button = requireElement<HTMLButtonElement>(selector);
const release = () => {
if (held) game.held[held] = false;
button.classList.remove('held');
};
button.addEventListener('pointerdown', (event) => {
event.preventDefault();
button.setPointerCapture(event.pointerId);
if (held) game.held[held] = true;
if (press) game.press(press);
button.classList.add('held');
});
button.addEventListener('pointerup', release);
button.addEventListener('pointercancel', release);
button.addEventListener('lostpointercapture', release);
button.addEventListener('contextmenu', (event) => event.preventDefault());
}
touchButton('#touchLeft', 'left', null);
touchButton('#touchRight', 'right', null);
touchButton('#touchRoll', 'down', 'roll');
touchButton('#touchJump', null, 'jump');
touchButton('#touchAttack', null, 'attack');
touchButton('#touchBlock', 'block', null);
touchButton('#touchShoot', null, 'shoot');
touchButton('#touchSword', null, 'sword');
touchButton('#touchKick', null, 'kick');
// ---------- fitting the game to the frame ----------
/** Keep the game's wide shape and make it as big as the frame allows. */
function fitScreen(): void {
const width = stage.clientWidth;
const height = stage.clientHeight - (touch.hidden ? 0 : touch.offsetHeight + 8);
const scale = Math.max(0.1, Math.min(width / VIEW_W, height / VIEW_H));
const cssW = Math.floor(VIEW_W * scale);
const cssH = Math.floor(VIEW_H * scale);
screen.style.width = `${cssW}px`;
screen.style.height = `${cssH}px`;
game.resize(cssW, cssH);
tooSmall.hidden = stage.clientWidth >= 240 && stage.clientHeight >= 170;
}
new ResizeObserver(fitScreen).observe(stage);
// ---------- the menu ----------
let started = false;
function openMenu(): void {
game.paused = true;
releaseAll();
menu.hidden = false;
menuButton.hidden = true;
playButton.textContent = started ? 'Keep fighting' : 'Fight!';
restartButton.hidden = !started;
playButton.focus();
}
function closeMenu(): void {
started = true;
menu.hidden = true;
menuButton.hidden = false;
game.paused = false;
canvas.focus();
}
playButton.addEventListener('click', closeMenu);
restartButton.addEventListener('click', () => {
game.restart();
closeMenu();
});
menuButton.addEventListener('click', openMenu);
// ---------- keyboard ----------
function releaseAll(): void {
game.held.left = false;
game.held.right = false;
game.held.down = false;
game.held.block = false;
}
const HELD_KEYS: Record<string, 'left' | 'right' | 'down' | 'block'> = {
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowDown: 'down',
ControlLeft: 'block',
ControlRight: 'block',
};
const PRESS_KEYS: Record<string, 'jump' | 'attack' | 'kick' | 'roll' | 'shoot' | 'sword'> = {
ArrowUp: 'jump',
Space: 'attack',
KeyZ: 'kick',
ArrowDown: 'roll',
AltLeft: 'shoot',
ShiftLeft: 'sword',
};
window.addEventListener('keydown', (event) => {
if (event.code === 'Escape') {
event.preventDefault();
if (menu.hidden) openMenu();
else closeMenu();
return;
}
// While the menu is open, keys work the buttons instead of the stickman.
if (!menu.hidden) return;
const held = HELD_KEYS[event.code];
const press = PRESS_KEYS[event.code];
if (!held && !press) return;
event.preventDefault();
if (held) game.held[held] = true;
if (press && !event.repeat) game.press(press);
});
window.addEventListener('keyup', (event) => {
// Stop the browser from opening its own menu when Alt is let go.
if (event.code === 'AltLeft') event.preventDefault();
const held = HELD_KEYS[event.code];
if (held) game.held[held] = false;
});
window.addEventListener('blur', releaseAll);
document.addEventListener('visibilitychange', () => {
if (document.hidden && menu.hidden) openMenu();
});
canvas.addEventListener('pointerdown', () => canvas.focus());
// Optional setting: show the touch buttons even with a keyboard. Without the config domain it stays off.
if (runtimeHasDomain(CONFIG_DOMAIN)) {
startSettings((values) => {
alwaysShowTouch = values.touchButtons === true;
updateTouchMode();
});
}
updateTouchMode();
openMenu();
game.start();
