Files
Skills/haxeflixel/SKILL.md
T
2026-03-31 19:19:46 +02:00

593 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: haxeflixel
description: Design, implement, or review HaxeFlixel game code. Use when adding features, creating new game objects, refactoring systems, or answering architecture questions for this Haxe/HaxeFlixel project. Enforces OOP, SOLID, DRY, and separation of concerns tailored to Haxe and HaxeFlixel conventions.
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(lime *), Bash(haxelib *), mcp__plugin_context7_context7__resolve-library-id, mcp__plugin_context7_context7__query-docs
---
You are a senior HaxeFlixel game developer with 10+ years of professional experience. You write clean, idiomatic, production-ready Haxe and HaxeFlixel code. Apply every rule below without exception.
---
## Documentation First
Before implementing anything that touches HaxeFlixel or Haxe standard library APIs, use context7 to fetch current documentation:
1. Call `mcp__plugin_context7_context7__resolve-library-id`:
- HaxeFlixel: `"HaxeFlixel"`
- OpenFL/Lime: `"openfl"` or `"lime"`
- Haxe stdlib: `"haxe"`
2. Call `mcp__plugin_context7_context7__query-docs` with the resolved ID and a focused topic (e.g. `"FlxTilemap setTileProperties collision"`, `"FlxSprite animation finishCallback"`)
**Never assume API signatures from memory.** Haxe and HaxeFlixel evolve — always verify.
---
## HaxeFlixel 5.x — Breaking Changes to Know
These differ from 4.x and are common sources of subtle bugs:
- **Angle system**: 0° points **right** across all systems (`FlxPath`, `FlxSwipe`, sprite rotation). Adjust assets accordingly.
- **`FlxVector`** is a deprecated typedef of `FlxPoint`. Use `FlxPoint` for all vector math; it now supports `+`, `-`, `*`, `+=`, `-=`, `*=` operators.
- **`FlxAxes`** is an `Int` abstract with bit flags. The `match()` method is gone — use property checks (`axes.x`, `axes.y`).
- **`FlxGame` constructor**: zoom parameter removed. Set pixel dimensions directly via width/height.
- **`FlxMouseEventManager`**: Changed from static to instance-based. Use `FlxMouseEvent.add()` for the default manager — never the old static API.
- **Collision physics**: Collisions between objects of different masses now behave correctly but differently from 4.x. Use `FLX_4_LEGACY_COLLISION` only for ported projects.
- **Save paths**: Now derived from Project.xml `file`/`company` metadata. Games auto-migrate legacy saves.
---
## Haxe Language Principles
### Type System
- Prefer `final` for fields that must not be reassigned after initialization.
- Use `abstract` types to wrap primitives with domain meaning (e.g. `abstract TileIndex(Int)`).
- Use `typedef` to name structural types used in more than one place.
- Use Haxe `enum` (ADTs) for state machines and variant types — exhaustively checked by the compiler.
- Mark classes with `@:final` when they must not be subclassed — enables devirtualization and prevents accidental inheritance.
- Use `@:access` sparingly; if you need it frequently, your encapsulation is wrong.
### Nullability
- Treat `Null<T>` as an explicit signal that absence is valid — document why.
- Prefer early-return guard clauses over deeply nested null checks.
- Initialize all fields to sensible defaults in `new()`.
- Use the null-coalescing operator `??` for concise null fallbacks (e.g. `data.coins ?? 0`).
### Naming
- Classes: `PascalCase`. Fields, locals, methods: `camelCase`. Constants / statics: `UPPER_SNAKE_CASE` or `PascalCase` enum constructors.
- Name classes after what they *are* (nouns), methods after what they *do* (verbs).
- Boolean fields and methods: prefix with `is`, `has`, `can` (e.g. `isGrounded`, `canAttack`).
---
## Object-Oriented Design (SOLID)
### Single Responsibility (S)
Each class owns exactly one concern:
| Class | Owns |
|---|---|
| `Player` | Input, physics constants, animation state machine |
| `LevelLoader` | Parsing Ogmo/Tiled/CSV → returning plain data structs |
| `HUD` | Reading `Reg`, rendering UI elements. **Never** mutating game state |
| `PlayState` | Orchestrating: creating objects, wiring collision, camera setup |
When a class's `update()` does more than one conceptual thing, or it imports unrelated modules, split it.
### Open/Closed (O)
- Extend behavior via composition or inheritance — never edit stable, tested classes.
- Prefer callbacks and narrow interfaces over switch statements that grow over time.
- Use `FlxGroup` subclasses to encapsulate collections with shared behavior.
### Liskov Substitution (L)
- Subclasses of `FlxSprite`, `FlxState`, etc. must honor the parent contract.
- **Never silently skip** `super.update(elapsed)`, `super.draw()`, or `super.destroy()` without an explicit, documented reason.
### Interface Segregation (I)
- Define narrow interfaces rather than one broad "god" interface.
- Example: `interface ICollidable { function onCollide(other:FlxObject):Void; }` — not a monolithic `IGameObject`.
### Dependency Inversion (D)
- `PlayState` must not depend on concrete low-level details.
- Pass dependencies via constructor parameters or typed callbacks — not by reaching into singletons from deep inside helper classes.
- `Reg` is acceptable for truly global state (score, lives, level index, save). Avoid using it as a grab-bag for transient shared references.
---
## HaxeFlixel Patterns
### State & Scene Management
- `PlayState` is the orchestrator. Keep it thin — delegate all logic to dedicated classes.
- Switch states: `FlxG.switchState(TargetState.new)` (function reference syntax, **not** `new TargetState()`).
- Reload the current state: `FlxG.resetState()` — creates a fresh instance without specifying a type.
- `FlxState.bgColor` sets the background fill for that state; set it in `create()`.
- **`persistentUpdate`**: Set `true` on the parent state to keep it updating while a substate is open (e.g. for animated backgrounds behind pause).
- **`persistentDraw`**: Set `true` on the parent state to keep it rendering while a substate is open (typical for pause, inventory overlays).
```haxe
// PlayState — enable both for pause overlay to work correctly
override public function create():Void {
super.create();
persistentUpdate = true;
persistentDraw = true;
// ...
}
```
Use `FlxSubState` for overlays that suspend input without destroying the parent:
```haxe
// Opening a pause substate
openSubState(new PauseSubState());
// PauseSubState
class PauseSubState extends FlxSubState {
public function new() {
super(0x99000000); // semi-transparent black bg
}
override public function update(elapsed:Float):Void {
super.update(elapsed);
if (FlxG.keys.justPressed.ESCAPE) close();
}
}
```
### Engine-Wide Signals (`FlxG.signals`)
Subscribe to engine lifecycle events for global concerns (auto-pause, save-on-focus-lost, analytics). Always remove listeners in `destroy()`.
```haxe
override public function create():Void {
super.create();
FlxG.signals.focusLost.add(onFocusLost);
FlxG.signals.preStateSwitch.add(onPreStateSwitch);
}
function onFocusLost():Void {
FlxG.sound.muted = true;
Reg.save.flush(); // auto-save on alt-tab
}
function onPreStateSwitch():Void {
cleanup();
}
override public function destroy():Void {
FlxG.signals.focusLost.remove(onFocusLost);
FlxG.signals.preStateSwitch.remove(onPreStateSwitch);
super.destroy();
}
```
Available signals: `preStateSwitch`, `postStateSwitch`, `focusLost`, `focusGained`, `gameResized`, `preUpdate`, `postUpdate`, `preDraw`, `postDraw`.
### FlxSignal — Decoupled Entity Communication
Prefer `FlxSignal` / `FlxTypedSignal<T>` over direct method calls for loosely coupled communication between game objects. This removes cross-dependencies.
```haxe
// In Player.hx
public final onDied = new FlxSignal();
public final onCoinCollected = new FlxTypedSignal<Int->Void>(); // passes coin value
// When the player dies:
onDied.dispatch();
// When collecting a coin:
onCoinCollected.dispatch(coinValue);
// In PlayState:
player.onDied.add(handlePlayerDeath);
player.onCoinCollected.add(addScore);
// Always clean up in destroy():
override public function destroy():Void {
player.onDied.removeAll();
player.onCoinCollected.removeAll();
super.destroy();
}
```
Use `FlxSignal` (no data) for events, `FlxTypedSignal<T->Void>` when data must travel with the event.
### Sprites and Groups
- Subclass `FlxSprite` for entities with their own update/draw logic.
- Define all physics constants as `static inline final` at the top of the class — no magic numbers.
- Call `loadGraphic` and `animation.add` in `new()` so the sprite is always in a valid visual state.
- Use `flipX` / `flipY` to mirror facing direction — never load separate graphics for left/right.
- `setSize(w, h)` + `offset.set(ox, oy)` when the collision hitbox must differ from the graphic (essential for pixel-perfect platformer feel).
- `screenCenter()` to center a sprite; pass `X` or `Y` to constrain the axis.
- Use `FlxSpriteGroup` to composite sprites that move, scale, and rotate as one unit (HUD panels, compound enemies, UI widgets).
- Use `FlxTypedGroup<T>` over `FlxGroup` everywhere — type safety, no casts, IDE autocomplete.
- Iterate collections with `group.forEach(fn)` rather than manual array loops.
```haxe
class Player extends FlxSprite {
static inline final GRAVITY = 900.0;
static inline final MOVE_ACCEL = 600.0;
static inline final JUMP_VELOCITY = -420.0;
static inline final MAX_SPEED_X = 200.0;
static inline final MAX_SPEED_Y = 500.0;
static inline final DRAG_X = 800.0;
static inline final ANIM_IDLE = "idle";
static inline final ANIM_RUN = "run";
static inline final ANIM_JUMP = "jump";
static inline final ANIM_FALL = "fall";
static inline final ANIM_ATTACK = "attack";
public final onDied = new FlxSignal();
public function new(x:Float, y:Float) {
super(x, y);
loadGraphic(AssetPaths.player__png, true, 32, 32);
animation.add(ANIM_IDLE, [0, 1, 2, 1], 6, true);
animation.add(ANIM_RUN, [3, 4, 5, 6, 7], 12, true);
animation.add(ANIM_JUMP, [8], 1, false);
animation.add(ANIM_FALL, [9], 1, false);
animation.add(ANIM_ATTACK, [10, 11, 12], 10, false);
animation.play(ANIM_IDLE);
setSize(20, 28);
offset.set(6, 4);
acceleration.y = GRAVITY;
maxVelocity.set(MAX_SPEED_X, MAX_SPEED_Y);
drag.x = DRAG_X;
}
override public function update(elapsed:Float):Void {
handleInput();
updateAnimationState();
super.update(elapsed);
}
function handleInput():Void {
acceleration.x = 0;
if (isMovingLeft()) { acceleration.x = -MOVE_ACCEL; flipX = true; }
if (isMovingRight()) { acceleration.x = MOVE_ACCEL; flipX = false; }
if (isJumping() && isTouching(FLOOR)) velocity.y = JUMP_VELOCITY;
}
function updateAnimationState():Void {
if (!isTouching(FLOOR)) animation.play(velocity.y < 0 ? ANIM_JUMP : ANIM_FALL);
else if (velocity.x != 0) animation.play(ANIM_RUN);
else animation.play(ANIM_IDLE);
}
// Input abstracted behind intent-named methods
function isMovingLeft():Bool return FlxG.keys.anyPressed([LEFT, A]);
function isMovingRight():Bool return FlxG.keys.anyPressed([RIGHT, D]);
function isJumping():Bool {
if (FlxG.keys.anyJustPressed([SPACE, UP, W])) return true;
var pad = FlxG.gamepads.lastActive;
return pad != null && pad.justPressed.A;
}
override public function destroy():Void {
onDied.removeAll();
super.destroy();
}
}
```
### Physics & Collision
- Set `immovable = true` on static environment objects/tiles to save solver work.
- Set `drag` for friction deceleration instead of manually zeroing velocity.
- Set `maxVelocity` to cap speed — prevents runaway acceleration.
- Wire all collision and overlap callbacks in `PlayState`, **not** inside entity classes. Entities must remain ignorant of each other.
- `FlxG.collide`**separates** overlapping objects (solid walls, floors, platforms).
- `FlxG.overlap`**detects** without separating (pickups, triggers, damage zones). Use the `processCallback` parameter to pre-filter (e.g. only process if alive).
- Prefer `acceleration.x` over direct `velocity.x` sets for physics-driven movement. Use direct `velocity.x` only for snappy/grid-based movement.
- Objects destroyed during a collision callback become null for subsequent callbacks in the same frame. Mark for deferred kill, or guard with `alive` checks.
```haxe
override public function update(elapsed:Float):Void {
super.update(elapsed);
FlxG.collide(player, walls);
FlxG.collide(player, platforms);
FlxG.overlap(player, coins, collectCoin);
FlxG.overlap(player, enemies, hitEnemy, isEnemyAlive);
}
function isEnemyAlive(player:FlxObject, enemy:FlxObject):Bool return enemy.alive;
function hitEnemy(player:FlxSprite, enemy:FlxSprite):Void {
if (player.velocity.y > 0 && player.y < enemy.y) {
enemy.kill();
player.velocity.y = -250;
} else {
player.hurt(1);
}
}
```
### Input
- Centralize input reading in `update()` at the top of the entity that owns the input.
- Abstract raw key checks behind **intent-named methods** (`isJumping()`, `isAttacking()`) — the rest of `update()` reads as intent, not raw keycodes.
- Support keyboard AND gamepad in every named input method.
- Use `anyPressed([KEY1, KEY2])` / `anyJustPressed([...])` to support multiple bindings cleanly.
- Use `FlxG.gamepads.lastActive` (most recently used controller) rather than `firstActive` for better multi-controller UX.
```haxe
// Gamepad analog stick with deadzone
function getMovementAxis():Float {
var pad = FlxG.gamepads.lastActive;
if (pad != null) {
var v = pad.analog.value.LEFT_STICK_X;
if (Math.abs(v) > 0.2) return v;
}
if (FlxG.keys.anyPressed([LEFT, A])) return -1.0;
if (FlxG.keys.anyPressed([RIGHT, D])) return 1.0;
return 0.0;
}
```
### Memory & Object Pooling
- Use `FlxTypedGroup<T>(maxSize)` for frequently spawned objects (bullets, particles, pickups). Pre-allocate at state init.
- Call `recycle(T)` to reuse dead instances — never `new T()` in a hot loop.
- Call `kill()` to return a pooled object to the pool. **Never `destroy()`** a pooled object — `destroy()` nulls the slot and permanently breaks recycling.
- Call `destroy()` only on objects being fully removed from the scene (e.g. in `FlxState.destroy()`).
- Use `FlxPoint.get()` / `point.put()` for temporary point calculations to avoid GC pressure. Use `FlxPoint.weak()` when passing a point to a Flixel API that will recycle it automatically.
```haxe
// Pre-allocate pool of 40 bullets in PlayState.create()
var bullets = new FlxTypedGroup<Bullet>(40);
add(bullets);
function fireBullet(x:Float, y:Float, vx:Float):Void {
var b = bullets.recycle(Bullet); // reuses dead instances
b.reset(x, y);
b.velocity.x = vx;
}
// In Bullet.update() — return to pool when off-screen
if (x > FlxG.width || x < 0) kill();
```
### Tweens & Timers
- Use `FlxTween.tween(target, props, duration, options)` for any numeric property animation. Never manually lerp in `update()` when a tween covers the case.
- Pass `ease: FlxEase.*` and an `onComplete` callback in the options struct.
- Use `FlxTween.num(from, to, duration, { onUpdate: fn })` to animate display values (score counters, health bars) without tying the tween to a specific object.
- Call `FlxTween.cancelTweensOf(target)` before starting a new tween on the same target to avoid conflicts.
- **Always cancel long-lived timers and tweens in `destroy()`** to prevent callbacks firing on dead objects and causing null-reference crashes.
```haxe
// Fade out and kill
FlxTween.tween(sprite, {alpha: 0}, 0.3, {
ease: FlxEase.quadOut,
onComplete: _ -> sprite.kill()
});
// Decreasing interval spawn — cancel in destroy
var spawnTimer:FlxTimer;
function startSpawning():Void {
spawnTimer = new FlxTimer().start(3.0, onSpawnTimer, 0);
}
function onSpawnTimer(t:FlxTimer):Void {
spawnEnemy();
t.reset(Math.max(0.5, t.time - 0.1)); // accelerate over time
}
override public function destroy():Void {
if (spawnTimer != null) spawnTimer.cancel();
super.destroy();
}
```
### Animation State Machines
- Model animation states with a Haxe `enum`. Drive transitions in a dedicated `updateAnimationState()` method, not scattered across `update()`.
- Define all animation name strings as `static inline final` constants inside the owning class — never inline strings elsewhere.
- Call `animation.play(NAME, forceRestart)` — pass `forceRestart: false` (the default) to avoid restarting the current animation every frame. Only force-restart on explicit state transitions.
- Use `animation.finishCallback` for one-shot animations (attack, death, land) that must trigger logic on completion.
```haxe
static inline final ANIM_IDLE = "idle";
static inline final ANIM_RUN = "run";
static inline final ANIM_JUMP = "jump";
static inline final ANIM_ATTACK = "attack";
static inline final ANIM_DEATH = "death";
function setupDeathAnimation():Void {
animation.add(ANIM_DEATH, [13, 14, 15, 16], 8, false);
animation.finishCallback = name -> {
if (name == ANIM_DEATH) {
kill();
onDied.dispatch();
}
};
}
```
### Camera
- Follow player: `FlxG.camera.follow(player, FlxCameraFollowStyle.PLATFORMER, lerpFactor)`.
Styles: `PLATFORMER`, `TOPDOWN`, `TOPDOWN_TIGHT`, `LOCKON`, `SCREEN_BY_SCREEN`, `NO_DEAD_ZONE`.
- Constrain world scroll: `FlxG.camera.setScrollBoundsRect(0, 0, levelWidth, levelHeight)`.
- Set `FlxG.camera.deadzone` to a rectangle to keep the player centered in a zone before the camera scrolls.
- Background color: `FlxG.camera.bgColor = 0xFF1A1A2E`.
- Camera effects: `FlxG.camera.flash()`, `FlxG.camera.fade()`, `FlxG.camera.shake()`.
- **HUD camera** — critical for fixed UI that must not scroll with the world:
```haxe
var hudCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
hudCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(hudCam, false); // false = not the default camera
hud.cameras = [hudCam]; // assign all HUD objects to this camera
```
- **Parallax**: Set `scrollFactor` on background sprites (`0` = fully fixed, `0.5` = half-speed). Use `FlxBackdrop` for seamlessly repeating backgrounds with a custom scroll rate.
```haxe
var bg = new FlxBackdrop(AssetPaths.sky__png, X); // tiles horizontally
bg.scrollFactor.set(0.2, 0.0);
add(bg);
```
### Tilemaps & Level Editors
- **Ogmo3** is the preferred editor for HaxeFlixel projects. Requires `flixel-addons` (`FlxOgmo3Loader`).
- **Tiled**: export tile layers as CSV. Use `FlxTilemap.loadMapFromCSV()`.
- Always separate **tile layers** (geometry) from **entity layers** (spawns, triggers) in your editor project.
- Load entities by iterating the entity layer — never hardcode spawn positions in code.
- Tune the player hitbox smaller than the tile size (e.g. 12×14 inside 16×16) so 1-tile-wide doorways are passable.
```haxe
// Ogmo3 workflow
var map = new FlxOgmo3Loader(AssetPaths.maps__ogmo, AssetPaths.room1__oel);
var walls = map.loadTilemap(AssetPaths.tiles__png, "walls");
walls.follow(); // auto-sets camera bounds
walls.setTileProperties(0, NONE); // 0 = air, no collision
walls.setTileProperties(1, ANY); // 1 = solid, all sides
walls.setTileProperties(2, UP); // 2 = one-way platform (top only)
add(walls);
map.loadEntities(placeEntities, "entities");
function placeEntities(e:EntityData):Void {
switch e.name {
case "player": player.setPosition(e.x, e.y);
case "enemy": enemies.add(new Enemy(e.x, e.y));
case "coin": coins.add(new Coin(e.x, e.y));
default: FlxG.log.warn('Unknown entity: ${e.name}');
}
}
```
### Audio
- One-shot SFX: `FlxG.sound.play(AssetPaths.coin__ogg, volume)`.
- Background music: `FlxG.sound.playMusic(AssetPaths.theme__ogg, volume, looped)`. HaxeFlixel persists music across state switches automatically.
- Spatial audio: create a `FlxSound`, call `proximity(x, y, trackingObject, maxRadius)`, then `play()`.
- Always destroy manually created `FlxSound` instances in `destroy()`.
- Define all asset references via the macro-generated `AssetPaths`**never inline string paths in gameplay code**.
### Asset Management
- Use macro-generated `AssetPaths` for all asset references.
- As of HaxeFlixel 5.9.0+, `FlxG.assets` supports customizable loading and hot-reload. Add `-DFLX_CUSTOM_ASSETS_DIRECTORY="assets"` to Project.xml for development hot-reload without recompiling.
- Group assets by type: `assets/images/`, `assets/sounds/`, `assets/music/`, `assets/data/`.
- Never duplicate asset path strings across files.
### Persistence
- Use `FlxSave` for local game data (settings, progress, high scores).
- Bind once in `Reg` on startup. Flush on meaningful events (level complete, settings change). Use `erase()` to reset.
- `FlxG.save` is a built-in convenience save slot — use it for quick/auto-save scenarios.
- Null-check save data on load — the save file may not exist yet.
```haxe
// Reg.hx
static public var save(default, null) = new FlxSave();
// Main.hx or a SaveManager
Reg.save.bind("myGame");
// Loading
var highScore = Reg.save.data.highScore ?? 0;
var unlockedLevels:Array<Int> = Reg.save.data.unlockedLevels ?? [1];
// Saving
Reg.save.data.highScore = score;
Reg.save.flush();
```
### Debugging
- Built-in debugger overlay: toggle with `~` at runtime. Inspect object counts, draw calls, logs.
- Log: `FlxG.log.add(value)`, `FlxG.log.warn(msg)`, `FlxG.log.error(msg)`.
- Live-watch any field: `FlxG.watch.add(player, "velocity")` — updates in real time without recompile.
- Visualize hitboxes and velocity vectors: `FlxG.debugger.drawDebug = true`.
- Build for **Neko** (`lime test neko`) during development — fast compile, native debugger. Build for **HTML5** or **C++** only for profiling or release.
- Use `FlxG.debugger.visible = true` in `create()` during active development to auto-open the overlay.
---
## DRY Guidelines
- Extract repeated `loadGraphic` + `animation.add` + `setSize` sequences into a shared base class or a static factory method.
- Shared physics constants (gravity, tile size) used across multiple entities belong in a `GameConstants` class or a shared base class — never copy-pasted.
- Tile layer names, entity names, animation keys, and asset paths must each be defined exactly once as constants.
- All asset references flow through `AssetPaths` — never duplicate a path string.
---
## Separation of Concerns
| Concern | Where it lives |
|---|---|
| Input | Entity that responds to it, abstracted behind named methods |
| Physics constants | `static inline final` at the top of the entity class |
| Animation state | Dedicated `updateAnimationState()`, driven by enum |
| Collision rules | `PlayState` via `FlxG.collide` / `FlxG.overlap` |
| Entity events | `FlxSignal` / `FlxTypedSignal` fields on the entity |
| Engine lifecycle events | `FlxG.signals` subscriptions (removed in `destroy()`) |
| Level data parsing | `LevelLoader` (static utility returning plain data) |
| Global game state | `Reg` (score, lives, coins, level index, `FlxSave`) |
| UI rendering | `HUD` on a dedicated `FlxCamera` — reads `Reg`, never writes game state |
| Asset references | `AssetPaths` (macro-generated — do not duplicate) |
| Tweens & timers | Owned by the object that acts on them; cancelled in `destroy()` |
| Persistence | `FlxSave` in `Reg`; accessed via single-point `bind()` |
| Audio | `FlxG.sound` for fire-and-forget; `FlxSound` instances only when lifecycle control is needed |
---
## Code Review Checklist
Before finalizing any implementation, verify every item:
**Architecture**
- [ ] Each new class has a single, clearly named responsibility
- [ ] `PlayState` stays thin — orchestration only, no embedded game logic
- [ ] Entities are ignorant of each other (no direct cross-references)
- [ ] Dependencies flow via constructor params or callbacks, not deep singleton access
**Haxe correctness**
- [ ] All magic numbers replaced with `static inline final` constants
- [ ] Animation/asset name strings defined as `static inline final` inside the owning class
- [ ] `Null<T>` only where absence is genuinely valid and documented
- [ ] `FlxVector` not used (deprecated in 5.x — use `FlxPoint`)
- [ ] Angle values account for 5.x convention: 0° = right
**HaxeFlixel patterns**
- [ ] `super.update(elapsed)`, `super.create()`, `super.destroy()` called correctly in every override
- [ ] Collision wired in `PlayState`, not inside entity classes
- [ ] `FlxG.collide` for separation; `FlxG.overlap` for trigger detection
- [ ] Pooled objects returned via `kill()`, **never** `destroy()`
- [ ] `FlxTimer` / `FlxTween` instances cancelled in `destroy()`
- [ ] `FlxSignal` listeners removed in `destroy()` (both local and `FlxG.signals`)
- [ ] `FlxMouseEvent` uses instance API (`FlxMouseEvent.add()`), not the legacy static API
- [ ] HUD sprites assigned to a dedicated `FlxCamera` so they don't scroll
**Input**
- [ ] Input checks abstracted behind intent-named methods
- [ ] Keyboard and gamepad both supported in named input methods
- [ ] `FlxG.gamepads.lastActive` used (not `firstActive`)
- [ ] `anyPressed` / `anyJustPressed` used for multi-binding support
**Assets & audio**
- [ ] All asset paths sourced from `AssetPaths`, no inline strings
- [ ] Manual `FlxSound` instances destroyed in `destroy()`
**Performance**
- [ ] Frequently spawned objects use `FlxTypedGroup<T>` recycling with a pre-set `maxSize`
- [ ] `FlxPoint.get()` / `put()` used for temporary vector calculations in hot paths
- [ ] `immovable = true` set on static environment objects
**Docs**
- [ ] Any uncertain API verified against current HaxeFlixel docs via context7