Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .claude/skills/littlejs-menu-ui-helpers/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: littlejs-menu-ui-helpers
description: API reference for building title screens, pause menus, options menus, medal grids, dialogs, and HUD toolbars with templates/menus.js in this LittleJS Arcade project. Use when creating or editing any menu/UI screen for a game in games/ — title, pause, options, confirm/alert dialogs, medals, best-score display, toasts, or toolbars.
---

# Menu UI (when using templates/menus.js)

- Start from templates/menuGame.html — wires up title, options (with separators + persisted slider/checkbox/color/input), medals (clickable grid + MenuMedal toasts), about (wrapping text), pause (with initialItemId), confirm/alert dialogs, HUD toolbar, and global navigation/activate sounds.
- API: createMenu({id, title, subtitle, items, dismissable, initialItemId, onShow, onHide, onStart}) and createToolbar({id, anchor, direction, items}). Item types: label, text (wrapping multi-line paragraph), separator, button, toggle, slider, checkbox, color (HTML color picker, fires onChange with hex), input (text field; arrow/Enter/Space pass through while focused), grid (per-cell onClick makes cells focusable buttons), custom (set focusable:true to opt into nav). Each menu returns a handle with show/hide/toggle/getItem/destroy. Items expose setLabel/setValue/getValue/setDisabled/setVisible. Grids also expose setCell(index, props).
- Per-item flags: `onUpdate(el)` — fires every frame while the parent menu is visible (live counters, animated text/custom DOM); `persist:'storeKey'` — slider/toggle/checkbox/color/input only, auto-loads from localStorage on init and auto-saves on change, with onChange firing once at init via a microtask so consumer effects apply the persisted value; `hideOnTouch:true` — toolbar items only, auto-hide on touch devices (fullscreen/music style buttons).
- onHide receives a reason: `'push'` when pushMenu hides this menu to surface a child, `'dismiss'` when Esc / B / backdrop / explicit hide closed the menu. Branch on it to avoid clobbering parent state when a sub-menu opens (e.g. `if (reason === 'dismiss') resetTitleFlag();`).
- createMenu's `onStart` runs when gamepad Start is pressed while the menu is on top — use it to launch the primary action (e.g. PLAY) directly without forcing the player to navigate to it first.
- Title-screen reveal: attachClickToReveal(menuId, canReveal?) installs a document-level click/Space/Enter/A/Start listener that shows the named menu the first time the user interacts with the canvas. Optional canReveal predicate (e.g. `() => !isPlaying`) gates the reveal so the listener can stay attached for the whole game. Pair with the menu's onShow hook to start title music. Returns a teardown fn.
- Title backdrop: don't leave the canvas black behind the title menu — it reads as a broken game. Build the world (board, rack, scrolling parallax, attract-mode animation) in gameInit and render it every frame from gameRender regardless of any `isPlaying` flag; only gate input on `isPlaying` so clicks fall through to the menu. menus.js's pause hook (`setMenuVisibilityCallback(v => paused = v)`) stops `gameUpdate` while the title menu is up, so the backdrop sits frozen instead of simulating, but it still renders. Reset the world on QUIT (call your `newGame()` helper from the quit confirm handler) so the title shows a clean state rather than a half-finished one.
- Title menu surfaces immediately — default. Prototypes should pass `createTitleMenu({revealOnClick:false})` and call `showMenu('title')` once at the end of `gameInit` so the menu appears on page load without a click. The click-to-reveal pattern (`revealOnClick:true`, which is the helper's default) is for polished titles where the press-any-key feel is intentional — opt in for those, not the other way around.
- Title shortcut: createTitleMenu({title, subtitle, onPlay, items, itemsBefore?, canReveal, playLabel?, id?, onShow?, onHide?, revealOnClick?}) wraps createMenu + a PLAY button + attachClickToReveal in one call for the standard title pattern. The menu is dismissable:false, gets id:'title' by default, the PLAY button gets id:'play', and `onStart` (gamepad Start) is auto-wired to onPlay. The wrapper auto-calls hideMenu(id) after onPlay runs — don't call it yourself. Item order: subtitle → itemsBefore → PLAY → items. Use `itemsBefore` for info that belongs above PLAY (for a Best score line, prefer `showBest:true` — see the Best-score bullet below — over a hand-rolled label item); use `items` for menu nav (OPTIONS / ABOUT). `canReveal: () => !isPlaying` is strongly recommended so clicks during gameplay don't re-show the title. Use this for every title screen; fall back to raw createMenu only when you need a non-standard structure (e.g. no PLAY button, level-select first).
- Title FX: pass `titleFx` to createTitleMenu / createMenu to juice the heading. A bare string picks one effect (`titleFx:'neon'`); an object composes channels (`titleFx:{fill:'neon', motion:'float', sparkle:true, hue:120, speed:1.5, color:'#f0a'}`). Fills: neon, rainbow, shine, fire, gold, outline, hardshadow, 3d, glitch, crt. Motion: wave, heartbeat, jelly, float. Overlay: sparkle. Tweaks: hue (deg), invert, speed (x), color, shadow (the hardshadow offset color), size (title font-size in em), spacing (letter-spacing in em). The same spec works on `showGameOverDialog`/`showAlertDialog`/`showConfirmDialog` (game over defaults to gold on a win, red glow on a loss; `titleFx:null` disables) and on any `label`/`text` menu item via `fx:`. Effects tear down on hide so timers never leak; `prefers-reduced-motion` suppresses motion + sparkle. `motion:'wave'` only works with the solid fills (neon, outline, hardshadow, 3d, glitch, crt) — it can't combine with a gradient fill (gold, rainbow, shine, fire), which would vanish, so pair those with float/heartbeat/jelly instead (the applicator warns and skips the wave if you try). Also avoid `motion:'wave'` on a `label`/`text` item whose `setLabel` is called while the menu is visible (e.g. a live counter) — wave splits the text into per-letter spans and `setLabel` overwrites them. Don't hand-roll title animation — use this or the `getTitleEl()` per-frame escape hatch.
- Menu theme: `setMenuTheme({...})` recolors/reshapes all menus from JS instead of a `<style>` block — the easy front door to the `--menu-*` CSS variables. Keys map to variables: `bg` (panel background), `fg` (text), `accent` (border + title default + selection + button hover), `border` (outline color if you want it ≠ accent), `itemBg`/`itemHoverBg` (button fills), `backdrop` (the dimmer behind modal menus), `disabled` (grayed text), `borderWidth`, `radius`, `font`. Only the keys you pass change; numbers for `borderWidth`/`radius` get `px` (pass a string like `'1.5em'` for other units). Or pass a preset name + optional overrides: `setMenuTheme('neon', {accent:'#f0a'})` — presets are `neon`, `casino`, `felt`, `retro`, `arcade`, `mono`. Call once (e.g. top of buildMenus); safe before any menu exists. Global per game — there's no per-menu override (use the per-panel CSS `data-menu-id` selector for that rare case). The raw CSS-variable reskin on `#littlejs-menus` still works for anyone who prefers it.
- Menu motion + emphasis: menus fade+scale in on show by default (~.18s, reduced-motion safe); `setMenuAnimations(false)` disables it (wire to an options checkbox). `createTitleMenu({playGlow:true})` pulses the PLAY button with an accent glow to draw the eye; any `button` menu item can opt in with `glow:true`.
- Menu Designer (dev tool, defined in templates/menuDesigner.js — opt-in, NOT loaded by default): add `<script src="../templates/menuDesigner.js">` AFTER menus.js to get a 🎨 button (bottom-left) that opens a live editor for the title FX + theme. Dropdowns/sliders/pickers apply live to the `'title'` menu (via the handle's `setTitleFx`) and the global theme (`setMenuTheme`), and a Copy button emits paste-ready `setMenuTheme(...)` + `titleFx:{...}` code. Use it to dial a game in, copy the snippet into the game, then remove the script tag. Menu handles expose `setTitleFx(spec)`/`getTitleFx()` for live title-FX swaps.
- Game-flow helpers (defined in menus.js, owned by it): setPlaying(p), isPlaying() (call as a function — it's a getter), quitToTitle(onCleanup?). Do NOT declare your own `let isPlaying` or `function setPlaying` in games — menus.js owns this. quitToTitle's `onCleanup` is for game-specific teardown (e.g. cancelling an AI search loop).
- Best score (defined in menus.js): getBestScore() reads the stored value, setBestScore(n) writes it, submitBestScore(n, {lowerIsBetter}) returns true on new best and saves, resetBestScore() clears to 0. All operate on the `bestScore` key inside the per-game save blob — sibling keys (e.g. jetpackDash's `coins`) are preserved by saveData's shallow merge. If a game's record doesn't fit this schema (custom name like `bestTime`/`bestLevel`/`bestShots`, or multi-entry like minesweeper's per-difficulty times), skip these helpers and use plain saveData/getSaveData with the manual `bestLabel` + onShow pattern. Examples: microRacer (bestTime), emojiSurvivors (bestTime + coins), frogger/sokoban (bestLevel), pool (bestShots), minesweeper (bestTimes per difficulty).
- Title best line: pass `showBest: true` (and optional `formatBest: n => 'BEST ' + n`) to createTitleMenu. The helper installs the label as the last item in itemsBefore and auto-refreshes it in onShow. Don't hand-roll the bestLabel item unless you need a layout beyond a single Best line.
- Reset Best in options: pass `showResetBest: true` to createOptionsMenu. Adds a RESET BEST button + confirm dialog wired to resetBestScore. Optional `resetBestMessage` / `resetBestLabel` overrides for custom wording.
- Game over dialog: showGameOverDialog({score, won?, format?, lowerIsBetter?, extraLines?, customMessage?, onContinue?, submitBest?=true}). Auto-submits to best score (skipped when caller omitted score), appends 'NEW BEST!' line on a new best. Title 'GAME OVER' / 'YOU WIN!' and icon 💥/🏆 swap based on `won`. onContinue defaults to quitToTitle.
- Sub-menu navigation: pushMenu(id) opens a child and remembers the parent; wire `onHide: popMenu` on the child so BACK / Esc / B / backdrop click returns to the parent. Call clearSubmenuStack() before quitting to title. Two one-shot dialog helpers (single options-object arg): showConfirmDialog({message, title?, icon?, onYes?, onNo?, yesLabel?, noLabel?}) for yes/no choices, and showAlertDialog({message, title?, icon?, onOk?, okLabel?}) for acknowledge-only popups (medal details, save confirmations). Both auto-render long or multi-line messages as a wrapping text item; icon renders as a 3em emoji block above the message.
- Tooltips: pass `title:'...'` on any item or grid cell to set a native mouseover tooltip. Grid cells fall back to `label` if no title is given; pass `title:''` to suppress.
- Debug overlay key: LittleJS uses Escape by default for its debug overlay, which collides with menus.js's pause/dismiss handling. menus.js auto-detects this on first `createMenu()` call and remaps the debug key to backtick (`` ` `` / `~`). Don't call `setDebugKey` yourself unless you want a different binding.
- Pause pattern: call `setMenuVisibilityCallback(v => paused = v)` once in gameInit. The callback fires for every menu show/hide, including dialogs created by showAlertDialog/showConfirmDialog — wiring per-menu onShow/onHide misses those and leaves paused=false, which lets gameUpdate run on the same frame as a menu Esc and stack pause on top of the surfaced menu. Do not toggle paused from gameUpdate (it doesn't run while paused).
- Pause hotkey: call `bindPauseKey({when: () => isPlaying && alive})` every frame from gameUpdate (per-frame poll, not a one-time install — early-return with `if (bindPauseKey(...)) return;`). Surfaces the named menu (default `'pause'`) on Esc / gamepad Start, plays the 'activate' sound, and clears the press so it doesn't bleed into the next frame's dismiss check. Always guards on `isMenuVisible()` so it never double-opens. Returns true if it triggered this frame.
- Inputs are handled automatically: arrows/Enter/Esc on keyboard, d-pad/stick/A/B/Start on gamepad, mouse/touch always. Toolbars are pointer-only.
- Sounds: setMenuSounds({select, activate}) wires global UI feedback — select fires on keyboard/gamepad navigation, activate on click/Enter/A. Construct LittleJS Sound objects with zzfx parameter arrays (`new Sound([...])`) and call `.play()` from the hook so master volume, muting, and user-gesture gating all just work.
- Selection follows input modality automatically: pointer-mode opens have no auto-selection (so the cursor doesn't drag a stale outline around); the first keyboard or gamepad press selects the initial item. Don't try to override this with manual focus calls.
- Toasts / achievements: showMenuToast({icon, title, text, duration, position}) renders a queued DOM notification in any corner (`'top-left'` default; also `'top-right'`/`'bottom-left'`/`'bottom-right'`) with pointer-events disabled so it can't block gameplay. AVOID the `'top-right'` corner for toasts (and for any game UI / important text generally): the default HUD toolbar (`installDefaultToolbar` — mute/fullscreen/library) anchors there, and on mobile the browser/OS chrome (URL / title bar) overlaps that corner too. Prefer `'top-left'` (the default) or the bottom corners; on screens whose top-left already holds a score HUD, use `'bottom-left'`/`'bottom-right'`. MenuMedal extends LittleJS's Medal so unlock() fires a toast instead of the engine's canvas overlay; same localStorage persistence (call `medalsInit('SaveName')`), same `medals` map, just a different display path. Use it everywhere you would have used `Medal`.
- Orientation lock: setOrientationLock('landscape'|'portrait', {icon, title, text}) declares the orientation a game wants. On touch devices held the wrong way it shows a full-screen "rotate your device" overlay above all menus and pauses the game (folds into the same visibility signal as menus, so `setMenuVisibilityCallback`/`installAutoPause` pause it automatically — no extra wiring). Off by default; `setOrientationLock(false)` clears it. Non-touch devices are never prompted (test in browser device-emulation). Overlay is themeable via `--orient-bg`/`--orient-icon-size` and the standard menu vars.
- Other helpers: getTopMenu(), getMenu(id), getToolbar(id), showMenu(id), hideMenu(id), hideAllMenus(), isMenuVisible(), playMenuSound(name).
- Theming: every color, font, size, and spacing is a CSS variable on `#littlejs-menus` — override in a `<style>` block to reskin without touching the helper. Per-menu CSS via the `data-menu-id` attribute on panels: `#littlejs-menus .ljs-menu-panel[data-menu-id="title"] { top: 65%; }`.
11 changes: 11 additions & 0 deletions .claude/skills/littlejs-specialty-game-helpers/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: littlejs-specialty-game-helpers
description: Reference for optional LittleJS Arcade helper modules not needed by every prototype — bloom.js (glow/bloom post-process), cards.js (card-game sprite atlas for templates/cardsGame.html), gameAI.js (alpha-beta AI for turn-based two-player games), and words.js (Wordle-style word lists). Use when building a card game, adding a turn-based AI opponent, building a word-guessing game, or adding a bloom/glow visual effect.
---

# Specialty game helpers

- Use the bloom module (defined in templates/bloom.js) for a multi-pass glow/bloom post-process — a higher-quality alternative to a hand-written PostProcessPlugin. It is NOT loaded by templates by default: add a `<script src="../templates/bloom.js">` tag (after engineLoader.js) and call `bloomInit()` once in gameInit to enable it. Including the script is a no-op until `bloomInit()` runs, so it's safe to load anywhere. It bypasses the engine's single-pass PostProcessPlugin and runs its own WebGL FBO ping-pong blur, so do NOT also create a PostProcessPlugin (the engine allows only one post-process regardless). Tune at init with `bloomInit({downsample, iterations, threshold, intensity, vignette, includeOverlay})`, live with `bloomSetParams(patch)`, or toggle with `setBloomEnabled(b)` / `isBloomEnabled()` (good for an options checkbox); `getBloomParams()` returns the current values. Params: `downsample` (1=full-res/tight, 2=balanced default, 4=wide/cheap but misses thin lines), `iterations` (blur passes, more=wider/softer), `threshold` (0..1 luminance cutoff to glow), `intensity` (glow add-back strength — usually >1 since an energy-conserving blur dilutes thin-line peaks), `vignette` (edge-darken strength, 0 disables), `includeOverlay` (default false, like PostProcessPlugin's `includeMainCanvas`: false uploads the WebGL scene directly and leaves the 2D HUD crisp on top — the cheap path; true folds the HUD canvas into the bloom so it glows too, at the cost of two full-res CPU canvas blits per frame). Call `bloomTweaks()` (after `bloomInit()`) to register a Bloom section of sliders in the tweakables panel (no-op if tweakables.js isn't loaded) — the params live in the `_bloom` object the render loop reads each frame, so the sliders drive them live. See [games/astroblast.html](../../../games/astroblast.html) for a working setup (with a Bloom tweak section).
- Use the cards.js helper (defined in templates/cards.js, loaded via a script tag in templates/cardsGame.html) for card games — it builds a procedural 52-card atlas on top of textureGenerator.js. Call `initCardAtlas()` once in gameInit (after `initDrawToTexture`), then `drawCard(pos, rank, suit)` (rank 0..12, suit 0..3), `drawCardBack(pos)`, and `drawCardShape(pos, size, color)` for silhouettes/shadows. Exposes constants CARD_SIZE, SUIT_HEARTS/SPADES/DIAMONDS/CLUBS, RANK_ACE/JACK/QUEEN/KING. It claims atlas tiles 0-12, 16-19, 24-26 of an `initDrawToTexture(8)` sheet; the other ~37 slots are yours. Customize via `initCardAtlas({redInk, blackInk, paintBack, rankLabels, suitGlyphs})`. When copying a template into games/, change the `<script src="cards.js">` path to `<script src="../templates/cards.js">`. See [games/klondike.html](../../../games/klondike.html) and [games/freecell.html](../../../games/freecell.html).
- Use the gameAI.js helper (defined in templates/gameAI.js) for turn-based two-player AI. It is NOT loaded by any template — add a `<script src="../templates/gameAI.js">` tag when you need it. Supply a pure-functional adapter `{getLegalMoves(state, player), applyMove(state, move, player)→freshState, evaluate(state, player)→number, isTerminal(state), getCurrentPlayer(state), getOpponent(player)}`, then `const move = await alphaBetaAI(game, state, maxDepth, {onProgress, yieldEveryMs})`. `alphaBetaAI` is the production strategy (iterative deepening + transposition table + killer-move heuristic) and is async — it yields to the event loop (~every 16ms = one frame) so the page stays responsive during search; `randomAI`/`greedyAI`/`minimaxAI` are naive synchronous references. All return `null` when there are no legal moves (caller decides to pass/end). See [games/chess.html](../../../games/chess.html), [games/reversi.html](../../../games/reversi.html), [games/connect4.html](../../../games/connect4.html).
- Use the words.js helper (defined in templates/words.js) for Wordle-style word games — it exposes `WORDLE_ANSWERS` (answer list) and `WORDLE_VALID` (accepted-guess list). NOT loaded by any template; add a `<script src="../templates/words.js">` tag when needed. See [games/wordly.html](../../../games/wordly.html).
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ docs/
.github/
.vscode/

# Claude Code settings and skills (local only)
.claude/
# Claude Code settings (local only), but keep skills in the repo
.claude/*
!.claude/skills

# playwright-mcp scratch dir
.playwright-mcp/
Expand Down
Loading