Skip to main content

Action Battle System

Advanced real-time action combat AI system for RPGJS. The AI controller manages behavior only. All stats, HP, SP, skills, items, classes, and states are configured with the standard RPGJS API.

Features

  • Adventure combat by default: three-hit player combo, charged attack, and dodge invulnerability
  • Reuses the standard RPGJS HUD and graphic-bound entity components
  • Impact visual preset with particles, animated combat typography, and camera shake
  • State machine AI with Idle, Alert, Combat, Flee, and Stunned
  • Multiple enemy types: Aggressive, Defensive, Ranged, Tank, Berserker
  • Attack patterns: Melee, Combo, Charged, Zone, DashAttack
  • Skill support with standard RPGJS skills
  • Dodge and counter-attack behaviors
  • Group behavior and waypoint patrols
  • Knockback driven by weapon configuration
  • Hook system with onBeforeHit and onAfterHit

Installation

Quick Start

When you build an object-based event for a map, type the factory as EventDefinition. The returned object only describes the event behavior. Placement data such as id, x, and y still belongs to the outer maps[].events wrapper.

Factions and targets

BattleAi can target players, other battle events, every combat entity, hostile factions, or an explicit list of factions.
Available target selectors:
  • players: target all players.
  • events: target action-battle events.
  • all: target players and action-battle events.
  • hostile: target entities with a different faction.
  • ["guards", "bandits"]: target only these factions.
  • (context) => boolean: fully custom target rule.
An allied event can share the player faction and attack only monster groups:
Two enemy groups can fight each other:
You can change faction and targets at runtime:
Players keep the previous behavior by default: their action attack targets action-battle events. To enable PvP or teams, set the player target selector and faction on the server:

Enable the module

Register the module on the server:

Adventure player combat

Action Battle now uses the adventure preset by default. Press the action control for a buffered three-hit combo, hold E and release it for a charged attack, and use the normal dash control (Shift by default) to dodge. Damage, charge duration, cooldowns, and invulnerability remain server-authoritative. Use preset: "classic" to retain the previous single-attack behavior and UI:
Every Adventure mechanic can be tuned independently:
Guard is frontal and server-authoritative. A hit received during the opening parry window is cancelled, staggers the attacker, and opens a short empowered counterattack window. Soft targeting only adjusts the attack facing inside the configured cone; it never moves the player or replaces manual direction. Action Battle does not register another player HUD. Keep using the standard RPGJS hud.ce component so HP, SP, face, and level remain consistent with the rest of the game. For enemies, BattleAi reuses the regular server Components.hpBar() and merges it into componentsTop. That native component is positioned from the rendered graphic bounds, not only from the entity hitbox, so it remains above tall or scaled sprites. Generated four-direction Studio spritesheets also exclude transparent frame padding from those bounds. Customize it through presentation.healthBar:
Set presentation.healthBar: false for entities that should not display it. No additional HUD or custom HP-bar component is registered by Action Battle. Mobile games can expose the charged attack through withMobile({ buttons: { heavy: true } }); the heavy button dispatches the same authoritative charge start/release controls as the keyboard.

Studio skills and keyboard shortcuts

Skills loaded from RPGJS Studio can define their complete Action Battle presentation and gameplay contract. The client only requests a skill use; the server verifies that the skill is learned, that SP is available, that its cooldown has elapsed, and that the selected target is valid.
action.visual controls client-only particle presentation and never changes server gameplay:
  • castFx is a short preset attached to the caster.
  • trailFx is a continuous preset attached to the moving projectile.
  • impactFx is a short preset attached to the target on impact.
Each field accepts a CanvasEngine built-in or custom FX preset name. Use "auto" to retain Action Battle defaults and "none" to disable that phase. The legacy visual.fx field remains supported as an impact alias, but impactFx takes precedence. key accepts the same keyboard names as project input controls. If it is omitted, learned skills use the numeric slots 1 through 0. The Adventure preset reserves its charged-attack and guard keys, so Studio reports E and F conflicts without blocking the save. targeting.range is expressed in map tiles. A projectile can override its travel range in pixels; otherwise Action Battle derives it from the targeting range and tile width. When action.mode is projectile and no custom projectile type is supplied, the built-in action-battle-skill CanvasEngine renderer displays projectile.graphic, applies scale, and optionally rotates the graphic along its trajectory. Damage and the impact animation/sound occur only when the authoritative projectile collides. Area masks use # for affected tiles and . for empty tiles. Studio exposes this as a visual grid and canonicalizes legacy binary masks by treating 1 as affected and 0 as empty. Use action.target to select enemy, ally, self, or any. Ranged ally skills and area shapes enter the targeting overlay; a single-target enemy projectile can use the soft target in front of the player. animations is optional. If you omit it, attacks keep using the default attack animation and no extra hurt, death, or skill-cast animation is played. Adventure attacks lock movement and facing through their active frames by default, then allow movement or dodge to cancel recovery. Control locks are leased independently, so a hurt, guard, dodge, or follow-up attack cannot let an older timer restore stale animation or direction flags.
Set lockMovement to false if you want players to keep moving while attacking. The client stops local predicted movement as soon as the action input is pressed and shows a short slash preview by default. Disable showPreview when you provide your own client-side attack effect. For precise attack-by-attack control, use the profile’s control block:
The impact visual preset adds CanvasEngine particles, anchored floating damage typography, screen shake, and a short render-only hit-stop. Accessibility controls can disable these independently without changing server combat:
New action-battle configuration is grouped by responsibility:
  • combat owns gameplay rules: player attack profile, damage, knockback, and hit hooks.
  • visual owns temporary combat feedback: sprite animations, flashes, damage text, CanvasEngine effects, and previews.
  • ui owns client components: action bar, targeting overlay, attack preview, and custom GUI or sprite components.
  • ai owns reusable AI behavior functions.
  • skills.targeting owns action targeting metadata for skills.
Use the same visual preset on the server and client when your project splits configuration by runtime. The server still decides when authoritative hit, hurt, skill, and enemy attack feedback should happen, but it sends one compact action-battle client visual event. The client resolves that visual locally and groups the flash, hit text, sound, component animation, or sprite animation. This keeps gameplay authority on the server while avoiding several visual packets for one combat moment. The client also triggers local input feedback such as the attack preview.
If your game installs action-battle as a combined module, you can import the same helpers from @rpgjs/action-battle.

Visual composition

createActionBattleVisual() accepts a preset or a map of visual parts. A visual part is a function receiving the current combat context and helper methods.
Available presets:
The impact preset selects an official CanvasEngine preset from the combat context: hitSpark for normal hits, slashSpark for combo finishers, impactBurst for charged or critical attacks, magicBurst for skills, and healPulse for healing. Its world-space damage popup changes font size, color, outline, movement, and duration for the same contexts. The built-in component animations are registered as action-battle-hit-fx and action-battle-damage. A skill or weapon action can override its impact presentation with serializable metadata:
These hints only affect the client presentation. Damage, SP consumption, targeting, and hit resolution remain server-authoritative. You can also call any component animation registered by your own client modules with fx.component(entity, id, params). Action-battle visual composition runs through the general Client Visuals mechanism for server-triggered combat feedback. Use direct server visual APIs for isolated one-off effects, and use visual here when you want action-battle to group combat presentation on the client. The legacy animations option still works for sprite animation names and temporary graphics. New orchestration should go through visual, while animations remains useful as the data source used by fx.graphic(entity, "attack"), fx.graphic(entity, "hurt"), and fx.graphic(entity, "castSkill").

Composable UI

Action Battle uses RPGJS’s generic, server-authoritative hotbar. Enable it on the server with ui.hotbar; targeting and attack-preview components remain client-owned and replaceable.
The client can still customize targeting and preview presentation:
The generic hotbar uses LT/RT to cycle through visible slots and LB to open the radial selector. Projects with a custom gamepad layout can override the standard Gamepad API button indexes:
Number keys use their assigned slot immediately. On a standard gamepad, LT/RT change the active slot and X uses it. Instant skills soft-target in front of the player; melee area skills keep the manual targeting confirmation. Shortcuts are accepted:

Compatibility

The legacy attack, systems.combat, systems.ai, and skills.getTargeting options are still supported. New code should prefer combat, ai, and skills.targeting so each part of the action-battle module stays independently replaceable.

Attack profile model

Use attack.profile to describe the timing model of a player attack in one typed object. A profile separates the attack into startup, active, and recovery phases so combat systems can share the same vocabulary.
The default profile mirrors the legacy attack lock: no startup, a short active window, and recovery that totals 350ms. The player attack runtime uses startupMs before creating the hitbox, activeMs to keep the hitbox active, and totalDurationMs for movement and direction locks. Weapons can override the player attack profile from their database entry:

Skill and weapon actions

Skills and weapons can define an action block for action-battle selection, while their effect stays automatic by default. BattleAi evaluates every learned skill and uses attackSkill as a priority hint. A skill that is cooling down, too expensive, out of range, or unable to cover the target does not block a normal attack. Player hotbar skills, enemy skills, and configured equipped weapons use the same executor, so onUse receives the same context in every case.
With no onUse, action-battle applies the standard RPGJS skill effect: SP cost, hit rate, states, and damage formulas. For weapons, the default effect is a physical hit using the equipped weapon stats and action-battle hit hooks. action.target can be "enemy", "ally", "self", or "any"; enemy resolution uses the attacker’s action-battle faction and targets selector. Projectile direction uses the same generic projectile options as map.projectiles.emit(), including spreadDegrees and accuracy. Use onUse(user, target, ctx) only when the action needs custom logic:
For full custom actions, skip ctx.defaultEffect():
Weapons use the same model:

Plugin-first extension points

Action battle is structured as replaceable systems. You can keep the default Zelda-like sword attack and only replace the pieces your game needs.
The public extension contracts are exported from @rpgjs/action-battle/server: ActionBattleCombatSystem, ActionBattleAiBehavior, ActionBattleHitHooks, and ActionBattleHitResult. For data-driven enemies, use createActionEnemy():
When the action targets a normal event with no BattleAi, the server lets the event handle onAction and does not create the combat hitbox. Enemy events with BattleAi still trigger the A-RPG attack.

Configure stats with the standard RPGJS API

The AI uses the event’s existing data.

Health and resources

Parameters

Skills

Items and equipment

Classes

States

AI configuration

All AI options are optional:
Per-enemy animations override the global provideActionBattle() animations. Use a string for a simple animation name, an object to temporarily switch graphics, or a resolver function for data-driven events. Return null or undefined from a resolver to skip the animation. attackProfiles lets enemies telegraph attacks with startupMs, keep hitboxes active for activeMs, and apply hit reactions. poise controls interruption: an incoming hit only stuns the enemy when its reaction.staggerPower is greater than or equal to the enemy’s poise. rewards are awarded once to the player who lands the killing blow. On defeat, Action Battle calls event.remove({ reason: "defeated", transition }). The server removes collision immediately while clients keep the sprite long enough to play its Studio die animation and a configurable CanvasEngine death effect. Use presentation.death to tune the effect, scale, shake, and duration, or set it to false for immediate removal. The legacy onDefeated(event, attacker) signature remains supported for two-argument callbacks. When combat spritesheets come from RPGJS Studio media fields, convert the media ids with createStudioActionBattleAnimations(). Studio-generated combat spritesheets are played with setGraphicAnimation("attack", graphic, 1) by default:
Without arguments, the helper reads the Studio project animations attached to the player at runtime by provideStudioGame(). You can still pass a static object when you want to override the media ids manually. Animation values may be media ids or media objects returned by the Studio game API. Studio four-direction attack spritesheets play in 350ms by default so their visual timing matches the default Adventure attack profile without changing the walk animation speed. A media record may override this client-side presentation with metadata.attackDurationMs; gameplay startup, active, and recovery windows remain server-authoritative and independent. For Studio enemies, the runtime reads enemy.animations automatically when an enemy is created from the Studio database. The supported Studio fields are attack, hurt, die, castSpell, guard, parry, and stagger; castSkill is also accepted when you configure action-battle directly. stagger falls back to the Studio hurt animation. These values are resolved on the server and sent to the client as plain animation data, so media IDs remain usable without transferring resolver functions.

Combat sounds and dynamic music

Action Battle can play local action cues and crossfade the current map BGM into a looping battle track:
A cue accepts one sound id, several variants, or { id, volume, cooldownMs }. Basic attacks are predicted locally and suppress the matching authoritative cue for 250ms. A skill’s own sound field takes priority over the generic skill cue. Cue cooldowns prevent repeated area hits from producing an unusable wall of sound. Combat threat is authoritative on the server and isolated per player. Alert, combat, fleeing, and stunned enemies keep that player in combat. On equal priority the currently selected enemy stays stable; bosses default to priority 100 and ordinary enemies to 0. Configure an enemy-specific track with:
Track precedence is enemy, map, project/default, then silence. The ambient sound layer is never stopped. With RPGJS Studio, use the combined preset on both client and server:
Studio reads project combatAudio, map combatMusic, and enemy combatMusic plus combatMusicPriority. createStudioActionBattleAudio(config) can provide the same settings statically before these fields are available in a project. Studio media ids are resolved lazily; resolving a new effect does not stop music that is already playing. The Adventure AI director limits how many enemies attack one target simultaneously. Other enemies keep repositioning instead of stacking the same attack:

Composable AI behaviors

Action Battle has three AI layers. They all run on the server and end in the same authoritative runtime for movement, attacks, skills, cooldowns, hit reactions, and rewards.
  • Presets are the fastest path for common enemies.
  • Simplified behaviors are readable rule lists that return intentions.
  • Behavior trees are the advanced API for bosses and custom enemy logic.

Presets

Use a built-in preset and override only what changes:
Built-in presets are aggressive, defensive, ranged, tank, and berserker. A preset is only behavior configuration. Stats still come from the event itself. Register project presets through provideActionBattle():
Then instantiate enemies from the preset:
Presets are composable. A project preset can extend a built-in preset or another project preset with preset: "name". Local options passed to new BattleAi() override the preset values.

Simplified behaviors

Use simpleBehavior when you want expressive rules without writing a full behavior tree. Each rule checks a condition and returns an intent:
Common condition helpers: Common intention helpers:

Behavior trees

Use behaviorTree when you need explicit tree control. A tree node returns success, failure, or running, optionally with an intent or decision.
The built-in tree helpers are intentionally small:

Dynamic behavior and memory

Intent functions receive the AI context and can use memory for per-enemy state:
The context includes:

Boss phases and delayed sequences

Phase helpers keep scripted behavior in the existing behavior-tree runtime. Their state is stored per AI instance in memory, so a preset can safely be shared by several enemies:
phase(key, hpRatio, action) completes once after HP falls below the ratio. once(key, action) provides the same one-time behavior without an HP condition. cooldown(key, ms, action) starts its cooldown only after the wrapped action succeeds. sequenceWithDelay(key, steps) advances across AI ticks, and wait(ms) uses the authoritative server clock without creating client-owned gameplay timers. Use run(callback) when project logic is local to the tree. For reusable or module-provided behavior, register a named action:
callAction() returns failure when its name is not registered, allowing a selector to continue to a fallback branch.

Server-driven AI visuals

visual() sends a JSON-shaped cue through the existing Action Battle client visual packet. The server decides when it happens; the client handler only renders it:
Unknown visual kinds are ignored. Keep cue payloads serializable and use once() or cooldown() around cues selected by a tree branch so they are not sent on every 100 ms AI tick. Visual handlers must never apply damage, change stats, select targets, or otherwise own gameplay state.

Movement and server actions

The advanced intent helpers are thin wrappers over the controlled event: Teleport helpers deliberately use the normal RPGJS teleport primitive and do not search for a collision-free position. Projects that need safe placement should resolve it in run() or a registered action before calling teleportTo().

Sample project

samples/sample-dev contains four AI demo enemies on center-map:
  • Preset Rusher uses a named preset.
  • Simple Kiter uses simpleBehavior and distance control.
  • Tree Elite uses a direct behaviorTree.
  • Phase Boss uses delayed phases, generic visuals, speed changes, teleportation, and a registered action.

Enemy types

Enemy types affect behavior, not stats:

Attack patterns

Use skills for attacks

All skills learned by the enemy are candidates. attackSkill makes one skill the preferred opener without excluding the others. Melee skills require contact, projectiles use their configured travel range, and instant area skills use targeting.range plus aoeMask. While no action is ready, the enemy approaches, retreats, or strafes toward the useful range of its next action. Self-targeted healing and support skills are used automatically below 60% HP. Ally-targeted skills are outside the current automatic planner.

Debug enemy decisions

Decision tracing is disabled by default. Enable it on the authoritative server and optionally filter one enemy or a set of categories:
decision logs include the distance, global cooldown, evaluated skills, effective ranges, rejection reasons, and the selected attack or repositioning request. Common rejection reasons are cooldown, insufficientSp, outOfRange, invalidTarget, maskMiss, and notUseful. For increasing levels of control, start with learned skills only, add attackSkill for an explicit priority, use defineActionBattleAiPreset() for reusable typed defaults, then use behaviorKey or a behavior tree for fully custom decisions.

Examples

Basic enemy

Mage with skills

Patrol guard

Player combat

The module handles player attacks via the action input:

Knockback system

Knockback force is driven by the equipped weapon’s knockbackForce property: