@rpgjs/action-battle 5.0.0-beta.6 → 5.0.0-beta.8
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 +15 -0
- package/README.md +46 -22
- package/dist/ai.server.d.ts +38 -2
- package/dist/client/index16.js +91 -29
- package/dist/index.d.ts +1 -1
- package/dist/server/index10.js +91 -29
- package/dist/server/index2.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/ui/state.d.ts +5 -5
- package/package.json +5 -5
- package/src/ai.server.spec.ts +120 -0
- package/src/ai.server.ts +190 -13
- package/src/index.ts +12 -1
- package/src/server.ts +12 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @rpgjs/action-battle
|
|
2
|
+
|
|
3
|
+
## 5.0.0-beta.8
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 35e7fa4: beta.8
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [35e7fa4]
|
|
12
|
+
- @rpgjs/client@5.0.0-beta.8
|
|
13
|
+
- @rpgjs/common@5.0.0-beta.8
|
|
14
|
+
- @rpgjs/server@5.0.0-beta.8
|
|
15
|
+
- @rpgjs/vite@5.0.0-beta.8
|
package/README.md
CHANGED
|
@@ -260,7 +260,7 @@ new BattleAi(event, {
|
|
|
260
260
|
groupBehavior: true,
|
|
261
261
|
|
|
262
262
|
// Callback when AI is defeated
|
|
263
|
-
onDefeated: (event, attacker) => {
|
|
263
|
+
onDefeated: ({ event, attacker }) => {
|
|
264
264
|
const name = attacker?.name?.() ?? "Unknown";
|
|
265
265
|
console.log(`${event.name()} was defeated by ${name}!`);
|
|
266
266
|
}
|
|
@@ -839,8 +839,9 @@ new BattleAi(this, {
|
|
|
839
839
|
});
|
|
840
840
|
```
|
|
841
841
|
|
|
842
|
-
`waitEnd: true`
|
|
843
|
-
|
|
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.
|
|
844
845
|
|
|
845
846
|
## Knockback System
|
|
846
847
|
|
|
@@ -992,25 +993,57 @@ console.log(`Player knockback force: ${force}`);
|
|
|
992
993
|
|
|
993
994
|
## onDefeated Hook
|
|
994
995
|
|
|
995
|
-
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:
|
|
996
1026
|
- Award experience, gold, or items to the player
|
|
997
1027
|
- Spawn loot drops
|
|
998
1028
|
- Trigger events or cutscenes
|
|
999
1029
|
- Update quest progress
|
|
1000
|
-
- Play death
|
|
1030
|
+
- Play death sounds
|
|
1001
1031
|
|
|
1002
1032
|
### Basic Usage
|
|
1003
1033
|
|
|
1004
1034
|
```typescript
|
|
1005
1035
|
new BattleAi(this, {
|
|
1006
1036
|
enemyType: EnemyType.Aggressive,
|
|
1007
|
-
onDefeated: (event, attacker) => {
|
|
1037
|
+
onDefeated: ({ event, attacker }) => {
|
|
1008
1038
|
const name = attacker?.name?.() ?? "Unknown";
|
|
1009
1039
|
console.log(`${event.name()} was defeated by ${name}!`);
|
|
1010
1040
|
}
|
|
1011
1041
|
});
|
|
1012
1042
|
```
|
|
1013
1043
|
|
|
1044
|
+
The legacy `(event, attacker)` callback signature is still supported for
|
|
1045
|
+
two-argument callbacks.
|
|
1046
|
+
|
|
1014
1047
|
### Award Rewards on Kill
|
|
1015
1048
|
|
|
1016
1049
|
```typescript
|
|
@@ -1025,19 +1058,10 @@ function Goblin() {
|
|
|
1025
1058
|
|
|
1026
1059
|
new BattleAi(this, {
|
|
1027
1060
|
enemyType: EnemyType.Aggressive,
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
attacker.gold += 25;
|
|
1033
|
-
|
|
1034
|
-
// Award experience
|
|
1035
|
-
attacker.exp += 50;
|
|
1036
|
-
|
|
1037
|
-
// Random loot drop
|
|
1038
|
-
if (Math.random() < 0.3) {
|
|
1039
|
-
attacker.addItem(HealthPotion);
|
|
1040
|
-
}
|
|
1061
|
+
rewards: {
|
|
1062
|
+
gold: 25,
|
|
1063
|
+
exp: 50,
|
|
1064
|
+
items: [{ item: HealthPotion, amount: 1, chance: 30 }]
|
|
1041
1065
|
}
|
|
1042
1066
|
});
|
|
1043
1067
|
}
|
|
@@ -1049,7 +1073,7 @@ function Goblin() {
|
|
|
1049
1073
|
|
|
1050
1074
|
```typescript
|
|
1051
1075
|
new BattleAi(this, {
|
|
1052
|
-
onDefeated: (event
|
|
1076
|
+
onDefeated: ({ event }) => {
|
|
1053
1077
|
const map = event.getCurrentMap();
|
|
1054
1078
|
if (!map) return;
|
|
1055
1079
|
|
|
@@ -1069,7 +1093,7 @@ new BattleAi(this, {
|
|
|
1069
1093
|
let killCount = 0;
|
|
1070
1094
|
|
|
1071
1095
|
new BattleAi(this, {
|
|
1072
|
-
onDefeated: (
|
|
1096
|
+
onDefeated: () => {
|
|
1073
1097
|
killCount++;
|
|
1074
1098
|
|
|
1075
1099
|
// Check quest progress
|
|
@@ -1093,7 +1117,7 @@ function DragonBoss() {
|
|
|
1093
1117
|
|
|
1094
1118
|
new BattleAi(this, {
|
|
1095
1119
|
enemyType: EnemyType.Tank,
|
|
1096
|
-
onDefeated: (event
|
|
1120
|
+
onDefeated: ({ event }) => {
|
|
1097
1121
|
const map = event.getCurrentMap();
|
|
1098
1122
|
|
|
1099
1123
|
// Announce victory
|
package/dist/ai.server.d.ts
CHANGED
|
@@ -5,7 +5,31 @@ import { NormalizedActionBattleHitReactionProfile, ActionBattleAnimationOptions
|
|
|
5
5
|
type RpgEventWithBattleAi = RpgEvent & {
|
|
6
6
|
battleAi?: BattleAi;
|
|
7
7
|
};
|
|
8
|
-
export interface
|
|
8
|
+
export interface BattleAiRewardItem {
|
|
9
|
+
item?: any;
|
|
10
|
+
itemId?: string;
|
|
11
|
+
amount?: number;
|
|
12
|
+
chance?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface BattleAiRewards {
|
|
15
|
+
exp?: number;
|
|
16
|
+
gold?: number;
|
|
17
|
+
items?: Array<BattleAiRewardItem | string>;
|
|
18
|
+
showNotification?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface BattleAiDefeatReward {
|
|
21
|
+
readonly awarded: boolean;
|
|
22
|
+
giveTo(player?: RpgPlayer | null): void;
|
|
23
|
+
}
|
|
24
|
+
export interface BattleAiDefeatedContext {
|
|
25
|
+
event: RpgEvent;
|
|
26
|
+
attacker?: RpgPlayer;
|
|
27
|
+
reward: BattleAiDefeatReward;
|
|
28
|
+
remove: () => void;
|
|
29
|
+
}
|
|
30
|
+
export type BattleAiDefeatedCallback = (context: BattleAiDefeatedContext) => void;
|
|
31
|
+
export type BattleAiLegacyDefeatedCallback = (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
32
|
+
export interface BattleAiBaseOptions {
|
|
9
33
|
enemyType?: EnemyType;
|
|
10
34
|
attackCooldown?: number;
|
|
11
35
|
visionRange?: number;
|
|
@@ -35,8 +59,16 @@ export interface BattleAiOptions {
|
|
|
35
59
|
};
|
|
36
60
|
behaviorKey?: string;
|
|
37
61
|
animations?: ActionBattleAnimationOptions;
|
|
62
|
+
rewards?: BattleAiRewards;
|
|
63
|
+
autoAwardRewards?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface BattleAiOptions extends BattleAiBaseOptions {
|
|
38
66
|
/** Callback called when the AI is defeated */
|
|
39
|
-
onDefeated?:
|
|
67
|
+
onDefeated?: BattleAiDefeatedCallback;
|
|
68
|
+
}
|
|
69
|
+
export interface BattleAiLegacyOptions extends BattleAiBaseOptions {
|
|
70
|
+
/** @deprecated Use the context callback signature instead. */
|
|
71
|
+
onDefeated?: BattleAiLegacyDefeatedCallback;
|
|
40
72
|
}
|
|
41
73
|
/**
|
|
42
74
|
* Hit result data returned after applying damage
|
|
@@ -272,6 +304,9 @@ export declare class BattleAi {
|
|
|
272
304
|
private damageCheckInterval;
|
|
273
305
|
private isMovingToTarget;
|
|
274
306
|
private onDefeatedCallback?;
|
|
307
|
+
private rewards?;
|
|
308
|
+
private autoAwardRewards;
|
|
309
|
+
private defeated;
|
|
275
310
|
private lastFacingDirection;
|
|
276
311
|
private behaviorScore;
|
|
277
312
|
private behaviorMode;
|
|
@@ -315,6 +350,7 @@ export declare class BattleAi {
|
|
|
315
350
|
* ```
|
|
316
351
|
*/
|
|
317
352
|
constructor(event: RpgEventWithBattleAi, options?: BattleAiOptions);
|
|
353
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiLegacyOptions);
|
|
318
354
|
/**
|
|
319
355
|
* Apply enemy type-specific behavior modifiers
|
|
320
356
|
*
|
package/dist/client/index16.js
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
import { isActionBattleEntityInvincible, setActionBattleInvincibility } from "./index2.js";
|
|
2
2
|
import { getActionBattleOptions } from "./index4.js";
|
|
3
|
-
import { getActionBattleAnimationRemovalDelay, playActionBattleAnimation } from "./index10.js";
|
|
3
|
+
import { getActionBattleAnimationRemovalDelay, playActionBattleAnimation, resolveActionBattleAnimation } from "./index10.js";
|
|
4
4
|
import { resolveActionBattleHitboxSpeed, scheduleActionBattleStartup } from "./index11.js";
|
|
5
5
|
import { getActionBattleSystems } from "./index14.js";
|
|
6
6
|
import { normalizeActionBattleEnemyAttackProfiles } from "./index15.js";
|
|
7
7
|
//#region src/ai.server.ts
|
|
8
8
|
var MAXHP = null;
|
|
9
9
|
var RpgPlayer = null;
|
|
10
|
+
var normalizeRewardItem = (item) => {
|
|
11
|
+
if (typeof item === "string") return {
|
|
12
|
+
itemId: item,
|
|
13
|
+
amount: 1,
|
|
14
|
+
chance: 100
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
...item,
|
|
18
|
+
amount: item.amount ?? 1,
|
|
19
|
+
chance: item.chance ?? 100
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
var getRewardItemRef = (item) => item.item ?? item.itemId;
|
|
23
|
+
var getPlayerMap = (player) => {
|
|
24
|
+
return typeof player.getCurrentMap === "function" ? player.getCurrentMap() : void 0;
|
|
25
|
+
};
|
|
26
|
+
var getRewardItemName = (inventoryItem, itemRef) => {
|
|
27
|
+
if (inventoryItem && typeof inventoryItem.name === "function") return inventoryItem.name();
|
|
28
|
+
if (inventoryItem?.name) return inventoryItem.name;
|
|
29
|
+
if (typeof itemRef === "string") return itemRef;
|
|
30
|
+
if (itemRef?.name) return itemRef.name;
|
|
31
|
+
if (itemRef?.id) return itemRef.id;
|
|
32
|
+
return "item";
|
|
33
|
+
};
|
|
34
|
+
var createDefeatReward = (rewards) => {
|
|
35
|
+
let awarded = false;
|
|
36
|
+
return {
|
|
37
|
+
get awarded() {
|
|
38
|
+
return awarded;
|
|
39
|
+
},
|
|
40
|
+
giveTo(player) {
|
|
41
|
+
if (!player || awarded || !rewards) return;
|
|
42
|
+
awarded = true;
|
|
43
|
+
const exp = rewards.exp ?? 0;
|
|
44
|
+
const gold = rewards.gold ?? 0;
|
|
45
|
+
if (exp > 0) player.exp += exp;
|
|
46
|
+
if (gold > 0) player.gold += gold;
|
|
47
|
+
if (rewards.showNotification && (exp > 0 || gold > 0)) player.showNotification(`You won ${exp} experience and ${gold} gold`);
|
|
48
|
+
for (const rawItem of rewards.items ?? []) {
|
|
49
|
+
const item = normalizeRewardItem(rawItem);
|
|
50
|
+
const itemRef = getRewardItemRef(item);
|
|
51
|
+
if (!itemRef) continue;
|
|
52
|
+
if (Math.random() * 100 >= (item.chance ?? 100)) continue;
|
|
53
|
+
const amount = item.amount ?? 1;
|
|
54
|
+
const inventoryItem = player.addItem(itemRef, amount);
|
|
55
|
+
if (rewards.showNotification) {
|
|
56
|
+
const itemData = typeof itemRef === "string" ? getPlayerMap(player)?.database?.()?.[itemRef] : void 0;
|
|
57
|
+
player.showNotification(`You won ${amount} ${getRewardItemName(inventoryItem, itemRef)}`, { icon: itemData?.icon });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
10
63
|
/**
|
|
11
64
|
* AI Debug Logger
|
|
12
65
|
*
|
|
@@ -178,6 +231,9 @@ var BattleAi = class {
|
|
|
178
231
|
damageCheckInterval = 2e3;
|
|
179
232
|
isMovingToTarget = false;
|
|
180
233
|
onDefeatedCallback;
|
|
234
|
+
rewards;
|
|
235
|
+
autoAwardRewards = true;
|
|
236
|
+
defeated = false;
|
|
181
237
|
lastFacingDirection = null;
|
|
182
238
|
behaviorScore = 50;
|
|
183
239
|
behaviorMode = "tactical";
|
|
@@ -196,30 +252,6 @@ var BattleAi = class {
|
|
|
196
252
|
poise = 0;
|
|
197
253
|
hitstunMs = 150;
|
|
198
254
|
invincibilityMs = 250;
|
|
199
|
-
/**
|
|
200
|
-
* Create a new Battle AI Controller
|
|
201
|
-
*
|
|
202
|
-
* The AI controls behavior only. Stats should be set on the event
|
|
203
|
-
* using standard RPGJS methods (hp, param, learnSkill, etc.)
|
|
204
|
-
*
|
|
205
|
-
* @param event - The event to control
|
|
206
|
-
* @param options - AI behavior configuration
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```ts
|
|
210
|
-
* // In your event's onInit
|
|
211
|
-
* this.hp = 100;
|
|
212
|
-
* this.param[ATK] = 20;
|
|
213
|
-
* this.learnSkill(FireBall);
|
|
214
|
-
*
|
|
215
|
-
* new BattleAi(this, {
|
|
216
|
-
* enemyType: EnemyType.Ranged,
|
|
217
|
-
* attackSkill: FireBall,
|
|
218
|
-
* visionRange: 200,
|
|
219
|
-
* fleeThreshold: 0.2
|
|
220
|
-
* });
|
|
221
|
-
* ```
|
|
222
|
-
*/
|
|
223
255
|
constructor(event, options = {}) {
|
|
224
256
|
event.battleAi = this;
|
|
225
257
|
this.event = event;
|
|
@@ -241,6 +273,8 @@ var BattleAi = class {
|
|
|
241
273
|
this.patrolWaypoints = options.patrolWaypoints || [];
|
|
242
274
|
this.currentPatrolIndex = 0;
|
|
243
275
|
this.onDefeatedCallback = options.onDefeated;
|
|
276
|
+
this.rewards = options.rewards;
|
|
277
|
+
this.autoAwardRewards = options.autoAwardRewards ?? true;
|
|
244
278
|
if (options.behavior) {
|
|
245
279
|
this.behaviorEnabled = true;
|
|
246
280
|
if (options.behavior.baseScore !== void 0) this.behaviorScore = options.behavior.baseScore;
|
|
@@ -1080,6 +1114,7 @@ var BattleAi = class {
|
|
|
1080
1114
|
* The actual damage is applied externally via RPGJS API.
|
|
1081
1115
|
*/
|
|
1082
1116
|
takeDamage(attacker) {
|
|
1117
|
+
if (this.defeated) return true;
|
|
1083
1118
|
const raw = this.event.applyDamage(attacker);
|
|
1084
1119
|
return this.handleDamage(attacker, {
|
|
1085
1120
|
damage: raw.damage ?? 0,
|
|
@@ -1088,6 +1123,7 @@ var BattleAi = class {
|
|
|
1088
1123
|
});
|
|
1089
1124
|
}
|
|
1090
1125
|
handleDamage(attacker, damageResult) {
|
|
1126
|
+
if (this.defeated) return true;
|
|
1091
1127
|
const damage = damageResult.damage;
|
|
1092
1128
|
this.debugLog("damage", `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || "?"})`);
|
|
1093
1129
|
this.event.flash({
|
|
@@ -1124,11 +1160,37 @@ var BattleAi = class {
|
|
|
1124
1160
|
* and removes the event from the map.
|
|
1125
1161
|
*/
|
|
1126
1162
|
kill(attacker) {
|
|
1127
|
-
|
|
1128
|
-
|
|
1163
|
+
if (this.defeated) return;
|
|
1164
|
+
this.defeated = true;
|
|
1165
|
+
const dieAnimation = resolveActionBattleAnimation("die", this.event, this.animations, { attacker });
|
|
1166
|
+
const removeDelay = getActionBattleAnimationRemovalDelay(dieAnimation);
|
|
1167
|
+
const reward = createDefeatReward(this.rewards);
|
|
1168
|
+
let removed = false;
|
|
1169
|
+
const remove = () => {
|
|
1170
|
+
if (removed) return;
|
|
1171
|
+
removed = true;
|
|
1172
|
+
this.event.remove({
|
|
1173
|
+
reason: "defeated",
|
|
1174
|
+
data: { animation: dieAnimation },
|
|
1175
|
+
transition: dieAnimation ? {
|
|
1176
|
+
animation: dieAnimation.animationName,
|
|
1177
|
+
graphic: dieAnimation.graphic,
|
|
1178
|
+
duration: removeDelay
|
|
1179
|
+
} : void 0,
|
|
1180
|
+
timeoutMs: removeDelay
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
if (this.autoAwardRewards) reward.giveTo(attacker);
|
|
1184
|
+
const context = {
|
|
1185
|
+
event: this.event,
|
|
1186
|
+
attacker,
|
|
1187
|
+
reward,
|
|
1188
|
+
remove
|
|
1189
|
+
};
|
|
1190
|
+
if (this.onDefeatedCallback) if (this.onDefeatedCallback.length >= 2) this.onDefeatedCallback(this.event, attacker);
|
|
1191
|
+
else this.onDefeatedCallback(context);
|
|
1129
1192
|
this.destroy();
|
|
1130
|
-
|
|
1131
|
-
else this.event.remove();
|
|
1193
|
+
remove();
|
|
1132
1194
|
}
|
|
1133
1195
|
/**
|
|
1134
1196
|
* Get distance between entities
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionBattleOptions } from './types';
|
|
2
2
|
export { BattleAi, AiState, EnemyType, AttackPattern, AiDebug, DEFAULT_KNOCKBACK } from './ai.server';
|
|
3
|
-
export type { HitResult, ApplyHitHooks, BattleAiOptions } from './ai.server';
|
|
3
|
+
export type { HitResult, ApplyHitHooks, BattleAiOptions, BattleAiDefeatedCallback, BattleAiDefeatedContext, BattleAiDefeatReward, BattleAiLegacyDefeatedCallback, BattleAiLegacyOptions, BattleAiRewardItem, BattleAiRewards, } from './ai.server';
|
|
4
4
|
export type { ActionBattleAnimationContext, ActionBattleAnimationEntity, ActionBattleAnimationKey, ActionBattleAnimationOptions, ActionBattleAnimationResolver, ActionBattleAnimationResult, ActionBattleOptions, ActionBattleActionBarData, ActionBattleActionBarItem, ActionBattleActionBarSkill, ActionBattleSkillTargeting, ActionBattleSkillTargetingResolver, ActionBattleAttackOptions, ActionBattleUiOptions, ActionBattleUiActionBarOptions, ActionBattleUiTargetingOptions, ActionBattleAttackDirection, ActionBattleAttackHitboxConfig, ActionBattleAttackHitboxMap, ActionBattleAttackHitPolicy, ActionBattleAttackProfile, ActionBattleDebugOptions, ActionBattleHitReactionProfile, NormalizedActionBattleHitReactionProfile, NormalizedActionBattleAttackProfile, ActionBattleCombatOptions, ActionBattleSystemOptions, ActionBattleAiSystemOptions, } from './types';
|
|
5
5
|
export type { ActionBattleAiBehavior, ActionBattleAiContext, ActionBattleAiDecision, ActionBattleAttackContext, ActionBattleCombatSystem, ActionBattleDamageContext, ActionBattleDamageResult, ActionBattleDirection, ActionBattleEntity, ActionBattleHitContext, ActionBattleHitHooks, ActionBattleHitResult, ActionBattleHitbox, ActionBattleKnockbackContext, ActionBattleKnockbackResult, ActionBattleSystems, } from './core/contracts';
|
|
6
6
|
export { DEFAULT_ACTION_BATTLE_ATTACK_PROFILE, normalizeActionBattleAttackProfile, type ActionBattleAttackProfileFallbacks, } from './core/attack-profile';
|
package/dist/server/index10.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getActionBattleAnimationRemovalDelay, playActionBattleAnimation } from "./index2.js";
|
|
1
|
+
import { getActionBattleAnimationRemovalDelay, playActionBattleAnimation, resolveActionBattleAnimation } from "./index2.js";
|
|
2
2
|
import { isActionBattleEntityInvincible, setActionBattleInvincibility } from "./index3.js";
|
|
3
3
|
import { getActionBattleOptions } from "./index5.js";
|
|
4
4
|
import { getActionBattleSystems } from "./index7.js";
|
|
@@ -6,6 +6,59 @@ import { normalizeActionBattleEnemyAttackProfiles } from "./index8.js";
|
|
|
6
6
|
import { resolveActionBattleHitboxSpeed, scheduleActionBattleStartup } from "./index9.js";
|
|
7
7
|
import { MAXHP, RpgPlayer } from "@rpgjs/server";
|
|
8
8
|
//#region src/ai.server.ts
|
|
9
|
+
var normalizeRewardItem = (item) => {
|
|
10
|
+
if (typeof item === "string") return {
|
|
11
|
+
itemId: item,
|
|
12
|
+
amount: 1,
|
|
13
|
+
chance: 100
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
...item,
|
|
17
|
+
amount: item.amount ?? 1,
|
|
18
|
+
chance: item.chance ?? 100
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
var getRewardItemRef = (item) => item.item ?? item.itemId;
|
|
22
|
+
var getPlayerMap = (player) => {
|
|
23
|
+
return typeof player.getCurrentMap === "function" ? player.getCurrentMap() : void 0;
|
|
24
|
+
};
|
|
25
|
+
var getRewardItemName = (inventoryItem, itemRef) => {
|
|
26
|
+
if (inventoryItem && typeof inventoryItem.name === "function") return inventoryItem.name();
|
|
27
|
+
if (inventoryItem?.name) return inventoryItem.name;
|
|
28
|
+
if (typeof itemRef === "string") return itemRef;
|
|
29
|
+
if (itemRef?.name) return itemRef.name;
|
|
30
|
+
if (itemRef?.id) return itemRef.id;
|
|
31
|
+
return "item";
|
|
32
|
+
};
|
|
33
|
+
var createDefeatReward = (rewards) => {
|
|
34
|
+
let awarded = false;
|
|
35
|
+
return {
|
|
36
|
+
get awarded() {
|
|
37
|
+
return awarded;
|
|
38
|
+
},
|
|
39
|
+
giveTo(player) {
|
|
40
|
+
if (!player || awarded || !rewards) return;
|
|
41
|
+
awarded = true;
|
|
42
|
+
const exp = rewards.exp ?? 0;
|
|
43
|
+
const gold = rewards.gold ?? 0;
|
|
44
|
+
if (exp > 0) player.exp += exp;
|
|
45
|
+
if (gold > 0) player.gold += gold;
|
|
46
|
+
if (rewards.showNotification && (exp > 0 || gold > 0)) player.showNotification(`You won ${exp} experience and ${gold} gold`);
|
|
47
|
+
for (const rawItem of rewards.items ?? []) {
|
|
48
|
+
const item = normalizeRewardItem(rawItem);
|
|
49
|
+
const itemRef = getRewardItemRef(item);
|
|
50
|
+
if (!itemRef) continue;
|
|
51
|
+
if (Math.random() * 100 >= (item.chance ?? 100)) continue;
|
|
52
|
+
const amount = item.amount ?? 1;
|
|
53
|
+
const inventoryItem = player.addItem(itemRef, amount);
|
|
54
|
+
if (rewards.showNotification) {
|
|
55
|
+
const itemData = typeof itemRef === "string" ? getPlayerMap(player)?.database?.()?.[itemRef] : void 0;
|
|
56
|
+
player.showNotification(`You won ${amount} ${getRewardItemName(inventoryItem, itemRef)}`, { icon: itemData?.icon });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
9
62
|
/**
|
|
10
63
|
* AI Debug Logger
|
|
11
64
|
*
|
|
@@ -177,6 +230,9 @@ var BattleAi = class {
|
|
|
177
230
|
damageCheckInterval = 2e3;
|
|
178
231
|
isMovingToTarget = false;
|
|
179
232
|
onDefeatedCallback;
|
|
233
|
+
rewards;
|
|
234
|
+
autoAwardRewards = true;
|
|
235
|
+
defeated = false;
|
|
180
236
|
lastFacingDirection = null;
|
|
181
237
|
behaviorScore = 50;
|
|
182
238
|
behaviorMode = "tactical";
|
|
@@ -195,30 +251,6 @@ var BattleAi = class {
|
|
|
195
251
|
poise = 0;
|
|
196
252
|
hitstunMs = 150;
|
|
197
253
|
invincibilityMs = 250;
|
|
198
|
-
/**
|
|
199
|
-
* Create a new Battle AI Controller
|
|
200
|
-
*
|
|
201
|
-
* The AI controls behavior only. Stats should be set on the event
|
|
202
|
-
* using standard RPGJS methods (hp, param, learnSkill, etc.)
|
|
203
|
-
*
|
|
204
|
-
* @param event - The event to control
|
|
205
|
-
* @param options - AI behavior configuration
|
|
206
|
-
*
|
|
207
|
-
* @example
|
|
208
|
-
* ```ts
|
|
209
|
-
* // In your event's onInit
|
|
210
|
-
* this.hp = 100;
|
|
211
|
-
* this.param[ATK] = 20;
|
|
212
|
-
* this.learnSkill(FireBall);
|
|
213
|
-
*
|
|
214
|
-
* new BattleAi(this, {
|
|
215
|
-
* enemyType: EnemyType.Ranged,
|
|
216
|
-
* attackSkill: FireBall,
|
|
217
|
-
* visionRange: 200,
|
|
218
|
-
* fleeThreshold: 0.2
|
|
219
|
-
* });
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
254
|
constructor(event, options = {}) {
|
|
223
255
|
event.battleAi = this;
|
|
224
256
|
this.event = event;
|
|
@@ -240,6 +272,8 @@ var BattleAi = class {
|
|
|
240
272
|
this.patrolWaypoints = options.patrolWaypoints || [];
|
|
241
273
|
this.currentPatrolIndex = 0;
|
|
242
274
|
this.onDefeatedCallback = options.onDefeated;
|
|
275
|
+
this.rewards = options.rewards;
|
|
276
|
+
this.autoAwardRewards = options.autoAwardRewards ?? true;
|
|
243
277
|
if (options.behavior) {
|
|
244
278
|
this.behaviorEnabled = true;
|
|
245
279
|
if (options.behavior.baseScore !== void 0) this.behaviorScore = options.behavior.baseScore;
|
|
@@ -1079,6 +1113,7 @@ var BattleAi = class {
|
|
|
1079
1113
|
* The actual damage is applied externally via RPGJS API.
|
|
1080
1114
|
*/
|
|
1081
1115
|
takeDamage(attacker) {
|
|
1116
|
+
if (this.defeated) return true;
|
|
1082
1117
|
const raw = this.event.applyDamage(attacker);
|
|
1083
1118
|
return this.handleDamage(attacker, {
|
|
1084
1119
|
damage: raw.damage ?? 0,
|
|
@@ -1087,6 +1122,7 @@ var BattleAi = class {
|
|
|
1087
1122
|
});
|
|
1088
1123
|
}
|
|
1089
1124
|
handleDamage(attacker, damageResult) {
|
|
1125
|
+
if (this.defeated) return true;
|
|
1090
1126
|
const damage = damageResult.damage;
|
|
1091
1127
|
this.debugLog("damage", `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || "?"})`);
|
|
1092
1128
|
this.event.flash({
|
|
@@ -1123,11 +1159,37 @@ var BattleAi = class {
|
|
|
1123
1159
|
* and removes the event from the map.
|
|
1124
1160
|
*/
|
|
1125
1161
|
kill(attacker) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1162
|
+
if (this.defeated) return;
|
|
1163
|
+
this.defeated = true;
|
|
1164
|
+
const dieAnimation = resolveActionBattleAnimation("die", this.event, this.animations, { attacker });
|
|
1165
|
+
const removeDelay = getActionBattleAnimationRemovalDelay(dieAnimation);
|
|
1166
|
+
const reward = createDefeatReward(this.rewards);
|
|
1167
|
+
let removed = false;
|
|
1168
|
+
const remove = () => {
|
|
1169
|
+
if (removed) return;
|
|
1170
|
+
removed = true;
|
|
1171
|
+
this.event.remove({
|
|
1172
|
+
reason: "defeated",
|
|
1173
|
+
data: { animation: dieAnimation },
|
|
1174
|
+
transition: dieAnimation ? {
|
|
1175
|
+
animation: dieAnimation.animationName,
|
|
1176
|
+
graphic: dieAnimation.graphic,
|
|
1177
|
+
duration: removeDelay
|
|
1178
|
+
} : void 0,
|
|
1179
|
+
timeoutMs: removeDelay
|
|
1180
|
+
});
|
|
1181
|
+
};
|
|
1182
|
+
if (this.autoAwardRewards) reward.giveTo(attacker);
|
|
1183
|
+
const context = {
|
|
1184
|
+
event: this.event,
|
|
1185
|
+
attacker,
|
|
1186
|
+
reward,
|
|
1187
|
+
remove
|
|
1188
|
+
};
|
|
1189
|
+
if (this.onDefeatedCallback) if (this.onDefeatedCallback.length >= 2) this.onDefeatedCallback(this.event, attacker);
|
|
1190
|
+
else this.onDefeatedCallback(context);
|
|
1128
1191
|
this.destroy();
|
|
1129
|
-
|
|
1130
|
-
else this.event.remove();
|
|
1192
|
+
remove();
|
|
1131
1193
|
}
|
|
1132
1194
|
/**
|
|
1133
1195
|
* Get distance between entities
|
package/dist/server/index2.js
CHANGED
|
@@ -58,4 +58,4 @@ function getActionBattleAnimationRemovalDelay(animation) {
|
|
|
58
58
|
return animation.waitEnd ? 500 : 0;
|
|
59
59
|
}
|
|
60
60
|
//#endregion
|
|
61
|
-
export { getActionBattleAnimationRemovalDelay, playActionBattleAnimation };
|
|
61
|
+
export { getActionBattleAnimationRemovalDelay, playActionBattleAnimation, resolveActionBattleAnimation };
|
package/dist/server.d.ts
CHANGED
|
@@ -103,4 +103,4 @@ export { DEFAULT_ACTION_BATTLE_HIT_REACTION, isActionBattleEntityInvincible, nor
|
|
|
103
103
|
export { DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES, normalizeActionBattleEnemyAttackProfiles, type ActionBattleEnemyAttackProfileKey, type ActionBattleEnemyAttackProfileMap, type NormalizedActionBattleEnemyAttackProfileMap, } from './core/enemy-attack-profiles';
|
|
104
104
|
export { resolveActionBattleWeaponAttackProfile } from './core/equipment';
|
|
105
105
|
export { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType, } from './ai.server';
|
|
106
|
-
export type { ApplyHitHooks, BattleAiOptions, HitResult } from './ai.server';
|
|
106
|
+
export type { ApplyHitHooks, BattleAiDefeatedCallback, BattleAiDefeatedContext, BattleAiDefeatReward, BattleAiLegacyDefeatedCallback, BattleAiLegacyOptions, BattleAiOptions, BattleAiRewardItem, BattleAiRewards, HitResult, } from './ai.server';
|
package/dist/ui/state.d.ts
CHANGED
|
@@ -18,10 +18,10 @@ export interface ActionBattleTargetingState {
|
|
|
18
18
|
};
|
|
19
19
|
aoeMask: string[] | string;
|
|
20
20
|
}
|
|
21
|
-
export declare const actionBattleUiOptions:
|
|
22
|
-
export declare const actionBattleSkillOptions:
|
|
23
|
-
export declare const actionBattleTargetingState:
|
|
24
|
-
export declare const actionBattleAttackPreviewState:
|
|
21
|
+
export declare const actionBattleUiOptions: any;
|
|
22
|
+
export declare const actionBattleSkillOptions: any;
|
|
23
|
+
export declare const actionBattleTargetingState: any;
|
|
24
|
+
export declare const actionBattleAttackPreviewState: any;
|
|
25
25
|
export declare const setActionBattleOptions: (options?: ActionBattleOptions) => void;
|
|
26
26
|
export declare const startTargeting: (skill: ActionBattleActionBarSkill) => void;
|
|
27
27
|
export declare const stopTargeting: () => void;
|
|
@@ -31,5 +31,5 @@ export declare const startAttackPreview: (options: {
|
|
|
31
31
|
durationMs?: number;
|
|
32
32
|
color?: number;
|
|
33
33
|
accentColor?: number;
|
|
34
|
-
}) =>
|
|
34
|
+
}) => any;
|
|
35
35
|
export declare const stopAttackPreview: (id?: number) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/action-battle",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.8",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@canvasengine/presets": "*",
|
|
26
|
-
"@rpgjs/client": "5.0.0-beta.
|
|
27
|
-
"@rpgjs/common": "5.0.0-beta.
|
|
28
|
-
"@rpgjs/server": "5.0.0-beta.
|
|
29
|
-
"@rpgjs/vite": "5.0.0-beta.
|
|
26
|
+
"@rpgjs/client": "5.0.0-beta.8",
|
|
27
|
+
"@rpgjs/common": "5.0.0-beta.8",
|
|
28
|
+
"@rpgjs/server": "5.0.0-beta.8",
|
|
29
|
+
"@rpgjs/vite": "5.0.0-beta.8",
|
|
30
30
|
"canvasengine": "*"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { MAXHP } from "@rpgjs/server";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { BattleAi } from "./ai.server";
|
|
4
|
+
|
|
5
|
+
const createEvent = () => ({
|
|
6
|
+
id: "monster-1",
|
|
7
|
+
hp: 0,
|
|
8
|
+
param: {
|
|
9
|
+
[MAXHP]: 10,
|
|
10
|
+
},
|
|
11
|
+
attachShape: vi.fn(),
|
|
12
|
+
flash: vi.fn(),
|
|
13
|
+
showHit: vi.fn(),
|
|
14
|
+
setGraphicAnimation: vi.fn(),
|
|
15
|
+
stopMoveTo: vi.fn(),
|
|
16
|
+
getCurrentMap: vi.fn(() => ({})),
|
|
17
|
+
remove: vi.fn(),
|
|
18
|
+
x: vi.fn(() => 0),
|
|
19
|
+
y: vi.fn(() => 0),
|
|
20
|
+
direction: vi.fn(() => "down"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createPlayer = () => ({
|
|
24
|
+
id: "player-1",
|
|
25
|
+
exp: 0,
|
|
26
|
+
gold: 0,
|
|
27
|
+
addItem: vi.fn(() => ({ name: () => "Potion" })),
|
|
28
|
+
showNotification: vi.fn(),
|
|
29
|
+
getCurrentMap: vi.fn(() => ({
|
|
30
|
+
database: () => ({
|
|
31
|
+
potion: { icon: "potion-icon" },
|
|
32
|
+
}),
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("BattleAi defeat flow", () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.useRealTimers();
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("awards the attacker and requests a defeated remove transition", () => {
|
|
43
|
+
const event = createEvent();
|
|
44
|
+
const attacker = createPlayer();
|
|
45
|
+
const ai = new BattleAi(event as any, {
|
|
46
|
+
animations: {
|
|
47
|
+
die: {
|
|
48
|
+
animationName: "die",
|
|
49
|
+
repeat: 1,
|
|
50
|
+
delayMs: 700,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
rewards: {
|
|
54
|
+
exp: 25,
|
|
55
|
+
gold: 7,
|
|
56
|
+
items: [{ itemId: "potion", amount: 2, chance: 100 }],
|
|
57
|
+
showNotification: true,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(ai.handleDamage(attacker as any, { damage: 10, defeated: true })).toBe(true);
|
|
62
|
+
|
|
63
|
+
expect(attacker.exp).toBe(25);
|
|
64
|
+
expect(attacker.gold).toBe(7);
|
|
65
|
+
expect(attacker.addItem).toHaveBeenCalledWith("potion", 2);
|
|
66
|
+
expect(event.setGraphicAnimation).not.toHaveBeenCalledWith("die", 1);
|
|
67
|
+
expect(event.remove).toHaveBeenCalledWith({
|
|
68
|
+
reason: "defeated",
|
|
69
|
+
data: {
|
|
70
|
+
animation: expect.objectContaining({
|
|
71
|
+
animationName: "die",
|
|
72
|
+
delayMs: 700,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
transition: {
|
|
76
|
+
animation: "die",
|
|
77
|
+
graphic: undefined,
|
|
78
|
+
duration: 700,
|
|
79
|
+
},
|
|
80
|
+
timeoutMs: 700,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("supports the context onDefeated callback and manual reward control", () => {
|
|
85
|
+
const event = createEvent();
|
|
86
|
+
const attacker = createPlayer();
|
|
87
|
+
const onDefeated = vi.fn(({ reward }) => {
|
|
88
|
+
expect(reward.awarded).toBe(false);
|
|
89
|
+
reward.giveTo(attacker as any);
|
|
90
|
+
expect(reward.awarded).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
const ai = new BattleAi(event as any, {
|
|
93
|
+
autoAwardRewards: false,
|
|
94
|
+
rewards: {
|
|
95
|
+
exp: 10,
|
|
96
|
+
},
|
|
97
|
+
onDefeated,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
ai.handleDamage(attacker as any, { damage: 10, defeated: true });
|
|
101
|
+
|
|
102
|
+
expect(onDefeated).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
event,
|
|
105
|
+
attacker,
|
|
106
|
+
reward: expect.any(Object),
|
|
107
|
+
remove: expect.any(Function),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
expect(attacker.exp).toBe(10);
|
|
111
|
+
expect(event.remove).toHaveBeenCalledWith({
|
|
112
|
+
reason: "defeated",
|
|
113
|
+
data: {
|
|
114
|
+
animation: null,
|
|
115
|
+
},
|
|
116
|
+
transition: undefined,
|
|
117
|
+
timeoutMs: 0,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/ai.server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { MAXHP, RpgEvent, RpgPlayer } from "@rpgjs/server";
|
|
|
2
2
|
import {
|
|
3
3
|
getActionBattleAnimationRemovalDelay,
|
|
4
4
|
playActionBattleAnimation,
|
|
5
|
+
resolveActionBattleAnimation,
|
|
5
6
|
} from "./animations";
|
|
6
7
|
import { getActionBattleOptions } from "./config";
|
|
7
8
|
import { getActionBattleSystems } from "./core/context";
|
|
@@ -29,7 +30,42 @@ type RpgEventWithBattleAi = RpgEvent & {
|
|
|
29
30
|
battleAi?: BattleAi;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
export interface
|
|
33
|
+
export interface BattleAiRewardItem {
|
|
34
|
+
item?: any;
|
|
35
|
+
itemId?: string;
|
|
36
|
+
amount?: number;
|
|
37
|
+
chance?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BattleAiRewards {
|
|
41
|
+
exp?: number;
|
|
42
|
+
gold?: number;
|
|
43
|
+
items?: Array<BattleAiRewardItem | string>;
|
|
44
|
+
showNotification?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BattleAiDefeatReward {
|
|
48
|
+
readonly awarded: boolean;
|
|
49
|
+
giveTo(player?: RpgPlayer | null): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BattleAiDefeatedContext {
|
|
53
|
+
event: RpgEvent;
|
|
54
|
+
attacker?: RpgPlayer;
|
|
55
|
+
reward: BattleAiDefeatReward;
|
|
56
|
+
remove: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type BattleAiDefeatedCallback = (
|
|
60
|
+
context: BattleAiDefeatedContext
|
|
61
|
+
) => void;
|
|
62
|
+
|
|
63
|
+
export type BattleAiLegacyDefeatedCallback = (
|
|
64
|
+
event: RpgEvent,
|
|
65
|
+
attacker?: RpgPlayer
|
|
66
|
+
) => void;
|
|
67
|
+
|
|
68
|
+
export interface BattleAiBaseOptions {
|
|
33
69
|
enemyType?: EnemyType;
|
|
34
70
|
attackCooldown?: number;
|
|
35
71
|
visionRange?: number;
|
|
@@ -56,8 +92,18 @@ export interface BattleAiOptions {
|
|
|
56
92
|
};
|
|
57
93
|
behaviorKey?: string;
|
|
58
94
|
animations?: ActionBattleAnimationOptions;
|
|
95
|
+
rewards?: BattleAiRewards;
|
|
96
|
+
autoAwardRewards?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BattleAiOptions extends BattleAiBaseOptions {
|
|
59
100
|
/** Callback called when the AI is defeated */
|
|
60
|
-
onDefeated?:
|
|
101
|
+
onDefeated?: BattleAiDefeatedCallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BattleAiLegacyOptions extends BattleAiBaseOptions {
|
|
105
|
+
/** @deprecated Use the context callback signature instead. */
|
|
106
|
+
onDefeated?: BattleAiLegacyDefeatedCallback;
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
/**
|
|
@@ -94,6 +140,84 @@ export interface HitResult {
|
|
|
94
140
|
target: RpgPlayer | RpgEvent;
|
|
95
141
|
}
|
|
96
142
|
|
|
143
|
+
const normalizeRewardItem = (
|
|
144
|
+
item: BattleAiRewardItem | string
|
|
145
|
+
): BattleAiRewardItem => {
|
|
146
|
+
if (typeof item === "string") {
|
|
147
|
+
return { itemId: item, amount: 1, chance: 100 };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...item,
|
|
151
|
+
amount: item.amount ?? 1,
|
|
152
|
+
chance: item.chance ?? 100,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const getRewardItemRef = (item: BattleAiRewardItem) => item.item ?? item.itemId;
|
|
157
|
+
|
|
158
|
+
const getPlayerMap = (player: RpgPlayer) => {
|
|
159
|
+
return typeof (player as any).getCurrentMap === "function"
|
|
160
|
+
? (player as any).getCurrentMap()
|
|
161
|
+
: undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const getRewardItemName = (inventoryItem: any, itemRef: any): string => {
|
|
165
|
+
if (inventoryItem && typeof inventoryItem.name === "function") {
|
|
166
|
+
return inventoryItem.name();
|
|
167
|
+
}
|
|
168
|
+
if (inventoryItem?.name) return inventoryItem.name;
|
|
169
|
+
if (typeof itemRef === "string") return itemRef;
|
|
170
|
+
if (itemRef?.name) return itemRef.name;
|
|
171
|
+
if (itemRef?.id) return itemRef.id;
|
|
172
|
+
return "item";
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const createDefeatReward = (
|
|
176
|
+
rewards: BattleAiRewards | undefined
|
|
177
|
+
): BattleAiDefeatReward => {
|
|
178
|
+
let awarded = false;
|
|
179
|
+
return {
|
|
180
|
+
get awarded() {
|
|
181
|
+
return awarded;
|
|
182
|
+
},
|
|
183
|
+
giveTo(player?: RpgPlayer | null) {
|
|
184
|
+
if (!player || awarded || !rewards) return;
|
|
185
|
+
awarded = true;
|
|
186
|
+
|
|
187
|
+
const exp = rewards.exp ?? 0;
|
|
188
|
+
const gold = rewards.gold ?? 0;
|
|
189
|
+
if (exp > 0) player.exp += exp;
|
|
190
|
+
if (gold > 0) player.gold += gold;
|
|
191
|
+
|
|
192
|
+
if (rewards.showNotification && (exp > 0 || gold > 0)) {
|
|
193
|
+
player.showNotification(`You won ${exp} experience and ${gold} gold`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const rawItem of rewards.items ?? []) {
|
|
197
|
+
const item = normalizeRewardItem(rawItem);
|
|
198
|
+
const itemRef = getRewardItemRef(item);
|
|
199
|
+
if (!itemRef) continue;
|
|
200
|
+
if (Math.random() * 100 >= (item.chance ?? 100)) continue;
|
|
201
|
+
|
|
202
|
+
const amount = item.amount ?? 1;
|
|
203
|
+
const inventoryItem = player.addItem(itemRef, amount);
|
|
204
|
+
if (rewards.showNotification) {
|
|
205
|
+
const itemData =
|
|
206
|
+
typeof itemRef === "string"
|
|
207
|
+
? getPlayerMap(player)?.database?.()?.[itemRef]
|
|
208
|
+
: undefined;
|
|
209
|
+
player.showNotification(
|
|
210
|
+
`You won ${amount} ${getRewardItemName(inventoryItem, itemRef)}`,
|
|
211
|
+
{
|
|
212
|
+
icon: itemData?.icon,
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
97
221
|
/**
|
|
98
222
|
* Hook options for customizing hit behavior
|
|
99
223
|
*
|
|
@@ -340,7 +464,12 @@ export class BattleAi {
|
|
|
340
464
|
private isMovingToTarget: boolean = false;
|
|
341
465
|
|
|
342
466
|
// Callback when AI is defeated
|
|
343
|
-
private onDefeatedCallback?:
|
|
467
|
+
private onDefeatedCallback?:
|
|
468
|
+
| BattleAiDefeatedCallback
|
|
469
|
+
| BattleAiLegacyDefeatedCallback;
|
|
470
|
+
private rewards?: BattleAiRewards;
|
|
471
|
+
private autoAwardRewards: boolean = true;
|
|
472
|
+
private defeated: boolean = false;
|
|
344
473
|
|
|
345
474
|
// Direction hysteresis to prevent animation flickering
|
|
346
475
|
private lastFacingDirection: string | null = null;
|
|
@@ -390,9 +519,11 @@ export class BattleAi {
|
|
|
390
519
|
* });
|
|
391
520
|
* ```
|
|
392
521
|
*/
|
|
522
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiOptions);
|
|
523
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiLegacyOptions);
|
|
393
524
|
constructor(
|
|
394
525
|
event: RpgEventWithBattleAi,
|
|
395
|
-
options: BattleAiOptions = {}
|
|
526
|
+
options: BattleAiOptions | BattleAiLegacyOptions = {}
|
|
396
527
|
) {
|
|
397
528
|
event.battleAi = this;
|
|
398
529
|
this.event = event;
|
|
@@ -428,6 +559,8 @@ export class BattleAi {
|
|
|
428
559
|
|
|
429
560
|
// Initialize defeat callback
|
|
430
561
|
this.onDefeatedCallback = options.onDefeated;
|
|
562
|
+
this.rewards = options.rewards;
|
|
563
|
+
this.autoAwardRewards = options.autoAwardRewards ?? true;
|
|
431
564
|
|
|
432
565
|
// Behavior gauge settings
|
|
433
566
|
if (options.behavior) {
|
|
@@ -1593,6 +1726,7 @@ export class BattleAi {
|
|
|
1593
1726
|
* The actual damage is applied externally via RPGJS API.
|
|
1594
1727
|
*/
|
|
1595
1728
|
takeDamage(attacker: RpgPlayer): boolean {
|
|
1729
|
+
if (this.defeated) return true;
|
|
1596
1730
|
// Apply damage using RPGJS system
|
|
1597
1731
|
const raw = this.event.applyDamage(attacker);
|
|
1598
1732
|
return this.handleDamage(attacker, {
|
|
@@ -1608,6 +1742,7 @@ export class BattleAi {
|
|
|
1608
1742
|
reaction?: NormalizedActionBattleHitReactionProfile;
|
|
1609
1743
|
}
|
|
1610
1744
|
): boolean {
|
|
1745
|
+
if (this.defeated) return true;
|
|
1611
1746
|
const damage = damageResult.damage;
|
|
1612
1747
|
this.debugLog('damage', `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || '?'})`);
|
|
1613
1748
|
|
|
@@ -1660,7 +1795,10 @@ export class BattleAi {
|
|
|
1660
1795
|
* and removes the event from the map.
|
|
1661
1796
|
*/
|
|
1662
1797
|
private kill(attacker?: RpgPlayer) {
|
|
1663
|
-
|
|
1798
|
+
if (this.defeated) return;
|
|
1799
|
+
this.defeated = true;
|
|
1800
|
+
|
|
1801
|
+
const dieAnimation = resolveActionBattleAnimation(
|
|
1664
1802
|
"die",
|
|
1665
1803
|
this.event,
|
|
1666
1804
|
this.animations,
|
|
@@ -1669,18 +1807,57 @@ export class BattleAi {
|
|
|
1669
1807
|
}
|
|
1670
1808
|
);
|
|
1671
1809
|
const removeDelay = getActionBattleAnimationRemovalDelay(dieAnimation);
|
|
1810
|
+
const reward = createDefeatReward(this.rewards);
|
|
1811
|
+
let removed = false;
|
|
1812
|
+
const remove = () => {
|
|
1813
|
+
if (removed) return;
|
|
1814
|
+
removed = true;
|
|
1815
|
+
this.event.remove({
|
|
1816
|
+
reason: "defeated",
|
|
1817
|
+
data: {
|
|
1818
|
+
animation: dieAnimation,
|
|
1819
|
+
},
|
|
1820
|
+
transition: dieAnimation
|
|
1821
|
+
? {
|
|
1822
|
+
animation: dieAnimation.animationName,
|
|
1823
|
+
graphic: dieAnimation.graphic,
|
|
1824
|
+
duration: removeDelay,
|
|
1825
|
+
}
|
|
1826
|
+
: undefined,
|
|
1827
|
+
timeoutMs: removeDelay,
|
|
1828
|
+
});
|
|
1829
|
+
};
|
|
1672
1830
|
|
|
1673
|
-
|
|
1831
|
+
if (this.autoAwardRewards) {
|
|
1832
|
+
reward.giveTo(attacker);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const context: BattleAiDefeatedContext = {
|
|
1836
|
+
event: this.event,
|
|
1837
|
+
attacker,
|
|
1838
|
+
reward,
|
|
1839
|
+
remove,
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
// Call onDefeated hook before cleanup. One-argument callbacks receive the
|
|
1843
|
+
// newer context object; two-argument callbacks keep the legacy signature.
|
|
1674
1844
|
if (this.onDefeatedCallback) {
|
|
1675
|
-
this.onDefeatedCallback
|
|
1845
|
+
if (this.onDefeatedCallback.length >= 2) {
|
|
1846
|
+
(
|
|
1847
|
+
this.onDefeatedCallback as (
|
|
1848
|
+
event: RpgEvent,
|
|
1849
|
+
attacker?: RpgPlayer
|
|
1850
|
+
) => void
|
|
1851
|
+
)(this.event, attacker);
|
|
1852
|
+
} else {
|
|
1853
|
+
(this.onDefeatedCallback as (context: BattleAiDefeatedContext) => void)(
|
|
1854
|
+
context
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1676
1857
|
}
|
|
1677
|
-
|
|
1858
|
+
|
|
1678
1859
|
this.destroy();
|
|
1679
|
-
|
|
1680
|
-
this.schedule(() => this.event.remove(), removeDelay);
|
|
1681
|
-
} else {
|
|
1682
|
-
this.event.remove();
|
|
1683
|
-
}
|
|
1860
|
+
remove();
|
|
1684
1861
|
}
|
|
1685
1862
|
|
|
1686
1863
|
/**
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,18 @@ import type { ActionBattleOptions } from "./types";
|
|
|
7
7
|
export { BattleAi, AiState, EnemyType, AttackPattern, AiDebug, DEFAULT_KNOCKBACK } from "./ai.server";
|
|
8
8
|
|
|
9
9
|
// Types exports
|
|
10
|
-
export type {
|
|
10
|
+
export type {
|
|
11
|
+
HitResult,
|
|
12
|
+
ApplyHitHooks,
|
|
13
|
+
BattleAiOptions,
|
|
14
|
+
BattleAiDefeatedCallback,
|
|
15
|
+
BattleAiDefeatedContext,
|
|
16
|
+
BattleAiDefeatReward,
|
|
17
|
+
BattleAiLegacyDefeatedCallback,
|
|
18
|
+
BattleAiLegacyOptions,
|
|
19
|
+
BattleAiRewardItem,
|
|
20
|
+
BattleAiRewards,
|
|
21
|
+
} from "./ai.server";
|
|
11
22
|
export type {
|
|
12
23
|
ActionBattleAnimationContext,
|
|
13
24
|
ActionBattleAnimationEntity,
|
package/src/server.ts
CHANGED
|
@@ -817,4 +817,15 @@ export {
|
|
|
817
817
|
DEFAULT_KNOCKBACK,
|
|
818
818
|
EnemyType,
|
|
819
819
|
} from "./ai.server";
|
|
820
|
-
export type {
|
|
820
|
+
export type {
|
|
821
|
+
ApplyHitHooks,
|
|
822
|
+
BattleAiDefeatedCallback,
|
|
823
|
+
BattleAiDefeatedContext,
|
|
824
|
+
BattleAiDefeatReward,
|
|
825
|
+
BattleAiLegacyDefeatedCallback,
|
|
826
|
+
BattleAiLegacyOptions,
|
|
827
|
+
BattleAiOptions,
|
|
828
|
+
BattleAiRewardItem,
|
|
829
|
+
BattleAiRewards,
|
|
830
|
+
HitResult,
|
|
831
|
+
} from "./ai.server";
|