@rpgjs/action-battle 5.0.0-beta.11 → 5.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/client/ai.server.d.ts +57 -8
- package/dist/client/attack-input.d.ts +3 -0
- package/dist/client/core/action-use.d.ts +18 -0
- package/dist/client/core/ai-behavior-tree.d.ts +99 -0
- package/dist/client/core/attack-runtime.d.ts +2 -0
- package/dist/client/core/defaults.d.ts +3 -2
- package/dist/client/core/equipment.d.ts +1 -0
- package/dist/client/core/targets.d.ts +15 -0
- package/dist/client/enemies/factory.d.ts +2 -0
- package/dist/client/index.d.ts +12 -7
- package/dist/client/index.js +16 -11
- package/dist/client/index10.js +32 -56
- package/dist/client/index11.js +99 -52
- package/dist/client/index12.js +76 -103
- package/dist/client/index13.js +72 -135
- package/dist/client/index14.js +67 -23
- package/dist/client/index15.js +197 -63
- package/dist/client/index16.js +112 -1337
- package/dist/client/index17.js +203 -7
- package/dist/client/index18.js +32 -58
- package/dist/client/index19.js +70 -8
- package/dist/client/index20.js +57 -501
- package/dist/client/index21.js +70 -0
- package/dist/client/index22.js +226 -0
- package/dist/client/index23.js +16 -0
- package/dist/client/index24.js +25 -0
- package/dist/client/index25.js +107 -0
- package/dist/client/index26.js +1949 -0
- package/dist/client/index27.js +12 -0
- package/dist/client/index28.js +589 -0
- package/dist/client/index4.js +79 -38
- package/dist/client/index6.js +65 -306
- package/dist/client/index7.js +33 -33
- package/dist/client/index8.js +24 -100
- package/dist/client/index9.js +293 -61
- package/dist/client/locomotion.d.ts +16 -0
- package/dist/client/movement.d.ts +14 -0
- package/dist/client/server.d.ts +7 -3
- package/dist/client/ui.d.ts +22 -0
- package/dist/client/visual.d.ts +15 -0
- package/dist/server/ai.server.d.ts +57 -8
- package/dist/server/attack-input.d.ts +3 -0
- package/dist/server/core/action-use.d.ts +18 -0
- package/dist/server/core/ai-behavior-tree.d.ts +99 -0
- package/dist/server/core/attack-runtime.d.ts +2 -0
- package/dist/server/core/defaults.d.ts +3 -2
- package/dist/server/core/equipment.d.ts +1 -0
- package/dist/server/core/targets.d.ts +15 -0
- package/dist/server/enemies/factory.d.ts +2 -0
- package/dist/server/index.d.ts +12 -7
- package/dist/server/index.js +14 -9
- package/dist/server/index10.js +64 -1336
- package/dist/server/index11.js +33 -33
- package/dist/server/index13.js +67 -11
- package/dist/server/index14.js +207 -484
- package/dist/server/index15.js +15 -9
- package/dist/server/index16.js +26 -0
- package/dist/server/index17.js +25 -0
- package/dist/server/index18.js +107 -0
- package/dist/server/index19.js +1949 -0
- package/dist/server/index2.js +10 -2
- package/dist/server/index20.js +37 -0
- package/dist/server/index21.js +588 -0
- package/dist/server/index22.js +78 -0
- package/dist/server/index23.js +12 -0
- package/dist/server/index5.js +79 -38
- package/dist/server/index6.js +192 -129
- package/dist/server/index7.js +208 -24
- package/dist/server/index8.js +28 -66
- package/dist/server/index9.js +68 -51
- package/dist/server/locomotion.d.ts +16 -0
- package/dist/server/movement.d.ts +14 -0
- package/dist/server/server.d.ts +7 -3
- package/dist/server/ui.d.ts +22 -0
- package/dist/server/visual.d.ts +15 -0
- package/package.json +5 -5
- package/src/ai.server.spec.ts +380 -1
- package/src/ai.server.ts +963 -137
- package/src/animations.spec.ts +40 -0
- package/src/animations.ts +31 -9
- package/src/attack-input.spec.ts +51 -0
- package/src/attack-input.ts +59 -0
- package/src/client.ts +75 -62
- package/src/config.ts +84 -37
- package/src/core/action-use.spec.ts +317 -0
- package/src/core/action-use.ts +387 -0
- package/src/core/ai-behavior-tree.spec.ts +116 -0
- package/src/core/ai-behavior-tree.ts +272 -0
- package/src/core/attack-profile.spec.ts +46 -0
- package/src/core/attack-runtime.spec.ts +35 -0
- package/src/core/attack-runtime.ts +32 -0
- package/src/core/context.ts +9 -0
- package/src/core/contracts.ts +146 -1
- package/src/core/defaults.ts +72 -1
- package/src/core/equipment.ts +9 -5
- package/src/core/hit.spec.ts +21 -0
- package/src/core/targets.spec.ts +124 -0
- package/src/core/targets.ts +150 -0
- package/src/enemies/factory.ts +8 -0
- package/src/index.ts +111 -2
- package/src/locomotion.spec.ts +51 -0
- package/src/locomotion.ts +48 -0
- package/src/movement.spec.ts +78 -0
- package/src/movement.ts +46 -0
- package/src/server.ts +242 -66
- package/src/types.ts +105 -35
- package/src/ui.ts +113 -0
- package/src/visual.spec.ts +166 -0
- package/src/visual.ts +285 -0
- package/README.md +0 -1242
package/README.md
DELETED
|
@@ -1,1242 +0,0 @@
|
|
|
1
|
-
# Action Battle System
|
|
2
|
-
|
|
3
|
-
Advanced real-time action combat AI system for RPGJS.
|
|
4
|
-
|
|
5
|
-
The AI controller manages **behavior only** - all stats (HP, ATK, skills, items, etc.) are configured using the standard RPGJS API.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **State Machine AI**: Enemies with dynamic behaviors (Idle, Alert, Combat, Flee, Stunned)
|
|
10
|
-
- **Plugin-first architecture**: Replace damage, hitboxes, knockback, hooks, and AI behaviors independently
|
|
11
|
-
- **Multiple Enemy Types**: Aggressive, Defensive, Ranged, Tank, Berserker
|
|
12
|
-
- **Attack Patterns**: Melee, Combo, Charged, Zone, Dash Attack
|
|
13
|
-
- **Skill Support**: AI can use any RPGJS skill
|
|
14
|
-
- **Dodge System**: Enemies can dodge and counter-attack
|
|
15
|
-
- **Group Behavior**: Enemies coordinate attacks and formations
|
|
16
|
-
- **Patrol System**: Waypoint-based patrolling
|
|
17
|
-
- **Knockback System**: Weapon-based knockback force
|
|
18
|
-
- **Hook System**: Customize hit behavior with `onBeforeHit` and `onAfterHit` hooks
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @rpgjs/action-battle
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Plugin-First Customization
|
|
27
|
-
|
|
28
|
-
`provideActionBattle()` ships with Zelda-like defaults, but each combat system
|
|
29
|
-
can be replaced without rewriting the module.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
33
|
-
|
|
34
|
-
export default provideActionBattle({
|
|
35
|
-
attack: {
|
|
36
|
-
lockMovement: true,
|
|
37
|
-
lockDurationMs: 280,
|
|
38
|
-
hitboxes: {
|
|
39
|
-
right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
systems: {
|
|
43
|
-
combat: {
|
|
44
|
-
damage({ attacker, target, skill }) {
|
|
45
|
-
const raw = target.applyDamage(attacker, skill);
|
|
46
|
-
return {
|
|
47
|
-
damage: raw.damage,
|
|
48
|
-
defeated: target.hp <= 0,
|
|
49
|
-
raw
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
knockback({ attacker, target }) {
|
|
53
|
-
const dx = target.x() - attacker.x();
|
|
54
|
-
const dy = target.y() - attacker.y();
|
|
55
|
-
const distance = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
56
|
-
return {
|
|
57
|
-
force: 70,
|
|
58
|
-
duration: 220,
|
|
59
|
-
direction: { x: dx / distance, y: dy / distance }
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
hooks: {
|
|
63
|
-
beforeHit(context) {
|
|
64
|
-
// Return false to cancel a hit, or return a modified context.
|
|
65
|
-
return context;
|
|
66
|
-
},
|
|
67
|
-
afterHit(result) {
|
|
68
|
-
console.log(`Damage: ${result.damage}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
ai: {
|
|
73
|
-
behaviors: {
|
|
74
|
-
slime({ hpPercent }) {
|
|
75
|
-
return {
|
|
76
|
-
mode: hpPercent !== null && hpPercent < 0.25 ? "retreat" : "assault",
|
|
77
|
-
attackCooldown: 900
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
The main extension contracts are:
|
|
87
|
-
|
|
88
|
-
- `ActionBattleCombatSystem`: resolves hitboxes, damage, knockback, and hooks.
|
|
89
|
-
- `ActionBattleAiBehavior`: returns lightweight AI decisions from event state.
|
|
90
|
-
- `ActionBattleHitHooks`: `beforeHit`, `afterDamage`, and `afterHit`.
|
|
91
|
-
|
|
92
|
-
Use `createActionEnemy()` when you want data-driven enemy presets:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
import { createActionEnemy, EnemyType } from "@rpgjs/action-battle/server";
|
|
96
|
-
|
|
97
|
-
const enemyPresets = {
|
|
98
|
-
slime: {
|
|
99
|
-
enemyType: EnemyType.Aggressive,
|
|
100
|
-
behaviorKey: "slime",
|
|
101
|
-
stats(event) {
|
|
102
|
-
event.hp = 40;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
createActionEnemy(this, "slime", enemyPresets);
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Quick Start
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import { createServer, RpgPlayer, RpgEvent, EventMode, ATK, PDEF, MAXHP } from "@rpgjs/server";
|
|
114
|
-
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
|
|
115
|
-
|
|
116
|
-
function GoblinEnemy() {
|
|
117
|
-
return {
|
|
118
|
-
name: "Goblin",
|
|
119
|
-
mode: EventMode.Scenario,
|
|
120
|
-
onInit() {
|
|
121
|
-
this.setGraphic("goblin");
|
|
122
|
-
|
|
123
|
-
// Configure stats using RPGJS API
|
|
124
|
-
this.hp = 80;
|
|
125
|
-
this.param[MAXHP] = 80;
|
|
126
|
-
this.param[ATK] = 15;
|
|
127
|
-
this.param[PDEF] = 5;
|
|
128
|
-
|
|
129
|
-
// Optional: Give skills
|
|
130
|
-
// this.learnSkill(Slash);
|
|
131
|
-
|
|
132
|
-
// Optional: Give items
|
|
133
|
-
// this.addItem(Potion, 2);
|
|
134
|
-
|
|
135
|
-
// Apply AI behavior
|
|
136
|
-
new BattleAi(this, {
|
|
137
|
-
enemyType: EnemyType.Aggressive
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Using RPGJS API for Stats
|
|
145
|
-
|
|
146
|
-
The AI uses the event's existing stats. Configure them in `onInit`:
|
|
147
|
-
|
|
148
|
-
### Health & Resources
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
this.hp = 100; // Current HP
|
|
152
|
-
this.param[MAXHP] = 100; // Max HP
|
|
153
|
-
this.sp = 50; // SP for skills
|
|
154
|
-
this.param[MAXSP] = 50; // Max SP
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Parameters
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { ATK, PDEF, SDEF } from "@rpgjs/server";
|
|
161
|
-
|
|
162
|
-
this.param[ATK] = 20; // Attack power
|
|
163
|
-
this.param[PDEF] = 10; // Physical defense
|
|
164
|
-
this.param[SDEF] = 8; // Special defense
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Skills
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { Fireball, Heal } from './database/skills';
|
|
171
|
-
|
|
172
|
-
this.learnSkill(Fireball);
|
|
173
|
-
this.learnSkill(Heal);
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Items & Equipment
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
import { Sword, Shield, Potion } from './database/items';
|
|
180
|
-
|
|
181
|
-
this.addItem(Potion, 3);
|
|
182
|
-
this.equip(Sword);
|
|
183
|
-
this.equip(Shield);
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Classes
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
import { WarriorClass } from './database/classes';
|
|
190
|
-
|
|
191
|
-
this.setClass(WarriorClass);
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### States
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
import { PoisonState } from './database/states';
|
|
198
|
-
|
|
199
|
-
this.addState(PoisonState);
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## AI Configuration
|
|
203
|
-
|
|
204
|
-
The AI only controls **behavior**. All options are optional:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
new BattleAi(event, {
|
|
208
|
-
// Enemy type (affects behavior, not stats)
|
|
209
|
-
enemyType: EnemyType.Aggressive,
|
|
210
|
-
|
|
211
|
-
// Skill to use for attacks (optional)
|
|
212
|
-
attackSkill: Fireball,
|
|
213
|
-
|
|
214
|
-
// Timing
|
|
215
|
-
attackCooldown: 1000, // ms between attacks
|
|
216
|
-
|
|
217
|
-
// Ranges
|
|
218
|
-
visionRange: 150, // Detection radius
|
|
219
|
-
attackRange: 60, // Attack distance
|
|
220
|
-
|
|
221
|
-
// Dodge behavior
|
|
222
|
-
dodgeChance: 0.2, // 0-1 probability
|
|
223
|
-
dodgeCooldown: 2000, // ms between dodges
|
|
224
|
-
|
|
225
|
-
// Flee behavior
|
|
226
|
-
fleeThreshold: 0.2, // Flee when HP < 20%
|
|
227
|
-
|
|
228
|
-
// Attack patterns
|
|
229
|
-
attackPatterns: [
|
|
230
|
-
AttackPattern.Melee,
|
|
231
|
-
AttackPattern.Combo,
|
|
232
|
-
AttackPattern.DashAttack
|
|
233
|
-
],
|
|
234
|
-
|
|
235
|
-
// Per-pattern enemy attack timing and reactions
|
|
236
|
-
attackProfiles: {
|
|
237
|
-
charged: {
|
|
238
|
-
startupMs: 900,
|
|
239
|
-
activeMs: 140,
|
|
240
|
-
recoveryMs: 300,
|
|
241
|
-
reaction: {
|
|
242
|
-
hitstunMs: 240,
|
|
243
|
-
staggerPower: 2
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
// Hit reaction tuning
|
|
249
|
-
poise: 1,
|
|
250
|
-
hitstunMs: 150,
|
|
251
|
-
invincibilityMs: 250,
|
|
252
|
-
|
|
253
|
-
// Patrol waypoints (for idle state)
|
|
254
|
-
patrolWaypoints: [
|
|
255
|
-
{ x: 100, y: 100 },
|
|
256
|
-
{ x: 300, y: 100 }
|
|
257
|
-
],
|
|
258
|
-
|
|
259
|
-
// Group coordination
|
|
260
|
-
groupBehavior: true,
|
|
261
|
-
|
|
262
|
-
// Callback when AI is defeated
|
|
263
|
-
onDefeated: ({ event, attacker }) => {
|
|
264
|
-
const name = attacker?.name?.() ?? "Unknown";
|
|
265
|
-
console.log(`${event.name} was defeated by ${name}!`);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
`attackProfiles` lets enemies telegraph attacks with `startupMs`, keep hitboxes
|
|
271
|
-
active for `activeMs`, and apply hit reactions. `poise` controls interruption:
|
|
272
|
-
an incoming hit only stuns the enemy when its `reaction.staggerPower` is greater
|
|
273
|
-
than or equal to the enemy's `poise`.
|
|
274
|
-
|
|
275
|
-
## Enemy Types
|
|
276
|
-
|
|
277
|
-
Types modify AI **behavior** (cooldowns, ranges, dodge), not stats:
|
|
278
|
-
|
|
279
|
-
| Type | Attack Speed | Dodge | Behavior |
|
|
280
|
-
|------|-------------|-------|----------|
|
|
281
|
-
| **Aggressive** | Fast | Low | Rushes player |
|
|
282
|
-
| **Defensive** | Slow | High | Counter-attacks |
|
|
283
|
-
| **Ranged** | Medium | Medium | Keeps distance |
|
|
284
|
-
| **Tank** | Slow | None | Stands ground |
|
|
285
|
-
| **Berserker** | Variable | Low | Faster when hurt |
|
|
286
|
-
|
|
287
|
-
## Using Skills for Attacks
|
|
288
|
-
|
|
289
|
-
The AI can use any RPGJS skill:
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
// In your database/skills.ts
|
|
293
|
-
import { Skill } from '@rpgjs/database';
|
|
294
|
-
|
|
295
|
-
@Skill({
|
|
296
|
-
name: 'Slash',
|
|
297
|
-
spCost: 5,
|
|
298
|
-
power: 25,
|
|
299
|
-
hitRate: 0.95
|
|
300
|
-
})
|
|
301
|
-
export class Slash {}
|
|
302
|
-
|
|
303
|
-
// In your event
|
|
304
|
-
onInit() {
|
|
305
|
-
this.hp = 100;
|
|
306
|
-
this.sp = 50;
|
|
307
|
-
this.learnSkill(Slash);
|
|
308
|
-
|
|
309
|
-
new BattleAi(this, {
|
|
310
|
-
attackSkill: Slash
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## AI States
|
|
316
|
-
|
|
317
|
-
```
|
|
318
|
-
┌─────────┐ detect ┌─────────┐ approach ┌─────────┐
|
|
319
|
-
│ Idle │ ──────────────> │ Alert │ ─────────────> │ Combat │
|
|
320
|
-
└─────────┘ └─────────┘ └─────────┘
|
|
321
|
-
^ │
|
|
322
|
-
│ │
|
|
323
|
-
│ ┌─────────┐ │
|
|
324
|
-
│ │ Stunned │ <────── take damage ───────┤
|
|
325
|
-
│ └─────────┘ │
|
|
326
|
-
│ │ │
|
|
327
|
-
│ v │
|
|
328
|
-
│ ┌─────────┐ │
|
|
329
|
-
└───────────── │ Flee │ <────── HP low ────────────┘
|
|
330
|
-
└─────────┘
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Attack Patterns
|
|
334
|
-
|
|
335
|
-
| Pattern | Description |
|
|
336
|
-
|---------|-------------|
|
|
337
|
-
| **Melee** | Single attack |
|
|
338
|
-
| **Combo** | 2-3 rapid attacks |
|
|
339
|
-
| **Charged** | Wind-up, stronger attack |
|
|
340
|
-
| **Zone** | 360° area attack |
|
|
341
|
-
| **DashAttack** | Rush toward target then attack |
|
|
342
|
-
|
|
343
|
-
## Examples
|
|
344
|
-
|
|
345
|
-
### Basic Enemy
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
function Goblin() {
|
|
349
|
-
return {
|
|
350
|
-
name: "Goblin",
|
|
351
|
-
onInit() {
|
|
352
|
-
this.setGraphic("goblin");
|
|
353
|
-
this.hp = 50;
|
|
354
|
-
this.param[MAXHP] = 50;
|
|
355
|
-
this.param[ATK] = 10;
|
|
356
|
-
|
|
357
|
-
new BattleAi(this);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Mage with Skills
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
function DarkMage() {
|
|
367
|
-
return {
|
|
368
|
-
name: "Dark Mage",
|
|
369
|
-
onInit() {
|
|
370
|
-
this.setGraphic("mage");
|
|
371
|
-
this.hp = 60;
|
|
372
|
-
this.sp = 100;
|
|
373
|
-
this.param[MAXHP] = 60;
|
|
374
|
-
this.param[MAXSP] = 100;
|
|
375
|
-
this.param[ATK] = 25;
|
|
376
|
-
|
|
377
|
-
this.learnSkill(Fireball);
|
|
378
|
-
this.learnSkill(IceSpike);
|
|
379
|
-
|
|
380
|
-
new BattleAi(this, {
|
|
381
|
-
enemyType: EnemyType.Ranged,
|
|
382
|
-
attackSkill: Fireball,
|
|
383
|
-
visionRange: 200
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Boss
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
function DragonBoss() {
|
|
394
|
-
return {
|
|
395
|
-
name: "Dragon",
|
|
396
|
-
onInit() {
|
|
397
|
-
this.setGraphic("dragon");
|
|
398
|
-
this.hp = 500;
|
|
399
|
-
this.param[MAXHP] = 500;
|
|
400
|
-
this.param[ATK] = 50;
|
|
401
|
-
this.param[PDEF] = 30;
|
|
402
|
-
|
|
403
|
-
this.learnSkill(FireBreath);
|
|
404
|
-
this.learnSkill(TailSwipe);
|
|
405
|
-
|
|
406
|
-
new BattleAi(this, {
|
|
407
|
-
enemyType: EnemyType.Tank,
|
|
408
|
-
attackSkill: FireBreath,
|
|
409
|
-
attackPatterns: [
|
|
410
|
-
AttackPattern.Melee,
|
|
411
|
-
AttackPattern.Zone,
|
|
412
|
-
AttackPattern.Charged
|
|
413
|
-
],
|
|
414
|
-
fleeThreshold: 0.1,
|
|
415
|
-
visionRange: 250
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
### Patrol Guard
|
|
423
|
-
|
|
424
|
-
```typescript
|
|
425
|
-
function PatrolGuard() {
|
|
426
|
-
return {
|
|
427
|
-
name: "Guard",
|
|
428
|
-
onInit() {
|
|
429
|
-
this.setGraphic("guard");
|
|
430
|
-
this.hp = 80;
|
|
431
|
-
this.param[MAXHP] = 80;
|
|
432
|
-
this.param[ATK] = 15;
|
|
433
|
-
|
|
434
|
-
new BattleAi(this, {
|
|
435
|
-
enemyType: EnemyType.Defensive,
|
|
436
|
-
patrolWaypoints: [
|
|
437
|
-
{ x: 100, y: 150 },
|
|
438
|
-
{ x: 300, y: 150 },
|
|
439
|
-
{ x: 300, y: 350 },
|
|
440
|
-
{ x: 100, y: 350 }
|
|
441
|
-
]
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### Wolf Pack (Group)
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
function Wolf() {
|
|
452
|
-
return {
|
|
453
|
-
name: "Wolf",
|
|
454
|
-
onInit() {
|
|
455
|
-
this.setGraphic("wolf");
|
|
456
|
-
this.hp = 40;
|
|
457
|
-
this.param[MAXHP] = 40;
|
|
458
|
-
this.param[ATK] = 12;
|
|
459
|
-
|
|
460
|
-
new BattleAi(this, {
|
|
461
|
-
enemyType: EnemyType.Aggressive,
|
|
462
|
-
groupBehavior: true,
|
|
463
|
-
attackPatterns: [
|
|
464
|
-
AttackPattern.Melee,
|
|
465
|
-
AttackPattern.Combo
|
|
466
|
-
]
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
### Complete Example with Weapons
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
import { createServer, RpgPlayer, RpgMap, EventMode, MAXHP, ATK, PDEF } from "@rpgjs/server";
|
|
477
|
-
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
|
|
478
|
-
|
|
479
|
-
// Define weapons with knockback
|
|
480
|
-
const IronSword = {
|
|
481
|
-
id: 'iron-sword',
|
|
482
|
-
name: 'Iron Sword',
|
|
483
|
-
description: 'A reliable iron sword',
|
|
484
|
-
atk: 15,
|
|
485
|
-
knockbackForce: 40,
|
|
486
|
-
_type: 'weapon' as const,
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const GiantMaul = {
|
|
490
|
-
id: 'giant-maul',
|
|
491
|
-
name: 'Giant Maul',
|
|
492
|
-
description: 'Massive hammer with devastating knockback',
|
|
493
|
-
atk: 30,
|
|
494
|
-
knockbackForce: 100,
|
|
495
|
-
_type: 'weapon' as const,
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
const GoblinDagger = {
|
|
499
|
-
id: 'goblin-dagger',
|
|
500
|
-
name: 'Goblin Dagger',
|
|
501
|
-
description: 'Small rusty dagger',
|
|
502
|
-
atk: 8,
|
|
503
|
-
knockbackForce: 20,
|
|
504
|
-
_type: 'weapon' as const,
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
// Enemy with weapon
|
|
508
|
-
function GoblinWarrior() {
|
|
509
|
-
return {
|
|
510
|
-
name: "Goblin Warrior",
|
|
511
|
-
mode: EventMode.Scenario,
|
|
512
|
-
onInit() {
|
|
513
|
-
this.setGraphic("goblin");
|
|
514
|
-
|
|
515
|
-
// Stats
|
|
516
|
-
this.hp = 60;
|
|
517
|
-
this.param[MAXHP] = 60;
|
|
518
|
-
this.param[ATK] = 12;
|
|
519
|
-
this.param[PDEF] = 5;
|
|
520
|
-
|
|
521
|
-
// Equip weapon (knockbackForce: 20)
|
|
522
|
-
this.addItem(GoblinDagger);
|
|
523
|
-
this.equip(GoblinDagger.id);
|
|
524
|
-
|
|
525
|
-
// AI
|
|
526
|
-
new BattleAi(this, {
|
|
527
|
-
enemyType: EnemyType.Aggressive,
|
|
528
|
-
attackRange: 45
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Server setup
|
|
535
|
-
export default createServer({
|
|
536
|
-
providers: [
|
|
537
|
-
provideActionBattle(),
|
|
538
|
-
{
|
|
539
|
-
database: {
|
|
540
|
-
'iron-sword': IronSword,
|
|
541
|
-
'giant-maul': GiantMaul,
|
|
542
|
-
'goblin-dagger': GoblinDagger
|
|
543
|
-
},
|
|
544
|
-
player: {
|
|
545
|
-
onJoinMap(player: RpgPlayer, map: RpgMap) {
|
|
546
|
-
// Setup player stats
|
|
547
|
-
player.hp = 100;
|
|
548
|
-
player.param[MAXHP] = 100;
|
|
549
|
-
player.param[ATK] = 15;
|
|
550
|
-
|
|
551
|
-
// Give player a weapon with high knockback
|
|
552
|
-
player.addItem(GiantMaul);
|
|
553
|
-
player.equip(GiantMaul.id);
|
|
554
|
-
|
|
555
|
-
// Player attacks will now knock enemies back with force 100
|
|
556
|
-
}
|
|
557
|
-
},
|
|
558
|
-
maps: [
|
|
559
|
-
{
|
|
560
|
-
id: 'battle-map',
|
|
561
|
-
events: [{ event: GoblinWarrior() }]
|
|
562
|
-
}
|
|
563
|
-
]
|
|
564
|
-
}
|
|
565
|
-
]
|
|
566
|
-
});
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
## API Reference
|
|
570
|
-
|
|
571
|
-
### BattleAi Methods
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
// Get current health (uses event.hp)
|
|
575
|
-
ai.getHealth(): number
|
|
576
|
-
|
|
577
|
-
// Get max health (uses event.param[MAXHP])
|
|
578
|
-
ai.getMaxHealth(): number
|
|
579
|
-
|
|
580
|
-
// Get current target
|
|
581
|
-
ai.getTarget(): RpgPlayer | null
|
|
582
|
-
|
|
583
|
-
// Get current AI state
|
|
584
|
-
ai.getState(): AiState
|
|
585
|
-
|
|
586
|
-
// Get enemy type
|
|
587
|
-
ai.getEnemyType(): EnemyType
|
|
588
|
-
|
|
589
|
-
// Handle damage (called automatically)
|
|
590
|
-
ai.takeDamage(attacker: RpgPlayer): boolean
|
|
591
|
-
|
|
592
|
-
// Clean up AI instance
|
|
593
|
-
ai.destroy(): void
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
## Player Combat
|
|
597
|
-
|
|
598
|
-
The module handles player attacks via the `action` input:
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
// Player presses action key -> attack animation + hitbox
|
|
602
|
-
// Hitbox detects enemy -> applyPlayerHitToEvent(player, event)
|
|
603
|
-
// Damage uses RPGJS formula: target.applyDamage(attacker)
|
|
604
|
-
// Knockback force is based on equipped weapon's knockbackForce property
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
By default, the player is locked in place for `350ms` when attacking, similar
|
|
608
|
-
to classic A-RPG combat where the attack resolves before movement resumes.
|
|
609
|
-
|
|
610
|
-
```ts
|
|
611
|
-
provideActionBattle({
|
|
612
|
-
attack: {
|
|
613
|
-
lockMovement: true,
|
|
614
|
-
lockDurationMs: 350,
|
|
615
|
-
showPreview: true,
|
|
616
|
-
previewDurationMs: 180,
|
|
617
|
-
previewColor: 0xfff3b0,
|
|
618
|
-
previewAccentColor: 0xffffff
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
Set `lockMovement` to `false` if your game should allow moving attacks.
|
|
624
|
-
The client also stops predicted movement immediately and shows a short slash
|
|
625
|
-
preview so the action feels responsive even before the server hit resolves. Set
|
|
626
|
-
`showPreview` to `false` if your project uses only spritesheet combat
|
|
627
|
-
animations.
|
|
628
|
-
|
|
629
|
-
Player attacks are resolved with `createMovingHitbox()` instead of a passive
|
|
630
|
-
contact collision. You can still customize the generated hitboxes with
|
|
631
|
-
`attack.hitboxes` or `attack.resolveHitboxes`.
|
|
632
|
-
|
|
633
|
-
### Attack profile model
|
|
634
|
-
|
|
635
|
-
Use `attack.profile` to describe the timing model of a player attack in one
|
|
636
|
-
typed object. A profile separates the attack into startup, active, and recovery
|
|
637
|
-
phases so combat systems can share the same vocabulary.
|
|
638
|
-
|
|
639
|
-
```ts
|
|
640
|
-
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
641
|
-
|
|
642
|
-
export default provideActionBattle({
|
|
643
|
-
attack: {
|
|
644
|
-
profile: {
|
|
645
|
-
id: "iron-sword",
|
|
646
|
-
startupMs: 80,
|
|
647
|
-
activeMs: 120,
|
|
648
|
-
recoveryMs: 180,
|
|
649
|
-
cooldownMs: 380,
|
|
650
|
-
movementLock: true,
|
|
651
|
-
directionLock: true,
|
|
652
|
-
animationKey: "attack",
|
|
653
|
-
hitPolicy: "oncePerTarget",
|
|
654
|
-
reaction: {
|
|
655
|
-
invincibilityMs: 250,
|
|
656
|
-
hitstunMs: 150,
|
|
657
|
-
staggerPower: 1
|
|
658
|
-
},
|
|
659
|
-
hitboxes: {
|
|
660
|
-
right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
lockDurationMs: 380
|
|
664
|
-
}
|
|
665
|
-
});
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
The default profile mirrors the legacy attack lock: no startup, a short active
|
|
669
|
-
window, and recovery that totals `350ms`. The player attack runtime uses
|
|
670
|
-
`startupMs` before creating the hitbox, `activeMs` to keep the hitbox active,
|
|
671
|
-
and `totalDurationMs` for movement and direction locks. `hitPolicy:
|
|
672
|
-
"oncePerTarget"` prevents the same attack window from damaging the same target
|
|
673
|
-
multiple times.
|
|
674
|
-
|
|
675
|
-
`reaction` describes what happens after the hit connects:
|
|
676
|
-
|
|
677
|
-
- `invincibilityMs`: temporary invincibility after damage.
|
|
678
|
-
- `hitstunMs`: stun duration requested by the hit.
|
|
679
|
-
- `staggerPower`: value compared against enemy `poise`.
|
|
680
|
-
|
|
681
|
-
```ts
|
|
682
|
-
import {
|
|
683
|
-
normalizeActionBattleAttackProfile,
|
|
684
|
-
type ActionBattleAttackProfile
|
|
685
|
-
} from "@rpgjs/action-battle/server";
|
|
686
|
-
|
|
687
|
-
const sword: ActionBattleAttackProfile = {
|
|
688
|
-
id: "sword",
|
|
689
|
-
startupMs: 70,
|
|
690
|
-
activeMs: 110,
|
|
691
|
-
recoveryMs: 170
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
const normalized = normalizeActionBattleAttackProfile(sword);
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
Equipped weapons can override the player attack profile:
|
|
698
|
-
|
|
699
|
-
```ts
|
|
700
|
-
const Dagger = {
|
|
701
|
-
id: "dagger",
|
|
702
|
-
name: "Dagger",
|
|
703
|
-
_type: "weapon" as const,
|
|
704
|
-
atk: 8,
|
|
705
|
-
knockbackForce: 20,
|
|
706
|
-
attackProfile: {
|
|
707
|
-
id: "dagger",
|
|
708
|
-
startupMs: 40,
|
|
709
|
-
activeMs: 70,
|
|
710
|
-
recoveryMs: 110
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
Enable lightweight attack logs while tuning profiles:
|
|
716
|
-
|
|
717
|
-
```ts
|
|
718
|
-
provideActionBattle({
|
|
719
|
-
debug: {
|
|
720
|
-
attacks: true
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
When the action targets a normal event with no `BattleAi`, the server lets the
|
|
726
|
-
event handle `onAction` and does not create the combat hitbox. Enemy events
|
|
727
|
-
with `BattleAi` still trigger the A-RPG attack.
|
|
728
|
-
|
|
729
|
-
## Configurable Combat Animations
|
|
730
|
-
|
|
731
|
-
By default, player and AI attacks keep using the existing `attack` animation:
|
|
732
|
-
|
|
733
|
-
```ts
|
|
734
|
-
player.setGraphicAnimation("attack", 1);
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
Use `animations` when your combat sprites are stored in separate graphics such
|
|
738
|
-
as `hero_attack`, `hero_hurt`, or `hero_die`.
|
|
739
|
-
|
|
740
|
-
```ts
|
|
741
|
-
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
742
|
-
|
|
743
|
-
export default provideActionBattle({
|
|
744
|
-
animations: {
|
|
745
|
-
attack: "attack",
|
|
746
|
-
hurt: "hurt",
|
|
747
|
-
die: {
|
|
748
|
-
animationName: "die",
|
|
749
|
-
repeat: 1,
|
|
750
|
-
delayMs: 500
|
|
751
|
-
},
|
|
752
|
-
castSkill: "skill"
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
RPGJS Studio stores combat animations as spritesheet media ids. If
|
|
758
|
-
`provideStudioGame()` is installed, `createStudioActionBattleAnimations()` can
|
|
759
|
-
read the project animations attached to the player at runtime. By default, the
|
|
760
|
-
helper plays Studio attack spritesheets with
|
|
761
|
-
`setGraphicAnimation("attack", graphic, 1)`:
|
|
762
|
-
|
|
763
|
-
```ts
|
|
764
|
-
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
765
|
-
import { createStudioActionBattleAnimations } from "@rpgjs/studio/server";
|
|
766
|
-
|
|
767
|
-
export default provideActionBattle({
|
|
768
|
-
animations: createStudioActionBattleAnimations()
|
|
769
|
-
});
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
You can also pass a static Studio animation object to override the media ids
|
|
773
|
-
manually. Animation values may be media ids or media objects returned by the
|
|
774
|
-
Studio game API.
|
|
775
|
-
|
|
776
|
-
The Studio field `castSpell` is accepted as an alias for action-battle's
|
|
777
|
-
`castSkill` animation key.
|
|
778
|
-
|
|
779
|
-
For data-driven spritesheets, use resolver functions:
|
|
780
|
-
|
|
781
|
-
```ts
|
|
782
|
-
provideActionBattle({
|
|
783
|
-
animations: {
|
|
784
|
-
attack: (entity) => ({
|
|
785
|
-
animationName: "walk",
|
|
786
|
-
graphic: entity.combatAnimations?.attack,
|
|
787
|
-
repeat: 1
|
|
788
|
-
}),
|
|
789
|
-
hurt: (entity) => ({
|
|
790
|
-
animationName: "walk",
|
|
791
|
-
graphic: entity.combatAnimations?.hurt,
|
|
792
|
-
repeat: 1
|
|
793
|
-
}),
|
|
794
|
-
die: (entity) => ({
|
|
795
|
-
animationName: "walk",
|
|
796
|
-
graphic: entity.combatAnimations?.die,
|
|
797
|
-
repeat: 1,
|
|
798
|
-
waitEnd: true
|
|
799
|
-
}),
|
|
800
|
-
castSkill: (entity, context) => ({
|
|
801
|
-
animationName: "walk",
|
|
802
|
-
graphic: entity.combatAnimations?.castSkill,
|
|
803
|
-
repeat: 1
|
|
804
|
-
})
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
When `graphic` is provided, action-battle calls:
|
|
810
|
-
|
|
811
|
-
```ts
|
|
812
|
-
entity.setGraphicAnimation(animationName, graphic, repeat);
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
Otherwise it calls:
|
|
816
|
-
|
|
817
|
-
```ts
|
|
818
|
-
entity.setGraphicAnimation(animationName, repeat);
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
Return `null` or `undefined` from a resolver to skip the animation. `BattleAi`
|
|
822
|
-
can also override the global configuration per enemy:
|
|
823
|
-
|
|
824
|
-
```ts
|
|
825
|
-
new BattleAi(this, {
|
|
826
|
-
animations: {
|
|
827
|
-
attack: {
|
|
828
|
-
animationName: "walk",
|
|
829
|
-
graphic: "slime_attack",
|
|
830
|
-
repeat: 1
|
|
831
|
-
},
|
|
832
|
-
die: {
|
|
833
|
-
animationName: "walk",
|
|
834
|
-
graphic: "slime_die",
|
|
835
|
-
repeat: 1,
|
|
836
|
-
delayMs: 700
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
`waitEnd: true` uses the default defeated transition timeout. Use `delayMs`
|
|
843
|
-
when you need an exact duration. The visual transition itself is handled by the
|
|
844
|
-
client `sprite.onBeforeRemove` hook.
|
|
845
|
-
|
|
846
|
-
## Knockback System
|
|
847
|
-
|
|
848
|
-
Knockback force is determined by the equipped weapon's `knockbackForce` property.
|
|
849
|
-
|
|
850
|
-
### Creating Weapons with Knockback
|
|
851
|
-
|
|
852
|
-
```typescript
|
|
853
|
-
// Light weapon - low knockback
|
|
854
|
-
const Dagger = {
|
|
855
|
-
id: 'dagger',
|
|
856
|
-
name: 'Iron Dagger',
|
|
857
|
-
atk: 10,
|
|
858
|
-
knockbackForce: 20,
|
|
859
|
-
_type: 'weapon' as const,
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
// Heavy weapon - high knockback
|
|
863
|
-
const Warhammer = {
|
|
864
|
-
id: 'warhammer',
|
|
865
|
-
name: 'War Hammer',
|
|
866
|
-
atk: 30,
|
|
867
|
-
knockbackForce: 100,
|
|
868
|
-
_type: 'weapon' as const,
|
|
869
|
-
};
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
### Default Knockback
|
|
873
|
-
|
|
874
|
-
If no weapon is equipped or the weapon doesn't have `knockbackForce`, the default value is used:
|
|
875
|
-
|
|
876
|
-
```typescript
|
|
877
|
-
import { DEFAULT_KNOCKBACK } from "@rpgjs/action-battle/server";
|
|
878
|
-
|
|
879
|
-
console.log(DEFAULT_KNOCKBACK.force); // 50
|
|
880
|
-
console.log(DEFAULT_KNOCKBACK.duration); // 300ms
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
## Hook System
|
|
884
|
-
|
|
885
|
-
Customize hit behavior using hooks. Available on both player-to-enemy and enemy-to-player hits.
|
|
886
|
-
|
|
887
|
-
### HitResult Interface
|
|
888
|
-
|
|
889
|
-
```typescript
|
|
890
|
-
interface HitResult {
|
|
891
|
-
damage: number; // Damage dealt
|
|
892
|
-
knockbackForce: number; // Knockback force (from weapon)
|
|
893
|
-
knockbackDuration: number; // Knockback duration in ms
|
|
894
|
-
defeated: boolean; // Whether target was defeated
|
|
895
|
-
attacker: RpgPlayer | RpgEvent;
|
|
896
|
-
target: RpgPlayer | RpgEvent;
|
|
897
|
-
}
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
### Using Hooks with applyPlayerHitToEvent
|
|
901
|
-
|
|
902
|
-
```typescript
|
|
903
|
-
import { applyPlayerHitToEvent } from "@rpgjs/action-battle/server";
|
|
904
|
-
|
|
905
|
-
// In your custom attack handler
|
|
906
|
-
const result = applyPlayerHitToEvent(player, event, {
|
|
907
|
-
onBeforeHit(hitResult) {
|
|
908
|
-
// Modify knockback for armored enemies
|
|
909
|
-
if ((hitResult.target as any).hasState?.('armored')) {
|
|
910
|
-
hitResult.knockbackForce *= 0.5;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Critical hit - double knockback
|
|
914
|
-
if (Math.random() < 0.1) {
|
|
915
|
-
hitResult.knockbackForce *= 2;
|
|
916
|
-
console.log('Critical hit!');
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
return hitResult; // Must return modified result
|
|
920
|
-
},
|
|
921
|
-
|
|
922
|
-
onAfterHit(hitResult) {
|
|
923
|
-
// Award gold on kill
|
|
924
|
-
if (hitResult.defeated) {
|
|
925
|
-
(hitResult.attacker as any).gold += 10;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// Apply poison on hit (30% chance)
|
|
929
|
-
if (Math.random() < 0.3) {
|
|
930
|
-
(hitResult.target as any).addState?.('poison');
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Play custom sound
|
|
934
|
-
playSound('hit');
|
|
935
|
-
}
|
|
936
|
-
});
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
### Custom Attack Implementation
|
|
940
|
-
|
|
941
|
-
Override the default attack to add custom hooks:
|
|
942
|
-
|
|
943
|
-
```typescript
|
|
944
|
-
import {
|
|
945
|
-
applyPlayerHitToEvent,
|
|
946
|
-
DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
947
|
-
getPlayerWeaponKnockbackForce
|
|
948
|
-
} from "@rpgjs/action-battle/server";
|
|
949
|
-
|
|
950
|
-
// Custom attack with hooks
|
|
951
|
-
function customAttack(player: RpgPlayer) {
|
|
952
|
-
player.setGraphicAnimation('attack', 1);
|
|
953
|
-
|
|
954
|
-
const direction = player.getDirection();
|
|
955
|
-
const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[direction] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
|
|
956
|
-
|
|
957
|
-
const hitboxes = [{
|
|
958
|
-
x: player.x() + hitboxConfig.offsetX,
|
|
959
|
-
y: player.y() + hitboxConfig.offsetY,
|
|
960
|
-
width: hitboxConfig.width,
|
|
961
|
-
height: hitboxConfig.height
|
|
962
|
-
}];
|
|
963
|
-
|
|
964
|
-
const map = player.getCurrentMap();
|
|
965
|
-
map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
|
|
966
|
-
next(hits) {
|
|
967
|
-
hits.forEach((hit) => {
|
|
968
|
-
if (hit instanceof RpgEvent) {
|
|
969
|
-
applyPlayerHitToEvent(player, hit, {
|
|
970
|
-
onBeforeHit(result) {
|
|
971
|
-
// Custom modifications
|
|
972
|
-
return result;
|
|
973
|
-
},
|
|
974
|
-
onAfterHit(result) {
|
|
975
|
-
// Custom effects
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
### Getting Weapon Knockback Force
|
|
986
|
-
|
|
987
|
-
```typescript
|
|
988
|
-
import { getPlayerWeaponKnockbackForce } from "@rpgjs/action-battle/server";
|
|
989
|
-
|
|
990
|
-
const force = getPlayerWeaponKnockbackForce(player);
|
|
991
|
-
console.log(`Player knockback force: ${force}`);
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
## onDefeated Hook
|
|
995
|
-
|
|
996
|
-
The `onDefeated` callback is triggered when an AI enemy is killed. The simplest
|
|
997
|
-
reward flow is configured directly on `BattleAi`; the reward is given to the
|
|
998
|
-
player who landed the killing blow.
|
|
999
|
-
|
|
1000
|
-
```typescript
|
|
1001
|
-
new BattleAi(this, {
|
|
1002
|
-
enemyType: EnemyType.Aggressive,
|
|
1003
|
-
animations: {
|
|
1004
|
-
die: {
|
|
1005
|
-
animationName: "die",
|
|
1006
|
-
graphic: "goblin_die",
|
|
1007
|
-
repeat: 1,
|
|
1008
|
-
delayMs: 700
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
rewards: {
|
|
1012
|
-
exp: 50,
|
|
1013
|
-
gold: 25,
|
|
1014
|
-
items: [{ itemId: "health_potion", amount: 1, chance: 30 }],
|
|
1015
|
-
showNotification: true
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
```
|
|
1019
|
-
|
|
1020
|
-
On defeat, `BattleAi` stops the AI, awards configured rewards once, and calls
|
|
1021
|
-
`event.remove({ reason: "defeated", transition })`. The client can use
|
|
1022
|
-
`sprite.onBeforeRemove` to play the `die` transition before the sprite
|
|
1023
|
-
disappears.
|
|
1024
|
-
|
|
1025
|
-
`onDefeated` receives a context object in new code:
|
|
1026
|
-
- Award experience, gold, or items to the player
|
|
1027
|
-
- Spawn loot drops
|
|
1028
|
-
- Trigger events or cutscenes
|
|
1029
|
-
- Update quest progress
|
|
1030
|
-
- Play death sounds
|
|
1031
|
-
|
|
1032
|
-
### Basic Usage
|
|
1033
|
-
|
|
1034
|
-
```typescript
|
|
1035
|
-
new BattleAi(this, {
|
|
1036
|
-
enemyType: EnemyType.Aggressive,
|
|
1037
|
-
onDefeated: ({ event, attacker }) => {
|
|
1038
|
-
const name = attacker?.name?.() ?? "Unknown";
|
|
1039
|
-
console.log(`${event.name} was defeated by ${name}!`);
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
The legacy `(event, attacker)` callback signature is still supported for
|
|
1045
|
-
two-argument callbacks.
|
|
1046
|
-
|
|
1047
|
-
### Award Rewards on Kill
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
function Goblin() {
|
|
1051
|
-
return {
|
|
1052
|
-
name: "Goblin",
|
|
1053
|
-
onInit() {
|
|
1054
|
-
this.setGraphic("goblin");
|
|
1055
|
-
this.hp = 50;
|
|
1056
|
-
this.param[MAXHP] = 50;
|
|
1057
|
-
this.param[ATK] = 10;
|
|
1058
|
-
|
|
1059
|
-
new BattleAi(this, {
|
|
1060
|
-
enemyType: EnemyType.Aggressive,
|
|
1061
|
-
rewards: {
|
|
1062
|
-
gold: 25,
|
|
1063
|
-
exp: 50,
|
|
1064
|
-
items: [{ item: HealthPotion, amount: 1, chance: 30 }]
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
### Spawn Loot on Death
|
|
1073
|
-
|
|
1074
|
-
```typescript
|
|
1075
|
-
new BattleAi(this, {
|
|
1076
|
-
onDefeated: ({ event }) => {
|
|
1077
|
-
const map = event.getCurrentMap();
|
|
1078
|
-
if (!map) return;
|
|
1079
|
-
|
|
1080
|
-
// Spawn loot at enemy position
|
|
1081
|
-
map.createDynamicEvent({
|
|
1082
|
-
x: event.x(),
|
|
1083
|
-
y: event.y(),
|
|
1084
|
-
event: LootChest({ items: [GoldCoin, HealthPotion] })
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
### Track Kill Count
|
|
1091
|
-
|
|
1092
|
-
```typescript
|
|
1093
|
-
let killCount = 0;
|
|
1094
|
-
|
|
1095
|
-
new BattleAi(this, {
|
|
1096
|
-
onDefeated: () => {
|
|
1097
|
-
killCount++;
|
|
1098
|
-
|
|
1099
|
-
// Check quest progress
|
|
1100
|
-
if (killCount >= 10) {
|
|
1101
|
-
triggerQuestComplete('slay_goblins');
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
### Boss Death Event
|
|
1108
|
-
|
|
1109
|
-
```typescript
|
|
1110
|
-
function DragonBoss() {
|
|
1111
|
-
return {
|
|
1112
|
-
name: "Ancient Dragon",
|
|
1113
|
-
onInit() {
|
|
1114
|
-
this.setGraphic("dragon");
|
|
1115
|
-
this.hp = 1000;
|
|
1116
|
-
this.param[MAXHP] = 1000;
|
|
1117
|
-
|
|
1118
|
-
new BattleAi(this, {
|
|
1119
|
-
enemyType: EnemyType.Tank,
|
|
1120
|
-
onDefeated: ({ event }) => {
|
|
1121
|
-
const map = event.getCurrentMap();
|
|
1122
|
-
|
|
1123
|
-
// Announce victory
|
|
1124
|
-
map?.getPlayersIn()?.forEach(player => {
|
|
1125
|
-
player.showNotification({
|
|
1126
|
-
message: "The Ancient Dragon has been slain!",
|
|
1127
|
-
time: 5000
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
// Reward all participants
|
|
1131
|
-
player.gold += 1000;
|
|
1132
|
-
player.exp += 5000;
|
|
1133
|
-
player.addItem(DragonScale);
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
// Open dungeon exit
|
|
1137
|
-
map?.setTileProperty(exitX, exitY, { passable: true });
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
## Visual Feedback
|
|
1146
|
-
|
|
1147
|
-
Automatic feedback:
|
|
1148
|
-
|
|
1149
|
-
- **Flash Effect**: Red flash when taking damage
|
|
1150
|
-
- **Damage Numbers**: Floating damage text
|
|
1151
|
-
- **Attack Animation**: Triggers `attack` animation
|
|
1152
|
-
- **Knockback**: Entities pushed back based on weapon `knockbackForce`
|
|
1153
|
-
|
|
1154
|
-
## Action Bar + AoE Targeting (client + server)
|
|
1155
|
-
|
|
1156
|
-
The action-battle package includes optional GUI components for an A-RPG action bar
|
|
1157
|
-
and AoE skill targeting. They are disabled by default and are configured via
|
|
1158
|
-
`provideActionBattle()`.
|
|
1159
|
-
|
|
1160
|
-
### Enable the Action Bar
|
|
1161
|
-
|
|
1162
|
-
```ts
|
|
1163
|
-
import { provideActionBattle } from "@rpgjs/action-battle";
|
|
1164
|
-
|
|
1165
|
-
export default provideActionBattle({
|
|
1166
|
-
ui: {
|
|
1167
|
-
actionBar: {
|
|
1168
|
-
enabled: true,
|
|
1169
|
-
autoOpen: true,
|
|
1170
|
-
mode: "both" // "items" | "skills" | "both"
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
You can open/close it manually on the server:
|
|
1177
|
-
|
|
1178
|
-
```ts
|
|
1179
|
-
import { openActionBattleActionBar } from "@rpgjs/action-battle/server";
|
|
1180
|
-
|
|
1181
|
-
openActionBattleActionBar(player);
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
### Skill Range + AoE Mask (ASCII)
|
|
1185
|
-
|
|
1186
|
-
Define range and AoE mask on the skill data (custom fields). The range uses
|
|
1187
|
-
Manhattan distance, and the mask is centered on the target tile.
|
|
1188
|
-
|
|
1189
|
-
```ts
|
|
1190
|
-
@Skill({
|
|
1191
|
-
name: "Nova",
|
|
1192
|
-
spCost: 12,
|
|
1193
|
-
// Custom fields used by action-battle
|
|
1194
|
-
range: 3,
|
|
1195
|
-
aoeMask: [
|
|
1196
|
-
".#.",
|
|
1197
|
-
"###",
|
|
1198
|
-
".#."
|
|
1199
|
-
]
|
|
1200
|
-
})
|
|
1201
|
-
export class Nova {}
|
|
1202
|
-
```
|
|
1203
|
-
|
|
1204
|
-
### Targeting Options
|
|
1205
|
-
|
|
1206
|
-
```ts
|
|
1207
|
-
export default provideActionBattle({
|
|
1208
|
-
ui: {
|
|
1209
|
-
actionBar: {
|
|
1210
|
-
enabled: true,
|
|
1211
|
-
autoOpen: false
|
|
1212
|
-
},
|
|
1213
|
-
targeting: {
|
|
1214
|
-
enabled: true,
|
|
1215
|
-
showGrid: true,
|
|
1216
|
-
colors: {
|
|
1217
|
-
area: 0x2f9ef7,
|
|
1218
|
-
edge: 0x1b6a98,
|
|
1219
|
-
cursor: 0xffd166
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
},
|
|
1223
|
-
targeting: {
|
|
1224
|
-
affects: "events", // "events" | "players" | "both"
|
|
1225
|
-
allowEmptyTarget: true
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
|
-
```
|
|
1229
|
-
|
|
1230
|
-
### Custom Targeting Resolver (optional)
|
|
1231
|
-
|
|
1232
|
-
If you prefer to compute targeting from your own skill schema, use `getTargeting`:
|
|
1233
|
-
|
|
1234
|
-
```ts
|
|
1235
|
-
export default provideActionBattle({
|
|
1236
|
-
skills: {
|
|
1237
|
-
getTargeting(skill) {
|
|
1238
|
-
return skill?.targeting;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
});
|
|
1242
|
-
```
|