@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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +19 -0
- package/README.md +392 -22
- package/dist/{ai.server.d.ts → client/ai.server.d.ts} +90 -28
- package/dist/client/animations.d.ts +16 -0
- package/dist/{client.d.ts → client/client.d.ts} +3 -2
- package/dist/{config.d.ts → client/config.d.ts} +2 -0
- package/dist/client/core/attack-profile.d.ts +9 -0
- package/dist/client/core/attack-runtime.d.ts +20 -0
- package/dist/client/core/context.d.ts +5 -0
- package/dist/client/core/defaults.d.ts +81 -0
- package/dist/client/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/client/core/equipment.d.ts +2 -0
- package/dist/client/core/hit-reaction.d.ts +5 -0
- package/dist/client/core/hit.d.ts +2 -0
- package/dist/client/enemies/factory.d.ts +7 -0
- package/dist/client/index.d.ts +21 -0
- package/dist/client/index.js +24 -31
- package/dist/client/index10.js +61 -0
- package/dist/client/index11.js +55 -0
- package/dist/client/index12.js +106 -0
- package/dist/client/index13.js +143 -0
- package/dist/client/index14.js +25 -0
- package/dist/client/index15.js +72 -0
- package/dist/client/index16.js +1343 -0
- package/dist/client/index17.js +13 -0
- package/dist/client/index18.js +60 -0
- package/dist/client/index19.js +10 -0
- package/dist/client/index2.js +30 -45
- package/dist/client/index20.js +504 -0
- package/dist/client/index3.js +45 -1288
- package/dist/client/index4.js +105 -330
- package/dist/client/index5.js +84 -291
- package/dist/client/index6.js +309 -95
- package/dist/client/index7.js +35 -59
- package/dist/client/index8.js +101 -54
- package/dist/client/index9.js +79 -30
- package/dist/{server.d.ts → client/server.d.ts} +12 -4
- package/dist/client/ui/state.d.ts +35 -0
- package/dist/server/ai.server.d.ts +569 -0
- package/dist/server/animations.d.ts +16 -0
- package/dist/server/config.d.ts +5 -0
- package/dist/server/core/attack-profile.d.ts +9 -0
- package/dist/server/core/attack-runtime.d.ts +20 -0
- package/dist/server/core/context.d.ts +5 -0
- package/dist/server/core/defaults.d.ts +81 -0
- package/dist/server/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/server/core/equipment.d.ts +2 -0
- package/dist/server/core/hit-reaction.d.ts +5 -0
- package/dist/server/core/hit.d.ts +2 -0
- package/dist/server/enemies/factory.d.ts +7 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.js +23 -31
- package/dist/server/index10.js +1342 -0
- package/dist/server/index11.js +37 -0
- package/dist/server/index12.js +60 -0
- package/dist/server/index13.js +13 -0
- package/dist/server/index14.js +503 -0
- package/dist/server/index15.js +10 -0
- package/dist/server/index2.js +59 -332
- package/dist/server/index3.js +29 -1286
- package/dist/server/index4.js +45 -53
- package/dist/server/index5.js +107 -29
- package/dist/server/index6.js +143 -0
- package/dist/server/index7.js +25 -0
- package/dist/server/index8.js +72 -0
- package/dist/server/index9.js +55 -0
- package/dist/server/server.d.ts +106 -0
- package/dist/server/targeting.d.ts +19 -0
- package/package.json +12 -12
- package/src/ai.server.spec.ts +120 -0
- package/src/ai.server.ts +515 -91
- package/src/animations.ts +149 -0
- package/src/canvas-engine-shim.ts +4 -0
- package/src/client.ts +130 -2
- package/src/components/action-bar.ce +5 -3
- package/src/components/attack-preview.ce +90 -0
- package/src/config.ts +61 -0
- package/src/core/attack-profile.spec.ts +118 -0
- package/src/core/attack-profile.ts +100 -0
- package/src/core/attack-runtime.spec.ts +103 -0
- package/src/core/attack-runtime.ts +83 -0
- package/src/core/context.ts +35 -0
- package/src/core/contracts.ts +126 -0
- package/src/core/defaults.ts +162 -0
- package/src/core/enemy-attack-profiles.spec.ts +35 -0
- package/src/core/enemy-attack-profiles.ts +103 -0
- package/src/core/equipment.spec.ts +37 -0
- package/src/core/equipment.ts +17 -0
- package/src/core/hit-reaction.spec.ts +43 -0
- package/src/core/hit-reaction.ts +70 -0
- package/src/core/hit.spec.ts +111 -0
- package/src/core/hit.ts +92 -0
- package/src/enemies/factory.ts +25 -0
- package/src/index.ts +94 -1
- package/src/server.ts +427 -93
- package/src/targeting.spec.ts +24 -0
- package/src/types/canvas-engine.d.ts +4 -0
- package/src/types.ts +148 -0
- package/src/ui/state.ts +57 -0
- package/dist/index.d.ts +0 -11
- package/dist/ui/state.d.ts +0 -18
- /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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
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: (
|
|
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
|
|
1120
|
+
onDefeated: ({ event }) => {
|
|
751
1121
|
const map = event.getCurrentMap();
|
|
752
1122
|
|
|
753
1123
|
// Announce victory
|