@rpgjs/action-battle 5.0.0-alpha.44 → 5.0.0-beta.10

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +19 -0
  3. package/README.md +392 -22
  4. package/dist/{ai.server.d.ts → client/ai.server.d.ts} +90 -28
  5. package/dist/client/animations.d.ts +16 -0
  6. package/dist/{client.d.ts → client/client.d.ts} +3 -2
  7. package/dist/{config.d.ts → client/config.d.ts} +2 -0
  8. package/dist/client/core/attack-profile.d.ts +9 -0
  9. package/dist/client/core/attack-runtime.d.ts +20 -0
  10. package/dist/client/core/context.d.ts +5 -0
  11. package/dist/client/core/defaults.d.ts +81 -0
  12. package/dist/client/core/enemy-attack-profiles.d.ts +6 -0
  13. package/dist/client/core/equipment.d.ts +2 -0
  14. package/dist/client/core/hit-reaction.d.ts +5 -0
  15. package/dist/client/core/hit.d.ts +2 -0
  16. package/dist/client/enemies/factory.d.ts +7 -0
  17. package/dist/client/index.d.ts +21 -0
  18. package/dist/client/index.js +24 -31
  19. package/dist/client/index10.js +61 -0
  20. package/dist/client/index11.js +55 -0
  21. package/dist/client/index12.js +106 -0
  22. package/dist/client/index13.js +143 -0
  23. package/dist/client/index14.js +25 -0
  24. package/dist/client/index15.js +72 -0
  25. package/dist/client/index16.js +1343 -0
  26. package/dist/client/index17.js +13 -0
  27. package/dist/client/index18.js +60 -0
  28. package/dist/client/index19.js +10 -0
  29. package/dist/client/index2.js +30 -45
  30. package/dist/client/index20.js +504 -0
  31. package/dist/client/index3.js +45 -1288
  32. package/dist/client/index4.js +105 -330
  33. package/dist/client/index5.js +84 -291
  34. package/dist/client/index6.js +309 -95
  35. package/dist/client/index7.js +35 -59
  36. package/dist/client/index8.js +101 -54
  37. package/dist/client/index9.js +79 -30
  38. package/dist/{server.d.ts → client/server.d.ts} +12 -4
  39. package/dist/client/ui/state.d.ts +35 -0
  40. package/dist/server/ai.server.d.ts +569 -0
  41. package/dist/server/animations.d.ts +16 -0
  42. package/dist/server/config.d.ts +5 -0
  43. package/dist/server/core/attack-profile.d.ts +9 -0
  44. package/dist/server/core/attack-runtime.d.ts +20 -0
  45. package/dist/server/core/context.d.ts +5 -0
  46. package/dist/server/core/defaults.d.ts +81 -0
  47. package/dist/server/core/enemy-attack-profiles.d.ts +6 -0
  48. package/dist/server/core/equipment.d.ts +2 -0
  49. package/dist/server/core/hit-reaction.d.ts +5 -0
  50. package/dist/server/core/hit.d.ts +2 -0
  51. package/dist/server/enemies/factory.d.ts +7 -0
  52. package/dist/server/index.d.ts +21 -0
  53. package/dist/server/index.js +23 -31
  54. package/dist/server/index10.js +1342 -0
  55. package/dist/server/index11.js +37 -0
  56. package/dist/server/index12.js +60 -0
  57. package/dist/server/index13.js +13 -0
  58. package/dist/server/index14.js +503 -0
  59. package/dist/server/index15.js +10 -0
  60. package/dist/server/index2.js +59 -332
  61. package/dist/server/index3.js +29 -1286
  62. package/dist/server/index4.js +45 -53
  63. package/dist/server/index5.js +107 -29
  64. package/dist/server/index6.js +143 -0
  65. package/dist/server/index7.js +25 -0
  66. package/dist/server/index8.js +72 -0
  67. package/dist/server/index9.js +55 -0
  68. package/dist/server/server.d.ts +106 -0
  69. package/dist/server/targeting.d.ts +19 -0
  70. package/package.json +12 -12
  71. package/src/ai.server.spec.ts +120 -0
  72. package/src/ai.server.ts +515 -91
  73. package/src/animations.ts +149 -0
  74. package/src/canvas-engine-shim.ts +4 -0
  75. package/src/client.ts +130 -2
  76. package/src/components/action-bar.ce +5 -3
  77. package/src/components/attack-preview.ce +90 -0
  78. package/src/config.ts +61 -0
  79. package/src/core/attack-profile.spec.ts +118 -0
  80. package/src/core/attack-profile.ts +100 -0
  81. package/src/core/attack-runtime.spec.ts +103 -0
  82. package/src/core/attack-runtime.ts +83 -0
  83. package/src/core/context.ts +35 -0
  84. package/src/core/contracts.ts +126 -0
  85. package/src/core/defaults.ts +162 -0
  86. package/src/core/enemy-attack-profiles.spec.ts +35 -0
  87. package/src/core/enemy-attack-profiles.ts +103 -0
  88. package/src/core/equipment.spec.ts +37 -0
  89. package/src/core/equipment.ts +17 -0
  90. package/src/core/hit-reaction.spec.ts +43 -0
  91. package/src/core/hit-reaction.ts +70 -0
  92. package/src/core/hit.spec.ts +111 -0
  93. package/src/core/hit.ts +92 -0
  94. package/src/enemies/factory.ts +25 -0
  95. package/src/index.ts +94 -1
  96. package/src/server.ts +427 -93
  97. package/src/targeting.spec.ts +24 -0
  98. package/src/types/canvas-engine.d.ts +4 -0
  99. package/src/types.ts +148 -0
  100. package/src/ui/state.ts +57 -0
  101. package/dist/index.d.ts +0 -11
  102. package/dist/ui/state.d.ts +0 -18
  103. /package/dist/{targeting.d.ts → client/targeting.d.ts} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # @rpgjs/action-battle
2
+
3
+ ## 5.0.0-beta.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @rpgjs/client@5.0.0-beta.10
9
+ - @rpgjs/server@5.0.0-beta.10
10
+ - @rpgjs/vite@5.0.0-beta.10
11
+
12
+ ## 5.0.0-beta.9
13
+
14
+ ### Major Changes
15
+
16
+ - c456d25: beta.9
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [c456d25]
21
+ - @rpgjs/client@5.0.0-beta.9
22
+ - @rpgjs/common@5.0.0-beta.9
23
+ - @rpgjs/server@5.0.0-beta.9
24
+ - @rpgjs/vite@5.0.0-beta.9
25
+
26
+ ## 5.0.0-beta.8
27
+
28
+ ### Major Changes
29
+
30
+ - 35e7fa4: beta.8
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [35e7fa4]
35
+ - @rpgjs/client@5.0.0-beta.8
36
+ - @rpgjs/common@5.0.0-beta.8
37
+ - @rpgjs/server@5.0.0-beta.8
38
+ - @rpgjs/vite@5.0.0-beta.8
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2026 by Samuel Ronce
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
package/README.md CHANGED
@@ -7,6 +7,7 @@ The AI controller manages **behavior only** - all stats (HP, ATK, skills, items,
7
7
  ## Features
8
8
 
9
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
10
11
  - **Multiple Enemy Types**: Aggressive, Defensive, Ranged, Tank, Berserker
11
12
  - **Attack Patterns**: Melee, Combo, Charged, Zone, Dash Attack
12
13
  - **Skill Support**: AI can use any RPGJS skill
@@ -22,6 +23,90 @@ The AI controller manages **behavior only** - all stats (HP, ATK, skills, items,
22
23
  npm install @rpgjs/action-battle
23
24
  ```
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
+
25
110
  ## Quick Start
26
111
 
27
112
  ```typescript
@@ -146,6 +231,24 @@ new BattleAi(event, {
146
231
  AttackPattern.Combo,
147
232
  AttackPattern.DashAttack
148
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,
149
252
 
150
253
  // Patrol waypoints (for idle state)
151
254
  patrolWaypoints: [
@@ -157,13 +260,18 @@ new BattleAi(event, {
157
260
  groupBehavior: true,
158
261
 
159
262
  // Callback when AI is defeated
160
- onDefeated: (event, attacker) => {
263
+ onDefeated: ({ event, attacker }) => {
161
264
  const name = attacker?.name?.() ?? "Unknown";
162
- console.log(`${event.name()} was defeated by ${name}!`);
265
+ console.log(`${event.name} was defeated by ${name}!`);
163
266
  }
164
267
  });
165
268
  ```
166
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
+
167
275
  ## Enemy Types
168
276
 
169
277
  Types modify AI **behavior** (cooldowns, ranges, dodge), not stats:
@@ -496,6 +604,245 @@ The module handles player attacks via the `action` input:
496
604
  // Knockback force is based on equipped weapon's knockbackForce property
497
605
  ```
498
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
+
499
846
  ## Knockback System
500
847
 
501
848
  Knockback force is determined by the equipped weapon's `knockbackForce` property.
@@ -646,25 +993,57 @@ console.log(`Player knockback force: ${force}`);
646
993
 
647
994
  ## onDefeated Hook
648
995
 
649
- The `onDefeated` callback is triggered when an AI enemy is killed. It receives the defeated event and the player who landed the killing blow (if available). Use it to:
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:
650
1026
  - Award experience, gold, or items to the player
651
1027
  - Spawn loot drops
652
1028
  - Trigger events or cutscenes
653
1029
  - Update quest progress
654
- - Play death animations or sounds
1030
+ - Play death sounds
655
1031
 
656
1032
  ### Basic Usage
657
1033
 
658
1034
  ```typescript
659
1035
  new BattleAi(this, {
660
1036
  enemyType: EnemyType.Aggressive,
661
- onDefeated: (event, attacker) => {
1037
+ onDefeated: ({ event, attacker }) => {
662
1038
  const name = attacker?.name?.() ?? "Unknown";
663
- console.log(`${event.name()} was defeated by ${name}!`);
1039
+ console.log(`${event.name} was defeated by ${name}!`);
664
1040
  }
665
1041
  });
666
1042
  ```
667
1043
 
1044
+ The legacy `(event, attacker)` callback signature is still supported for
1045
+ two-argument callbacks.
1046
+
668
1047
  ### Award Rewards on Kill
669
1048
 
670
1049
  ```typescript
@@ -679,19 +1058,10 @@ function Goblin() {
679
1058
 
680
1059
  new BattleAi(this, {
681
1060
  enemyType: EnemyType.Aggressive,
682
- onDefeated: (event, attacker) => {
683
- if (!attacker) return;
684
-
685
- // Award gold
686
- attacker.gold += 25;
687
-
688
- // Award experience
689
- attacker.exp += 50;
690
-
691
- // Random loot drop
692
- if (Math.random() < 0.3) {
693
- attacker.addItem(HealthPotion);
694
- }
1061
+ rewards: {
1062
+ gold: 25,
1063
+ exp: 50,
1064
+ items: [{ item: HealthPotion, amount: 1, chance: 30 }]
695
1065
  }
696
1066
  });
697
1067
  }
@@ -703,7 +1073,7 @@ function Goblin() {
703
1073
 
704
1074
  ```typescript
705
1075
  new BattleAi(this, {
706
- onDefeated: (event, attacker) => {
1076
+ onDefeated: ({ event }) => {
707
1077
  const map = event.getCurrentMap();
708
1078
  if (!map) return;
709
1079
 
@@ -723,7 +1093,7 @@ new BattleAi(this, {
723
1093
  let killCount = 0;
724
1094
 
725
1095
  new BattleAi(this, {
726
- onDefeated: (event, attacker) => {
1096
+ onDefeated: () => {
727
1097
  killCount++;
728
1098
 
729
1099
  // Check quest progress
@@ -747,7 +1117,7 @@ function DragonBoss() {
747
1117
 
748
1118
  new BattleAi(this, {
749
1119
  enemyType: EnemyType.Tank,
750
- onDefeated: (event, attacker) => {
1120
+ onDefeated: ({ event }) => {
751
1121
  const map = event.getCurrentMap();
752
1122
 
753
1123
  // Announce victory