SOURCE / PINNED RELEASE
Made of little things.
Chase'n Those Narly Waves
- Release
- 0c7dde9d4921…
- Author-recorded commit
- 88e58516be17…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-836af5a08b9
Archive hash verified: e1acfdbe67c54dd4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Starts the game: sets up the screen, menus, controls and saving your best score.
import { KEYS_DOMAIN, STORAGE_DOMAIN, keys, storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { createGame, popup, update, type Game } from './game.js';
import { Input, attachKeyboard, attachTouchButtons } from './input.js';
import { drawHud } from './render/hud.js';
import { World } from './render/scene.js';
import { BASIC_TRICKS, TRICKS, comboText } from './tricks.js';
import './styles.css';
const BEST_KEY = 'best-score';
function $<T extends HTMLElement>(selector: string): T {
const element = document.querySelector<T>(selector);
if (!element) throw new Error(`Missing element: ${selector}`);
return element;
}
const canvas = $<HTMLCanvasElement>('#scene');
const hudCanvas = $<HTMLCanvasElement>('#hud');
const hud = hudCanvas.getContext('2d')!;
const world = createWorld();
/** The 3D world needs WebGL. If this device can't do it, say so instead of crashing. */
function createWorld(): World | null {
try {
return new World(canvas);
} catch {
$('#noWebgl').hidden = false;
return null;
}
}
const screens = {
title: $<HTMLElement>('#titleScreen'),
pause: $<HTMLElement>('#pauseScreen'),
over: $<HTMLElement>('#overScreen'),
};
const touch = $<HTMLElement>('#touch');
const pauseButton = $<HTMLButtonElement>('#pauseButton');
const isTouch = window.matchMedia('(pointer: coarse)').matches;
const input = new Input();
let game: Game = createGame(0);
// ---------- Screens ----------
function showScreen(): void {
screens.title.hidden = game.phase !== 'title';
screens.pause.hidden = game.phase !== 'paused';
screens.over.hidden = game.phase !== 'over';
touch.hidden = !(isTouch && game.phase === 'playing');
pauseButton.hidden = game.phase !== 'playing';
const focusTarget: Record<Game['phase'], HTMLElement> = {
title: $('#startButton'),
paused: $('#resumeButton'),
over: $('#againButton'),
playing: canvas,
};
focusTarget[game.phase].focus({ preventScroll: true });
}
function startGame(): void {
game = createGame(game.best);
game.phase = 'playing';
input.clear();
popup(game, 'SURF\'S UP, BRO!', 'Tap W when a wave hits the green zone', '#2de1c2');
showScreen();
}
function togglePause(): void {
if (game.phase === 'playing') game.phase = 'paused';
else if (game.phase === 'paused') game.phase = 'playing';
else return;
input.clear();
showScreen();
}
function restart(): void {
if (game.phase === 'title') return;
startGame();
}
function gameOver(): void {
$('#finalScore').textContent = `${game.score} points`;
const isNewBest = game.score > 0 && game.score >= game.best && game.score > savedBest;
$('#finalBest').textContent = isNewBest ? 'NEW BEST SCORE! Totally rad!' : `Best: ${game.best}`;
if (isNewBest) saveBest(game.score);
showScreen();
}
$('#startButton').addEventListener('click', startGame);
$('#againButton').addEventListener('click', startGame);
$('#resumeButton').addEventListener('click', togglePause);
$('#restartButton').addEventListener('click', startGame);
pauseButton.addEventListener('click', togglePause);
$('#secretTricks').textContent =
`...plus ${TRICKS.length - BASIC_TRICKS.length} secret tricks with 3 and 4 keys. Can you find them all?`;
$('#trickList').replaceChildren(
...BASIC_TRICKS.map((trick) => {
const item = document.createElement('li');
const name = document.createElement('span');
name.textContent = trick.name;
const combo = document.createElement('span');
combo.className = 'combo';
combo.textContent = comboText(trick.combo);
item.append(name, combo);
return item;
}),
);
// ---------- Controls ----------
attachKeyboard(input, () => game.phase === 'playing');
attachTouchButtons(touch, input);
// Pause and restart are "actions" the host app (shell) can show and let
// players re-map (NAP-KEYS). If the host doesn't support that, we listen ourselves.
let shellHandlesKeys = false;
if (runtimeHasDomain(KEYS_DOMAIN)) {
Promise.all([
keys.register({ id: 'game.pause', label: 'Pause / resume', defaultKey: 'P' }, togglePause),
keys.register({ id: 'game.restart', label: 'Start over', defaultKey: 'R' }, restart),
])
.then((bindings) => {
shellHandlesKeys = true;
window.addEventListener('pagehide', () => bindings.forEach((b) => b.close()), { once: true });
})
.catch(() => undefined);
}
window.addEventListener('keydown', (event) => {
if (event.code === 'Escape') togglePause();
if (shellHandlesKeys) return;
if (event.code === 'KeyP') togglePause();
if (event.code === 'KeyR' && game.phase !== 'playing') restart();
});
// Pause automatically if you click away or switch tabs.
window.addEventListener('blur', () => {
if (game.phase === 'playing') togglePause();
});
document.addEventListener('visibilitychange', () => {
if (document.hidden && game.phase === 'playing') togglePause();
});
// ---------- Best score (saved through the host, if it allows it) ----------
let savedBest = 0;
function saveBest(score: number): void {
savedBest = score;
if (!runtimeHasDomain(STORAGE_DOMAIN)) return;
storage.setItem(BEST_KEY, String(score)).catch(() => undefined);
}
function showTitleBest(): void {
$('#titleBest').textContent = game.best > 0 ? `Your best: ${game.best}` : '';
}
if (runtimeHasDomain(STORAGE_DOMAIN)) {
storage
.getItem(BEST_KEY)
.then((value) => {
const best = Number(value);
if (Number.isFinite(best) && best > game.best) {
game.best = best;
savedBest = best;
showTitleBest();
}
})
.catch(() => undefined);
}
// ---------- Screen size ----------
let width = 0;
let height = 0;
function resize(): void {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(2, window.devicePixelRatio || 1);
width = Math.max(1, rect.width);
height = Math.max(1, rect.height);
world?.resize(width, height);
hudCanvas.width = Math.round(width * dpr);
hudCanvas.height = Math.round(height * dpr);
hud.setTransform(dpr, 0, 0, dpr, 0, 0);
}
new ResizeObserver(resize).observe(canvas);
resize();
// ---------- Game loop ----------
let last = performance.now();
function frame(now: number): void {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
const wasPlaying = game.phase === 'playing';
if (game.phase === 'title') game.scroll += 6 * dt; // gentle ocean on the title screen
update(game, input, dt);
if (wasPlaying && game.phase === 'over') gameOver();
const bottomSpace = isTouch && game.phase === 'playing' ? touch.offsetHeight : 0;
world?.render(game);
drawHud(hud, game, {
width,
height,
bottomSpace,
sharkWarnings: world?.sharkWarnings(game) ?? [],
});
requestAnimationFrame(frame);
}
showTitleBest();
showScreen();
requestAnimationFrame(frame);
