npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

miaoda-game-brawler-core

v0.3.2

Published

Engine-agnostic beat-'em-up combat brain: an attack-string runner with startup/active/recovery frame windows and cancel-buffered combos (it says when to enable a combat2d hitbox), an accumulated-knockback hurt/knockdown state machine (light hits stagger,

Readme

miaoda-game-brawler-core

Engine-independent, frame-based beat-'em-up combat flow: attack timing and combos, accumulated stagger/knockdown, and shared enemy attack slots. Use it above miaoda-game-combat2d-core, which still decides whether active hitboxes connect.

pnpm add miaoda-game-brawler-core

Combo timing

const runner = new ComboRunner([jab, cross, heavy]);
if (attackPressed) runner.press();
const frame = runner.tick();
hitbox.enabled = frame.hitboxActive;
if (frame.justActivated) combat.addHitbox({attackId: frame.attackId, ...box});

Each attack uses startup, active, and recovery frames. Inputs inside its cancel window advance the combo, with a configurable frame buffer. Each active window receives a unique attackId so combat hit deduplication resets correctly.

Hurt flow and crowd pacing

const hurt = new HurtState({knockdownThreshold: 40, hurtFrames: 12, downFrames: 40, getupFrames: 16});
const hit = hurt.hit(event.damageKnockback);
if (hit.knockedDown) body.push(directionX * 200, 0, 320);
hurt.tick();

const tokens = new AttackTokenPool(2);
if (tokens.tryAcquire(enemy.id)) enterAttack();
tokens.release(enemy.id, 30);
tokens.tick();

Knockback accumulates and decays between hits; crossing the threshold enters knockdown, then down and get-up frames. superArmor suppresses ordinary stagger but not threshold knockdown. Attack tokens cap simultaneous enemy commitments and use frame-based cooldowns.

ComboRunner, HurtState, and AttackTokenPool expose detached snapshots of all timing state. This package does not detect hits, apply HP damage, move bodies, steer enemies, or render animations.

Coordinated belt crowd

BrawlerCrowdCoordinator owns tactical roles, slot leases, and attack eligibility. It is measured in fixed simulation ticks; the game supplies candidate world positions and validity, then applies each returned target through its steering/movement owner.

import { BrawlerCrowdCoordinator, classicBeltCrowdPreset } from 'miaoda-game-brawler-core';

const crowd = new BrawlerCrowdCoordinator(classicBeltCrowdPreset({ maxAttackers: 2 }));
const result = crowd.update({
  target: { x: player.x, depth: player.y },
  enemies: enemies.map((enemy) => ({ id: enemy.id, position: { x: enemy.x, depth: enemy.y } })),
  slots: authoredSlots.map((slot) => ({
    slotId: slot.id,
    world: { x: player.x + slot.offset.x, depth: player.y + slot.offset.depth },
    valid: !wallQuery.blocks(slot),
  })),
});
for (const assignment of result.assignments) {
  steering.setTarget(assignment.enemyId, assignment.target);
  ai.setRole(assignment.enemyId, assignment.role, assignment.attackEligible);
}
for (const waiting of result.waiting) telemetry.waiting(waiting.enemyId, waiting.code);

Assignments and diagnostics are detached. The coordinator does not move actors, query walls, run FSMs, or resolve attacks. Its attack eligibility is the single slot-level permission for this coordinated crowd; do not combine it with an unrelated second attack-slot authority. Use HurtState for simple stagger/knockdown, and the advanced reaction resolver when airborne or bounded-juggle rules are needed.

Use crowd.inspect() for detached runtime telemetry (assignments plus the latest valid/occupied/unassigned diagnostics). Use crowd.snapshot only for persistence; diagnostics are intentionally excluded from save data.

Atomic grapples and throws

GrappleCoordinator owns the only authoritative holder/victim relationship. Actors remain free of writable isGrabbing flags; the host presents returned phases and applies the detached throw outcome to movement and combat systems.

import { GrappleCoordinator, classicBeltGrapplePreset } from 'miaoda-game-brawler-core';

const grapples = new GrappleCoordinator();
const move = classicBeltGrapplePreset();
const begin = grapples.tryBegin({
  holder: { id: player.id, position: player.ground, grabForce: player.grabForce },
  victim: { id: enemy.id, position: enemy.ground, canBeGrabbed: !enemy.invulnerable },
  definition: move,
  alignment: arena.canAlign(player, enemy, move.desiredSpacing),
});
if (begin.ok) {
  host.presentGrapple(begin.session);
  for (const event of begin.events) host.presentGrappleEvent(event);
  const strikeEvents = grapples.requestStrike(begin.session.id, `${player.id}:grab-strike`);
  strikeEvents.forEach(host.presentGrappleEvent);
  grapples.requestThrow(begin.session.id).forEach(host.presentGrappleEvent);
  for (const event of grapples.advance(begin.session.id, move)) {
    if (event.type === 'thrown') {
      stats.applyDamage(event.outcome.victimId, event.outcome.damage);
      beltBody(event.outcome.victimId).push(event.outcome.velocity.x, event.outcome.velocity.depth, event.outcome.velocity.height);
    }
    host.presentGrappleEvent(event);
  }
} else {
  telemetry.grappleRejected(begin.code, begin.guidance);
}

Call advance from the fixed simulation owner, not from animation completion. The coordinator validates competition, range, eligibility, and host alignment atomically; it does not move engine nodes, apply HP, create hitboxes, or play animation. release/removeActor releases both participants together and is idempotent. Snapshots contain session IDs and logical state only; restore with the same static definitions and present the returned semantic events yourself.

Advanced reactions and bounded juggling

Use HitReactionResolver instead of HurtState when hits need airborne follow-ups, bounce limits, downed-hit rules, or shared co-op juggle budgets. One resolve call checks and consumes resources atomically.

import { HitReactionResolver, classicBrawlerReactionPreset } from 'miaoda-game-brawler-core';

const reactions = new HitReactionResolver(classicBrawlerReactionPreset({
  scope: 'team', maxJuggle: 8, maxGroundBounces: 1, maxWallBounces: 0,
}));
const decision = reactions.resolve({
  victimId: enemy.id,
  attackerId: player.id,
  teamId: player.teamId,
  bodyPhase: enemy.body.z > 0 ? 'airborne' : enemy.downed ? 'downed' : 'grounded',
  invulnerable: enemy.invulnerable,
  grappled: grapples.inspect().some((session) => session.victimId === enemy.id),
  groundBounceAvailable: arena.hasFloorBelow(enemy),
  request: {
    requestedPhase: attack.launch ? 'launch' : 'stagger',
    hitstunTicks: attack.hitstunTicks,
    velocity: attack.velocity,
    juggleCost: attack.juggleCost,
    canHitDowned: attack.canHitDowned,
    tags: attack.reactionTags,
  },
});
if (decision.accepted) {
  stats.applyDamage(enemy.id, attack.damage);
  const velocity = decision.outcome!.velocity;
  enemy.body.push(velocity.x, velocity.depth, velocity.height);
  animation.play(decision.outcome!.animationTag);
} else telemetry.reactionRejected(enemy.id, decision.code, decision.guidance);

Returned decisions are detached. Apply damage and BeltBody.push exactly once in the host. Never check juggle eligibility and subtract it in separate calls. Reset a victim after the configured clean landing/get-up boundary with reactions.reset(victimId). Do not apply both HurtState and HitReactionResolver to the same hit: choose the simple posture model or the advanced reaction model per actor/profile.

All authoritative actor, slot, session, cooldown, and juggle-entry ordering uses locale-independent UTF-16 code-unit order. Juggle victim/source identities use collision-free tuple keys, so IDs containing punctuation or control characters remain independent. Hosts should still use stable IDs across save and replay.

Canonical fixed-tick composition

The fixed-step owner calls the pure systems in this order. The host owns the bridge objects and applies each detached result once.

fixedStep.step(1, ({ dt }) => {
  input.commit();
  const crowdResult = crowd.update({ target: player.ground, enemies: enemyInputs(), slots: hostSlotCandidates() });
  applyCrowdTargets(crowdResult.assignments); // steering only; no body integration here

  for (const request of actionRequests()) actionRuntime.request(request.id);
  const actionFrame = actionRuntime.tick();
  applyActionFrame(actionFrame); // timeline/tags, not animation completion

  for (const event of grapples.tick((session) => grappleDefinition(session.moveId))) present(event);
  combatField.updateAll(bodyPositions());
  for (const hit of combatField.resolve(iframes)) {
    const decision = reactionProfile(hit.target).resolve(reactionContext(hit));
    if (decision.accepted) applyDamageAndVelocity(hit, decision.outcome!);
  }
  for (const body of bodies) body.step(dt);
});

fixedStep is the only frame scheduler. Crowd coordination chooses targets, grapple coordination owns paired relationships, combat produces raw contacts, reaction resolution chooses one semantic result, and BeltBody integrates motion. The engine adapter renders after this tick and never makes animation completion authoritative.

Avoid these compositions: assigning slots independently inside every enemy FSM; using a crowd attack permit plus an unrelated second attack permit; storing writable isGrabbing and isGrabbed flags; moving engine nodes from grapple callbacks; checking and consuming juggle budget in separate calls; applying both HurtState and the advanced resolver to one hit; or ticking these systems once per rendered frame when a fixed-step owner is required.