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

26 KiB
Raw Blame History

name, description, allowed-tools
name description allowed-tools
haxeflixel 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. 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).
// 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:

// 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().

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.

// 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.
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.collideseparates overlapping objects (solid walls, floors, platforms).
  • FlxG.overlapdetects 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.
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.
// 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.
// 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.
// 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.
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:
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.
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.
// 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 AssetPathsnever 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.
// 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