diff --git a/haxeflixel/SKILL.md b/haxeflixel/SKILL.md new file mode 100644 index 0000000..084079d --- /dev/null +++ b/haxeflixel/SKILL.md @@ -0,0 +1,592 @@ +--- +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` 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` 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 FlxTypedSignalVoid>(); // 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, `FlxTypedSignalVoid>` 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` 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(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(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 = 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` 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` 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