@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 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`