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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rpgjs/action-battle

v5.0.0-alpha.29

Published

RPGJS is a framework for creating RPG/MMORPG games

Readme

Action Battle System

Advanced real-time action combat AI system for RPGJS.

The AI controller manages behavior only - all stats (HP, ATK, skills, items, etc.) are configured using the standard RPGJS API.

Features

  • State Machine AI: Enemies with dynamic behaviors (Idle, Alert, Combat, Flee, Stunned)
  • Multiple Enemy Types: Aggressive, Defensive, Ranged, Tank, Berserker
  • Attack Patterns: Melee, Combo, Charged, Zone, Dash Attack
  • Skill Support: AI can use any RPGJS skill
  • Dodge System: Enemies can dodge and counter-attack
  • Group Behavior: Enemies coordinate attacks and formations
  • Patrol System: Waypoint-based patrolling
  • Knockback System: Weapon-based knockback force
  • Hook System: Customize hit behavior with onBeforeHit and onAfterHit hooks

Installation

npm install @rpgjs/action-battle

Quick Start

import { createServer, RpgPlayer, RpgEvent, EventMode, ATK, PDEF, MAXHP } from "@rpgjs/server";
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";

function GoblinEnemy() {
  return {
    name: "Goblin",
    mode: EventMode.Scenario,
    onInit() {
      this.setGraphic("goblin");
      
      // Configure stats using RPGJS API
      this.hp = 80;
      this.param[MAXHP] = 80;
      this.param[ATK] = 15;
      this.param[PDEF] = 5;
      
      // Optional: Give skills
      // this.learnSkill(Slash);
      
      // Optional: Give items
      // this.addItem(Potion, 2);
      
      // Apply AI behavior
      new BattleAi(this, {
        enemyType: EnemyType.Aggressive
      });
    }
  };
}

Using RPGJS API for Stats

The AI uses the event's existing stats. Configure them in onInit:

Health & Resources

this.hp = 100;           // Current HP
this.param[MAXHP] = 100; // Max HP
this.sp = 50;            // SP for skills
this.param[MAXSP] = 50;  // Max SP

Parameters

import { ATK, PDEF, SDEF } from "@rpgjs/server";

this.param[ATK] = 20;   // Attack power
this.param[PDEF] = 10;  // Physical defense
this.param[SDEF] = 8;   // Special defense

Skills

import { Fireball, Heal } from './database/skills';

this.learnSkill(Fireball);
this.learnSkill(Heal);

Items & Equipment

import { Sword, Shield, Potion } from './database/items';

this.addItem(Potion, 3);
this.equip(Sword);
this.equip(Shield);

Classes

import { WarriorClass } from './database/classes';

this.setClass(WarriorClass);

States

import { PoisonState } from './database/states';

this.addState(PoisonState);

AI Configuration

The AI only controls behavior. All options are optional:

new BattleAi(event, {
  // Enemy type (affects behavior, not stats)
  enemyType: EnemyType.Aggressive,
  
  // Skill to use for attacks (optional)
  attackSkill: Fireball,
  
  // Timing
  attackCooldown: 1000,  // ms between attacks
  
  // Ranges
  visionRange: 150,      // Detection radius
  attackRange: 60,       // Attack distance
  
  // Dodge behavior
  dodgeChance: 0.2,      // 0-1 probability
  dodgeCooldown: 2000,   // ms between dodges
  
  // Flee behavior
  fleeThreshold: 0.2,    // Flee when HP < 20%
  
  // Attack patterns
  attackPatterns: [
    AttackPattern.Melee,
    AttackPattern.Combo,
    AttackPattern.DashAttack
  ],
  
  // Patrol waypoints (for idle state)
  patrolWaypoints: [
    { x: 100, y: 100 },
    { x: 300, y: 100 }
  ],
  
  // Group coordination
  groupBehavior: true,
  
  // Callback when AI is defeated
  onDefeated: (event) => {
    console.log(`${event.name()} was defeated!`);
  }
});

Enemy Types

Types modify AI behavior (cooldowns, ranges, dodge), not stats:

| Type | Attack Speed | Dodge | Behavior | |------|-------------|-------|----------| | Aggressive | Fast | Low | Rushes player | | Defensive | Slow | High | Counter-attacks | | Ranged | Medium | Medium | Keeps distance | | Tank | Slow | None | Stands ground | | Berserker | Variable | Low | Faster when hurt |

Using Skills for Attacks

The AI can use any RPGJS skill:

// In your database/skills.ts
import { Skill } from '@rpgjs/database';

@Skill({
  name: 'Slash',
  spCost: 5,
  power: 25,
  hitRate: 0.95
})
export class Slash {}

// In your event
onInit() {
  this.hp = 100;
  this.sp = 50;
  this.learnSkill(Slash);
  
  new BattleAi(this, {
    attackSkill: Slash
  });
}

AI States

┌─────────┐     detect      ┌─────────┐    approach    ┌─────────┐
│  Idle   │ ──────────────> │  Alert  │ ─────────────> │ Combat  │
└─────────┘                 └─────────┘                └─────────┘
     ^                                                      │
     │                                                      │
     │              ┌─────────┐                            │
     │              │ Stunned │ <────── take damage ───────┤
     │              └─────────┘                            │
     │                   │                                  │
     │                   v                                  │
     │              ┌─────────┐                            │
     └───────────── │  Flee   │ <────── HP low ────────────┘
                    └─────────┘

Attack Patterns

| Pattern | Description | |---------|-------------| | Melee | Single attack | | Combo | 2-3 rapid attacks | | Charged | Wind-up, stronger attack | | Zone | 360° area attack | | DashAttack | Rush toward target then attack |

Examples

Basic Enemy

function Goblin() {
  return {
    name: "Goblin",
    onInit() {
      this.setGraphic("goblin");
      this.hp = 50;
      this.param[MAXHP] = 50;
      this.param[ATK] = 10;
      
      new BattleAi(this);
    }
  };
}

Mage with Skills

function DarkMage() {
  return {
    name: "Dark Mage",
    onInit() {
      this.setGraphic("mage");
      this.hp = 60;
      this.sp = 100;
      this.param[MAXHP] = 60;
      this.param[MAXSP] = 100;
      this.param[ATK] = 25;
      
      this.learnSkill(Fireball);
      this.learnSkill(IceSpike);
      
      new BattleAi(this, {
        enemyType: EnemyType.Ranged,
        attackSkill: Fireball,
        visionRange: 200
      });
    }
  };
}

Boss

function DragonBoss() {
  return {
    name: "Dragon",
    onInit() {
      this.setGraphic("dragon");
      this.hp = 500;
      this.param[MAXHP] = 500;
      this.param[ATK] = 50;
      this.param[PDEF] = 30;
      
      this.learnSkill(FireBreath);
      this.learnSkill(TailSwipe);
      
      new BattleAi(this, {
        enemyType: EnemyType.Tank,
        attackSkill: FireBreath,
        attackPatterns: [
          AttackPattern.Melee,
          AttackPattern.Zone,
          AttackPattern.Charged
        ],
        fleeThreshold: 0.1,
        visionRange: 250
      });
    }
  };
}

Patrol Guard

function PatrolGuard() {
  return {
    name: "Guard",
    onInit() {
      this.setGraphic("guard");
      this.hp = 80;
      this.param[MAXHP] = 80;
      this.param[ATK] = 15;
      
      new BattleAi(this, {
        enemyType: EnemyType.Defensive,
        patrolWaypoints: [
          { x: 100, y: 150 },
          { x: 300, y: 150 },
          { x: 300, y: 350 },
          { x: 100, y: 350 }
        ]
      });
    }
  };
}

Wolf Pack (Group)

function Wolf() {
  return {
    name: "Wolf",
    onInit() {
      this.setGraphic("wolf");
      this.hp = 40;
      this.param[MAXHP] = 40;
      this.param[ATK] = 12;
      
      new BattleAi(this, {
        enemyType: EnemyType.Aggressive,
        groupBehavior: true,
        attackPatterns: [
          AttackPattern.Melee,
          AttackPattern.Combo
        ]
      });
    }
  };
}

Complete Example with Weapons

import { createServer, RpgPlayer, RpgMap, EventMode, MAXHP, ATK, PDEF } from "@rpgjs/server";
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";

// Define weapons with knockback
const IronSword = {
  id: 'iron-sword',
  name: 'Iron Sword',
  description: 'A reliable iron sword',
  atk: 15,
  knockbackForce: 40,
  _type: 'weapon' as const,
};

const GiantMaul = {
  id: 'giant-maul',
  name: 'Giant Maul',
  description: 'Massive hammer with devastating knockback',
  atk: 30,
  knockbackForce: 100,
  _type: 'weapon' as const,
};

const GoblinDagger = {
  id: 'goblin-dagger',
  name: 'Goblin Dagger',
  description: 'Small rusty dagger',
  atk: 8,
  knockbackForce: 20,
  _type: 'weapon' as const,
};

// Enemy with weapon
function GoblinWarrior() {
  return {
    name: "Goblin Warrior",
    mode: EventMode.Scenario,
    onInit() {
      this.setGraphic("goblin");
      
      // Stats
      this.hp = 60;
      this.param[MAXHP] = 60;
      this.param[ATK] = 12;
      this.param[PDEF] = 5;
      
      // Equip weapon (knockbackForce: 20)
      this.addItem(GoblinDagger);
      this.equip(GoblinDagger.id);
      
      // AI
      new BattleAi(this, {
        enemyType: EnemyType.Aggressive,
        attackRange: 45
      });
    }
  };
}

// Server setup
export default createServer({
  providers: [
    provideActionBattle(),
    {
      database: {
        'iron-sword': IronSword,
        'giant-maul': GiantMaul,
        'goblin-dagger': GoblinDagger
      },
      player: {
        onJoinMap(player: RpgPlayer, map: RpgMap) {
          // Setup player stats
          player.hp = 100;
          player.param[MAXHP] = 100;
          player.param[ATK] = 15;
          
          // Give player a weapon with high knockback
          player.addItem(GiantMaul);
          player.equip(GiantMaul.id);
          
          // Player attacks will now knock enemies back with force 100
        }
      },
      maps: [
        {
          id: 'battle-map',
          events: [{ event: GoblinWarrior() }]
        }
      ]
    }
  ]
});

API Reference

BattleAi Methods

// Get current health (uses event.hp)
ai.getHealth(): number

// Get max health (uses event.param[MAXHP])
ai.getMaxHealth(): number

// Get current target
ai.getTarget(): RpgPlayer | null

// Get current AI state
ai.getState(): AiState

// Get enemy type
ai.getEnemyType(): EnemyType

// Handle damage (called automatically)
ai.takeDamage(attacker: RpgPlayer): boolean

// Clean up AI instance
ai.destroy(): void

Player Combat

The module handles player attacks via the action input:

// Player presses action key -> attack animation + hitbox
// Hitbox detects enemy -> applyPlayerHitToEvent(player, event)
// Damage uses RPGJS formula: target.applyDamage(attacker)
// Knockback force is based on equipped weapon's knockbackForce property

Knockback System

Knockback force is determined by the equipped weapon's knockbackForce property.

Creating Weapons with Knockback

// Light weapon - low knockback
const Dagger = {
  id: 'dagger',
  name: 'Iron Dagger',
  atk: 10,
  knockbackForce: 20,
  _type: 'weapon' as const,
};

// Heavy weapon - high knockback
const Warhammer = {
  id: 'warhammer',
  name: 'War Hammer',
  atk: 30,
  knockbackForce: 100,
  _type: 'weapon' as const,
};

Default Knockback

If no weapon is equipped or the weapon doesn't have knockbackForce, the default value is used:

import { DEFAULT_KNOCKBACK } from "@rpgjs/action-battle/server";

console.log(DEFAULT_KNOCKBACK.force);    // 50
console.log(DEFAULT_KNOCKBACK.duration); // 300ms

Hook System

Customize hit behavior using hooks. Available on both player-to-enemy and enemy-to-player hits.

HitResult Interface

interface HitResult {
  damage: number;           // Damage dealt
  knockbackForce: number;   // Knockback force (from weapon)
  knockbackDuration: number; // Knockback duration in ms
  defeated: boolean;        // Whether target was defeated
  attacker: RpgPlayer | RpgEvent;
  target: RpgPlayer | RpgEvent;
}

Using Hooks with applyPlayerHitToEvent

import { applyPlayerHitToEvent } from "@rpgjs/action-battle/server";

// In your custom attack handler
const result = applyPlayerHitToEvent(player, event, {
  onBeforeHit(hitResult) {
    // Modify knockback for armored enemies
    if ((hitResult.target as any).hasState?.('armored')) {
      hitResult.knockbackForce *= 0.5;
    }
    
    // Critical hit - double knockback
    if (Math.random() < 0.1) {
      hitResult.knockbackForce *= 2;
      console.log('Critical hit!');
    }
    
    return hitResult; // Must return modified result
  },
  
  onAfterHit(hitResult) {
    // Award gold on kill
    if (hitResult.defeated) {
      (hitResult.attacker as any).gold += 10;
    }
    
    // Apply poison on hit (30% chance)
    if (Math.random() < 0.3) {
      (hitResult.target as any).addState?.('poison');
    }
    
    // Play custom sound
    playSound('hit');
  }
});

Custom Attack Implementation

Override the default attack to add custom hooks:

import { 
  applyPlayerHitToEvent, 
  DEFAULT_PLAYER_ATTACK_HITBOXES,
  getPlayerWeaponKnockbackForce 
} from "@rpgjs/action-battle/server";

// Custom attack with hooks
function customAttack(player: RpgPlayer) {
  player.setAnimation('attack', 1);
  
  const direction = player.getDirection();
  const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[direction] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
  
  const hitboxes = [{
    x: player.x() + hitboxConfig.offsetX,
    y: player.y() + hitboxConfig.offsetY,
    width: hitboxConfig.width,
    height: hitboxConfig.height
  }];

  const map = player.getCurrentMap();
  map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
    next(hits) {
      hits.forEach((hit) => {
        if (hit instanceof RpgEvent) {
          applyPlayerHitToEvent(player, hit, {
            onBeforeHit(result) {
              // Custom modifications
              return result;
            },
            onAfterHit(result) {
              // Custom effects
            }
          });
        }
      });
    }
  });
}

Getting Weapon Knockback Force

import { getPlayerWeaponKnockbackForce } from "@rpgjs/action-battle/server";

const force = getPlayerWeaponKnockbackForce(player);
console.log(`Player knockback force: ${force}`);

onDefeated Hook

The onDefeated callback is triggered when an AI enemy is killed. Use it to:

  • Award experience, gold, or items to the player
  • Spawn loot drops
  • Trigger events or cutscenes
  • Update quest progress
  • Play death animations or sounds

Basic Usage

new BattleAi(this, {
  enemyType: EnemyType.Aggressive,
  onDefeated: (event) => {
    console.log(`${event.name()} was defeated!`);
  }
});

Award Rewards on Kill

function Goblin() {
  return {
    name: "Goblin",
    onInit() {
      this.setGraphic("goblin");
      this.hp = 50;
      this.param[MAXHP] = 50;
      this.param[ATK] = 10;
      
      new BattleAi(this, {
        enemyType: EnemyType.Aggressive,
        onDefeated: (event) => {
          // Find the player who killed this enemy
          const map = event.getCurrentMap();
          const players = map?.getPlayersIn() || [];
          
          players.forEach(player => {
            // Award gold
            player.gold += 25;
            
            // Award experience
            player.exp += 50;
            
            // Random loot drop
            if (Math.random() < 0.3) {
              player.addItem(HealthPotion);
            }
          });
        }
      });
    }
  };
}

Spawn Loot on Death

new BattleAi(this, {
  onDefeated: (event) => {
    const map = event.getCurrentMap();
    if (!map) return;
    
    // Spawn loot at enemy position
    map.createDynamicEvent({
      x: event.x(),
      y: event.y(),
      event: LootChest({ items: [GoldCoin, HealthPotion] })
    });
  }
});

Track Kill Count

let killCount = 0;

new BattleAi(this, {
  onDefeated: (event) => {
    killCount++;
    
    // Check quest progress
    if (killCount >= 10) {
      triggerQuestComplete('slay_goblins');
    }
  }
});

Boss Death Event

function DragonBoss() {
  return {
    name: "Ancient Dragon",
    onInit() {
      this.setGraphic("dragon");
      this.hp = 1000;
      this.param[MAXHP] = 1000;
      
      new BattleAi(this, {
        enemyType: EnemyType.Tank,
        onDefeated: (event) => {
          const map = event.getCurrentMap();
          
          // Announce victory
          map?.getPlayersIn()?.forEach(player => {
            player.showNotification({
              message: "The Ancient Dragon has been slain!",
              time: 5000
            });
            
            // Reward all participants
            player.gold += 1000;
            player.exp += 5000;
            player.addItem(DragonScale);
          });
          
          // Open dungeon exit
          map?.setTileProperty(exitX, exitY, { passable: true });
        }
      });
    }
  };
}

Visual Feedback

Automatic feedback:

  • Flash Effect: Red flash when taking damage
  • Damage Numbers: Floating damage text
  • Attack Animation: Triggers attack animation
  • Knockback: Entities pushed back based on weapon knockbackForce