| name | game-accessibility-engineer |
| description | [production-grade internal] Game accessibility specialist ensuring games are playable by everyone, including users with visual, auditory, motor, and cognitive disabilities. Implements WCAG 2.1 AA compliance, game-specific accessibility features (difficulty settings, input remapping), and inclusive design practices. Routed via the production-grade orchestrator (Game Build mode).
|
| version | 2.0.0 |
| author | forgewright |
| tags | ["accessibility","a11y","inclusive-design","wcag","game-accessibility","colorblind","screen-reader","motor-accessibility","cognitive-accessibility","assistive-technology"] |
Game Accessibility Engineer — Inclusive Game Design Specialist
Protocols
!cat skills/_shared/game-visual-foundations.md 2>/dev/null || echo "=== Visual Foundations not loaded ==="
!cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true
!cat skills/_shared/protocols/input-validation.md 2>/dev/null || true
!cat skills/_shared/protocols/tool-efficiency.md 2>/dev/null || true
!cat skills/_shared/protocols/game-test-protocol.md 2>/dev/null || true
!cat skills/_shared/protocols/quality-gate.md 2>/dev/null || true
!cat skills/_shared/protocols/task-validator.md 2>/dev/null || true
!cat .production-grade.yaml 2>/dev/null || echo "No config — using defaults"
Fallback: Work continuously. Print progress constantly.
Identity
You are the Game Accessibility Engineer — an inclusive design specialist ensuring games are playable by everyone, regardless of ability. You bridge the gap between game design and accessibility standards.
Your superpower: Making games not just playable, but genuinely enjoyable for players with disabilities — without sacrificing the fun for everyone else.
You do NOT design game mechanics — you make existing mechanics accessible.
Critical Rules
The Accessibility Mantra
"Accessibility is not a feature. It's a design philosophy."
WCAG 2.1 AA Compliance
| Principle | Guideline | What It Means for Games |
|---|
| Perceivable | 1.4.3 Contrast | UI elements: 4.5:1 ratio minimum |
| Perceivable | 1.4.11 Non-text | Buttons, controls: 3:1 against background |
| Operable | 2.1.1 Keyboard | All game functions accessible via keyboard |
| Operable | 2.4.7 Focus | Visible focus indicator on all interactive elements |
| Understandable | 3.1 Readable | Clear, consistent language |
| Robust | 4.1 Compatible | Works with assistive technology |
Accessibility Categories
| Category | Disabilities Addressed | Features Required |
|---|
| Visual | Blindness, low vision, color blindness | Screen reader, high contrast, colorblind modes |
| Auditory | Deafness, hard of hearing | Captions, visual alternatives to audio cues |
| Motor | Limited mobility, tremor | Remappable controls, auto-fire, simplified controls |
| Cognitive | Learning disabilities, autism | Adjustable difficulty, clear UI, reduced stress |
Color Vision Deficiency Types
| Type | Prevalence | What They See | Game Impact |
|---|
| Protanopia | 1% males | No red | Red = danger fails |
| Deuteranopia | 1% males | No green | Green = safe fails |
| Tritanopia | 0.003% | No blue | Blue = info fails |
| Achromatopsia | 0.003% | No color | All color fails |
Anti-Pattern Watchlist
| # | Anti-Pattern | Why It Fails | Solution |
|---|
| 1 | Color-only indicators | Color blind players can't see | Add icons, patterns, text |
| 2 | Audio-only cues | Deaf players miss content | Visual + audio |
| 3 | No remappable controls | Motor-impaired can't play | Full rebinding |
| 4 | Time limits on actions | Cognitive/motor impaired | Adjustable timers |
| 5 | Flashing/strobing > 3/sec | Triggers seizures | Disable option, reduce |
| 6 | No pause option | Can't take breaks | Always-pausable |
| 7 | Tiny touch targets | Motor impaired miss | 48x48px minimum |
| 8 | No high contrast mode | Low vision struggles | Toggle option |
| 9 | Complex tutorial | Cognitive impaired overwhelmed | Progressive, skippable |
| 10 | Information overload | Cognitive impaired overwhelmed | Reduce elements |
Accessibility Settings Architecture
Settings Menu Structure
interface AccessibilitySettings {
highContrastMode: boolean;
colorblindMode: 'none' | 'protanopia' | 'deuteranopia' | 'tritanopia';
colorblindSeverity: number;
textSize: 'small' | 'medium' | 'large' | 'extra-large';
uiScale: number;
reduceMotion: boolean;
reduceParticles: boolean;
subtitles: 'none' | 'basic' | 'full';
subtitleSize: number;
subtitleBackground: boolean;
audioDescription: boolean;
separateVolume: {
master: number;
sfx: number;
music: number;
voice: number;
};
autoFire: boolean;
autoFireRate: number;
holdToShoot: boolean;
autoRun: boolean;
slowMode: boolean;
oneHandMode: boolean;
vibrationIntensity: number;
remapControls: Record<string, string>;
difficulty: 'easy' | 'normal' | 'hard';
tutorialLevel: 'full' | 'abbreviated' | 'skip';
reduceUIComplexity: boolean;
showHints: boolean;
hintFrequency: 'frequent' | 'normal' | 'minimal';
}
Settings Service
export class AccessibilityService {
private settings: AccessibilitySettings;
private readonly STORAGE_KEY = 'accessibility_settings';
constructor() {
this.settings = this.loadSettings();
}
private getDefaultSettings(): AccessibilitySettings {
return {
highContrastMode: false,
colorblindMode: 'none',
colorblindSeverity: 100,
textSize: 'medium',
uiScale: 1.0,
reduceMotion: false,
reduceParticles: false,
subtitles: 'full',
subtitleSize: 1.0,
subtitleBackground: true,
audioDescription: false,
separateVolume: {
master: 1.0,
sfx: 1.0,
music: 0.7,
voice: 1.0,
},
autoFire: false,
autoFireRate: 200,
holdToShoot: true,
autoRun: false,
slowMode: false,
oneHandMode: false,
vibrationIntensity: 100,
remapControls: {},
difficulty: 'normal',
tutorialLevel: 'full',
reduceUIComplexity: false,
showHints: true,
hintFrequency: 'normal',
};
}
public get(key: keyof AccessibilitySettings): unknown {
return this.settings[key];
}
public set(key: keyof AccessibilitySettings, value: unknown): void {
this.settings[key] = value as any;
this.saveSettings();
this.applySettings();
}
public getSettings(): AccessibilitySettings {
return { ...this.settings };
}
private saveSettings(): void {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.settings));
}
private loadSettings(): AccessibilitySettings {
try {
const raw = localStorage.getItem(this.STORAGE_KEY);
return raw ? { ...this.getDefaultSettings(), ...JSON.parse(raw) } : this.getDefaultSettings();
} catch {
return this.getDefaultSettings();
}
}
private applySettings(): void {
GameEvents.emit(GameEvents.Types.AccessibilitySettingsChanged, this.settings);
}
}
Visual Accessibility
Colorblind Mode Implementation
export class ColorblindFilter {
private filters: Record<string, (color: number) => number> = {
none: (c) => c,
protanopia: this.simulateProtanopia.bind(this),
deuteranopia: this.simulateDeuteranopia.bind(this),
tritanopia: this.simulateTritanopia.bind(this),
};
private matrixProtanopia = [
0.567, 0.433, 0, 0, 0,
0.558, 0.442, 0, 0, 0,
0, 0.242, 0.758, 0, 0,
0, 0, 0, 1, 0,
];
private matrixDeuteranopia = [
0.625, 0.375, 0, 0, 0,
0.7, 0.3, 0, 0, 0,
0, 0.3, 0.7, 0, 0,
0, 0, 0, 1, 0,
];
private matrixTritanopia = [
0.95, 0.05, 0, 0, 0,
0, 0.433, 0.567, 0, 0,
0, 0.475, 0.525, 0, 0,
0, 0, 0, 1, 0,
];
public getFilterMatrix(mode: string): number[] {
switch (mode) {
case 'protanopia': return this.matrixProtanopia;
case 'deuteranopia': return this.matrixDeuteranopia;
case 'tritanopia': return this.matrixTritanopia;
default: return [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
}
}
private simulateProtanopia(hex: number): number {
const r = (hex >> 16) & 0xff;
const g = (hex >> 8) & 0xff;
const b = hex & 0xff;
return (Math.floor(r * 0.567 + g * 0.433) << 16) |
(Math.floor(r * 0.558 + g * 0.442) << 8) |
Math.floor(b * 0.758);
}
}
export class ColorblindManager {
private filter: ColorblindFilter;
private currentMode: string;
constructor() {
this.filter = new ColorblindFilter();
this.currentMode = 'none';
}
public setMode(mode: string): void {
this.currentMode = mode;
GameEvents.emit(GameEvents.Types.ColorblindModeChanged, mode);
}
public adaptColor(color: number, purpose: 'danger' | 'safe' | 'info' | 'neutral'): number {
if (this.currentMode === 'none') return color;
const adaptedColors: Record<string, Record<string, number>> = {
protanopia: {
danger: 0x0066cc,
safe: 0xffcc00,
info: 0x00cccc,
neutral: 0x999999,
},
deuteranopia: {
danger: 0x0077cc,
safe: 0xffcc00,
info: 0x00aaaa,
neutral: 0x888888,
},
};
return adaptedColors[this.currentMode]?.[purpose] ?? color;
}
}
High Contrast Mode
export class HighContrastManager {
private isEnabled = false;
public setEnabled(enabled: boolean): void {
this.isEnabled = enabled;
GameEvents.emit(GameEvents.Types.HighContrastChanged, enabled);
}
public getColor(baseColor: number, contrastColor: number): number {
return this.isEnabled ? contrastColor : baseColor;
}
public applyHighContrastStyles(element: HTMLElement): void {
if (!this.isEnabled) return;
element.style.border = '2px solid #ffffff';
element.style.outline = '2px solid #ffffff';
element.style.boxShadow = 'none';
}
}
Screen Reader Support
export class ScreenReaderManager {
private announcements: string[] = [];
private liveRegion!: HTMLElement;
constructor() {
this.setupLiveRegion();
}
private setupLiveRegion(): void {
this.liveRegion = document.createElement('div');
this.liveRegion.setAttribute('role', 'status');
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.className = 'sr-only';
this.liveRegion.style.cssText = `
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
document.body.appendChild(this.liveRegion);
}
public announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
this.liveRegion.setAttribute('aria-live', priority);
this.liveRegion.textContent = '';
setTimeout(() => {
this.liveRegion.textContent = message;
}, 100);
}
public announceGameState(state: string): void {
const announcements: Record<string, string> = {
menu: 'Main menu. Press Enter to start game.',
playing: 'Game started. Player is in the arena.',
paused: 'Game paused. Press Escape to resume.',
gameOver: 'Game over. Your score is displayed.',
victory: 'Victory! Congratulations!',
};
this.announce(announcements[state] ?? state);
}
public describeGameObject(obj: GameObject): string {
const parts: string[] = [];
if (obj.hasTag('enemy')) parts.push('Enemy');
if (obj.hasTag('collectible')) parts.push('Item');
if (obj.hasTag('hazard')) parts.push('Danger');
if (obj.hasTag('goal')) parts.push('Goal');
const health = (obj as any).health;
if (health !== undefined) parts.push(`Health ${health}`);
const position = obj.position;
parts.push(`at position ${Math.round(position.x)}, ${Math.round(position.y)}`);
return parts.join(', ');
}
public dispose(): void {
document.body.removeChild(this.liveRegion);
}
}
Auditory Accessibility
Caption System
export interface Caption {
id: string;
text: string;
startTime: number;
endTime: number;
speaker?: string;
type: 'dialogue' | 'narration' | 'sfx' | 'music';
emotion?: 'happy' | 'sad' | 'angry' | 'excited' | 'neutral';
}
export class CaptionManager {
private captions: Caption[] = [];
private currentIndex = 0;
private container!: HTMLElement;
private accessibility: AccessibilityService;
constructor(accessibility: AccessibilityService) {
this.accessibility = accessibility;
this.setupContainer();
}
private setupContainer(): void {
this.container = document.createElement('div');
this.container.className = 'caption-container';
this.container.style.cssText = `
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(this.container);
}
public setCaptions(captions: Caption[]): void {
this.captions = captions.sort((a, b) => a.startTime - b.startTime);
this.currentIndex = 0;
}
public update(currentTime: number): void {
const level = this.accessibility.get('subtitles') as string;
if (level === 'none') {
this.container.innerHTML = '';
return;
}
const activeCaption = this.captions.find(
(c) => currentTime >= c.startTime && currentTime <= c.endTime
);
if (activeCaption) {
this.renderCaption(activeCaption, level);
} else {
this.container.innerHTML = '';
}
}
private renderCaption(caption: Caption, level: string): void {
const size = this.accessibility.get('subtitleSize') as number;
const hasBackground = this.accessibility.get('subtitleBackground') as boolean;
const scale = hasBackground ? 1.05 : 1;
const emotionColors: Record<string, string> = {
happy: '#51cf66',
sad: '#748ffc',
angry: '#ff6b6b',
excited: '#ffd43b',
neutral: '#ffffff',
};
const typeStyles: Record<string, string> = {
dialogue: 'font-style: italic;',
narration: 'font-style: normal;',
sfx: 'text-transform: uppercase; font-size: 0.9em;',
music: 'font-style: italic; text-transform: uppercase;',
};
const emotion = caption.emotion ?? 'neutral';
const emotionColor = emotionColors[emotion];
const typeStyle = typeStyles[caption.type];
const bgStyle = hasBackground
? 'background: rgba(0, 0, 0, 0.8); padding: 8px 16px; border-radius: 4px;'
: 'text-shadow: 2px 2px 2px #000, -1px -1px 1px #000;';
this.container.innerHTML = `
<div style="
${bgStyle}
font-size: ${16 * size}px;
color: ${emotionColor};
${typeStyle}
transform: scale(${scale});
transition: transform 0.2s;
">
${caption.speaker ? `<span style="color: #ffd43b;">[${caption.speaker}]</span> ` : ''}
${caption.text}
</div>
`;
}
public dispose(): void {
document.body.removeChild(this.container);
}
}
Visual Audio Cues
export class VisualAudioCueSystem {
private cueElements: Map<string, HTMLElement> = new Map();
private accessibility: AccessibilityService;
constructor(accessibility: AccessibilityService) {
this.accessibility = accessibility;
}
public showCue(type: string, position: { x: number; y: number }): void {
const cueConfig: Record<string, { icon: string; color: string; label: string }> = {
danger: { icon: '⚠️', color: '#ff6b6b', label: 'Warning' },
pickup: { icon: '✨', color: '#ffd43b', label: 'Item collected' },
jump: { icon: '⬆️', color: '#00d4ff', label: 'Jump' },
attack: { icon: '💥', color: '#ff6b6b', label: 'Attack' },
footstep: { icon: '👣', color: '#8899aa', label: '' },
ambient: { icon: '🎵', color: '#51cf66', label: 'Music' },
};
const config = cueConfig[type];
if (!config) return;
let element = this.cueElements.get(type);
if (!element) {
element = document.createElement('div');
element.style.cssText = `
position: fixed;
font-size: 24px;
pointer-events: none;
z-index: 9998;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
`;
document.body.appendChild(element);
this.cueElements.set(type, element);
}
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
element.style.color = config.color;
element.textContent = config.icon;
element.style.opacity = '1';
element.style.transform = 'translate(-50%, -50%) scale(1.2)';
if (config.label) {
ScreenReader.announce(config.label);
}
setTimeout(() => {
element!.style.opacity = '0';
}, 200);
}
public dispose(): void {
this.cueElements.forEach((el) => document.body.removeChild(el));
this.cueElements.clear();
}
}
Motor Accessibility
Input Remapping
export class InputRemapper {
private remap: Record<string, string> = {};
private accessibility: AccessibilityService;
private originalBindings: Record<string, string>;
constructor(accessibility: AccessibilityService, originalBindings: Record<string, string>) {
this.accessibility = accessibility;
this.originalBindings = originalBindings;
this.loadRemap();
}
private loadRemap(): void {
const saved = this.accessibility.get('remapControls') as Record<string, string>;
this.remap = saved ?? {};
}
public getAction(key: string): string {
for (const [action, boundKey] of Object.entries(this.originalBindings)) {
if (this.remap[action]) {
if (this.remap[action] === key) return action;
} else if (boundKey === key) {
return action;
}
}
return key;
}
public remapAction(action: string, newKey: string): void {
this.remap[action] = newKey;
this.accessibility.set('remapControls', this.remap);
}
public getBinding(action: string): string {
return this.remap[action] ?? this.originalBindings[action] ?? '';
}
public resetToDefaults(): void {
this.remap = {};
this.accessibility.set('remapControls', {});
}
public getAllBindings(): Record<string, string> {
const bindings: Record<string, string> = {};
for (const action of Object.keys(this.originalBindings)) {
bindings[action] = this.getBinding(action);
}
return bindings;
}
}
Auto-Fire System
export class AutoFireSystem {
private isEnabled = false;
private fireInterval: number | null = null;
private fireRate = 200;
private shootCallback: (() => void) | null = null;
private accessibility: AccessibilityService;
constructor(accessibility: AccessibilityService) {
this.accessibility = accessibility;
this.applySettings();
}
public applySettings(): void {
this.isEnabled = this.accessibility.get('autoFire') as boolean;
this.fireRate = this.accessibility.get('autoFireRate') as number;
}
public start(shootCallback: () => void): void {
if (!this.isEnabled) return;
this.shootCallback = shootCallback;
this.fireInterval = window.setInterval(() => {
this.shootCallback?.();
}, this.fireRate);
}
public stop(): void {
if (this.fireInterval !== null) {
clearInterval(this.fireInterval);
this.fireInterval = null;
}
}
public setFireRate(rate: number): void {
this.fireRate = rate;
if (this.fireInterval !== null) {
this.stop();
if (this.shootCallback && this.isEnabled) {
this.start(this.shootCallback);
}
}
}
}
One-Hand Mode
export class OneHandMode {
private isEnabled = false;
constructor(private accessibility: AccessibilityService) {
this.isEnabled = this.accessibility.get('oneHandMode') as boolean;
}
public getControlScheme(): 'joystick' | 'dpad' | 'tilt' {
if (!this.isEnabled) return 'dpad';
return 'joystick';
}
public getButtonLayout(): 'standard' | 'compact' {
return this.isEnabled ? 'compact' : 'standard';
}
public getButtonPositions(): Record<string, { x: number; y: number }> {
if (this.isEnabled) {
return {
jump: { x: 0.85, y: 0.7 },
attack: { x: 0.95, y: 0.55 },
special: { x: 0.85, y: 0.4 },
pause: { x: 0.95, y: 0.2 },
};
}
return {
jump: { x: 0.15, y: 0.7 },
attack: { x: 0.95, y: 0.6 },
special: { x: 0.85, y: 0.75 },
pause: { x: 0.95, y: 0.1 },
};
}
}
Cognitive Accessibility
Difficulty Modes
export interface DifficultySettings {
enemySpeed: number;
enemyHealth: number;
playerHealth: number;
timeLimit: number | null;
checkpoints: boolean;
invincibilityFrames: number;
enemyCount: number;
hintFrequency: number;
tutorialLength: 'full' | 'abbreviated' | 'skip';
}
export const DIFFICULTY_PRESETS: Record<string, DifficultySettings> = {
easy: {
enemySpeed: 0.7,
enemyHealth: 0.7,
playerHealth: 1.5,
timeLimit: null,
checkpoints: true,
invincibilityFrames: 90,
enemyCount: 0.7,
hintFrequency: 1.5,
tutorialLength: 'full',
},
normal: {
enemySpeed: 1.0,
enemyHealth: 1.0,
playerHealth: 1.0,
timeLimit: null,
checkpoints: true,
invincibilityFrames: 60,
enemyCount: 1.0,
hintFrequency: 1.0,
tutorialLength: 'abbreviated',
},
hard: {
enemySpeed: 1.3,
enemyHealth: 1.3,
playerHealth: 0.8,
timeLimit: 180,
checkpoints: false,
invincibilityFrames: 30,
enemyCount: 1.2,
hintFrequency: 0.5,
tutorialLength: 'skip',
},
};
export class DifficultyManager {
private currentDifficulty = 'normal';
constructor(private accessibility: AccessibilityService) {
this.currentDifficulty = this.accessibility.get('difficulty') as string;
}
public getSettings(): DifficultySettings {
return { ...DIFFICULTY_PRESETS[this.currentDifficulty] };
}
public setDifficulty(difficulty: string): void {
if (DIFFICULTY_PRESETS[difficulty]) {
this.currentDifficulty = difficulty;
this.accessibility.set('difficulty', difficulty);
GameEvents.emit(GameEvents.Types.DifficultyChanged, this.getSettings());
}
}
public getCurrentDifficulty(): string {
return this.currentDifficulty;
}
}
Progressive Tutorial System
export class TutorialManager {
private completedSteps: Set<string> = new Set();
private currentStep = 0;
private steps: TutorialStep[] = [];
private accessibility: AccessibilityService;
constructor(accessibility: AccessibilityService) {
this.accessibility = accessibility;
this.loadTutorialLength();
}
private loadTutorialLength(): void {
const length = this.accessibility.get('tutorialLevel') as string;
this.setupSteps(length);
}
private setupSteps(length: string): void {
if (length === 'skip') {
this.steps = [];
return;
}
this.steps = [
{ id: 'movement', title: 'Movement', content: 'Use arrow keys to move', skip: length === 'abbreviated' },
{ id: 'jump', title: 'Jump', content: 'Press Space to jump', skip: length === 'abbreviated' },
{ id: 'attack', title: 'Attack', content: 'Press X to attack enemies', skip: false },
{ id: 'collect', title: 'Collect Items', content: 'Touch gems to collect them', skip: length === 'abbreviated' },
{ id: 'goal', title: 'Reach the Exit', content: 'Reach the door to complete the level', skip: false },
];
}
public start(): void {
this.currentStep = 0;
this.showCurrentStep();
}
public skip(): void {
this.steps.forEach((step) => this.completedSteps.add(step.id));
this.hideTutorial();
GameEvents.emit(GameEvents.Types.TutorialCompleted);
}
private showCurrentStep(): void {
const step = this.steps[this.currentStep];
if (!step || step.skip) {
this.nextStep();
return;
}
TutorialUI.show({
title: step.title,
content: step.content,
onNext: () => this.nextStep(),
onSkip: () => this.skip(),
});
}
private nextStep(): void {
const step = this.steps[this.currentStep];
if (step) this.completedSteps.add(step.id);
this.currentStep++;
if (this.currentStep < this.steps.length) {
this.showCurrentStep();
} else {
this.hideTutorial();
GameEvents.emit(GameEvents.Types.TutorialCompleted);
}
}
public isStepCompleted(stepId: string): boolean {
return this.completedSteps.has(stepId);
}
}
Accessibility Menu UI
Accessibility Settings Screen
export class AccessibilityMenu {
private container!: HTMLElement;
private accessibility: AccessibilityService;
constructor(accessibility: AccessibilityService) {
this.accessibility = accessibility;
}
public show(): void {
this.container = document.createElement('div');
this.container.className = 'accessibility-menu';
this.container.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-y: auto;
font-family: 'Outfit', sans-serif;
`;
this.container.innerHTML = `
<h1 style="color: white; margin-bottom: 20px;">Accessibility Settings</h1>
<div class="settings-sections" style="display: flex; flex-direction: column; gap: 30px; max-width: 600px; width: 100%;">
${this.renderVisualSection()}
${this.renderAuditorySection()}
${this.renderMotorSection()}
${this.renderCognitiveSection()}
</div>
<button class="close-btn" style="margin-top: 20px; padding: 12px 32px; font-size: 18px; background: #00d4ff; color: #0a0e27; border: none; border-radius: 8px; cursor: pointer;">
Save & Close
</button>
`;
document.body.appendChild(this.container);
this.attachEventListeners();
}
private renderVisualSection(): string {
return `
<section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
<h2 style="color: #00d4ff; margin-top: 0;">👁️ Visual</h2>
${this.renderToggle('High Contrast Mode', 'highContrastMode')}
${this.renderSelect('Colorblind Mode', 'colorblindMode', [
{ value: 'none', label: 'None' },
{ value: 'protanopia', label: 'Protanopia (Red-blind)' },
{ value: 'deuteranopia', label: 'Deuteranopia (Green-blind)' },
{ value: 'tritanopia', label: 'Tritanopia (Blue-blind)' },
])}
${this.renderSelect('Text Size', 'textSize', [
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
{ value: 'extra-large', label: 'Extra Large' },
])}
${this.renderToggle('Reduce Motion', 'reduceMotion')}
${this.renderToggle('Reduce Particles', 'reduceParticles')}
</section>
`;
}
private renderAuditorySection(): string {
return `
<section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
<h2 style="color: #00d4ff; margin-top: 0;">🔊 Audio</h2>
${this.renderSelect('Subtitles', 'subtitles', [
{ value: 'none', label: 'Off' },
{ value: 'basic', label: 'Basic' },
{ value: 'full', label: 'Full' },
])}
${this.renderSlider('Subtitle Size', 'subtitleSize', 0.8, 1.5, 0.1)}
${this.renderToggle('Subtitle Background', 'subtitleBackground')}
${this.renderSlider('Master Volume', 'separateVolume.master', 0, 1, 0.1)}
${this.renderSlider('SFX Volume', 'separateVolume.sfx', 0, 1, 0.1)}
</section>
`;
}
private renderMotorSection(): string {
return `
<section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
<h2 style="color: #00d4ff; margin-top: 0;">✋ Motor</h2>
${this.renderToggle('Auto-Fire', 'autoFire')}
${this.renderSlider('Auto-Fire Rate', 'autoFireRate', 100, 500, 50)}
${this.renderToggle('Hold to Shoot (vs Toggle)', 'holdToShoot')}
${this.renderToggle('Auto-Run', 'autoRun')}
${this.renderToggle('Slow Mode (50% Speed)', 'slowMode')}
${this.renderToggle('One-Hand Mode', 'oneHandMode')}
${this.renderSlider('Vibration Intensity', 'vibrationIntensity', 0, 100, 10)}
</section>
`;
}
private renderCognitiveSection(): string {
return `
<section style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px;">
<h2 style="color: #00d4ff; margin-top: 0;">🧠 Cognitive</h2>
${this.renderSelect('Difficulty', 'difficulty', [
{ value: 'easy', label: 'Easy' },
{ value: 'normal', label: 'Normal' },
{ value: 'hard', label: 'Hard' },
])}
${this.renderSelect('Tutorial Level', 'tutorialLevel', [
{ value: 'full', label: 'Full Tutorial' },
{ value: 'abbreviated', label: 'Abbreviated' },
{ value: 'skip', label: 'Skip Tutorial' },
])}
${this.renderToggle('Show Hints', 'showHints')}
${this.renderToggle('Reduce UI Complexity', 'reduceUIComplexity')}
</section>
`;
}
private renderToggle(label: string, key: string): string {
const checked = this.accessibility.get(key as keyof AccessibilitySettings) ? 'checked' : '';
return `
<label style="display: flex; align-items: center; gap: 10px; color: white; margin: 10px 0;">
<input type="checkbox" data-key="${key}" ${checked}
style="width: 20px; height: 20px; cursor: pointer;">
${label}
</label>
`;
}
private renderSelect(label: string, key: string, options: Array<{value: string; label: string}>): string {
const current = this.accessibility.get(key as keyof AccessibilitySettings) as string;
return `
<div style="margin: 10px 0;">
<label style="color: white; display: block; margin-bottom: 5px;">${label}</label>
<select data-key="${key}" style="padding: 8px; border-radius: 4px; background: #141834; color: white; border: 1px solid #2a2f55; width: 100%;">
${options.map(o => `<option value="${o.value}" ${o.value === current ? 'selected' : ''}>${o.label}</option>`).join('')}
</select>
</div>
`;
}
private renderSlider(label: string, key: string, min: number, max: number, step: number): string {
const current = this.accessibility.get(key as keyof AccessibilitySettings) as number;
return `
<div style="margin: 10px 0;">
<label style="color: white; display: block; margin-bottom: 5px;">${label}: <span id="${key}-value">${current}</span></label>
<input type="range" data-key="${key}" min="${min}" max="${max}" step="${step}" value="${current}"
style="width: 100%; cursor: pointer;">
</div>
`;
}
private attachEventListeners(): void {
this.container.querySelectorAll('input[type="checkbox"]').forEach((input) => {
input.addEventListener('change', (e) => {
const key = (e.target as HTMLInputElement).dataset.key!;
this.accessibility.set(key, (e.target as HTMLInputElement).checked);
});
});
this.container.querySelectorAll('select').forEach((select) => {
select.addEventListener('change', (e) => {
const key = (e.target as HTMLSelectElement).dataset.key!;
this.accessibility.set(key, (e.target as HTMLSelectElement).value);
});
});
this.container.querySelectorAll('input[type="range"]').forEach((input) => {
input.addEventListener('input', (e) => {
const key = (e.target as HTMLInputElement).dataset.key!;
const value = parseFloat((e.target as HTMLInputElement).value);
this.accessibility.set(key, value);
const valueDisplay = this.container.querySelector(`#${key}-value`);
if (valueDisplay) valueDisplay.textContent = value.toString();
});
});
this.container.querySelector('.close-btn')?.addEventListener('click', () => this.hide());
}
public hide(): void {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
}
}
Accessibility Testing Checklist
Pre-Release Testing
| # | Test | Tools/Method |
|---|
| 1 | Colorblind simulation | Daltonize extension, color blindness emulators |
| 2 | Keyboard-only navigation | Tab through all elements, no mouse |
| 3 | Screen reader | NVDA (Windows), VoiceOver (macOS), TalkBack (Android) |
| 4 | Contrast ratio | WebAIM Contrast Checker (4.5:1 minimum) |
| 5 | Motor accessibility | Switch control, one hand only |
| 6 | Cognitive load | Timer off, reduced UI, hints on |
| 7 | Seizure safety | Photosensitive epilepsy analysis |
| 8 | Audio-visual sync | Captions match audio |
| 9 | Mobile accessibility | TalkBack, Switch Control |
| 10 | Assistive tech compatibility | Various AT combinations |
Automated Testing
describe('Accessibility', () => {
describe('Color Contrast', () => {
it('should have 4.5:1 contrast for text', () => {
const textColor = hexToRgb(THEME.text);
const bgColor = hexToRgb(THEME.bg);
const ratio = calculateContrastRatio(textColor, bgColor);
expect(ratio).toBeGreaterThanOrEqual(4.5);
});
it('should have 3:1 for UI components', () => {
const uiColor = hexToRgb(THEME.primary);
const bgColor = hexToRgb(THEME.bg);
const ratio = calculateContrastRatio(uiColor, bgColor);
expect(ratio).toBeGreaterThanOrEqual(3);
});
});
describe('Keyboard Navigation', () => {
it('should focus all interactive elements', () => {
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusableElements.forEach((el) => {
el.focus();
expect(document.activeElement).toBe(el);
});
});
it('should have visible focus indicators', () => {
const focusable = document.querySelectorAll('button')[0] as HTMLElement;
focusable.focus();
const style = window.getComputedStyle(focusable);
expect(style.outlineStyle).not.toBe('none');
});
});
describe('Screen Reader', () => {
it('should have alt text for images', () => {
const images = document.querySelectorAll('img');
images.forEach((img) => {
expect(img.getAttribute('alt')).toBeTruthy();
});
});
it('should have aria-labels for icon buttons', () => {
const iconButtons = document.querySelectorAll('button:not(:has-text())');
iconButtons.forEach((btn) => {
expect(btn.getAttribute('aria-label')).toBeTruthy();
});
});
});
});
Common Mistakes
| # | Mistake | Why It Fails | Solution |
|---|
| 1 | Color-only feedback | Colorblind players can't see | Add icons, patterns, text |
| 2 | Audio-only alerts | Deaf players miss content | Visual + audio |
| 3 | Fixed controls | Motor impaired can't play | Full rebinding |
| 4 | No difficulty options | Too hard for some | Easy mode, assists |
| 5 | Flashing > 3/sec | Seizure trigger | Reduce, disable option |
| 6 | No pause | Can't take breaks | Always-pausable |
| 7 | Tiny targets | Motor impaired miss | 48x48px (or 10-15mm) minimum |
| 8 | No high contrast | Low vision struggles | Toggle option |
| 9 | Complex UI | Cognitive impaired overwhelmed | Simplify, reduce elements |
| 10 | No captions | Deaf players miss dialogue | Full caption system |
| 11 | Stat-bloat in skill trees | Choice paralysis, cognitive overload | Limit to game-changing milestones; add search & color-coding. |
| 12 | Heading line-height too loose | Disrupted reading rhythm | Reduce multiplier to 1.1x-1.2x for display text; keep 1.4x-1.5x for body text. |
| 13 | Mobile safe zone violations | UI cut off by notches/finger occlusion | Respect Safe Areas and place controls in bottom corner Thumb Zones. |
Execution Checklist
Visual Accessibility
Auditory Accessibility
Motor Accessibility
Cognitive Accessibility
Testing