@rpgjs/action-battle 5.0.0-alpha.28
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/README.md +784 -0
- package/dist/ai.server.d.ts +482 -0
- package/dist/client/index.js +25 -0
- package/dist/client/index2.js +13 -0
- package/dist/client/index3.js +1129 -0
- package/dist/client/index4.js +137 -0
- package/dist/client.d.ts +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index2.js +138 -0
- package/dist/server/index3.js +1128 -0
- package/dist/server.d.ts +93 -0
- package/package.json +45 -0
- package/src/ai.server.ts +1430 -0
- package/src/client.ts +11 -0
- package/src/index.ts +21 -0
- package/src/server.ts +227 -0
- package/vite.config.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
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
|
+
- **Multiple Enemy Types**: Aggressive, Defensive, Ranged, Tank, Berserker
|
|
11
|
+
- **Attack Patterns**: Melee, Combo, Charged, Zone, Dash Attack
|
|
12
|
+
- **Skill Support**: AI can use any RPGJS skill
|
|
13
|
+
- **Dodge System**: Enemies can dodge and counter-attack
|
|
14
|
+
- **Group Behavior**: Enemies coordinate attacks and formations
|
|
15
|
+
- **Patrol System**: Waypoint-based patrolling
|
|
16
|
+
- **Knockback System**: Weapon-based knockback force
|
|
17
|
+
- **Hook System**: Customize hit behavior with `onBeforeHit` and `onAfterHit` hooks
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @rpgjs/action-battle
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createServer, RpgPlayer, RpgEvent, EventMode, ATK, PDEF, MAXHP } from "@rpgjs/server";
|
|
29
|
+
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
|
|
30
|
+
|
|
31
|
+
function GoblinEnemy() {
|
|
32
|
+
return {
|
|
33
|
+
name: "Goblin",
|
|
34
|
+
mode: EventMode.Scenario,
|
|
35
|
+
onInit() {
|
|
36
|
+
this.setGraphic("goblin");
|
|
37
|
+
|
|
38
|
+
// Configure stats using RPGJS API
|
|
39
|
+
this.hp = 80;
|
|
40
|
+
this.param[MAXHP] = 80;
|
|
41
|
+
this.param[ATK] = 15;
|
|
42
|
+
this.param[PDEF] = 5;
|
|
43
|
+
|
|
44
|
+
// Optional: Give skills
|
|
45
|
+
// this.learnSkill(Slash);
|
|
46
|
+
|
|
47
|
+
// Optional: Give items
|
|
48
|
+
// this.addItem(Potion, 2);
|
|
49
|
+
|
|
50
|
+
// Apply AI behavior
|
|
51
|
+
new BattleAi(this, {
|
|
52
|
+
enemyType: EnemyType.Aggressive
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Using RPGJS API for Stats
|
|
60
|
+
|
|
61
|
+
The AI uses the event's existing stats. Configure them in `onInit`:
|
|
62
|
+
|
|
63
|
+
### Health & Resources
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
this.hp = 100; // Current HP
|
|
67
|
+
this.param[MAXHP] = 100; // Max HP
|
|
68
|
+
this.sp = 50; // SP for skills
|
|
69
|
+
this.param[MAXSP] = 50; // Max SP
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Parameters
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { ATK, PDEF, SDEF } from "@rpgjs/server";
|
|
76
|
+
|
|
77
|
+
this.param[ATK] = 20; // Attack power
|
|
78
|
+
this.param[PDEF] = 10; // Physical defense
|
|
79
|
+
this.param[SDEF] = 8; // Special defense
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Skills
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Fireball, Heal } from './database/skills';
|
|
86
|
+
|
|
87
|
+
this.learnSkill(Fireball);
|
|
88
|
+
this.learnSkill(Heal);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Items & Equipment
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { Sword, Shield, Potion } from './database/items';
|
|
95
|
+
|
|
96
|
+
this.addItem(Potion, 3);
|
|
97
|
+
this.equip(Sword);
|
|
98
|
+
this.equip(Shield);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Classes
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { WarriorClass } from './database/classes';
|
|
105
|
+
|
|
106
|
+
this.setClass(WarriorClass);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### States
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { PoisonState } from './database/states';
|
|
113
|
+
|
|
114
|
+
this.addState(PoisonState);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## AI Configuration
|
|
118
|
+
|
|
119
|
+
The AI only controls **behavior**. All options are optional:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
new BattleAi(event, {
|
|
123
|
+
// Enemy type (affects behavior, not stats)
|
|
124
|
+
enemyType: EnemyType.Aggressive,
|
|
125
|
+
|
|
126
|
+
// Skill to use for attacks (optional)
|
|
127
|
+
attackSkill: Fireball,
|
|
128
|
+
|
|
129
|
+
// Timing
|
|
130
|
+
attackCooldown: 1000, // ms between attacks
|
|
131
|
+
|
|
132
|
+
// Ranges
|
|
133
|
+
visionRange: 150, // Detection radius
|
|
134
|
+
attackRange: 60, // Attack distance
|
|
135
|
+
|
|
136
|
+
// Dodge behavior
|
|
137
|
+
dodgeChance: 0.2, // 0-1 probability
|
|
138
|
+
dodgeCooldown: 2000, // ms between dodges
|
|
139
|
+
|
|
140
|
+
// Flee behavior
|
|
141
|
+
fleeThreshold: 0.2, // Flee when HP < 20%
|
|
142
|
+
|
|
143
|
+
// Attack patterns
|
|
144
|
+
attackPatterns: [
|
|
145
|
+
AttackPattern.Melee,
|
|
146
|
+
AttackPattern.Combo,
|
|
147
|
+
AttackPattern.DashAttack
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
// Patrol waypoints (for idle state)
|
|
151
|
+
patrolWaypoints: [
|
|
152
|
+
{ x: 100, y: 100 },
|
|
153
|
+
{ x: 300, y: 100 }
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
// Group coordination
|
|
157
|
+
groupBehavior: true,
|
|
158
|
+
|
|
159
|
+
// Callback when AI is defeated
|
|
160
|
+
onDefeated: (event) => {
|
|
161
|
+
console.log(`${event.name()} was defeated!`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Enemy Types
|
|
167
|
+
|
|
168
|
+
Types modify AI **behavior** (cooldowns, ranges, dodge), not stats:
|
|
169
|
+
|
|
170
|
+
| Type | Attack Speed | Dodge | Behavior |
|
|
171
|
+
|------|-------------|-------|----------|
|
|
172
|
+
| **Aggressive** | Fast | Low | Rushes player |
|
|
173
|
+
| **Defensive** | Slow | High | Counter-attacks |
|
|
174
|
+
| **Ranged** | Medium | Medium | Keeps distance |
|
|
175
|
+
| **Tank** | Slow | None | Stands ground |
|
|
176
|
+
| **Berserker** | Variable | Low | Faster when hurt |
|
|
177
|
+
|
|
178
|
+
## Using Skills for Attacks
|
|
179
|
+
|
|
180
|
+
The AI can use any RPGJS skill:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// In your database/skills.ts
|
|
184
|
+
import { Skill } from '@rpgjs/database';
|
|
185
|
+
|
|
186
|
+
@Skill({
|
|
187
|
+
name: 'Slash',
|
|
188
|
+
spCost: 5,
|
|
189
|
+
power: 25,
|
|
190
|
+
hitRate: 0.95
|
|
191
|
+
})
|
|
192
|
+
export class Slash {}
|
|
193
|
+
|
|
194
|
+
// In your event
|
|
195
|
+
onInit() {
|
|
196
|
+
this.hp = 100;
|
|
197
|
+
this.sp = 50;
|
|
198
|
+
this.learnSkill(Slash);
|
|
199
|
+
|
|
200
|
+
new BattleAi(this, {
|
|
201
|
+
attackSkill: Slash
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## AI States
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
┌─────────┐ detect ┌─────────┐ approach ┌─────────┐
|
|
210
|
+
│ Idle │ ──────────────> │ Alert │ ─────────────> │ Combat │
|
|
211
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
212
|
+
^ │
|
|
213
|
+
│ │
|
|
214
|
+
│ ┌─────────┐ │
|
|
215
|
+
│ │ Stunned │ <────── take damage ───────┤
|
|
216
|
+
│ └─────────┘ │
|
|
217
|
+
│ │ │
|
|
218
|
+
│ v │
|
|
219
|
+
│ ┌─────────┐ │
|
|
220
|
+
└───────────── │ Flee │ <────── HP low ────────────┘
|
|
221
|
+
└─────────┘
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Attack Patterns
|
|
225
|
+
|
|
226
|
+
| Pattern | Description |
|
|
227
|
+
|---------|-------------|
|
|
228
|
+
| **Melee** | Single attack |
|
|
229
|
+
| **Combo** | 2-3 rapid attacks |
|
|
230
|
+
| **Charged** | Wind-up, stronger attack |
|
|
231
|
+
| **Zone** | 360° area attack |
|
|
232
|
+
| **DashAttack** | Rush toward target then attack |
|
|
233
|
+
|
|
234
|
+
## Examples
|
|
235
|
+
|
|
236
|
+
### Basic Enemy
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
function Goblin() {
|
|
240
|
+
return {
|
|
241
|
+
name: "Goblin",
|
|
242
|
+
onInit() {
|
|
243
|
+
this.setGraphic("goblin");
|
|
244
|
+
this.hp = 50;
|
|
245
|
+
this.param[MAXHP] = 50;
|
|
246
|
+
this.param[ATK] = 10;
|
|
247
|
+
|
|
248
|
+
new BattleAi(this);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Mage with Skills
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
function DarkMage() {
|
|
258
|
+
return {
|
|
259
|
+
name: "Dark Mage",
|
|
260
|
+
onInit() {
|
|
261
|
+
this.setGraphic("mage");
|
|
262
|
+
this.hp = 60;
|
|
263
|
+
this.sp = 100;
|
|
264
|
+
this.param[MAXHP] = 60;
|
|
265
|
+
this.param[MAXSP] = 100;
|
|
266
|
+
this.param[ATK] = 25;
|
|
267
|
+
|
|
268
|
+
this.learnSkill(Fireball);
|
|
269
|
+
this.learnSkill(IceSpike);
|
|
270
|
+
|
|
271
|
+
new BattleAi(this, {
|
|
272
|
+
enemyType: EnemyType.Ranged,
|
|
273
|
+
attackSkill: Fireball,
|
|
274
|
+
visionRange: 200
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Boss
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
function DragonBoss() {
|
|
285
|
+
return {
|
|
286
|
+
name: "Dragon",
|
|
287
|
+
onInit() {
|
|
288
|
+
this.setGraphic("dragon");
|
|
289
|
+
this.hp = 500;
|
|
290
|
+
this.param[MAXHP] = 500;
|
|
291
|
+
this.param[ATK] = 50;
|
|
292
|
+
this.param[PDEF] = 30;
|
|
293
|
+
|
|
294
|
+
this.learnSkill(FireBreath);
|
|
295
|
+
this.learnSkill(TailSwipe);
|
|
296
|
+
|
|
297
|
+
new BattleAi(this, {
|
|
298
|
+
enemyType: EnemyType.Tank,
|
|
299
|
+
attackSkill: FireBreath,
|
|
300
|
+
attackPatterns: [
|
|
301
|
+
AttackPattern.Melee,
|
|
302
|
+
AttackPattern.Zone,
|
|
303
|
+
AttackPattern.Charged
|
|
304
|
+
],
|
|
305
|
+
fleeThreshold: 0.1,
|
|
306
|
+
visionRange: 250
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Patrol Guard
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
function PatrolGuard() {
|
|
317
|
+
return {
|
|
318
|
+
name: "Guard",
|
|
319
|
+
onInit() {
|
|
320
|
+
this.setGraphic("guard");
|
|
321
|
+
this.hp = 80;
|
|
322
|
+
this.param[MAXHP] = 80;
|
|
323
|
+
this.param[ATK] = 15;
|
|
324
|
+
|
|
325
|
+
new BattleAi(this, {
|
|
326
|
+
enemyType: EnemyType.Defensive,
|
|
327
|
+
patrolWaypoints: [
|
|
328
|
+
{ x: 100, y: 150 },
|
|
329
|
+
{ x: 300, y: 150 },
|
|
330
|
+
{ x: 300, y: 350 },
|
|
331
|
+
{ x: 100, y: 350 }
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Wolf Pack (Group)
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
function Wolf() {
|
|
343
|
+
return {
|
|
344
|
+
name: "Wolf",
|
|
345
|
+
onInit() {
|
|
346
|
+
this.setGraphic("wolf");
|
|
347
|
+
this.hp = 40;
|
|
348
|
+
this.param[MAXHP] = 40;
|
|
349
|
+
this.param[ATK] = 12;
|
|
350
|
+
|
|
351
|
+
new BattleAi(this, {
|
|
352
|
+
enemyType: EnemyType.Aggressive,
|
|
353
|
+
groupBehavior: true,
|
|
354
|
+
attackPatterns: [
|
|
355
|
+
AttackPattern.Melee,
|
|
356
|
+
AttackPattern.Combo
|
|
357
|
+
]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Complete Example with Weapons
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { createServer, RpgPlayer, RpgMap, EventMode, MAXHP, ATK, PDEF } from "@rpgjs/server";
|
|
368
|
+
import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
|
|
369
|
+
|
|
370
|
+
// Define weapons with knockback
|
|
371
|
+
const IronSword = {
|
|
372
|
+
id: 'iron-sword',
|
|
373
|
+
name: 'Iron Sword',
|
|
374
|
+
description: 'A reliable iron sword',
|
|
375
|
+
atk: 15,
|
|
376
|
+
knockbackForce: 40,
|
|
377
|
+
_type: 'weapon' as const,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const GiantMaul = {
|
|
381
|
+
id: 'giant-maul',
|
|
382
|
+
name: 'Giant Maul',
|
|
383
|
+
description: 'Massive hammer with devastating knockback',
|
|
384
|
+
atk: 30,
|
|
385
|
+
knockbackForce: 100,
|
|
386
|
+
_type: 'weapon' as const,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const GoblinDagger = {
|
|
390
|
+
id: 'goblin-dagger',
|
|
391
|
+
name: 'Goblin Dagger',
|
|
392
|
+
description: 'Small rusty dagger',
|
|
393
|
+
atk: 8,
|
|
394
|
+
knockbackForce: 20,
|
|
395
|
+
_type: 'weapon' as const,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Enemy with weapon
|
|
399
|
+
function GoblinWarrior() {
|
|
400
|
+
return {
|
|
401
|
+
name: "Goblin Warrior",
|
|
402
|
+
mode: EventMode.Scenario,
|
|
403
|
+
onInit() {
|
|
404
|
+
this.setGraphic("goblin");
|
|
405
|
+
|
|
406
|
+
// Stats
|
|
407
|
+
this.hp = 60;
|
|
408
|
+
this.param[MAXHP] = 60;
|
|
409
|
+
this.param[ATK] = 12;
|
|
410
|
+
this.param[PDEF] = 5;
|
|
411
|
+
|
|
412
|
+
// Equip weapon (knockbackForce: 20)
|
|
413
|
+
this.addItem(GoblinDagger);
|
|
414
|
+
this.equip(GoblinDagger.id);
|
|
415
|
+
|
|
416
|
+
// AI
|
|
417
|
+
new BattleAi(this, {
|
|
418
|
+
enemyType: EnemyType.Aggressive,
|
|
419
|
+
attackRange: 45
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Server setup
|
|
426
|
+
export default createServer({
|
|
427
|
+
providers: [
|
|
428
|
+
provideActionBattle(),
|
|
429
|
+
{
|
|
430
|
+
database: {
|
|
431
|
+
'iron-sword': IronSword,
|
|
432
|
+
'giant-maul': GiantMaul,
|
|
433
|
+
'goblin-dagger': GoblinDagger
|
|
434
|
+
},
|
|
435
|
+
player: {
|
|
436
|
+
onJoinMap(player: RpgPlayer, map: RpgMap) {
|
|
437
|
+
// Setup player stats
|
|
438
|
+
player.hp = 100;
|
|
439
|
+
player.param[MAXHP] = 100;
|
|
440
|
+
player.param[ATK] = 15;
|
|
441
|
+
|
|
442
|
+
// Give player a weapon with high knockback
|
|
443
|
+
player.addItem(GiantMaul);
|
|
444
|
+
player.equip(GiantMaul.id);
|
|
445
|
+
|
|
446
|
+
// Player attacks will now knock enemies back with force 100
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
maps: [
|
|
450
|
+
{
|
|
451
|
+
id: 'battle-map',
|
|
452
|
+
events: [{ event: GoblinWarrior() }]
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## API Reference
|
|
461
|
+
|
|
462
|
+
### BattleAi Methods
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// Get current health (uses event.hp)
|
|
466
|
+
ai.getHealth(): number
|
|
467
|
+
|
|
468
|
+
// Get max health (uses event.param[MAXHP])
|
|
469
|
+
ai.getMaxHealth(): number
|
|
470
|
+
|
|
471
|
+
// Get current target
|
|
472
|
+
ai.getTarget(): RpgPlayer | null
|
|
473
|
+
|
|
474
|
+
// Get current AI state
|
|
475
|
+
ai.getState(): AiState
|
|
476
|
+
|
|
477
|
+
// Get enemy type
|
|
478
|
+
ai.getEnemyType(): EnemyType
|
|
479
|
+
|
|
480
|
+
// Handle damage (called automatically)
|
|
481
|
+
ai.takeDamage(attacker: RpgPlayer): boolean
|
|
482
|
+
|
|
483
|
+
// Clean up AI instance
|
|
484
|
+
ai.destroy(): void
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Player Combat
|
|
488
|
+
|
|
489
|
+
The module handles player attacks via the `action` input:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// Player presses action key -> attack animation + hitbox
|
|
493
|
+
// Hitbox detects enemy -> applyPlayerHitToEvent(player, event)
|
|
494
|
+
// Damage uses RPGJS formula: target.applyDamage(attacker)
|
|
495
|
+
// Knockback force is based on equipped weapon's knockbackForce property
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Knockback System
|
|
499
|
+
|
|
500
|
+
Knockback force is determined by the equipped weapon's `knockbackForce` property.
|
|
501
|
+
|
|
502
|
+
### Creating Weapons with Knockback
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// Light weapon - low knockback
|
|
506
|
+
const Dagger = {
|
|
507
|
+
id: 'dagger',
|
|
508
|
+
name: 'Iron Dagger',
|
|
509
|
+
atk: 10,
|
|
510
|
+
knockbackForce: 20,
|
|
511
|
+
_type: 'weapon' as const,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Heavy weapon - high knockback
|
|
515
|
+
const Warhammer = {
|
|
516
|
+
id: 'warhammer',
|
|
517
|
+
name: 'War Hammer',
|
|
518
|
+
atk: 30,
|
|
519
|
+
knockbackForce: 100,
|
|
520
|
+
_type: 'weapon' as const,
|
|
521
|
+
};
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Default Knockback
|
|
525
|
+
|
|
526
|
+
If no weapon is equipped or the weapon doesn't have `knockbackForce`, the default value is used:
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { DEFAULT_KNOCKBACK } from "@rpgjs/action-battle/server";
|
|
530
|
+
|
|
531
|
+
console.log(DEFAULT_KNOCKBACK.force); // 50
|
|
532
|
+
console.log(DEFAULT_KNOCKBACK.duration); // 300ms
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Hook System
|
|
536
|
+
|
|
537
|
+
Customize hit behavior using hooks. Available on both player-to-enemy and enemy-to-player hits.
|
|
538
|
+
|
|
539
|
+
### HitResult Interface
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
interface HitResult {
|
|
543
|
+
damage: number; // Damage dealt
|
|
544
|
+
knockbackForce: number; // Knockback force (from weapon)
|
|
545
|
+
knockbackDuration: number; // Knockback duration in ms
|
|
546
|
+
defeated: boolean; // Whether target was defeated
|
|
547
|
+
attacker: RpgPlayer | RpgEvent;
|
|
548
|
+
target: RpgPlayer | RpgEvent;
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Using Hooks with applyPlayerHitToEvent
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
import { applyPlayerHitToEvent } from "@rpgjs/action-battle/server";
|
|
556
|
+
|
|
557
|
+
// In your custom attack handler
|
|
558
|
+
const result = applyPlayerHitToEvent(player, event, {
|
|
559
|
+
onBeforeHit(hitResult) {
|
|
560
|
+
// Modify knockback for armored enemies
|
|
561
|
+
if ((hitResult.target as any).hasState?.('armored')) {
|
|
562
|
+
hitResult.knockbackForce *= 0.5;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Critical hit - double knockback
|
|
566
|
+
if (Math.random() < 0.1) {
|
|
567
|
+
hitResult.knockbackForce *= 2;
|
|
568
|
+
console.log('Critical hit!');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return hitResult; // Must return modified result
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
onAfterHit(hitResult) {
|
|
575
|
+
// Award gold on kill
|
|
576
|
+
if (hitResult.defeated) {
|
|
577
|
+
(hitResult.attacker as any).gold += 10;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Apply poison on hit (30% chance)
|
|
581
|
+
if (Math.random() < 0.3) {
|
|
582
|
+
(hitResult.target as any).addState?.('poison');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Play custom sound
|
|
586
|
+
playSound('hit');
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Custom Attack Implementation
|
|
592
|
+
|
|
593
|
+
Override the default attack to add custom hooks:
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
import {
|
|
597
|
+
applyPlayerHitToEvent,
|
|
598
|
+
DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
599
|
+
getPlayerWeaponKnockbackForce
|
|
600
|
+
} from "@rpgjs/action-battle/server";
|
|
601
|
+
|
|
602
|
+
// Custom attack with hooks
|
|
603
|
+
function customAttack(player: RpgPlayer) {
|
|
604
|
+
player.setAnimation('attack', 1);
|
|
605
|
+
|
|
606
|
+
const direction = player.getDirection();
|
|
607
|
+
const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[direction] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
|
|
608
|
+
|
|
609
|
+
const hitboxes = [{
|
|
610
|
+
x: player.x() + hitboxConfig.offsetX,
|
|
611
|
+
y: player.y() + hitboxConfig.offsetY,
|
|
612
|
+
width: hitboxConfig.width,
|
|
613
|
+
height: hitboxConfig.height
|
|
614
|
+
}];
|
|
615
|
+
|
|
616
|
+
const map = player.getCurrentMap();
|
|
617
|
+
map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
|
|
618
|
+
next(hits) {
|
|
619
|
+
hits.forEach((hit) => {
|
|
620
|
+
if (hit instanceof RpgEvent) {
|
|
621
|
+
applyPlayerHitToEvent(player, hit, {
|
|
622
|
+
onBeforeHit(result) {
|
|
623
|
+
// Custom modifications
|
|
624
|
+
return result;
|
|
625
|
+
},
|
|
626
|
+
onAfterHit(result) {
|
|
627
|
+
// Custom effects
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Getting Weapon Knockback Force
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { getPlayerWeaponKnockbackForce } from "@rpgjs/action-battle/server";
|
|
641
|
+
|
|
642
|
+
const force = getPlayerWeaponKnockbackForce(player);
|
|
643
|
+
console.log(`Player knockback force: ${force}`);
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## onDefeated Hook
|
|
647
|
+
|
|
648
|
+
The `onDefeated` callback is triggered when an AI enemy is killed. Use it to:
|
|
649
|
+
- Award experience, gold, or items to the player
|
|
650
|
+
- Spawn loot drops
|
|
651
|
+
- Trigger events or cutscenes
|
|
652
|
+
- Update quest progress
|
|
653
|
+
- Play death animations or sounds
|
|
654
|
+
|
|
655
|
+
### Basic Usage
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
new BattleAi(this, {
|
|
659
|
+
enemyType: EnemyType.Aggressive,
|
|
660
|
+
onDefeated: (event) => {
|
|
661
|
+
console.log(`${event.name()} was defeated!`);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Award Rewards on Kill
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
function Goblin() {
|
|
670
|
+
return {
|
|
671
|
+
name: "Goblin",
|
|
672
|
+
onInit() {
|
|
673
|
+
this.setGraphic("goblin");
|
|
674
|
+
this.hp = 50;
|
|
675
|
+
this.param[MAXHP] = 50;
|
|
676
|
+
this.param[ATK] = 10;
|
|
677
|
+
|
|
678
|
+
new BattleAi(this, {
|
|
679
|
+
enemyType: EnemyType.Aggressive,
|
|
680
|
+
onDefeated: (event) => {
|
|
681
|
+
// Find the player who killed this enemy
|
|
682
|
+
const map = event.getCurrentMap();
|
|
683
|
+
const players = map?.getPlayersIn() || [];
|
|
684
|
+
|
|
685
|
+
players.forEach(player => {
|
|
686
|
+
// Award gold
|
|
687
|
+
player.gold += 25;
|
|
688
|
+
|
|
689
|
+
// Award experience
|
|
690
|
+
player.exp += 50;
|
|
691
|
+
|
|
692
|
+
// Random loot drop
|
|
693
|
+
if (Math.random() < 0.3) {
|
|
694
|
+
player.addItem(HealthPotion);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Spawn Loot on Death
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
new BattleAi(this, {
|
|
708
|
+
onDefeated: (event) => {
|
|
709
|
+
const map = event.getCurrentMap();
|
|
710
|
+
if (!map) return;
|
|
711
|
+
|
|
712
|
+
// Spawn loot at enemy position
|
|
713
|
+
map.createDynamicEvent({
|
|
714
|
+
x: event.x(),
|
|
715
|
+
y: event.y(),
|
|
716
|
+
event: LootChest({ items: [GoldCoin, HealthPotion] })
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Track Kill Count
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
let killCount = 0;
|
|
726
|
+
|
|
727
|
+
new BattleAi(this, {
|
|
728
|
+
onDefeated: (event) => {
|
|
729
|
+
killCount++;
|
|
730
|
+
|
|
731
|
+
// Check quest progress
|
|
732
|
+
if (killCount >= 10) {
|
|
733
|
+
triggerQuestComplete('slay_goblins');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Boss Death Event
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
function DragonBoss() {
|
|
743
|
+
return {
|
|
744
|
+
name: "Ancient Dragon",
|
|
745
|
+
onInit() {
|
|
746
|
+
this.setGraphic("dragon");
|
|
747
|
+
this.hp = 1000;
|
|
748
|
+
this.param[MAXHP] = 1000;
|
|
749
|
+
|
|
750
|
+
new BattleAi(this, {
|
|
751
|
+
enemyType: EnemyType.Tank,
|
|
752
|
+
onDefeated: (event) => {
|
|
753
|
+
const map = event.getCurrentMap();
|
|
754
|
+
|
|
755
|
+
// Announce victory
|
|
756
|
+
map?.getPlayersIn()?.forEach(player => {
|
|
757
|
+
player.showNotification({
|
|
758
|
+
message: "The Ancient Dragon has been slain!",
|
|
759
|
+
time: 5000
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Reward all participants
|
|
763
|
+
player.gold += 1000;
|
|
764
|
+
player.exp += 5000;
|
|
765
|
+
player.addItem(DragonScale);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Open dungeon exit
|
|
769
|
+
map?.setTileProperty(exitX, exitY, { passable: true });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## Visual Feedback
|
|
778
|
+
|
|
779
|
+
Automatic feedback:
|
|
780
|
+
|
|
781
|
+
- **Flash Effect**: Red flash when taking damage
|
|
782
|
+
- **Damage Numbers**: Floating damage text
|
|
783
|
+
- **Attack Animation**: Triggers `attack` animation
|
|
784
|
+
- **Knockback**: Entities pushed back based on weapon `knockbackForce`
|