@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/src/ai.server.ts
CHANGED
|
@@ -1,9 +1,111 @@
|
|
|
1
1
|
import { MAXHP, RpgEvent, RpgPlayer } from "@rpgjs/server";
|
|
2
|
+
import {
|
|
3
|
+
getActionBattleAnimationRemovalDelay,
|
|
4
|
+
playActionBattleAnimation,
|
|
5
|
+
resolveActionBattleAnimation,
|
|
6
|
+
} from "./animations";
|
|
7
|
+
import { getActionBattleOptions } from "./config";
|
|
8
|
+
import { getActionBattleSystems } from "./core/context";
|
|
9
|
+
import {
|
|
10
|
+
isActionBattleEntityInvincible,
|
|
11
|
+
setActionBattleInvincibility,
|
|
12
|
+
} from "./core/hit-reaction";
|
|
13
|
+
import {
|
|
14
|
+
normalizeActionBattleEnemyAttackProfiles,
|
|
15
|
+
type ActionBattleEnemyAttackProfileMap,
|
|
16
|
+
type NormalizedActionBattleEnemyAttackProfileMap,
|
|
17
|
+
} from "./core/enemy-attack-profiles";
|
|
18
|
+
import {
|
|
19
|
+
resolveActionBattleHitboxSpeed,
|
|
20
|
+
scheduleActionBattleStartup,
|
|
21
|
+
} from "./core/attack-runtime";
|
|
22
|
+
import type { ActionBattleDamageResult } from "./core/contracts";
|
|
23
|
+
import type {
|
|
24
|
+
NormalizedActionBattleAttackProfile,
|
|
25
|
+
NormalizedActionBattleHitReactionProfile,
|
|
26
|
+
} from "./types";
|
|
27
|
+
import type { ActionBattleAnimationOptions } from "./types";
|
|
2
28
|
|
|
3
29
|
type RpgEventWithBattleAi = RpgEvent & {
|
|
4
|
-
battleAi
|
|
30
|
+
battleAi?: BattleAi;
|
|
5
31
|
};
|
|
6
32
|
|
|
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 {
|
|
69
|
+
enemyType?: EnemyType;
|
|
70
|
+
attackCooldown?: number;
|
|
71
|
+
visionRange?: number;
|
|
72
|
+
attackRange?: number;
|
|
73
|
+
dodgeChance?: number;
|
|
74
|
+
dodgeCooldown?: number;
|
|
75
|
+
fleeThreshold?: number;
|
|
76
|
+
attackSkill?: any;
|
|
77
|
+
attackPatterns?: AttackPattern[];
|
|
78
|
+
attackProfiles?: ActionBattleEnemyAttackProfileMap;
|
|
79
|
+
patrolWaypoints?: Array<{ x: number; y: number }>;
|
|
80
|
+
groupBehavior?: boolean;
|
|
81
|
+
moveToCooldown?: number;
|
|
82
|
+
retreatCooldown?: number;
|
|
83
|
+
poise?: number;
|
|
84
|
+
hitstunMs?: number;
|
|
85
|
+
invincibilityMs?: number;
|
|
86
|
+
behavior?: {
|
|
87
|
+
baseScore?: number;
|
|
88
|
+
updateInterval?: number;
|
|
89
|
+
minStateDuration?: number;
|
|
90
|
+
assaultThreshold?: number;
|
|
91
|
+
retreatThreshold?: number;
|
|
92
|
+
};
|
|
93
|
+
behaviorKey?: string;
|
|
94
|
+
animations?: ActionBattleAnimationOptions;
|
|
95
|
+
rewards?: BattleAiRewards;
|
|
96
|
+
autoAwardRewards?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BattleAiOptions extends BattleAiBaseOptions {
|
|
100
|
+
/** Callback called when the AI is defeated */
|
|
101
|
+
onDefeated?: BattleAiDefeatedCallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BattleAiLegacyOptions extends BattleAiBaseOptions {
|
|
105
|
+
/** @deprecated Use the context callback signature instead. */
|
|
106
|
+
onDefeated?: BattleAiLegacyDefeatedCallback;
|
|
107
|
+
}
|
|
108
|
+
|
|
7
109
|
/**
|
|
8
110
|
* Hit result data returned after applying damage
|
|
9
111
|
*
|
|
@@ -38,6 +140,84 @@ export interface HitResult {
|
|
|
38
140
|
target: RpgPlayer | RpgEvent;
|
|
39
141
|
}
|
|
40
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
|
+
|
|
41
221
|
/**
|
|
42
222
|
* Hook options for customizing hit behavior
|
|
43
223
|
*
|
|
@@ -99,7 +279,9 @@ export interface ApplyHitHooks {
|
|
|
99
279
|
*/
|
|
100
280
|
export const AiDebug = {
|
|
101
281
|
/** Enable/disable all AI debug logs */
|
|
102
|
-
enabled:
|
|
282
|
+
enabled:
|
|
283
|
+
((globalThis as { process?: { env?: Record<string, string> } }).process
|
|
284
|
+
?.env?.RPGJS_DEBUG_AI === "1") || false,
|
|
103
285
|
|
|
104
286
|
/** Filter logs to a specific event ID (null = all events) */
|
|
105
287
|
filterEventId: null as string | null,
|
|
@@ -243,21 +425,23 @@ export class BattleAi {
|
|
|
243
425
|
|
|
244
426
|
// Enemy type and behavior
|
|
245
427
|
private enemyType: EnemyType;
|
|
246
|
-
private attackCooldown: number;
|
|
247
|
-
private visionRange: number;
|
|
248
|
-
private attackRange: number;
|
|
428
|
+
private attackCooldown: number = 1000;
|
|
429
|
+
private visionRange: number = 150;
|
|
430
|
+
private attackRange: number = 60;
|
|
249
431
|
|
|
250
432
|
// Dodge system
|
|
251
|
-
private dodgeChance: number;
|
|
252
|
-
private dodgeCooldown: number;
|
|
433
|
+
private dodgeChance: number = 0.2;
|
|
434
|
+
private dodgeCooldown: number = 2000;
|
|
253
435
|
private lastDodgeTime: number = 0;
|
|
254
436
|
|
|
255
437
|
// Flee threshold (HP percentage)
|
|
256
|
-
private fleeThreshold: number;
|
|
438
|
+
private fleeThreshold: number = 0.2;
|
|
257
439
|
|
|
258
440
|
// Attack configuration
|
|
259
441
|
private attackSkill: any | null; // Skill to use for attacks
|
|
260
442
|
private attackPatterns: AttackPattern[];
|
|
443
|
+
private attackProfiles: NormalizedActionBattleEnemyAttackProfileMap;
|
|
444
|
+
private animations?: ActionBattleAnimationOptions;
|
|
261
445
|
private comboCount: number = 0;
|
|
262
446
|
private comboMax: number = 3;
|
|
263
447
|
private chargingAttack: boolean = false;
|
|
@@ -280,7 +464,12 @@ export class BattleAi {
|
|
|
280
464
|
private isMovingToTarget: boolean = false;
|
|
281
465
|
|
|
282
466
|
// Callback when AI is defeated
|
|
283
|
-
private onDefeatedCallback?:
|
|
467
|
+
private onDefeatedCallback?:
|
|
468
|
+
| BattleAiDefeatedCallback
|
|
469
|
+
| BattleAiLegacyDefeatedCallback;
|
|
470
|
+
private rewards?: BattleAiRewards;
|
|
471
|
+
private autoAwardRewards: boolean = true;
|
|
472
|
+
private defeated: boolean = false;
|
|
284
473
|
|
|
285
474
|
// Direction hysteresis to prevent animation flickering
|
|
286
475
|
private lastFacingDirection: string | null = null;
|
|
@@ -300,6 +489,11 @@ export class BattleAi {
|
|
|
300
489
|
private lastMoveToTime: number = 0;
|
|
301
490
|
private retreatCooldown: number = 600;
|
|
302
491
|
private lastRetreatTime: number = 0;
|
|
492
|
+
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
493
|
+
private behaviorKey?: string;
|
|
494
|
+
private poise: number = 0;
|
|
495
|
+
private hitstunMs: number = 150;
|
|
496
|
+
private invincibilityMs: number = 250;
|
|
303
497
|
|
|
304
498
|
/**
|
|
305
499
|
* Create a new Battle AI Controller
|
|
@@ -325,42 +519,26 @@ export class BattleAi {
|
|
|
325
519
|
* });
|
|
326
520
|
* ```
|
|
327
521
|
*/
|
|
522
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiOptions);
|
|
523
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiLegacyOptions);
|
|
328
524
|
constructor(
|
|
329
525
|
event: RpgEventWithBattleAi,
|
|
330
|
-
options: {
|
|
331
|
-
enemyType?: EnemyType;
|
|
332
|
-
attackCooldown?: number;
|
|
333
|
-
visionRange?: number;
|
|
334
|
-
attackRange?: number;
|
|
335
|
-
dodgeChance?: number;
|
|
336
|
-
dodgeCooldown?: number;
|
|
337
|
-
fleeThreshold?: number;
|
|
338
|
-
attackSkill?: any;
|
|
339
|
-
attackPatterns?: AttackPattern[];
|
|
340
|
-
patrolWaypoints?: Array<{ x: number; y: number }>;
|
|
341
|
-
groupBehavior?: boolean;
|
|
342
|
-
moveToCooldown?: number;
|
|
343
|
-
retreatCooldown?: number;
|
|
344
|
-
behavior?: {
|
|
345
|
-
baseScore?: number;
|
|
346
|
-
updateInterval?: number;
|
|
347
|
-
minStateDuration?: number;
|
|
348
|
-
assaultThreshold?: number;
|
|
349
|
-
retreatThreshold?: number;
|
|
350
|
-
};
|
|
351
|
-
/** Callback called when the AI is defeated */
|
|
352
|
-
onDefeated?: (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
353
|
-
} = {}
|
|
526
|
+
options: BattleAiOptions | BattleAiLegacyOptions = {}
|
|
354
527
|
) {
|
|
355
528
|
event.battleAi = this;
|
|
356
529
|
this.event = event;
|
|
357
530
|
|
|
358
531
|
// Set enemy type and apply behavior modifiers
|
|
359
532
|
this.enemyType = options.enemyType || EnemyType.Aggressive;
|
|
533
|
+
this.behaviorKey = options.behaviorKey ?? this.enemyType;
|
|
360
534
|
this.applyEnemyTypeBehavior(options);
|
|
361
535
|
|
|
362
536
|
// Store attack skill reference
|
|
363
537
|
this.attackSkill = options.attackSkill || null;
|
|
538
|
+
this.animations = {
|
|
539
|
+
...getActionBattleOptions().animations,
|
|
540
|
+
...options.animations,
|
|
541
|
+
};
|
|
364
542
|
|
|
365
543
|
// Initialize attack patterns
|
|
366
544
|
this.attackPatterns = options.attackPatterns || [
|
|
@@ -368,6 +546,9 @@ export class BattleAi {
|
|
|
368
546
|
AttackPattern.Combo,
|
|
369
547
|
AttackPattern.DashAttack
|
|
370
548
|
];
|
|
549
|
+
this.attackProfiles = normalizeActionBattleEnemyAttackProfiles(
|
|
550
|
+
options.attackProfiles
|
|
551
|
+
);
|
|
371
552
|
|
|
372
553
|
// Initialize group behavior
|
|
373
554
|
this.groupBehavior = options.groupBehavior || false;
|
|
@@ -378,6 +559,8 @@ export class BattleAi {
|
|
|
378
559
|
|
|
379
560
|
// Initialize defeat callback
|
|
380
561
|
this.onDefeatedCallback = options.onDefeated;
|
|
562
|
+
this.rewards = options.rewards;
|
|
563
|
+
this.autoAwardRewards = options.autoAwardRewards ?? true;
|
|
381
564
|
|
|
382
565
|
// Behavior gauge settings
|
|
383
566
|
if (options.behavior) {
|
|
@@ -405,11 +588,22 @@ export class BattleAi {
|
|
|
405
588
|
if (options.retreatCooldown !== undefined) {
|
|
406
589
|
this.retreatCooldown = options.retreatCooldown;
|
|
407
590
|
}
|
|
591
|
+
if (options.poise !== undefined) {
|
|
592
|
+
this.poise = Math.max(0, options.poise);
|
|
593
|
+
}
|
|
594
|
+
if (options.hitstunMs !== undefined) {
|
|
595
|
+
this.hitstunMs = Math.max(0, options.hitstunMs);
|
|
596
|
+
}
|
|
597
|
+
if (options.invincibilityMs !== undefined) {
|
|
598
|
+
this.invincibilityMs = Math.max(0, options.invincibilityMs);
|
|
599
|
+
}
|
|
408
600
|
|
|
409
601
|
// Setup AI systems
|
|
410
602
|
this.setupVision();
|
|
411
603
|
this.startAiBehaviorLoop();
|
|
412
|
-
this.
|
|
604
|
+
if (this.patrolWaypoints.length > 0) {
|
|
605
|
+
this.startPatrol();
|
|
606
|
+
}
|
|
413
607
|
|
|
414
608
|
this.debugLog('init', `AI created (type=${this.enemyType}, visionRange=${this.visionRange}, attackRange=${this.attackRange})`);
|
|
415
609
|
}
|
|
@@ -516,6 +710,9 @@ export class BattleAi {
|
|
|
516
710
|
* Change AI state with validated transitions
|
|
517
711
|
*/
|
|
518
712
|
private changeState(newState: AiState) {
|
|
713
|
+
if (newState === this.state) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
519
716
|
const validTransitions: Record<AiState, AiState[]> = {
|
|
520
717
|
[AiState.Idle]: [AiState.Alert, AiState.Combat],
|
|
521
718
|
[AiState.Alert]: [AiState.Idle, AiState.Combat],
|
|
@@ -587,6 +784,8 @@ export class BattleAi {
|
|
|
587
784
|
this.updateBehavior(currentTime);
|
|
588
785
|
}
|
|
589
786
|
|
|
787
|
+
this.applyCustomBehavior(currentTime);
|
|
788
|
+
|
|
590
789
|
// State-specific behavior
|
|
591
790
|
switch (this.state) {
|
|
592
791
|
case AiState.Idle:
|
|
@@ -620,6 +819,7 @@ export class BattleAi {
|
|
|
620
819
|
|
|
621
820
|
if (distance < 10) {
|
|
622
821
|
this.currentPatrolIndex = (this.currentPatrolIndex + 1) % this.patrolWaypoints.length;
|
|
822
|
+
this.startPatrol();
|
|
623
823
|
}
|
|
624
824
|
}
|
|
625
825
|
}
|
|
@@ -676,9 +876,10 @@ export class BattleAi {
|
|
|
676
876
|
// Try dodge
|
|
677
877
|
if (this.canDodge() && this.shouldDodge()) {
|
|
678
878
|
this.debugLog('combat', 'Attempting dodge');
|
|
679
|
-
this.
|
|
680
|
-
|
|
681
|
-
|
|
879
|
+
if (this.tryDodge()) {
|
|
880
|
+
this.isMovingToTarget = false;
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
682
883
|
}
|
|
683
884
|
|
|
684
885
|
if (this.behaviorEnabled) {
|
|
@@ -865,27 +1066,52 @@ export class BattleAi {
|
|
|
865
1066
|
*/
|
|
866
1067
|
private performMeleeAttack() {
|
|
867
1068
|
if (!this.target) return;
|
|
1069
|
+
const profile = this.getAttackProfile(AttackPattern.Melee);
|
|
868
1070
|
|
|
869
1071
|
this.faceTarget();
|
|
870
|
-
this.
|
|
1072
|
+
this.telegraphAttack(profile);
|
|
1073
|
+
playActionBattleAnimation("attack", this.event, this.animations, {
|
|
1074
|
+
target: this.target,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1078
|
+
this.executeMeleeAttack(profile, AttackPattern.Melee);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private executeMeleeAttack(
|
|
1083
|
+
profile: NormalizedActionBattleAttackProfile,
|
|
1084
|
+
pattern: AttackPattern
|
|
1085
|
+
) {
|
|
1086
|
+
if (!this.target) return;
|
|
1087
|
+
this.debugLog('attack', `Applying ${pattern} hit`);
|
|
871
1088
|
|
|
872
1089
|
// Use skill if available
|
|
873
1090
|
if (this.attackSkill) {
|
|
874
1091
|
try {
|
|
1092
|
+
playActionBattleAnimation("castSkill", this.event, this.animations, {
|
|
1093
|
+
skill: this.attackSkill,
|
|
1094
|
+
target: this.target,
|
|
1095
|
+
});
|
|
875
1096
|
this.event.useSkill(this.attackSkill, this.target);
|
|
876
1097
|
} catch (e) {
|
|
877
1098
|
// Skill failed (no SP, etc.) - fall back to basic attack
|
|
878
|
-
this.performBasicHitbox();
|
|
1099
|
+
this.performBasicHitbox(profile, pattern);
|
|
879
1100
|
}
|
|
880
1101
|
} else {
|
|
881
|
-
this.performBasicHitbox();
|
|
1102
|
+
this.performBasicHitbox(profile, pattern);
|
|
882
1103
|
}
|
|
883
1104
|
}
|
|
884
1105
|
|
|
885
1106
|
/**
|
|
886
1107
|
* Perform basic hitbox attack when no skill is set
|
|
887
1108
|
*/
|
|
888
|
-
private performBasicHitbox(
|
|
1109
|
+
private performBasicHitbox(
|
|
1110
|
+
profile: NormalizedActionBattleAttackProfile = this.getAttackProfile(
|
|
1111
|
+
AttackPattern.Melee
|
|
1112
|
+
),
|
|
1113
|
+
pattern: AttackPattern = AttackPattern.Melee
|
|
1114
|
+
) {
|
|
889
1115
|
if (!this.target) return;
|
|
890
1116
|
|
|
891
1117
|
const eventX = this.event.x();
|
|
@@ -907,11 +1133,13 @@ export class BattleAi {
|
|
|
907
1133
|
}];
|
|
908
1134
|
|
|
909
1135
|
const map = this.event.getCurrentMap();
|
|
910
|
-
map?.createMovingHitbox(hitboxes, {
|
|
911
|
-
|
|
912
|
-
|
|
1136
|
+
map?.createMovingHitbox(hitboxes, {
|
|
1137
|
+
speed: resolveActionBattleHitboxSpeed(profile, hitboxes.length),
|
|
1138
|
+
}).subscribe({
|
|
1139
|
+
next: (hits: any[]) => {
|
|
1140
|
+
hits.forEach((hit: any) => {
|
|
913
1141
|
if (hit instanceof RpgPlayer && hit !== this.event) {
|
|
914
|
-
this.applyHit(hit);
|
|
1142
|
+
this.applyHit(hit, undefined, profile, pattern);
|
|
915
1143
|
}
|
|
916
1144
|
});
|
|
917
1145
|
},
|
|
@@ -946,7 +1174,25 @@ export class BattleAi {
|
|
|
946
1174
|
* });
|
|
947
1175
|
* ```
|
|
948
1176
|
*/
|
|
949
|
-
private applyHit(
|
|
1177
|
+
private applyHit(
|
|
1178
|
+
target: RpgPlayer,
|
|
1179
|
+
hooks?: ApplyHitHooks,
|
|
1180
|
+
profile: NormalizedActionBattleAttackProfile = this.getAttackProfile(
|
|
1181
|
+
AttackPattern.Melee
|
|
1182
|
+
),
|
|
1183
|
+
pattern: AttackPattern = AttackPattern.Melee
|
|
1184
|
+
): HitResult {
|
|
1185
|
+
if (isActionBattleEntityInvincible(target)) {
|
|
1186
|
+
return {
|
|
1187
|
+
damage: 0,
|
|
1188
|
+
knockbackForce: 0,
|
|
1189
|
+
knockbackDuration: 0,
|
|
1190
|
+
defeated: false,
|
|
1191
|
+
attacker: this.event,
|
|
1192
|
+
target
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
950
1196
|
// Use RPGJS damage formula
|
|
951
1197
|
const { damage } = target.applyDamage(this.event as any);
|
|
952
1198
|
|
|
@@ -980,6 +1226,10 @@ export class BattleAi {
|
|
|
980
1226
|
cycles: 1
|
|
981
1227
|
});
|
|
982
1228
|
target.showHit(`-${hitResult.damage}`);
|
|
1229
|
+
setActionBattleInvincibility(
|
|
1230
|
+
target,
|
|
1231
|
+
profile.reaction.invincibilityMs
|
|
1232
|
+
);
|
|
983
1233
|
|
|
984
1234
|
// Apply knockback
|
|
985
1235
|
if (hitResult.knockbackForce > 0) {
|
|
@@ -1043,10 +1293,18 @@ export class BattleAi {
|
|
|
1043
1293
|
if (!this.target) return;
|
|
1044
1294
|
|
|
1045
1295
|
this.comboCount++;
|
|
1046
|
-
this.
|
|
1296
|
+
const profile = this.getAttackProfile(AttackPattern.Combo);
|
|
1297
|
+
this.faceTarget();
|
|
1298
|
+
this.telegraphAttack(profile);
|
|
1299
|
+
playActionBattleAnimation("attack", this.event, this.animations, {
|
|
1300
|
+
target: this.target,
|
|
1301
|
+
});
|
|
1302
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1303
|
+
this.executeMeleeAttack(profile, AttackPattern.Combo);
|
|
1304
|
+
});
|
|
1047
1305
|
|
|
1048
1306
|
if (this.comboCount < this.comboMax) {
|
|
1049
|
-
|
|
1307
|
+
this.schedule(() => {
|
|
1050
1308
|
if (this.target && this.state === AiState.Combat) {
|
|
1051
1309
|
this.performComboAttack();
|
|
1052
1310
|
} else {
|
|
@@ -1063,37 +1321,42 @@ export class BattleAi {
|
|
|
1063
1321
|
*/
|
|
1064
1322
|
private performChargedAttack() {
|
|
1065
1323
|
if (!this.target) return;
|
|
1324
|
+
const profile = this.getAttackProfile(AttackPattern.Charged);
|
|
1066
1325
|
|
|
1067
1326
|
this.chargingAttack = true;
|
|
1068
1327
|
this.faceTarget();
|
|
1069
|
-
this.
|
|
1328
|
+
this.telegraphAttack(profile);
|
|
1329
|
+
playActionBattleAnimation(
|
|
1330
|
+
"attack",
|
|
1331
|
+
this.event,
|
|
1332
|
+
this.animations,
|
|
1333
|
+
{
|
|
1334
|
+
target: this.target,
|
|
1335
|
+
},
|
|
1336
|
+
{ repeat: 2 }
|
|
1337
|
+
);
|
|
1070
1338
|
|
|
1071
|
-
|
|
1339
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1072
1340
|
if (!this.target || this.state !== AiState.Combat) {
|
|
1073
1341
|
this.chargingAttack = false;
|
|
1074
1342
|
return;
|
|
1075
1343
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
try {
|
|
1080
|
-
this.event.useSkill(this.attackSkill, this.target);
|
|
1081
|
-
} catch (e) {
|
|
1082
|
-
this.performBasicHitbox();
|
|
1083
|
-
}
|
|
1084
|
-
} else {
|
|
1085
|
-
this.performBasicHitbox();
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1344
|
+
this.executeMeleeAttack(profile, AttackPattern.Charged);
|
|
1345
|
+
});
|
|
1346
|
+
this.schedule(() => {
|
|
1088
1347
|
this.chargingAttack = false;
|
|
1089
|
-
},
|
|
1348
|
+
}, profile.totalDurationMs);
|
|
1090
1349
|
}
|
|
1091
1350
|
|
|
1092
1351
|
/**
|
|
1093
1352
|
* Perform zone attack (360 degrees)
|
|
1094
1353
|
*/
|
|
1095
1354
|
private performZoneAttack() {
|
|
1096
|
-
this.
|
|
1355
|
+
const profile = this.getAttackProfile(AttackPattern.Zone);
|
|
1356
|
+
this.telegraphAttack(profile);
|
|
1357
|
+
playActionBattleAnimation("attack", this.event, this.animations, {
|
|
1358
|
+
target: this.target ?? undefined,
|
|
1359
|
+
});
|
|
1097
1360
|
|
|
1098
1361
|
const eventX = this.event.x();
|
|
1099
1362
|
const eventY = this.event.y();
|
|
@@ -1112,15 +1375,19 @@ export class BattleAi {
|
|
|
1112
1375
|
});
|
|
1113
1376
|
});
|
|
1114
1377
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1378
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1379
|
+
const map = this.event.getCurrentMap();
|
|
1380
|
+
map?.createMovingHitbox(hitboxes, {
|
|
1381
|
+
speed: resolveActionBattleHitboxSpeed(profile, hitboxes.length),
|
|
1382
|
+
}).subscribe({
|
|
1383
|
+
next: (hits: any[]) => {
|
|
1384
|
+
hits.forEach((hit: any) => {
|
|
1385
|
+
if (hit instanceof RpgPlayer && hit !== this.event) {
|
|
1386
|
+
this.applyHit(hit, undefined, profile, AttackPattern.Zone);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
},
|
|
1390
|
+
});
|
|
1124
1391
|
});
|
|
1125
1392
|
}
|
|
1126
1393
|
|
|
@@ -1129,6 +1396,7 @@ export class BattleAi {
|
|
|
1129
1396
|
*/
|
|
1130
1397
|
private performDashAttack() {
|
|
1131
1398
|
if (!this.target) return;
|
|
1399
|
+
const profile = this.getAttackProfile(AttackPattern.DashAttack);
|
|
1132
1400
|
|
|
1133
1401
|
const dx = this.target.x() - this.event.x();
|
|
1134
1402
|
const dy = this.target.y() - this.event.y();
|
|
@@ -1140,12 +1408,43 @@ export class BattleAi {
|
|
|
1140
1408
|
const dirY = dy / dist;
|
|
1141
1409
|
|
|
1142
1410
|
this.faceTarget();
|
|
1143
|
-
this.
|
|
1411
|
+
this.telegraphAttack(profile);
|
|
1144
1412
|
|
|
1145
|
-
|
|
1413
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1146
1414
|
if (!this.target || this.state !== AiState.Combat) return;
|
|
1147
|
-
this.
|
|
1148
|
-
|
|
1415
|
+
this.event.dash({ x: dirX, y: dirY }, 10, 200);
|
|
1416
|
+
this.schedule(() => {
|
|
1417
|
+
if (!this.target || this.state !== AiState.Combat) return;
|
|
1418
|
+
this.executeMeleeAttack(profile, AttackPattern.DashAttack);
|
|
1419
|
+
}, 200);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private getAttackProfile(
|
|
1424
|
+
pattern: AttackPattern
|
|
1425
|
+
): NormalizedActionBattleAttackProfile {
|
|
1426
|
+
return this.attackProfiles[
|
|
1427
|
+
pattern as keyof NormalizedActionBattleEnemyAttackProfileMap
|
|
1428
|
+
] ?? this.attackProfiles.melee;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private telegraphAttack(profile: NormalizedActionBattleAttackProfile) {
|
|
1432
|
+
if (profile.startupMs <= 0) return;
|
|
1433
|
+
this.event.flash({
|
|
1434
|
+
type: 'tint',
|
|
1435
|
+
tint: 'white',
|
|
1436
|
+
duration: Math.min(profile.startupMs, 300),
|
|
1437
|
+
cycles: 1
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private scheduleAttackStartup(
|
|
1442
|
+
profile: NormalizedActionBattleAttackProfile,
|
|
1443
|
+
callback: () => void
|
|
1444
|
+
) {
|
|
1445
|
+
return scheduleActionBattleStartup(profile, callback, (scheduled, delay) =>
|
|
1446
|
+
this.schedule(scheduled, delay)
|
|
1447
|
+
);
|
|
1149
1448
|
}
|
|
1150
1449
|
|
|
1151
1450
|
/**
|
|
@@ -1207,24 +1506,24 @@ export class BattleAi {
|
|
|
1207
1506
|
/**
|
|
1208
1507
|
* Try to dodge
|
|
1209
1508
|
*/
|
|
1210
|
-
private tryDodge() {
|
|
1509
|
+
private tryDodge(): boolean {
|
|
1211
1510
|
const currentTime = Date.now();
|
|
1212
1511
|
|
|
1213
1512
|
if (currentTime - this.lastDodgeTime < this.dodgeCooldown) {
|
|
1214
1513
|
this.debugLog('dodge', `Dodge on cooldown (${this.dodgeCooldown - (currentTime - this.lastDodgeTime)}ms remaining)`);
|
|
1215
|
-
return;
|
|
1514
|
+
return false;
|
|
1216
1515
|
}
|
|
1217
1516
|
if (Math.random() > this.dodgeChance) {
|
|
1218
1517
|
this.debugLog('dodge', `Dodge roll failed (chance=${(this.dodgeChance * 100).toFixed(0)}%)`);
|
|
1219
|
-
return;
|
|
1518
|
+
return false;
|
|
1220
1519
|
}
|
|
1221
|
-
if (!this.target) return;
|
|
1520
|
+
if (!this.target) return false;
|
|
1222
1521
|
|
|
1223
1522
|
const dx = this.target.x() - this.event.x();
|
|
1224
1523
|
const dy = this.target.y() - this.event.y();
|
|
1225
1524
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1226
1525
|
|
|
1227
|
-
if (dist === 0) return;
|
|
1526
|
+
if (dist === 0) return false;
|
|
1228
1527
|
|
|
1229
1528
|
// Perpendicular direction
|
|
1230
1529
|
const dodgeDirX = -dy / dist;
|
|
@@ -1238,12 +1537,13 @@ export class BattleAi {
|
|
|
1238
1537
|
// Counter-attack for defensive types
|
|
1239
1538
|
if (this.enemyType === EnemyType.Defensive && Math.random() < 0.5) {
|
|
1240
1539
|
this.debugLog('dodge', 'Counter-attack after dodge');
|
|
1241
|
-
|
|
1540
|
+
this.schedule(() => {
|
|
1242
1541
|
if (this.target && this.state === AiState.Combat) {
|
|
1243
1542
|
this.selectAndPerformAttack();
|
|
1244
1543
|
}
|
|
1245
1544
|
}, 400);
|
|
1246
1545
|
}
|
|
1546
|
+
return true;
|
|
1247
1547
|
}
|
|
1248
1548
|
|
|
1249
1549
|
private canDodge(): boolean {
|
|
@@ -1426,9 +1726,24 @@ export class BattleAi {
|
|
|
1426
1726
|
* The actual damage is applied externally via RPGJS API.
|
|
1427
1727
|
*/
|
|
1428
1728
|
takeDamage(attacker: RpgPlayer): boolean {
|
|
1729
|
+
if (this.defeated) return true;
|
|
1429
1730
|
// Apply damage using RPGJS system
|
|
1430
|
-
const
|
|
1731
|
+
const raw = this.event.applyDamage(attacker);
|
|
1732
|
+
return this.handleDamage(attacker, {
|
|
1733
|
+
damage: raw.damage ?? 0,
|
|
1734
|
+
defeated: this.event.hp <= 0,
|
|
1735
|
+
raw,
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1431
1738
|
|
|
1739
|
+
handleDamage(
|
|
1740
|
+
attacker: RpgPlayer,
|
|
1741
|
+
damageResult: ActionBattleDamageResult & {
|
|
1742
|
+
reaction?: NormalizedActionBattleHitReactionProfile;
|
|
1743
|
+
}
|
|
1744
|
+
): boolean {
|
|
1745
|
+
if (this.defeated) return true;
|
|
1746
|
+
const damage = damageResult.damage;
|
|
1432
1747
|
this.debugLog('damage', `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || '?'})`);
|
|
1433
1748
|
|
|
1434
1749
|
// Visual feedback
|
|
@@ -1439,20 +1754,32 @@ export class BattleAi {
|
|
|
1439
1754
|
cycles: 1
|
|
1440
1755
|
});
|
|
1441
1756
|
this.event.showHit(`-${damage}`);
|
|
1757
|
+
playActionBattleAnimation("hurt", this.event, this.animations, {
|
|
1758
|
+
attacker,
|
|
1759
|
+
});
|
|
1442
1760
|
|
|
1443
1761
|
// Track damage
|
|
1444
1762
|
this.recentDamageTaken += damage;
|
|
1445
1763
|
|
|
1764
|
+
const reaction = damageResult.reaction;
|
|
1765
|
+
const staggerPower = reaction?.staggerPower ?? damage;
|
|
1766
|
+
const hitstunMs = reaction?.hitstunMs ?? this.hitstunMs;
|
|
1767
|
+
const shouldStun = staggerPower >= this.poise && hitstunMs > 0;
|
|
1768
|
+
setActionBattleInvincibility(
|
|
1769
|
+
this.event,
|
|
1770
|
+
reaction?.invincibilityMs ?? this.invincibilityMs
|
|
1771
|
+
);
|
|
1772
|
+
|
|
1446
1773
|
// Brief stun
|
|
1447
|
-
if (this.state !== AiState.Stunned && this.state !== AiState.Flee) {
|
|
1774
|
+
if (shouldStun && this.state !== AiState.Stunned && this.state !== AiState.Flee) {
|
|
1448
1775
|
this.debugLog('damage', 'Stunned from damage');
|
|
1449
1776
|
this.isMovingToTarget = false;
|
|
1450
|
-
this.stunnedUntil = Date.now() +
|
|
1777
|
+
this.stunnedUntil = Date.now() + hitstunMs;
|
|
1451
1778
|
this.changeState(AiState.Stunned);
|
|
1452
1779
|
}
|
|
1453
1780
|
|
|
1454
1781
|
// Check death
|
|
1455
|
-
if (this.event.hp <= 0) {
|
|
1782
|
+
if (damageResult.defeated || this.event.hp <= 0) {
|
|
1456
1783
|
this.debugLog('damage', 'Defeated!');
|
|
1457
1784
|
this.kill(attacker);
|
|
1458
1785
|
return true;
|
|
@@ -1468,13 +1795,69 @@ export class BattleAi {
|
|
|
1468
1795
|
* and removes the event from the map.
|
|
1469
1796
|
*/
|
|
1470
1797
|
private kill(attacker?: RpgPlayer) {
|
|
1471
|
-
|
|
1798
|
+
if (this.defeated) return;
|
|
1799
|
+
this.defeated = true;
|
|
1800
|
+
|
|
1801
|
+
const dieAnimation = resolveActionBattleAnimation(
|
|
1802
|
+
"die",
|
|
1803
|
+
this.event,
|
|
1804
|
+
this.animations,
|
|
1805
|
+
{
|
|
1806
|
+
attacker,
|
|
1807
|
+
}
|
|
1808
|
+
);
|
|
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
|
+
};
|
|
1830
|
+
|
|
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.
|
|
1472
1844
|
if (this.onDefeatedCallback) {
|
|
1473
|
-
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
|
+
}
|
|
1474
1857
|
}
|
|
1475
|
-
|
|
1858
|
+
|
|
1476
1859
|
this.destroy();
|
|
1477
|
-
|
|
1860
|
+
remove();
|
|
1478
1861
|
}
|
|
1479
1862
|
|
|
1480
1863
|
/**
|
|
@@ -1544,6 +1927,36 @@ export class BattleAi {
|
|
|
1544
1927
|
}
|
|
1545
1928
|
}
|
|
1546
1929
|
|
|
1930
|
+
private applyCustomBehavior(currentTime: number) {
|
|
1931
|
+
if (!this.behaviorKey) return;
|
|
1932
|
+
const behavior = getActionBattleSystems().ai.behaviors[this.behaviorKey];
|
|
1933
|
+
if (!behavior) return;
|
|
1934
|
+
const maxHp = this.event.param[MAXHP];
|
|
1935
|
+
const decision = behavior({
|
|
1936
|
+
event: this.event,
|
|
1937
|
+
target: this.target,
|
|
1938
|
+
state: this.state,
|
|
1939
|
+
enemyType: this.enemyType,
|
|
1940
|
+
distance: this.target ? this.getDistance(this.event, this.target) : null,
|
|
1941
|
+
hpPercent: maxHp ? this.event.hp / maxHp : null,
|
|
1942
|
+
now: currentTime,
|
|
1943
|
+
});
|
|
1944
|
+
if (!decision) return;
|
|
1945
|
+
if (decision.attackCooldown !== undefined) {
|
|
1946
|
+
this.attackCooldown = decision.attackCooldown;
|
|
1947
|
+
}
|
|
1948
|
+
if (decision.moveToCooldown !== undefined) {
|
|
1949
|
+
this.moveToCooldown = decision.moveToCooldown;
|
|
1950
|
+
}
|
|
1951
|
+
if (decision.attackPatterns?.length) {
|
|
1952
|
+
this.attackPatterns = decision.attackPatterns;
|
|
1953
|
+
}
|
|
1954
|
+
if (decision.mode) {
|
|
1955
|
+
this.behaviorMode = decision.mode;
|
|
1956
|
+
this.behaviorEnabled = true;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1547
1960
|
private handleTacticalMovement(distance: number) {
|
|
1548
1961
|
if (!this.target) return;
|
|
1549
1962
|
const minRange = this.attackRange * 0.7;
|
|
@@ -1600,6 +2013,15 @@ export class BattleAi {
|
|
|
1600
2013
|
return true;
|
|
1601
2014
|
}
|
|
1602
2015
|
|
|
2016
|
+
private schedule(callback: () => void, delay: number) {
|
|
2017
|
+
const timer = setTimeout(() => {
|
|
2018
|
+
this.timers = this.timers.filter((entry) => entry !== timer);
|
|
2019
|
+
callback();
|
|
2020
|
+
}, delay);
|
|
2021
|
+
this.timers.push(timer);
|
|
2022
|
+
return timer;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1603
2025
|
// Public getters
|
|
1604
2026
|
getHealth(): number { return this.event.hp; }
|
|
1605
2027
|
getMaxHealth(): number { return this.event.param[MAXHP]; }
|
|
@@ -1617,5 +2039,7 @@ export class BattleAi {
|
|
|
1617
2039
|
}
|
|
1618
2040
|
this.target = null;
|
|
1619
2041
|
this.nearbyEnemies = [];
|
|
2042
|
+
this.timers.forEach((timer) => clearTimeout(timer));
|
|
2043
|
+
this.timers = [];
|
|
1620
2044
|
}
|
|
1621
2045
|
}
|