plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md
This skill should be used when making a Phaser 4 game accessible — honoring prefers-reduced-motion, pausing on window/tab blur, keyboard navigation and visible focus for menus, colorblind-safe palettes, and a screen-reader live region that announces game state through the DOM. Use it when building menus, settings, HUD, or any player-facing UI, or when an accessibility requirement comes up. Pairs with phaser-services, phaser-i18n, and the official scenes skill.
npx skillsauth add codyswanngt/lisa phaser-accessibilityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The canvas is opaque to assistive tech, so accessibility is something you build
in, not get for free. Five pillars carry most of the value: respect
prefers-reduced-motion, pause when the window loses focus, make menus fully
keyboard-navigable with visible focus, use colorblind-safe palettes, and mirror
game state into a DOM live region for screen readers. These wire through the
shared services ([[phaser-services]]) and use translated strings ([[phaser-i18n]]).
Read the media query once and gate non-essential motion (screen shake, parallax, big tweens, particle bursts) behind it. Reduced motion means "convey the same information with less movement", not "remove feedback".
// src/services/a11y.ts
export const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// in a scene
if (!reduceMotion) this.cameras.main.shake(120, 0.004);
this.tweens.add({ targets: panel, alpha: 1, duration: reduceMotion ? 0 : 250 });
Expose it as a togglable setting too (some players want it on regardless of OS), stored via SaveService ([[phaser-services]]).
A game that keeps running while the tab is hidden drains battery, desyncs audio, and is hostile to players who switch away. Phaser already exposes blur/focus events — pause the loop and audio, resume on focus.
this.game.events.on(Phaser.Core.Events.BLUR, () => { this.scene.pause(); this.sound.pauseAll(); });
this.game.events.on(Phaser.Core.Events.FOCUS, () => { this.scene.resume(); this.sound.resumeAll(); });
Set Phaser.Types.Core.GameConfig.autoFocus and disableContextMenu as needed.
(These are listeners — remove them in shutdown per the on/off discipline in
[[phaser-services]].) For competitive/timed play, pausing on blur is also the
fair behavior.
Every menu must be operable without a pointer: arrow keys / Tab move focus, Enter or Space activates, Escape backs out. Keep a focus index over a typed list of items and render an unmistakable focus indicator (outline/scale, not color alone).
private items: MenuItem[] = [];
private focus = 0;
private move(delta: number) {
this.items[this.focus].setFocused(false);
this.focus = Phaser.Math.Wrap(this.focus + delta, 0, this.items.length);
this.items[this.focus].setFocused(true); // visible outline + announce
this.announce(this.items[this.focus].label);
}
create() {
this.input.keyboard!.on("keydown-DOWN", () => this.move(1));
this.input.keyboard!.on("keydown-UP", () => this.move(-1));
this.input.keyboard!.on("keydown-ENTER",() => this.items[this.focus].activate());
}
Use the semantic InputService actions ([[phaser-services]]) so the same bindings serve gamepad and touch. Never rely on hover-only affordances.
Never encode meaning in hue alone — pair every color cue with a shape, icon, label, or pattern (red enemy and a spike icon; green/red status and a check/x). Choose palettes that stay distinguishable across deuteranopia/protanopia /tritanopia, and offer a colorblind palette option in settings.
// src/logic/palette.ts — pure, testable, no phaser import
export const palettes = {
default: { ally: 0x2e7d32, enemy: 0xc62828, neutral: 0xf9a825 },
deuter: { ally: 0x0072b2, enemy: 0xd55e00, neutral: 0xf0e442 }, // Okabe–Ito, CB-safe
} as const;
Keep palette selection in src/logic/** so contrast/choice logic is unit-tested,
and store the player's choice via SaveService.
Mirror important state changes into an ARIA live region in the DOM (outside the canvas) so screen readers announce them. One polite region for status, one assertive for urgent events.
<!-- index.html, alongside the game container -->
<div id="sr-status" aria-live="polite" class="sr-only"></div>
<div id="sr-alert" aria-live="assertive" class="sr-only"></div>
// src/services/a11y.ts
const status = document.getElementById("sr-status")!;
export function announce(msg: string, urgent = false) {
const el = urgent ? document.getElementById("sr-alert")! : status;
el.textContent = ""; el.textContent = msg; // clear+set so identical repeats re-announce
}
Announce menu focus, score milestones, level changes, and game-over. Messages
come from the i18n catalog ([[phaser-i18n]]) — never hardcoded — and .sr-only
keeps the region visually hidden but readable by AT.
src/services/a11y.ts; palette/choice logic lives in
src/logic/** (unit-tested).Verified by manual passes that double as runtime checks: tab through every menu with the pointer unplugged and confirm visible focus + activation; set the OS to reduce-motion and confirm shake/parallax stop while feedback remains; blur the tab and confirm the game pauses and audio stops; and run a screen reader (VoiceOver /NVDA) to confirm the live region announces menu focus and game state. </content>
development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.