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