plugins/game-dev/skills/pixi-vector-arcade/SKILL.md
Bootstrap browser-based games with PixiJS 8 and a modern retro/vector aesthetic (Geometry Wars, Asteroids, Tempest, Tron). Use when creating a new game, starting a browser game project, building an arcade game, prototyping a game, setting up PixiJS, or when the user mentions vector graphics, neon aesthetics, or arcade-style gameplay. Provides project scaffolding, ECS-lite architecture, performance patterns (pooling, spatial hashing, fixed timestep), and visual design system.
npx skillsauth add rbergman/dark-matter-marketplace pixi-vector-arcadeInstall 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.
Purpose: Scaffold browser-based games with PixiJS 8 and modern retro/vector aesthetics. Produces architecture that handles 500+ entities at 60fps with no memory growth over 30+ minute sessions.
Activate this skill when:
Keywords: game, arcade, retro, vector, pixijs, pixi, ecs, prototype, browser game, neon, glow
{
"dependencies": {
"pixi.js": "^8.6.6",
"@pixi/layout": "^2.0.0",
"stats.js": "^0.17.0"
},
"devDependencies": {
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0",
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
"eslint": "^9.18.0",
"vite": "^6.0.7",
"vitest": "^3.0.4",
"prettier": "^3.4.0",
"lint-staged": "^15.4.0"
}
}
// Application init is now async
const app = new Application();
await app.init({
resizeTo: window,
backgroundColor: 0x000000,
preference: 'webgpu', // Falls back to WebGL
});
// Graphics API is chainable
const g = new Graphics()
.rect(0, 0, 100, 50)
.fill({ color: 0xff0000 })
.stroke({ width: 2, color: 0xffffff });
// ParticleContainer uses Particle objects, not Sprites
const particles = new ParticleContainer({
dynamicProperties: {
position: true,
scale: true,
rotation: false,
tint: false,
alpha: true,
},
});
const particle = new Particle({ texture, anchorX: 0.5, anchorY: 0.5 });
particles.addParticle(particle);
When querying up-to-date PixiJS docs, use library IDs:
/pixijs/pixijs/v8_12_0/llmstxt/pixijs_llms-full_txtproject/
├── src/
│ ├── index.ts # Entry point, app bootstrap
│ ├── game.ts # Game class - orchestrates everything
│ │
│ ├── core/ # Engine-level systems (game-agnostic)
│ │ ├── clock.ts # Adjustable game timer
│ │ ├── ecs.ts # Entity manager, component arrays
│ │ ├── pool.ts # Generic object pooling
│ │ ├── spatial-hash.ts # Collision broadphase
│ │ └── input.ts # Keyboard/mouse state
│ │
│ ├── components/ # Pure data (no logic)
│ │ ├── transform.ts # Position, rotation, scale
│ │ ├── velocity.ts # Linear + angular velocity
│ │ ├── collider.ts # Radius, collision mask/layer
│ │ ├── health.ts # HP, max HP, invincibility
│ │ ├── lifetime.ts # TTL for projectiles, particles
│ │ └── renderable.ts # Graphics reference, layer
│ │
│ ├── systems/ # Logic that operates on components
│ │ ├── physics.ts # Velocity → position, wrapping
│ │ ├── collision.ts # Spatial hash queries, response
│ │ ├── render.ts # Sync components → PIXI graphics
│ │ └── [game-specific] # Weapon, enemy AI, etc.
│ │
│ ├── data/ # Content definitions (pure data)
│ │ └── config.ts # Tuning constants
│ │
│ ├── rendering/ # PIXI-specific
│ │ ├── layers.ts # Container hierarchy
│ │ ├── viewport.ts # Full viewport scaling
│ │ ├── particles.ts # ParticleContainer system
│ │ ├── design-system.ts # Colors, visual constants
│ │ └── shaders/ # Custom GLSL effects
│ │
│ ├── ui/ # HUD, menus
│ │ └── hud.ts
│ │
│ ├── debug/ # Dev tools
│ │ └── stats.ts # Performance monitor
│ │
│ └── types/ # Type declarations
│ └── stats.js.d.ts
│
├── references/ # Specs, original designs
├── docs/plans/ # Architecture docs
├── history/ # Ephemeral scratch (gitignored)
└── [config files]
Independent of framerate and wall clock. Supports pause, slow-mo, frame stepping.
interface Clock {
elapsed: number; // Total game time (scaled)
delta: number; // Fixed tick duration (1/60)
scale: number; // 1.0 = normal, 0 = paused
wallDelta: number; // Real time (for UI animations)
wallElapsed: number;
}
interface ClockController extends Clock {
pause(): void;
resume(): void;
setScale(scale: number): void;
step(delta: number): void; // Advance one tick (debugging)
}
Physics at fixed 60Hz. Render interpolates for smooth visuals.
const TICK_RATE = 60;
const TICK_DURATION = 1 / TICK_RATE;
let accumulator = 0;
app.ticker.add(() => {
const wallDelta = app.ticker.deltaMS / 1000;
accumulator += wallDelta * clock.scale;
// Cap to prevent spiral of death
accumulator = Math.min(accumulator, TICK_DURATION * 5);
while (accumulator >= TICK_DURATION) {
clock.delta = TICK_DURATION;
clock.elapsed += TICK_DURATION;
// Systems update in deterministic order
inputSystem.update();
physicsSystem.update();
collisionSystem.update();
// ... more systems
world.flush(); // Apply deferred destructions
accumulator -= TICK_DURATION;
}
// Render with interpolation
const alpha = accumulator / TICK_DURATION;
renderSystem.update(alpha);
});
Entities are numbers. Components are typed maps. Zero allocation in hot paths.
type Entity = number;
class ComponentArray<T> {
private readonly data = new Map<Entity, T>();
set(entity: Entity, component: T): void { ... }
get(entity: Entity): T | undefined { ... }
has(entity: Entity): boolean { ... }
remove(entity: Entity): void { ... }
*entries(): IterableIterator<[Entity, T]> { yield* this.data.entries(); }
}
class World {
private nextId = 0;
private readonly alive = new Set<Entity>();
private readonly toDestroy: Entity[] = []; // Deferred
// Component arrays
readonly transform = new ComponentArray<Transform>();
readonly velocity = new ComponentArray<Velocity>();
// ...
// Type markers (for fast iteration)
readonly asteroids = new Set<Entity>();
readonly projectiles = new Set<Entity>();
// ...
spawn(): Entity { ... }
destroy(entity: Entity): void { this.toDestroy.push(entity); }
flush(): void { /* Actually remove */ }
}
Critical for preventing GC during gameplay.
interface Pool<T> {
acquire(): T;
release(item: T): void;
prewarm(count: number): void;
readonly activeCount: number;
readonly pooledCount: number;
}
class ObjectPool<T> implements Pool<T> {
constructor(
private factory: () => T,
private reset: (item: T) => void,
private dispose?: (item: T) => void
) {}
// ...
}
Rules:
reset() hides but doesn't destroy (for Graphics: set visible=false, move offscreen)destroy() only on full shutdownO(n) collision instead of O(n²).
interface SpatialHash {
cellSize: number;
clear(): void;
insert(entity: Entity, x: number, y: number, radius: number): void;
queryRadius(x: number, y: number, radius: number): readonly Entity[];
}
These rules prevent crashes from naive implementations. See game-perf skill for full details.
// ❌ BAD - creates new array every frame
const nearby = entities.filter(e => e.active);
// ✅ GOOD - reuse scratch array
scratchArray.length = 0;
for (const e of entities) {
if (e.active) scratchArray.push(e);
}
Avoid in hot-path systems: filter(), map(), reduce(), spread operator ([...arr]), object literals in loops.
// ❌ BAD - modifies collection during iteration
for (const e of world.asteroids) {
if (dead) world.asteroids.delete(e);
}
// ✅ GOOD - defer destruction
world.destroy(e); // Marks for deletion
// ... after all systems ...
world.flush(); // Actually removes
// ❌ BAD - creates Graphics during gameplay
const explosion = new Graphics();
effectLayer.addChild(explosion);
// ✅ GOOD - acquire from pool, never destroy during gameplay
const explosion = pools.effects.acquire();
explosion.visible = true;
// ... on done ...
pools.effects.release(explosion);
Skip rendering for off-screen entities. Critical when world is larger than viewport.
function isInViewport(x: number, y: number, radius: number, viewport: Rectangle): boolean {
return x + radius > viewport.x &&
x - radius < viewport.x + viewport.width &&
y + radius > viewport.y &&
y - radius < viewport.y + viewport.height;
}
// In render system
for (const [entity, transform] of world.transform.entries()) {
const renderable = world.renderable.get(entity);
if (!renderable) continue;
// Cull off-screen entities
const inView = isInViewport(transform.x, transform.y, renderable.radius, viewport);
renderable.graphics.visible = inView;
if (inView) {
renderable.graphics.position.set(transform.x, transform.y);
renderable.graphics.rotation = transform.rotation;
}
}
Rules:
visible = false for culled objects (GPU skips them entirely)For detailed design guidance, consult references/visual-design.md.
| Principle | Rule | |-----------|------| | Glow | Everything emits light. Lines are energy, not matter. | | Contrast | Pure black void vs vivid neon. No middle ground. | | Motion | Trails, particles, pulses. Nothing is static. | | Hierarchy | Brightness = importance. Player brightest, debris dimmest. | | Feedback | Every action has visible consequence. |
const Colors = {
background: 0x000000,
playerCore: 0xffffff,
playerGlow: 0x00ffff,
hazardPrimary: 0xffaa00,
enemyPrimary: 0xff0044,
rewardCore: 0x00ff00,
uiPrimary: 0x00ffff,
};
const LineWeight = {
hairline: 1,
thin: 1.5,
normal: 2,
bold: 3,
heavy: 4,
};
Apply BlurFilter per-layer (not per-object):
layer.filters = [new BlurFilter({ strength: 4, quality: 2 })];
Play area expands with viewport (not letterboxed):
const MIN_WIDTH = 1200;
const MIN_HEIGHT = 800;
const MAX_ASPECT_RATIO = 21 / 9;
const MIN_ASPECT_RATIO = 4 / 3;
Canvas UI is inherently "raw"—you're building from primitives. Use the right tool for each UI type:
| UI Type | Approach | Rationale | |---------|----------|-----------| | In-game HUD (score, lives, health) | @pixi/ui + @pixi/layout | Benefits from canvas effects (glow, shake) | | Pause screen | @pixi/ui | Keeps visual consistency with game | | Settings/keybinds menu | HTML/DOM overlay | Forms are easier in DOM | | Dev tools | Tweakpane | Already in stack, tree-shakes from prod |
Use @pixi/layout (Yoga-powered flexbox) for positioning:
import { Layout } from '@pixi/layout';
import { FancyButton, ProgressBar } from '@pixi/ui';
const hud = new Layout({
id: 'hud',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
padding: 20,
width: viewport.width,
maxWidth: 1400, // Prevents ultra-wide spreading
});
hud.addChild(scoreText, healthBar, pauseButton);
layers.ui.addChild(hud);
Create a centralized theme to avoid repetitive styling:
// src/ui/theme.ts
export const UITheme = {
colors: {
primary: 0x00ffff,
danger: 0xff0044,
success: 0x00ff00,
neutral: 0x888888,
},
fonts: {
hud: { fontFamily: 'monospace', fontSize: 24, fill: 0x00ffff },
title: { fontFamily: 'monospace', fontSize: 48, fill: 0xffffff },
},
button: {
padding: 12,
borderRadius: 4,
borderWidth: 2,
},
} as const;
Build reusable UI factories to reduce repetition:
// src/ui/factory.ts
import { FancyButton, ProgressBar } from '@pixi/ui';
import { Graphics, Text } from 'pixi.js';
import { UITheme } from './theme';
export function createButton(
label: string,
onClick: () => void,
variant: 'primary' | 'danger' = 'primary'
): FancyButton {
const color = UITheme.colors[variant];
const { padding, borderRadius, borderWidth } = UITheme.button;
const makeView = (fill: number, stroke: number) =>
new Graphics()
.roundRect(0, 0, 120, 40, borderRadius)
.fill(fill)
.stroke({ width: borderWidth, color: stroke });
return new FancyButton({
defaultView: makeView(0x000000, color),
hoverView: makeView(color, color),
pressedView: makeView(color, 0xffffff),
text: new Text({ text: label, style: UITheme.fonts.hud }),
anchor: 0.5,
}).on('pointerup', onClick);
}
export function createHealthBar(maxHealth: number): ProgressBar {
return new ProgressBar({
bg: new Graphics().roundRect(0, 0, 200, 20, 4).fill(0x222222),
fill: new Graphics().roundRect(0, 0, 200, 20, 4).fill(UITheme.colors.success),
progress: 100,
});
}
export function createScoreText(): Text {
return new Text({ text: 'SCORE: 0', style: UITheme.fonts.hud });
}
For settings, keybinds, or form-heavy UI, use HTML overlay:
// In your HTML
// <div id="dom-ui" class="hidden">
// <div id="settings-menu">...</div>
// </div>
class Game {
private domUI = document.getElementById('dom-ui')!;
showSettings(): void {
this.pause();
this.domUI.classList.remove('hidden');
}
hideSettings(): void {
this.domUI.classList.add('hidden');
this.resume();
}
}
#dom-ui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
#dom-ui > * {
pointer-events: auto;
}
#dom-ui.hidden {
display: none;
}
DOM overlay caveats:
| Component | Use Case |
|-----------|----------|
| FancyButton | Buttons with hover/pressed states |
| Button | Simple button |
| CheckBox | Toggle settings |
| Slider / DoubleSlider | Volume, sensitivity |
| Input | Text entry (limited—consider DOM for complex forms) |
| ScrollBox | Scrollable lists (high scores, inventory) |
| Select | Dropdowns |
| ProgressBar / CircularProgressBar | Health, loading |
| RadioGroup | Mutually exclusive options |
| List | Arranged child elements |
Strict rules that cannot be disabled via eslint-disable comments. See references/eslint-config.md for full configuration.
Key rules:
complexity: 10 maxmax-lines-per-function: 60max-lines: 400 per file@typescript-eslint/no-explicit-any: error// Toggle with Cmd+Shift+S (Mac) or Ctrl+Shift+S (Windows)
const stats = createStatsMonitor();
stats.begin();
// ... update ...
stats.end();
clock.pause(); // Freeze game
clock.setScale(0.2); // 20% speed (slow-mo)
clock.step(1/60); // Advance one frame
When creating a new game project:
The architecture is game-agnostic—the same foundation works for shooters, survivors, puzzle games, or anything with real-time gameplay.
For dev tools (Tweakpane, stats), pause system, keybind settings, lifecycle management (Disposable pattern), and DOM overlay patterns, see references/dev-tools-ui.md.
Additional dependencies: @pixi/ui (runtime), tweakpane (devDependency, tree-shakes from prod).
development
Initialize a new repository with standard scaffolding - git, gitignore, AGENTS.md, justfile, mise, beads, and timbers. Use when starting a new project or setting up an existing repo for Claude Code workflows.
data-ai
Activate at session start when using Agent Teams for complex multi-agent work. Establishes team lead role with delegation protocols, teammate spawning, model selection, and beads integration. You coordinate the team; teammates implement.
data-ai
Use when creating a worktree, setting up a worktree, starting feature work that needs isolation, or before executing implementation plans. Covers git worktree creation under .worktrees/, gitignore setup, beads integration, and merge guardrails.
data-ai
Activate when you are a delegated subagent (not the orchestrator). Establishes subagent protocol with terse returns, details to history/, file ownership boundaries, and escalation rules. You implement; orchestrator reviews and commits.