@rpgjs/action-battle 5.0.0-beta.5 → 5.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -22
- package/dist/ai.server.d.ts +55 -4
- package/dist/client/index.js +13 -8
- package/dist/client/index10.js +54 -136
- package/dist/client/index11.js +52 -23
- package/dist/client/index12.js +101 -1217
- package/dist/client/index13.js +139 -42
- package/dist/client/index14.js +23 -8
- package/dist/client/index15.js +68 -444
- 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 +25 -87
- package/dist/client/index20.js +504 -0
- package/dist/client/index3.js +45 -83
- package/dist/client/index4.js +98 -297
- package/dist/client/index5.js +81 -33
- package/dist/client/index6.js +284 -78
- package/dist/client/index7.js +33 -74
- package/dist/client/index8.js +95 -55
- package/dist/client/index9.js +75 -96
- package/dist/core/attack-profile.d.ts +9 -0
- package/dist/core/attack-runtime.d.ts +20 -0
- package/dist/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/core/equipment.d.ts +2 -0
- package/dist/core/hit-reaction.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/server/index.js +12 -7
- package/dist/server/index10.js +1340 -8
- 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 +1 -1
- package/dist/server/index3.js +25 -87
- package/dist/server/index4.js +45 -141
- package/dist/server/index5.js +104 -21
- package/dist/server/index6.js +137 -1215
- package/dist/server/index7.js +22 -34
- package/dist/server/index8.js +70 -44
- package/dist/server/index9.js +44 -437
- package/dist/server.d.ts +8 -2
- 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 +362 -56
- package/src/client.ts +21 -12
- package/src/config.ts +17 -2
- 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/contracts.ts +3 -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 +54 -1
- package/src/core/hit.ts +26 -0
- package/src/index.ts +48 -1
- package/src/server.ts +192 -34
- package/src/types.ts +62 -6
package/src/ai.server.ts
CHANGED
|
@@ -2,17 +2,70 @@ 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";
|
|
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";
|
|
8
22
|
import type { ActionBattleDamageResult } from "./core/contracts";
|
|
23
|
+
import type {
|
|
24
|
+
NormalizedActionBattleAttackProfile,
|
|
25
|
+
NormalizedActionBattleHitReactionProfile,
|
|
26
|
+
} from "./types";
|
|
9
27
|
import type { ActionBattleAnimationOptions } from "./types";
|
|
10
28
|
|
|
11
29
|
type RpgEventWithBattleAi = RpgEvent & {
|
|
12
30
|
battleAi?: BattleAi;
|
|
13
31
|
};
|
|
14
32
|
|
|
15
|
-
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 {
|
|
16
69
|
enemyType?: EnemyType;
|
|
17
70
|
attackCooldown?: number;
|
|
18
71
|
visionRange?: number;
|
|
@@ -22,10 +75,14 @@ export interface BattleAiOptions {
|
|
|
22
75
|
fleeThreshold?: number;
|
|
23
76
|
attackSkill?: any;
|
|
24
77
|
attackPatterns?: AttackPattern[];
|
|
78
|
+
attackProfiles?: ActionBattleEnemyAttackProfileMap;
|
|
25
79
|
patrolWaypoints?: Array<{ x: number; y: number }>;
|
|
26
80
|
groupBehavior?: boolean;
|
|
27
81
|
moveToCooldown?: number;
|
|
28
82
|
retreatCooldown?: number;
|
|
83
|
+
poise?: number;
|
|
84
|
+
hitstunMs?: number;
|
|
85
|
+
invincibilityMs?: number;
|
|
29
86
|
behavior?: {
|
|
30
87
|
baseScore?: number;
|
|
31
88
|
updateInterval?: number;
|
|
@@ -35,8 +92,18 @@ export interface BattleAiOptions {
|
|
|
35
92
|
};
|
|
36
93
|
behaviorKey?: string;
|
|
37
94
|
animations?: ActionBattleAnimationOptions;
|
|
95
|
+
rewards?: BattleAiRewards;
|
|
96
|
+
autoAwardRewards?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BattleAiOptions extends BattleAiBaseOptions {
|
|
38
100
|
/** Callback called when the AI is defeated */
|
|
39
|
-
onDefeated?:
|
|
101
|
+
onDefeated?: BattleAiDefeatedCallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BattleAiLegacyOptions extends BattleAiBaseOptions {
|
|
105
|
+
/** @deprecated Use the context callback signature instead. */
|
|
106
|
+
onDefeated?: BattleAiLegacyDefeatedCallback;
|
|
40
107
|
}
|
|
41
108
|
|
|
42
109
|
/**
|
|
@@ -73,6 +140,84 @@ export interface HitResult {
|
|
|
73
140
|
target: RpgPlayer | RpgEvent;
|
|
74
141
|
}
|
|
75
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
|
+
|
|
76
221
|
/**
|
|
77
222
|
* Hook options for customizing hit behavior
|
|
78
223
|
*
|
|
@@ -295,6 +440,7 @@ export class BattleAi {
|
|
|
295
440
|
// Attack configuration
|
|
296
441
|
private attackSkill: any | null; // Skill to use for attacks
|
|
297
442
|
private attackPatterns: AttackPattern[];
|
|
443
|
+
private attackProfiles: NormalizedActionBattleEnemyAttackProfileMap;
|
|
298
444
|
private animations?: ActionBattleAnimationOptions;
|
|
299
445
|
private comboCount: number = 0;
|
|
300
446
|
private comboMax: number = 3;
|
|
@@ -318,7 +464,12 @@ export class BattleAi {
|
|
|
318
464
|
private isMovingToTarget: boolean = false;
|
|
319
465
|
|
|
320
466
|
// Callback when AI is defeated
|
|
321
|
-
private onDefeatedCallback?:
|
|
467
|
+
private onDefeatedCallback?:
|
|
468
|
+
| BattleAiDefeatedCallback
|
|
469
|
+
| BattleAiLegacyDefeatedCallback;
|
|
470
|
+
private rewards?: BattleAiRewards;
|
|
471
|
+
private autoAwardRewards: boolean = true;
|
|
472
|
+
private defeated: boolean = false;
|
|
322
473
|
|
|
323
474
|
// Direction hysteresis to prevent animation flickering
|
|
324
475
|
private lastFacingDirection: string | null = null;
|
|
@@ -340,6 +491,9 @@ export class BattleAi {
|
|
|
340
491
|
private lastRetreatTime: number = 0;
|
|
341
492
|
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
342
493
|
private behaviorKey?: string;
|
|
494
|
+
private poise: number = 0;
|
|
495
|
+
private hitstunMs: number = 150;
|
|
496
|
+
private invincibilityMs: number = 250;
|
|
343
497
|
|
|
344
498
|
/**
|
|
345
499
|
* Create a new Battle AI Controller
|
|
@@ -365,9 +519,11 @@ export class BattleAi {
|
|
|
365
519
|
* });
|
|
366
520
|
* ```
|
|
367
521
|
*/
|
|
522
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiOptions);
|
|
523
|
+
constructor(event: RpgEventWithBattleAi, options?: BattleAiLegacyOptions);
|
|
368
524
|
constructor(
|
|
369
525
|
event: RpgEventWithBattleAi,
|
|
370
|
-
options: BattleAiOptions = {}
|
|
526
|
+
options: BattleAiOptions | BattleAiLegacyOptions = {}
|
|
371
527
|
) {
|
|
372
528
|
event.battleAi = this;
|
|
373
529
|
this.event = event;
|
|
@@ -390,6 +546,9 @@ export class BattleAi {
|
|
|
390
546
|
AttackPattern.Combo,
|
|
391
547
|
AttackPattern.DashAttack
|
|
392
548
|
];
|
|
549
|
+
this.attackProfiles = normalizeActionBattleEnemyAttackProfiles(
|
|
550
|
+
options.attackProfiles
|
|
551
|
+
);
|
|
393
552
|
|
|
394
553
|
// Initialize group behavior
|
|
395
554
|
this.groupBehavior = options.groupBehavior || false;
|
|
@@ -400,6 +559,8 @@ export class BattleAi {
|
|
|
400
559
|
|
|
401
560
|
// Initialize defeat callback
|
|
402
561
|
this.onDefeatedCallback = options.onDefeated;
|
|
562
|
+
this.rewards = options.rewards;
|
|
563
|
+
this.autoAwardRewards = options.autoAwardRewards ?? true;
|
|
403
564
|
|
|
404
565
|
// Behavior gauge settings
|
|
405
566
|
if (options.behavior) {
|
|
@@ -427,6 +588,15 @@ export class BattleAi {
|
|
|
427
588
|
if (options.retreatCooldown !== undefined) {
|
|
428
589
|
this.retreatCooldown = options.retreatCooldown;
|
|
429
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
|
+
}
|
|
430
600
|
|
|
431
601
|
// Setup AI systems
|
|
432
602
|
this.setupVision();
|
|
@@ -896,12 +1066,26 @@ export class BattleAi {
|
|
|
896
1066
|
*/
|
|
897
1067
|
private performMeleeAttack() {
|
|
898
1068
|
if (!this.target) return;
|
|
1069
|
+
const profile = this.getAttackProfile(AttackPattern.Melee);
|
|
899
1070
|
|
|
900
1071
|
this.faceTarget();
|
|
1072
|
+
this.telegraphAttack(profile);
|
|
901
1073
|
playActionBattleAnimation("attack", this.event, this.animations, {
|
|
902
1074
|
target: this.target,
|
|
903
1075
|
});
|
|
904
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`);
|
|
1088
|
+
|
|
905
1089
|
// Use skill if available
|
|
906
1090
|
if (this.attackSkill) {
|
|
907
1091
|
try {
|
|
@@ -912,17 +1096,22 @@ export class BattleAi {
|
|
|
912
1096
|
this.event.useSkill(this.attackSkill, this.target);
|
|
913
1097
|
} catch (e) {
|
|
914
1098
|
// Skill failed (no SP, etc.) - fall back to basic attack
|
|
915
|
-
this.performBasicHitbox();
|
|
1099
|
+
this.performBasicHitbox(profile, pattern);
|
|
916
1100
|
}
|
|
917
1101
|
} else {
|
|
918
|
-
this.performBasicHitbox();
|
|
1102
|
+
this.performBasicHitbox(profile, pattern);
|
|
919
1103
|
}
|
|
920
1104
|
}
|
|
921
1105
|
|
|
922
1106
|
/**
|
|
923
1107
|
* Perform basic hitbox attack when no skill is set
|
|
924
1108
|
*/
|
|
925
|
-
private performBasicHitbox(
|
|
1109
|
+
private performBasicHitbox(
|
|
1110
|
+
profile: NormalizedActionBattleAttackProfile = this.getAttackProfile(
|
|
1111
|
+
AttackPattern.Melee
|
|
1112
|
+
),
|
|
1113
|
+
pattern: AttackPattern = AttackPattern.Melee
|
|
1114
|
+
) {
|
|
926
1115
|
if (!this.target) return;
|
|
927
1116
|
|
|
928
1117
|
const eventX = this.event.x();
|
|
@@ -944,11 +1133,13 @@ export class BattleAi {
|
|
|
944
1133
|
}];
|
|
945
1134
|
|
|
946
1135
|
const map = this.event.getCurrentMap();
|
|
947
|
-
map?.createMovingHitbox(hitboxes, {
|
|
948
|
-
|
|
949
|
-
|
|
1136
|
+
map?.createMovingHitbox(hitboxes, {
|
|
1137
|
+
speed: resolveActionBattleHitboxSpeed(profile, hitboxes.length),
|
|
1138
|
+
}).subscribe({
|
|
1139
|
+
next: (hits: any[]) => {
|
|
1140
|
+
hits.forEach((hit: any) => {
|
|
950
1141
|
if (hit instanceof RpgPlayer && hit !== this.event) {
|
|
951
|
-
this.applyHit(hit);
|
|
1142
|
+
this.applyHit(hit, undefined, profile, pattern);
|
|
952
1143
|
}
|
|
953
1144
|
});
|
|
954
1145
|
},
|
|
@@ -983,7 +1174,25 @@ export class BattleAi {
|
|
|
983
1174
|
* });
|
|
984
1175
|
* ```
|
|
985
1176
|
*/
|
|
986
|
-
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
|
+
|
|
987
1196
|
// Use RPGJS damage formula
|
|
988
1197
|
const { damage } = target.applyDamage(this.event as any);
|
|
989
1198
|
|
|
@@ -1017,6 +1226,10 @@ export class BattleAi {
|
|
|
1017
1226
|
cycles: 1
|
|
1018
1227
|
});
|
|
1019
1228
|
target.showHit(`-${hitResult.damage}`);
|
|
1229
|
+
setActionBattleInvincibility(
|
|
1230
|
+
target,
|
|
1231
|
+
profile.reaction.invincibilityMs
|
|
1232
|
+
);
|
|
1020
1233
|
|
|
1021
1234
|
// Apply knockback
|
|
1022
1235
|
if (hitResult.knockbackForce > 0) {
|
|
@@ -1080,7 +1293,15 @@ export class BattleAi {
|
|
|
1080
1293
|
if (!this.target) return;
|
|
1081
1294
|
|
|
1082
1295
|
this.comboCount++;
|
|
1083
|
-
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
|
+
});
|
|
1084
1305
|
|
|
1085
1306
|
if (this.comboCount < this.comboMax) {
|
|
1086
1307
|
this.schedule(() => {
|
|
@@ -1100,9 +1321,11 @@ export class BattleAi {
|
|
|
1100
1321
|
*/
|
|
1101
1322
|
private performChargedAttack() {
|
|
1102
1323
|
if (!this.target) return;
|
|
1324
|
+
const profile = this.getAttackProfile(AttackPattern.Charged);
|
|
1103
1325
|
|
|
1104
1326
|
this.chargingAttack = true;
|
|
1105
1327
|
this.faceTarget();
|
|
1328
|
+
this.telegraphAttack(profile);
|
|
1106
1329
|
playActionBattleAnimation(
|
|
1107
1330
|
"attack",
|
|
1108
1331
|
this.event,
|
|
@@ -1113,35 +1336,24 @@ export class BattleAi {
|
|
|
1113
1336
|
{ repeat: 2 }
|
|
1114
1337
|
);
|
|
1115
1338
|
|
|
1116
|
-
this.
|
|
1339
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1117
1340
|
if (!this.target || this.state !== AiState.Combat) {
|
|
1118
1341
|
this.chargingAttack = false;
|
|
1119
1342
|
return;
|
|
1120
1343
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
try {
|
|
1125
|
-
playActionBattleAnimation("castSkill", this.event, this.animations, {
|
|
1126
|
-
skill: this.attackSkill,
|
|
1127
|
-
target: this.target,
|
|
1128
|
-
});
|
|
1129
|
-
this.event.useSkill(this.attackSkill, this.target);
|
|
1130
|
-
} catch (e) {
|
|
1131
|
-
this.performBasicHitbox();
|
|
1132
|
-
}
|
|
1133
|
-
} else {
|
|
1134
|
-
this.performBasicHitbox();
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1344
|
+
this.executeMeleeAttack(profile, AttackPattern.Charged);
|
|
1345
|
+
});
|
|
1346
|
+
this.schedule(() => {
|
|
1137
1347
|
this.chargingAttack = false;
|
|
1138
|
-
},
|
|
1348
|
+
}, profile.totalDurationMs);
|
|
1139
1349
|
}
|
|
1140
1350
|
|
|
1141
1351
|
/**
|
|
1142
1352
|
* Perform zone attack (360 degrees)
|
|
1143
1353
|
*/
|
|
1144
1354
|
private performZoneAttack() {
|
|
1355
|
+
const profile = this.getAttackProfile(AttackPattern.Zone);
|
|
1356
|
+
this.telegraphAttack(profile);
|
|
1145
1357
|
playActionBattleAnimation("attack", this.event, this.animations, {
|
|
1146
1358
|
target: this.target ?? undefined,
|
|
1147
1359
|
});
|
|
@@ -1163,15 +1375,19 @@ export class BattleAi {
|
|
|
1163
1375
|
});
|
|
1164
1376
|
});
|
|
1165
1377
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
+
});
|
|
1175
1391
|
});
|
|
1176
1392
|
}
|
|
1177
1393
|
|
|
@@ -1180,6 +1396,7 @@ export class BattleAi {
|
|
|
1180
1396
|
*/
|
|
1181
1397
|
private performDashAttack() {
|
|
1182
1398
|
if (!this.target) return;
|
|
1399
|
+
const profile = this.getAttackProfile(AttackPattern.DashAttack);
|
|
1183
1400
|
|
|
1184
1401
|
const dx = this.target.x() - this.event.x();
|
|
1185
1402
|
const dy = this.target.y() - this.event.y();
|
|
@@ -1191,12 +1408,43 @@ export class BattleAi {
|
|
|
1191
1408
|
const dirY = dy / dist;
|
|
1192
1409
|
|
|
1193
1410
|
this.faceTarget();
|
|
1194
|
-
this.
|
|
1411
|
+
this.telegraphAttack(profile);
|
|
1195
1412
|
|
|
1196
|
-
this.
|
|
1413
|
+
this.scheduleAttackStartup(profile, () => {
|
|
1197
1414
|
if (!this.target || this.state !== AiState.Combat) return;
|
|
1198
|
-
this.
|
|
1199
|
-
|
|
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
|
+
);
|
|
1200
1448
|
}
|
|
1201
1449
|
|
|
1202
1450
|
/**
|
|
@@ -1478,6 +1726,7 @@ export class BattleAi {
|
|
|
1478
1726
|
* The actual damage is applied externally via RPGJS API.
|
|
1479
1727
|
*/
|
|
1480
1728
|
takeDamage(attacker: RpgPlayer): boolean {
|
|
1729
|
+
if (this.defeated) return true;
|
|
1481
1730
|
// Apply damage using RPGJS system
|
|
1482
1731
|
const raw = this.event.applyDamage(attacker);
|
|
1483
1732
|
return this.handleDamage(attacker, {
|
|
@@ -1487,7 +1736,13 @@ export class BattleAi {
|
|
|
1487
1736
|
});
|
|
1488
1737
|
}
|
|
1489
1738
|
|
|
1490
|
-
handleDamage(
|
|
1739
|
+
handleDamage(
|
|
1740
|
+
attacker: RpgPlayer,
|
|
1741
|
+
damageResult: ActionBattleDamageResult & {
|
|
1742
|
+
reaction?: NormalizedActionBattleHitReactionProfile;
|
|
1743
|
+
}
|
|
1744
|
+
): boolean {
|
|
1745
|
+
if (this.defeated) return true;
|
|
1491
1746
|
const damage = damageResult.damage;
|
|
1492
1747
|
this.debugLog('damage', `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || '?'})`);
|
|
1493
1748
|
|
|
@@ -1506,11 +1761,20 @@ export class BattleAi {
|
|
|
1506
1761
|
// Track damage
|
|
1507
1762
|
this.recentDamageTaken += damage;
|
|
1508
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
|
+
|
|
1509
1773
|
// Brief stun
|
|
1510
|
-
if (this.state !== AiState.Stunned && this.state !== AiState.Flee) {
|
|
1774
|
+
if (shouldStun && this.state !== AiState.Stunned && this.state !== AiState.Flee) {
|
|
1511
1775
|
this.debugLog('damage', 'Stunned from damage');
|
|
1512
1776
|
this.isMovingToTarget = false;
|
|
1513
|
-
this.stunnedUntil = Date.now() +
|
|
1777
|
+
this.stunnedUntil = Date.now() + hitstunMs;
|
|
1514
1778
|
this.changeState(AiState.Stunned);
|
|
1515
1779
|
}
|
|
1516
1780
|
|
|
@@ -1531,7 +1795,10 @@ export class BattleAi {
|
|
|
1531
1795
|
* and removes the event from the map.
|
|
1532
1796
|
*/
|
|
1533
1797
|
private kill(attacker?: RpgPlayer) {
|
|
1534
|
-
|
|
1798
|
+
if (this.defeated) return;
|
|
1799
|
+
this.defeated = true;
|
|
1800
|
+
|
|
1801
|
+
const dieAnimation = resolveActionBattleAnimation(
|
|
1535
1802
|
"die",
|
|
1536
1803
|
this.event,
|
|
1537
1804
|
this.animations,
|
|
@@ -1540,18 +1807,57 @@ export class BattleAi {
|
|
|
1540
1807
|
}
|
|
1541
1808
|
);
|
|
1542
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
|
+
};
|
|
1543
1830
|
|
|
1544
|
-
|
|
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.
|
|
1545
1844
|
if (this.onDefeatedCallback) {
|
|
1546
|
-
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
|
+
}
|
|
1547
1857
|
}
|
|
1548
|
-
|
|
1858
|
+
|
|
1549
1859
|
this.destroy();
|
|
1550
|
-
|
|
1551
|
-
this.schedule(() => this.event.remove(), removeDelay);
|
|
1552
|
-
} else {
|
|
1553
|
-
this.event.remove();
|
|
1554
|
-
}
|
|
1860
|
+
remove();
|
|
1555
1861
|
}
|
|
1556
1862
|
|
|
1557
1863
|
/**
|