Files
Skills/haxeflixel/SKILL.md
T

593 lines
26 KiB
Markdown
Raw Normal View History

2026-03-31 19:19:46 +02:00
---
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