miaoda-game-combat2d-phaser
v0.3.3
Published
Phaser 4 Scene host for miaoda-game-combat2d-core with Game Object-bound hitboxes and hurtboxes.
Readme
miaoda-game-combat2d-phaser
Use this Phaser 4 adapter to bind combat boxes to Game Objects, synchronize their world positions, and deliver miaoda-game-combat2d-core hit events during Scene updates.
Install
pnpm add miaoda-game-combat2d-core miaoda-game-combat2d-phaserCreate and bind a combat world
Register Combat2dPlugin as a Scene Plugin and create one world per independent arena:
const combat = this.combat2d.create({ defaultIframes: 6 });
combat.bindHurtbox(enemyHurtbox, enemy);
combat.bindHitbox(swordHitbox, player, { x: 30, y: 0 });
combat.onHit((hit) => applyDamage(hit.target, hit.damage));The adapter reads bound object positions before resolving each Scene update. defaultIframes counts logic frames. Objects with active === false or no Scene are unbound automatically.
Strict fixed-step ownership
The default remains Scene-owned for compatibility. A strict simulation instead selects manual ownership once and advances the combat world in seconds:
const combat = this.combat2d.create({
tickSource: 'manual',
defaultIframes: 6,
});
fixedStepper.advance(frameDeltaSeconds, (dtSeconds) => {
combat.stepSimulation(dtSeconds);
});stepSimulation means one combat logic tick. Combat i-frames and per-frame pierce are deliberately tick-counted, so the selected fixed rate defines their wall-clock duration. Calling it in host mode, after destruction, or with a malformed seconds value throws before positions, i-frames, hit counts, or listeners change. setTicking(false) remains a separate local pause and does not change which driver owns time.
Legacy autoUpdate: false still selects manual ownership, and legacy update(deltaMs) still expects Phaser milliseconds. New coordinator code should prefer tickSource: 'manual' plus stepSimulation(dtSeconds) so the unit and authority are explicit.
Use an external physics broadphase
For large scenes, let Arcade or Matter produce candidate pairs and configure external resolution:
const combat = this.combat2d.create({ resolution: 'external' });
const contacts = collectPhysicsContacts();
// Once after the physics step in each logic frame:
combat.resolveContacts(contacts);External contacts still go through masks, attack deduplication, invincibility frames, and pierce limits. With resolution: 'external', the adapter does not also run its all-pairs geometry scan.
For strict fixed-step physics, combine both options and submit the complete tick batch through the single manual entry:
const combat = this.combat2d.create({
tickSource: 'manual',
resolution: 'external',
});
fixedStepper.advance(frameDeltaSeconds, (dtSeconds) => {
const contacts = stepPhysicsAndCollectContacts(dtSeconds);
combat.stepSimulation(dtSeconds, contacts); // pass [] when this tick had no contacts
});An explicit empty batch matters: it commits a combat tick and ages i-frames exactly once. Omitting the batch in external mode is an actionable error. resolveContacts remains available for existing integrations, but strict coordinators should not split synchronization and combat commit across separate public calls.
Lifecycle and control
combat.setTicking(false); // pause synchronization and resolution
combat.setTicking(true);
combat.remove('sword');
combat.core.resetAttack('swing-42');update receives Phaser milliseconds and ignores non-positive or non-finite values. destroy() is idempotent and terminal: it removes Scene listeners, bindings, callbacks, and boxes. The adapter reports accepted hits; your game decides damage, status effects, knockback, and presentation.
