@rpgjs/action-battle 5.0.0-beta.5 → 5.0.0-beta.6
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 +115 -0
- package/dist/ai.server.d.ts +17 -2
- 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 +1281 -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 +6 -1
- package/dist/server/index.js +12 -7
- package/dist/server/index10.js +1278 -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/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 +7 -1
- package/package.json +5 -5
- package/src/ai.server.ts +172 -43
- 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 +36 -0
- package/src/server.ts +180 -33
- package/src/types.ts +62 -6
package/dist/client/index12.js
CHANGED
|
@@ -1,1222 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (this.categories.length > 0 && !this.categories.includes(category)) return;
|
|
41
|
-
const prefix = `[AI:${category}]${eventId ? ` [${eventId.substring(0, 8)}]` : ""}`;
|
|
42
|
-
if (data !== void 0) console.log(prefix, message, data);
|
|
43
|
-
else console.log(prefix, message);
|
|
44
|
-
}
|
|
1
|
+
import { normalizeActionBattleOptions } from "./index4.js";
|
|
2
|
+
import { setActionBattleOptions, startAttackPreview, stopAttackPreview } from "./index5.js";
|
|
3
|
+
import component from "./index6.js";
|
|
4
|
+
import component$1 from "./index8.js";
|
|
5
|
+
import component$2 from "./index9.js";
|
|
6
|
+
import { resolveActionBattleAnimation } from "./index10.js";
|
|
7
|
+
import { getNormalizedActionBattleAttackProfile } from "./index11.js";
|
|
8
|
+
import { PrebuiltComponentAnimations, RpgClientEngine, RpgGui, inject } from "@rpgjs/client";
|
|
9
|
+
import { defineModule } from "@rpgjs/common";
|
|
10
|
+
//#region src/client.ts
|
|
11
|
+
var DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
|
|
12
|
+
var beginLocalPlayerAttackLock = (engine, durationMs, locks) => {
|
|
13
|
+
if (durationMs <= 0) return true;
|
|
14
|
+
const player = engine.scene?.getCurrentPlayer?.();
|
|
15
|
+
if (!player) return true;
|
|
16
|
+
const runtimePlayer = player;
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (typeof runtimePlayer.__actionBattleAttackLockedUntil === "number" && runtimePlayer.__actionBattleAttackLockedUntil > now) return false;
|
|
19
|
+
const lockId = (runtimePlayer.__actionBattleAttackLockId ?? 0) + 1;
|
|
20
|
+
runtimePlayer.__actionBattleAttackLockId = lockId;
|
|
21
|
+
runtimePlayer.__actionBattleAttackLockedUntil = now + durationMs;
|
|
22
|
+
const previousCanMove = typeof player.canMove === "function" ? player.canMove() : true;
|
|
23
|
+
const previousDirectionFixed = player.directionFixed;
|
|
24
|
+
const previousAnimationFixed = player.animationFixed;
|
|
25
|
+
if (locks.movement) {
|
|
26
|
+
if (typeof engine.interruptCurrentPlayerMovement === "function") engine.interruptCurrentPlayerMovement(player);
|
|
27
|
+
else engine.scene?.stopMovement?.(player);
|
|
28
|
+
player.canMove.set(false);
|
|
29
|
+
}
|
|
30
|
+
if (locks.direction) player.directionFixed = true;
|
|
31
|
+
player.animationFixed = true;
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
|
|
34
|
+
runtimePlayer.__actionBattleAttackLockedUntil = 0;
|
|
35
|
+
player.canMove.set(previousCanMove);
|
|
36
|
+
player.directionFixed = previousDirectionFixed;
|
|
37
|
+
player.animationFixed = previousAnimationFixed;
|
|
38
|
+
}, durationMs);
|
|
39
|
+
return true;
|
|
45
40
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*/
|
|
51
|
-
var AiState = /* @__PURE__ */ function(AiState) {
|
|
52
|
-
AiState["Idle"] = "idle";
|
|
53
|
-
AiState["Alert"] = "alert";
|
|
54
|
-
AiState["Combat"] = "combat";
|
|
55
|
-
AiState["Flee"] = "flee";
|
|
56
|
-
AiState["Stunned"] = "stunned";
|
|
57
|
-
return AiState;
|
|
58
|
-
}({});
|
|
59
|
-
/**
|
|
60
|
-
* Enemy Type enumeration
|
|
61
|
-
*
|
|
62
|
-
* Defines different enemy archetypes with unique behaviors.
|
|
63
|
-
* Stats (HP, ATK, etc.) should be set on the event itself via onInit.
|
|
64
|
-
*/
|
|
65
|
-
var EnemyType = /* @__PURE__ */ function(EnemyType) {
|
|
66
|
-
EnemyType["Aggressive"] = "aggressive";
|
|
67
|
-
EnemyType["Defensive"] = "defensive";
|
|
68
|
-
EnemyType["Ranged"] = "ranged";
|
|
69
|
-
EnemyType["Tank"] = "tank";
|
|
70
|
-
EnemyType["Berserker"] = "berserker";
|
|
71
|
-
return EnemyType;
|
|
72
|
-
}({});
|
|
73
|
-
/**
|
|
74
|
-
* Attack Pattern enumeration
|
|
75
|
-
*
|
|
76
|
-
* Different attack patterns the AI can use.
|
|
77
|
-
*/
|
|
78
|
-
var AttackPattern = /* @__PURE__ */ function(AttackPattern) {
|
|
79
|
-
AttackPattern["Melee"] = "melee";
|
|
80
|
-
AttackPattern["Combo"] = "combo";
|
|
81
|
-
AttackPattern["Charged"] = "charged";
|
|
82
|
-
AttackPattern["Zone"] = "zone";
|
|
83
|
-
AttackPattern["DashAttack"] = "dashAttack";
|
|
84
|
-
return AttackPattern;
|
|
85
|
-
}({});
|
|
86
|
-
/**
|
|
87
|
-
* Default knockback configuration
|
|
88
|
-
*
|
|
89
|
-
* Used when no weapon is equipped or weapon doesn't specify knockback.
|
|
90
|
-
*/
|
|
91
|
-
var DEFAULT_KNOCKBACK = {
|
|
92
|
-
/** Default knockback force */
|
|
93
|
-
force: 50,
|
|
94
|
-
/** Default knockback duration in milliseconds */
|
|
95
|
-
duration: 300
|
|
41
|
+
var resolveLocalPlayerDirection = (player) => {
|
|
42
|
+
if (typeof player.getDirection === "function") return player.getDirection();
|
|
43
|
+
if (typeof player.direction === "function") return player.direction();
|
|
44
|
+
return player.direction ?? "down";
|
|
96
45
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
state = AiState.Idle;
|
|
151
|
-
stateStartTime = 0;
|
|
152
|
-
stunnedUntil = 0;
|
|
153
|
-
enemyType;
|
|
154
|
-
attackCooldown = 1e3;
|
|
155
|
-
visionRange = 150;
|
|
156
|
-
attackRange = 60;
|
|
157
|
-
dodgeChance = .2;
|
|
158
|
-
dodgeCooldown = 2e3;
|
|
159
|
-
lastDodgeTime = 0;
|
|
160
|
-
fleeThreshold = .2;
|
|
161
|
-
attackSkill;
|
|
162
|
-
attackPatterns;
|
|
163
|
-
animations;
|
|
164
|
-
comboCount = 0;
|
|
165
|
-
comboMax = 3;
|
|
166
|
-
chargingAttack = false;
|
|
167
|
-
groupBehavior;
|
|
168
|
-
nearbyEnemies = [];
|
|
169
|
-
groupUpdateInterval = 0;
|
|
170
|
-
patrolWaypoints = [];
|
|
171
|
-
currentPatrolIndex = 0;
|
|
172
|
-
lastHpCheck = 0;
|
|
173
|
-
recentDamageTaken = 0;
|
|
174
|
-
damageCheckInterval = 2e3;
|
|
175
|
-
isMovingToTarget = false;
|
|
176
|
-
onDefeatedCallback;
|
|
177
|
-
lastFacingDirection = null;
|
|
178
|
-
behaviorScore = 50;
|
|
179
|
-
behaviorMode = "tactical";
|
|
180
|
-
behaviorLastUpdate = 0;
|
|
181
|
-
behaviorUpdateInterval = 400;
|
|
182
|
-
behaviorAssaultThreshold = 65;
|
|
183
|
-
behaviorRetreatThreshold = 35;
|
|
184
|
-
behaviorMinStateDuration = 600;
|
|
185
|
-
behaviorEnabled = false;
|
|
186
|
-
moveToCooldown = 400;
|
|
187
|
-
lastMoveToTime = 0;
|
|
188
|
-
retreatCooldown = 600;
|
|
189
|
-
lastRetreatTime = 0;
|
|
190
|
-
timers = [];
|
|
191
|
-
behaviorKey;
|
|
192
|
-
/**
|
|
193
|
-
* Create a new Battle AI Controller
|
|
194
|
-
*
|
|
195
|
-
* The AI controls behavior only. Stats should be set on the event
|
|
196
|
-
* using standard RPGJS methods (hp, param, learnSkill, etc.)
|
|
197
|
-
*
|
|
198
|
-
* @param event - The event to control
|
|
199
|
-
* @param options - AI behavior configuration
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* ```ts
|
|
203
|
-
* // In your event's onInit
|
|
204
|
-
* this.hp = 100;
|
|
205
|
-
* this.param[ATK] = 20;
|
|
206
|
-
* this.learnSkill(FireBall);
|
|
207
|
-
*
|
|
208
|
-
* new BattleAi(this, {
|
|
209
|
-
* enemyType: EnemyType.Ranged,
|
|
210
|
-
* attackSkill: FireBall,
|
|
211
|
-
* visionRange: 200,
|
|
212
|
-
* fleeThreshold: 0.2
|
|
213
|
-
* });
|
|
214
|
-
* ```
|
|
215
|
-
*/
|
|
216
|
-
constructor(event, options = {}) {
|
|
217
|
-
event.battleAi = this;
|
|
218
|
-
this.event = event;
|
|
219
|
-
this.enemyType = options.enemyType || EnemyType.Aggressive;
|
|
220
|
-
this.behaviorKey = options.behaviorKey ?? this.enemyType;
|
|
221
|
-
this.applyEnemyTypeBehavior(options);
|
|
222
|
-
this.attackSkill = options.attackSkill || null;
|
|
223
|
-
this.animations = {
|
|
224
|
-
...getActionBattleOptions().animations,
|
|
225
|
-
...options.animations
|
|
226
|
-
};
|
|
227
|
-
this.attackPatterns = options.attackPatterns || [
|
|
228
|
-
AttackPattern.Melee,
|
|
229
|
-
AttackPattern.Combo,
|
|
230
|
-
AttackPattern.DashAttack
|
|
231
|
-
];
|
|
232
|
-
this.groupBehavior = options.groupBehavior || false;
|
|
233
|
-
this.patrolWaypoints = options.patrolWaypoints || [];
|
|
234
|
-
this.currentPatrolIndex = 0;
|
|
235
|
-
this.onDefeatedCallback = options.onDefeated;
|
|
236
|
-
if (options.behavior) {
|
|
237
|
-
this.behaviorEnabled = true;
|
|
238
|
-
if (options.behavior.baseScore !== void 0) this.behaviorScore = options.behavior.baseScore;
|
|
239
|
-
if (options.behavior.updateInterval !== void 0) this.behaviorUpdateInterval = options.behavior.updateInterval;
|
|
240
|
-
if (options.behavior.minStateDuration !== void 0) this.behaviorMinStateDuration = options.behavior.minStateDuration;
|
|
241
|
-
if (options.behavior.assaultThreshold !== void 0) this.behaviorAssaultThreshold = options.behavior.assaultThreshold;
|
|
242
|
-
if (options.behavior.retreatThreshold !== void 0) this.behaviorRetreatThreshold = options.behavior.retreatThreshold;
|
|
243
|
-
}
|
|
244
|
-
if (options.moveToCooldown !== void 0) this.moveToCooldown = options.moveToCooldown;
|
|
245
|
-
if (options.retreatCooldown !== void 0) this.retreatCooldown = options.retreatCooldown;
|
|
246
|
-
this.setupVision();
|
|
247
|
-
this.startAiBehaviorLoop();
|
|
248
|
-
if (this.patrolWaypoints.length > 0) this.startPatrol();
|
|
249
|
-
this.debugLog("init", `AI created (type=${this.enemyType}, visionRange=${this.visionRange}, attackRange=${this.attackRange})`);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Apply enemy type-specific behavior modifiers
|
|
253
|
-
*
|
|
254
|
-
* This only affects AI behavior (cooldowns, ranges, dodge).
|
|
255
|
-
* Stats should be set on the event itself.
|
|
256
|
-
*/
|
|
257
|
-
applyEnemyTypeBehavior(options) {
|
|
258
|
-
switch (this.enemyType) {
|
|
259
|
-
case EnemyType.Aggressive:
|
|
260
|
-
this.attackCooldown = options.attackCooldown ?? 600;
|
|
261
|
-
this.visionRange = options.visionRange ?? 150;
|
|
262
|
-
this.attackRange = options.attackRange ?? 50;
|
|
263
|
-
this.dodgeChance = options.dodgeChance ?? .1;
|
|
264
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 3e3;
|
|
265
|
-
this.fleeThreshold = options.fleeThreshold ?? .15;
|
|
266
|
-
break;
|
|
267
|
-
case EnemyType.Defensive:
|
|
268
|
-
this.attackCooldown = options.attackCooldown ?? 1500;
|
|
269
|
-
this.visionRange = options.visionRange ?? 120;
|
|
270
|
-
this.attackRange = options.attackRange ?? 60;
|
|
271
|
-
this.dodgeChance = options.dodgeChance ?? .5;
|
|
272
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 1500;
|
|
273
|
-
this.fleeThreshold = options.fleeThreshold ?? .3;
|
|
274
|
-
break;
|
|
275
|
-
case EnemyType.Ranged:
|
|
276
|
-
this.attackCooldown = options.attackCooldown ?? 1200;
|
|
277
|
-
this.visionRange = options.visionRange ?? 200;
|
|
278
|
-
this.attackRange = options.attackRange ?? 120;
|
|
279
|
-
this.dodgeChance = options.dodgeChance ?? .4;
|
|
280
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 2e3;
|
|
281
|
-
this.fleeThreshold = options.fleeThreshold ?? .25;
|
|
282
|
-
break;
|
|
283
|
-
case EnemyType.Tank:
|
|
284
|
-
this.attackCooldown = options.attackCooldown ?? 2e3;
|
|
285
|
-
this.visionRange = options.visionRange ?? 100;
|
|
286
|
-
this.attackRange = options.attackRange ?? 50;
|
|
287
|
-
this.dodgeChance = 0;
|
|
288
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 5e3;
|
|
289
|
-
this.fleeThreshold = options.fleeThreshold ?? .1;
|
|
290
|
-
break;
|
|
291
|
-
case EnemyType.Berserker:
|
|
292
|
-
this.attackCooldown = options.attackCooldown ?? 800;
|
|
293
|
-
this.visionRange = options.visionRange ?? 180;
|
|
294
|
-
this.attackRange = options.attackRange ?? 55;
|
|
295
|
-
this.dodgeChance = options.dodgeChance ?? .15;
|
|
296
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 2500;
|
|
297
|
-
this.fleeThreshold = options.fleeThreshold ?? .05;
|
|
298
|
-
break;
|
|
299
|
-
default:
|
|
300
|
-
this.attackCooldown = options.attackCooldown ?? 1e3;
|
|
301
|
-
this.visionRange = options.visionRange ?? 150;
|
|
302
|
-
this.attackRange = options.attackRange ?? 60;
|
|
303
|
-
this.dodgeChance = options.dodgeChance ?? .2;
|
|
304
|
-
this.dodgeCooldown = options.dodgeCooldown ?? 2e3;
|
|
305
|
-
this.fleeThreshold = options.fleeThreshold ?? .2;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Setup vision detection
|
|
310
|
-
*/
|
|
311
|
-
setupVision() {
|
|
312
|
-
const diameter = this.visionRange * 2;
|
|
313
|
-
this.event.attachShape(`vision_${this.event.id}`, {
|
|
314
|
-
radius: this.visionRange,
|
|
315
|
-
width: diameter,
|
|
316
|
-
height: diameter,
|
|
317
|
-
angle: 360
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Start the AI behavior loop
|
|
322
|
-
*/
|
|
323
|
-
startAiBehaviorLoop() {
|
|
324
|
-
const updateInterval = setInterval(() => {
|
|
325
|
-
if (!this.event.getCurrentMap()) {
|
|
326
|
-
this.destroy();
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
this.updateAiBehavior();
|
|
330
|
-
}, 100);
|
|
331
|
-
this.updateInterval = updateInterval;
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Change AI state with validated transitions
|
|
335
|
-
*/
|
|
336
|
-
changeState(newState) {
|
|
337
|
-
if (newState === this.state) return;
|
|
338
|
-
if (!{
|
|
339
|
-
[AiState.Idle]: [AiState.Alert, AiState.Combat],
|
|
340
|
-
[AiState.Alert]: [AiState.Idle, AiState.Combat],
|
|
341
|
-
[AiState.Combat]: [
|
|
342
|
-
AiState.Idle,
|
|
343
|
-
AiState.Flee,
|
|
344
|
-
AiState.Stunned
|
|
345
|
-
],
|
|
346
|
-
[AiState.Flee]: [AiState.Idle, AiState.Combat],
|
|
347
|
-
[AiState.Stunned]: [AiState.Combat, AiState.Idle]
|
|
348
|
-
}[this.state].includes(newState)) {
|
|
349
|
-
this.debugLog("state", `INVALID transition ${this.state} -> ${newState}`);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
this.debugLog("state", `STATE change: ${this.state} -> ${newState}`);
|
|
353
|
-
this.state = newState;
|
|
354
|
-
this.stateStartTime = Date.now();
|
|
355
|
-
switch (newState) {
|
|
356
|
-
case AiState.Idle:
|
|
357
|
-
if (this.patrolWaypoints.length > 0) this.startPatrol();
|
|
358
|
-
break;
|
|
359
|
-
case AiState.Alert:
|
|
360
|
-
this.event.stopMoveTo();
|
|
361
|
-
break;
|
|
362
|
-
case AiState.Combat:
|
|
363
|
-
this.comboCount = 0;
|
|
364
|
-
break;
|
|
365
|
-
case AiState.Flee:
|
|
366
|
-
if (this.target) this.fleeFromTarget();
|
|
367
|
-
break;
|
|
368
|
-
case AiState.Stunned:
|
|
369
|
-
this.event.stopMoveTo();
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Main AI behavior update loop
|
|
375
|
-
*/
|
|
376
|
-
updateAiBehavior() {
|
|
377
|
-
const currentTime = Date.now();
|
|
378
|
-
if (this.groupBehavior) this.updateGroupBehavior();
|
|
379
|
-
if (this.state === AiState.Stunned) {
|
|
380
|
-
if (currentTime >= this.stunnedUntil) this.changeState(AiState.Combat);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (this.enemyType === EnemyType.Berserker && this.event.param[MAXHP]) {
|
|
384
|
-
const hpPercent = this.event.hp / this.event.param[MAXHP];
|
|
385
|
-
const berserkerModifier = Math.max(.3, hpPercent);
|
|
386
|
-
this.attackCooldown = 800 * berserkerModifier;
|
|
387
|
-
}
|
|
388
|
-
if (this.behaviorEnabled) this.updateBehavior(currentTime);
|
|
389
|
-
this.applyCustomBehavior(currentTime);
|
|
390
|
-
switch (this.state) {
|
|
391
|
-
case AiState.Idle:
|
|
392
|
-
this.updateIdleBehavior();
|
|
393
|
-
break;
|
|
394
|
-
case AiState.Alert:
|
|
395
|
-
this.updateAlertBehavior();
|
|
396
|
-
break;
|
|
397
|
-
case AiState.Combat:
|
|
398
|
-
this.updateCombatBehavior(currentTime);
|
|
399
|
-
break;
|
|
400
|
-
case AiState.Flee:
|
|
401
|
-
this.updateFleeBehavior();
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
this.checkDamageTaken();
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Update idle behavior (patrolling)
|
|
408
|
-
*/
|
|
409
|
-
updateIdleBehavior() {
|
|
410
|
-
if (this.patrolWaypoints.length > 0) {
|
|
411
|
-
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
412
|
-
if (this.getDistance(this.event, {
|
|
413
|
-
x: () => waypoint.x,
|
|
414
|
-
y: () => waypoint.y
|
|
415
|
-
}) < 10) {
|
|
416
|
-
this.currentPatrolIndex = (this.currentPatrolIndex + 1) % this.patrolWaypoints.length;
|
|
417
|
-
this.startPatrol();
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Update alert behavior
|
|
423
|
-
*/
|
|
424
|
-
updateAlertBehavior() {
|
|
425
|
-
if (this.target) {
|
|
426
|
-
this.faceTarget();
|
|
427
|
-
if (this.getDistance(this.event, this.target) <= this.attackRange * 1.5) this.changeState(AiState.Combat);
|
|
428
|
-
} else this.changeState(AiState.Idle);
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Update combat behavior
|
|
432
|
-
*/
|
|
433
|
-
updateCombatBehavior(currentTime) {
|
|
434
|
-
if (!this.target) {
|
|
435
|
-
this.debugLog("combat", "No target, returning to idle");
|
|
436
|
-
this.changeState(AiState.Idle);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
const distance = this.getDistance(this.event, this.target);
|
|
440
|
-
if (distance > this.visionRange * 1.5) {
|
|
441
|
-
this.debugLog("combat", `Target out of range (dist=${distance.toFixed(1)}, maxRange=${(this.visionRange * 1.5).toFixed(1)})`);
|
|
442
|
-
this.target = null;
|
|
443
|
-
this.isMovingToTarget = false;
|
|
444
|
-
this.event.stopMoveTo();
|
|
445
|
-
this.changeState(AiState.Idle);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (this.event.param[MAXHP]) {
|
|
449
|
-
const hpPercent = this.event.hp / this.event.param[MAXHP];
|
|
450
|
-
if (hpPercent <= this.fleeThreshold) {
|
|
451
|
-
this.debugLog("combat", `HP low (${(hpPercent * 100).toFixed(0)}%), fleeing`);
|
|
452
|
-
this.isMovingToTarget = false;
|
|
453
|
-
this.changeState(AiState.Flee);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (this.canDodge() && this.shouldDodge()) {
|
|
458
|
-
this.debugLog("combat", "Attempting dodge");
|
|
459
|
-
if (this.tryDodge()) {
|
|
460
|
-
this.isMovingToTarget = false;
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (this.behaviorEnabled) {
|
|
465
|
-
if (this.behaviorMode === "tactical") this.handleTacticalMovement(distance);
|
|
466
|
-
else if (this.behaviorMode === "assault") this.handleAssaultMovement(distance);
|
|
467
|
-
else if (this.behaviorMode === "retreat") {
|
|
468
|
-
this.isMovingToTarget = false;
|
|
469
|
-
this.fleeFromTarget();
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if (this.behaviorEnabled && this.behaviorMode === "assault") {} else if (this.behaviorEnabled && this.behaviorMode === "tactical") {} else if (this.enemyType === EnemyType.Ranged) {
|
|
474
|
-
if (distance < this.attackRange * .6) {
|
|
475
|
-
this.debugLog("movement", `Retreating (dist=${distance.toFixed(1)}, minRange=${(this.attackRange * .6).toFixed(1)})`);
|
|
476
|
-
this.isMovingToTarget = false;
|
|
477
|
-
this.retreatFromTarget();
|
|
478
|
-
} else if (distance > this.attackRange) {
|
|
479
|
-
if (!this.isMovingToTarget) {
|
|
480
|
-
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
481
|
-
this.isMovingToTarget = true;
|
|
482
|
-
this.requestMoveTo(this.target);
|
|
483
|
-
}
|
|
484
|
-
} else if (this.isMovingToTarget) {
|
|
485
|
-
this.debugLog("movement", `In range, stopping (dist=${distance.toFixed(1)})`);
|
|
486
|
-
this.isMovingToTarget = false;
|
|
487
|
-
this.event.stopMoveTo();
|
|
488
|
-
}
|
|
489
|
-
} else if (distance > this.attackRange) {
|
|
490
|
-
if (!this.isMovingToTarget) {
|
|
491
|
-
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
492
|
-
this.isMovingToTarget = true;
|
|
493
|
-
this.requestMoveTo(this.target);
|
|
494
|
-
}
|
|
495
|
-
} else if (this.isMovingToTarget) {
|
|
496
|
-
this.debugLog("movement", `In range, stopping (dist=${distance.toFixed(1)})`);
|
|
497
|
-
this.isMovingToTarget = false;
|
|
498
|
-
this.event.stopMoveTo();
|
|
499
|
-
}
|
|
500
|
-
if (distance <= this.attackRange && currentTime - this.lastAttackTime >= this.attackCooldown) {
|
|
501
|
-
if (!this.chargingAttack) {
|
|
502
|
-
this.debugLog("attack", `Attacking (dist=${distance.toFixed(1)}, cooldown=${this.attackCooldown}ms)`);
|
|
503
|
-
this.selectAndPerformAttack();
|
|
504
|
-
this.lastAttackTime = currentTime;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Update flee behavior
|
|
510
|
-
*/
|
|
511
|
-
updateFleeBehavior() {
|
|
512
|
-
if (!this.target) {
|
|
513
|
-
this.changeState(AiState.Idle);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const distance = this.getDistance(this.event, this.target);
|
|
517
|
-
if (this.event.param[MAXHP]) {
|
|
518
|
-
if (this.event.hp / this.event.param[MAXHP] > this.fleeThreshold * 1.5 || distance > this.visionRange * 2) {
|
|
519
|
-
this.changeState(AiState.Combat);
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
this.fleeFromTarget();
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Select and perform an attack pattern
|
|
527
|
-
*/
|
|
528
|
-
selectAndPerformAttack() {
|
|
529
|
-
if (!this.target) return;
|
|
530
|
-
if (this.comboCount > 0 && this.comboCount < this.comboMax) {
|
|
531
|
-
this.debugLog("attack", `Continuing combo (${this.comboCount}/${this.comboMax})`);
|
|
532
|
-
this.performComboAttack();
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const pattern = this.selectAttackPattern();
|
|
536
|
-
this.debugLog("attack", `Selected pattern: ${pattern}`);
|
|
537
|
-
this.performAttackPattern(pattern);
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Select attack pattern with weighted probability
|
|
541
|
-
*/
|
|
542
|
-
selectAttackPattern() {
|
|
543
|
-
const weights = {
|
|
544
|
-
[AttackPattern.Melee]: 40,
|
|
545
|
-
[AttackPattern.Combo]: 25,
|
|
546
|
-
[AttackPattern.Charged]: 15,
|
|
547
|
-
[AttackPattern.Zone]: 10,
|
|
548
|
-
[AttackPattern.DashAttack]: 10
|
|
549
|
-
};
|
|
550
|
-
switch (this.enemyType) {
|
|
551
|
-
case EnemyType.Aggressive:
|
|
552
|
-
weights[AttackPattern.Combo] += 20;
|
|
553
|
-
weights[AttackPattern.DashAttack] += 15;
|
|
554
|
-
break;
|
|
555
|
-
case EnemyType.Defensive:
|
|
556
|
-
weights[AttackPattern.Charged] += 25;
|
|
557
|
-
break;
|
|
558
|
-
case EnemyType.Ranged:
|
|
559
|
-
weights[AttackPattern.Zone] += 20;
|
|
560
|
-
break;
|
|
561
|
-
case EnemyType.Tank:
|
|
562
|
-
weights[AttackPattern.Charged] += 30;
|
|
563
|
-
weights[AttackPattern.Zone] += 15;
|
|
564
|
-
break;
|
|
565
|
-
case EnemyType.Berserker:
|
|
566
|
-
weights[AttackPattern.Combo] += 35;
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
let total = 0;
|
|
570
|
-
const available = [];
|
|
571
|
-
this.attackPatterns.forEach((p) => {
|
|
572
|
-
const weight = weights[p] || 10;
|
|
573
|
-
total += weight;
|
|
574
|
-
available.push({
|
|
575
|
-
pattern: p,
|
|
576
|
-
weight
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
let random = Math.random() * total;
|
|
580
|
-
for (const item of available) {
|
|
581
|
-
random -= item.weight;
|
|
582
|
-
if (random <= 0) return item.pattern;
|
|
583
|
-
}
|
|
584
|
-
return this.attackPatterns[0] || AttackPattern.Melee;
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Perform attack pattern
|
|
588
|
-
*/
|
|
589
|
-
performAttackPattern(pattern) {
|
|
590
|
-
switch (pattern) {
|
|
591
|
-
case AttackPattern.Melee:
|
|
592
|
-
this.performMeleeAttack();
|
|
593
|
-
break;
|
|
594
|
-
case AttackPattern.Combo:
|
|
595
|
-
this.performComboAttack();
|
|
596
|
-
break;
|
|
597
|
-
case AttackPattern.Charged:
|
|
598
|
-
this.performChargedAttack();
|
|
599
|
-
break;
|
|
600
|
-
case AttackPattern.Zone:
|
|
601
|
-
this.performZoneAttack();
|
|
602
|
-
break;
|
|
603
|
-
case AttackPattern.DashAttack:
|
|
604
|
-
this.performDashAttack();
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* Perform melee attack
|
|
610
|
-
* Uses skill if configured, otherwise creates hitbox
|
|
611
|
-
*/
|
|
612
|
-
performMeleeAttack() {
|
|
613
|
-
if (!this.target) return;
|
|
614
|
-
this.faceTarget();
|
|
615
|
-
playActionBattleAnimation("attack", this.event, this.animations, { target: this.target });
|
|
616
|
-
if (this.attackSkill) try {
|
|
617
|
-
playActionBattleAnimation("castSkill", this.event, this.animations, {
|
|
618
|
-
skill: this.attackSkill,
|
|
619
|
-
target: this.target
|
|
620
|
-
});
|
|
621
|
-
this.event.useSkill(this.attackSkill, this.target);
|
|
622
|
-
} catch (e) {
|
|
623
|
-
this.performBasicHitbox();
|
|
624
|
-
}
|
|
625
|
-
else this.performBasicHitbox();
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Perform basic hitbox attack when no skill is set
|
|
629
|
-
*/
|
|
630
|
-
performBasicHitbox() {
|
|
631
|
-
if (!this.target) return;
|
|
632
|
-
const eventX = this.event.x();
|
|
633
|
-
const eventY = this.event.y();
|
|
634
|
-
const dx = this.target.x() - eventX;
|
|
635
|
-
const dy = this.target.y() - eventY;
|
|
636
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
637
|
-
if (dist === 0) return;
|
|
638
|
-
const dirX = dx / dist;
|
|
639
|
-
const dirY = dy / dist;
|
|
640
|
-
const hitboxes = [{
|
|
641
|
-
x: eventX + dirX * 30,
|
|
642
|
-
y: eventY + dirY * 30,
|
|
643
|
-
width: 40,
|
|
644
|
-
height: 40
|
|
645
|
-
}];
|
|
646
|
-
this.event.getCurrentMap()?.createMovingHitbox(hitboxes, { speed: 5 }).subscribe({ next: (hits) => {
|
|
647
|
-
hits.forEach((hit) => {
|
|
648
|
-
if (hit instanceof RpgPlayer && hit !== this.event) this.applyHit(hit);
|
|
649
|
-
});
|
|
650
|
-
} });
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Apply hit to target using RPGJS damage system with knockback
|
|
654
|
-
*
|
|
655
|
-
* Calculates damage using RPGJS formula, applies knockback based on
|
|
656
|
-
* equipped weapon's knockbackForce property, and triggers visual effects.
|
|
657
|
-
* Supports hooks for customizing behavior.
|
|
658
|
-
*
|
|
659
|
-
* @param target - The player or entity being hit
|
|
660
|
-
* @param hooks - Optional hooks for customizing hit behavior
|
|
661
|
-
* @returns The hit result containing damage and knockback info
|
|
662
|
-
*
|
|
663
|
-
* @example
|
|
664
|
-
* ```ts
|
|
665
|
-
* // Basic hit
|
|
666
|
-
* this.applyHit(player);
|
|
667
|
-
*
|
|
668
|
-
* // With custom hooks
|
|
669
|
-
* this.applyHit(player, {
|
|
670
|
-
* onBeforeHit(result) {
|
|
671
|
-
* result.knockbackForce *= 1.5; // Increase knockback
|
|
672
|
-
* return result;
|
|
673
|
-
* },
|
|
674
|
-
* onAfterHit(result) {
|
|
675
|
-
* console.log(`Dealt ${result.damage} damage!`);
|
|
676
|
-
* }
|
|
677
|
-
* });
|
|
678
|
-
* ```
|
|
679
|
-
*/
|
|
680
|
-
applyHit(target, hooks) {
|
|
681
|
-
const { damage } = target.applyDamage(this.event);
|
|
682
|
-
let hitResult = {
|
|
683
|
-
damage,
|
|
684
|
-
knockbackForce: this.getWeaponKnockbackForce(),
|
|
685
|
-
knockbackDuration: DEFAULT_KNOCKBACK.duration,
|
|
686
|
-
defeated: target.hp <= 0,
|
|
687
|
-
attacker: this.event,
|
|
688
|
-
target
|
|
689
|
-
};
|
|
690
|
-
if (hooks?.onBeforeHit) {
|
|
691
|
-
const modified = hooks.onBeforeHit(hitResult);
|
|
692
|
-
if (modified) hitResult = modified;
|
|
693
|
-
}
|
|
694
|
-
target.flash({
|
|
695
|
-
type: "tint",
|
|
696
|
-
tint: "red",
|
|
697
|
-
duration: 200,
|
|
698
|
-
cycles: 1
|
|
699
|
-
});
|
|
700
|
-
target.showHit(`-${hitResult.damage}`);
|
|
701
|
-
if (hitResult.knockbackForce > 0) {
|
|
702
|
-
const dx = target.x() - this.event.x();
|
|
703
|
-
const dy = target.y() - this.event.y();
|
|
704
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
705
|
-
if (distance > 0) {
|
|
706
|
-
const knockbackDirection = {
|
|
707
|
-
x: dx / distance,
|
|
708
|
-
y: dy / distance
|
|
709
|
-
};
|
|
710
|
-
target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (hooks?.onAfterHit) hooks.onAfterHit(hitResult);
|
|
714
|
-
return hitResult;
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Get knockback force from equipped weapon
|
|
718
|
-
*
|
|
719
|
-
* Retrieves the knockbackForce property from the event's equipped weapon.
|
|
720
|
-
* Falls back to DEFAULT_KNOCKBACK.force if no weapon or property is set.
|
|
721
|
-
*
|
|
722
|
-
* @returns Knockback force value
|
|
723
|
-
*
|
|
724
|
-
* @example
|
|
725
|
-
* ```ts
|
|
726
|
-
* // Weapon with knockbackForce: 80
|
|
727
|
-
* const force = this.getWeaponKnockbackForce(); // 80
|
|
728
|
-
*
|
|
729
|
-
* // No weapon equipped
|
|
730
|
-
* const force = this.getWeaponKnockbackForce(); // 50 (default)
|
|
731
|
-
* ```
|
|
732
|
-
*/
|
|
733
|
-
getWeaponKnockbackForce() {
|
|
734
|
-
try {
|
|
735
|
-
const equipments = this.event.equipments?.() || [];
|
|
736
|
-
for (const item of equipments) {
|
|
737
|
-
const itemData = this.event.databaseById?.(item.id());
|
|
738
|
-
if (itemData?._type === "weapon" && itemData.knockbackForce !== void 0) return itemData.knockbackForce;
|
|
739
|
-
}
|
|
740
|
-
} catch {}
|
|
741
|
-
return DEFAULT_KNOCKBACK.force;
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* Perform combo attack
|
|
745
|
-
*/
|
|
746
|
-
performComboAttack() {
|
|
747
|
-
if (!this.target) return;
|
|
748
|
-
this.comboCount++;
|
|
749
|
-
this.performMeleeAttack();
|
|
750
|
-
if (this.comboCount < this.comboMax) this.schedule(() => {
|
|
751
|
-
if (this.target && this.state === AiState.Combat) this.performComboAttack();
|
|
752
|
-
else this.comboCount = 0;
|
|
753
|
-
}, 300);
|
|
754
|
-
else this.comboCount = 0;
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Perform charged attack
|
|
758
|
-
*/
|
|
759
|
-
performChargedAttack() {
|
|
760
|
-
if (!this.target) return;
|
|
761
|
-
this.chargingAttack = true;
|
|
762
|
-
this.faceTarget();
|
|
763
|
-
playActionBattleAnimation("attack", this.event, this.animations, { target: this.target }, { repeat: 2 });
|
|
764
|
-
this.schedule(() => {
|
|
765
|
-
if (!this.target || this.state !== AiState.Combat) {
|
|
766
|
-
this.chargingAttack = false;
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (this.attackSkill) try {
|
|
770
|
-
playActionBattleAnimation("castSkill", this.event, this.animations, {
|
|
771
|
-
skill: this.attackSkill,
|
|
772
|
-
target: this.target
|
|
773
|
-
});
|
|
774
|
-
this.event.useSkill(this.attackSkill, this.target);
|
|
775
|
-
} catch (e) {
|
|
776
|
-
this.performBasicHitbox();
|
|
777
|
-
}
|
|
778
|
-
else this.performBasicHitbox();
|
|
779
|
-
this.chargingAttack = false;
|
|
780
|
-
}, 800);
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Perform zone attack (360 degrees)
|
|
784
|
-
*/
|
|
785
|
-
performZoneAttack() {
|
|
786
|
-
playActionBattleAnimation("attack", this.event, this.animations, { target: this.target ?? void 0 });
|
|
787
|
-
const eventX = this.event.x();
|
|
788
|
-
const eventY = this.event.y();
|
|
789
|
-
const radius = 50;
|
|
790
|
-
const hitboxes = [];
|
|
791
|
-
[
|
|
792
|
-
0,
|
|
793
|
-
90,
|
|
794
|
-
180,
|
|
795
|
-
270
|
|
796
|
-
].forEach((angle) => {
|
|
797
|
-
const rad = angle * Math.PI / 180;
|
|
798
|
-
hitboxes.push({
|
|
799
|
-
x: eventX + Math.cos(rad) * radius,
|
|
800
|
-
y: eventY + Math.sin(rad) * radius,
|
|
801
|
-
width: 40,
|
|
802
|
-
height: 40
|
|
803
|
-
});
|
|
804
|
-
});
|
|
805
|
-
this.event.getCurrentMap()?.createMovingHitbox(hitboxes, { speed: 5 }).subscribe({ next: (hits) => {
|
|
806
|
-
hits.forEach((hit) => {
|
|
807
|
-
if (hit instanceof RpgPlayer && hit !== this.event) this.applyHit(hit);
|
|
46
|
+
var playLocalPlayerAttackAnimation = (player, options) => {
|
|
47
|
+
if (!player || typeof player.setAnimation !== "function") return;
|
|
48
|
+
const animation = resolveActionBattleAnimation("attack", player, options.animations);
|
|
49
|
+
if (!animation) return;
|
|
50
|
+
if (animation.graphic !== void 0) {
|
|
51
|
+
player.setAnimation(animation.animationName, animation.graphic, animation.repeat);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
player.setAnimation(animation.animationName, animation.repeat);
|
|
55
|
+
};
|
|
56
|
+
var showLocalAttackPreview = (player, options) => {
|
|
57
|
+
if (!player || options.attack?.showPreview === false) return;
|
|
58
|
+
const durationMs = Math.max(1, options.attack?.previewDurationMs ?? 180);
|
|
59
|
+
const previewId = startAttackPreview({
|
|
60
|
+
direction: resolveLocalPlayerDirection(player),
|
|
61
|
+
durationMs,
|
|
62
|
+
color: options.attack?.previewColor,
|
|
63
|
+
accentColor: options.attack?.previewAccentColor
|
|
64
|
+
});
|
|
65
|
+
setTimeout(() => stopAttackPreview(previewId), durationMs);
|
|
66
|
+
};
|
|
67
|
+
var createActionBattleClient = (options = {}) => {
|
|
68
|
+
const normalized = normalizeActionBattleOptions(options);
|
|
69
|
+
setActionBattleOptions(normalized);
|
|
70
|
+
const actionBarEnabled = normalized.ui?.actionBar?.enabled;
|
|
71
|
+
const componentsInFront = [...normalized.ui?.targeting?.enabled ? [component$1] : [], component$2];
|
|
72
|
+
const hitComponent = PrebuiltComponentAnimations?.Hit;
|
|
73
|
+
return defineModule({
|
|
74
|
+
componentAnimations: hitComponent ? [{
|
|
75
|
+
id: "hit",
|
|
76
|
+
component: hitComponent
|
|
77
|
+
}] : [],
|
|
78
|
+
gui: actionBarEnabled ? [{
|
|
79
|
+
id: "action-battle-action-bar",
|
|
80
|
+
component,
|
|
81
|
+
dependencies: () => {
|
|
82
|
+
return [inject(RpgClientEngine).scene.currentPlayer];
|
|
83
|
+
}
|
|
84
|
+
}] : [],
|
|
85
|
+
sprite: { componentsInFront },
|
|
86
|
+
sceneMap: { onAfterLoading() {
|
|
87
|
+
if (actionBarEnabled && normalized.ui?.actionBar?.autoOpen) inject(RpgGui).display("action-battle-action-bar");
|
|
88
|
+
} },
|
|
89
|
+
engine: { onInput(engine, { input }) {
|
|
90
|
+
if (input !== "action") return;
|
|
91
|
+
const player = engine.scene?.getCurrentPlayer?.();
|
|
92
|
+
if (!player) return;
|
|
93
|
+
const attackProfile = getNormalizedActionBattleAttackProfile(normalized);
|
|
94
|
+
const lockDurationMs = Math.max(0, attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS);
|
|
95
|
+
if (attackProfile.movementLock || attackProfile.directionLock) beginLocalPlayerAttackLock(engine, lockDurationMs, {
|
|
96
|
+
movement: attackProfile.movementLock,
|
|
97
|
+
direction: attackProfile.directionLock
|
|
808
98
|
});
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
*/
|
|
814
|
-
performDashAttack() {
|
|
815
|
-
if (!this.target) return;
|
|
816
|
-
const dx = this.target.x() - this.event.x();
|
|
817
|
-
const dy = this.target.y() - this.event.y();
|
|
818
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
819
|
-
if (dist === 0) return;
|
|
820
|
-
const dirX = dx / dist;
|
|
821
|
-
const dirY = dy / dist;
|
|
822
|
-
this.faceTarget();
|
|
823
|
-
this.event.dash({
|
|
824
|
-
x: dirX,
|
|
825
|
-
y: dirY
|
|
826
|
-
}, 10, 200);
|
|
827
|
-
this.schedule(() => {
|
|
828
|
-
if (!this.target || this.state !== AiState.Combat) return;
|
|
829
|
-
this.performMeleeAttack();
|
|
830
|
-
}, 200);
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Face the current target with hysteresis to prevent animation flickering
|
|
834
|
-
*
|
|
835
|
-
* Uses multiple strategies to prevent flickering:
|
|
836
|
-
* 1. When very close to target (collision), keep current direction
|
|
837
|
-
* 2. When near diagonal, require significant difference to change
|
|
838
|
-
* 3. Only change if direction is clearly wrong (opposite)
|
|
839
|
-
*/
|
|
840
|
-
faceTarget() {
|
|
841
|
-
if (!this.target) return;
|
|
842
|
-
const dx = this.target.x() - this.event.x();
|
|
843
|
-
const dy = this.target.y() - this.event.y();
|
|
844
|
-
const absX = Math.abs(dx);
|
|
845
|
-
const absY = Math.abs(dy);
|
|
846
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
847
|
-
if (this.lastFacingDirection && distance < 40) return;
|
|
848
|
-
let newDirection;
|
|
849
|
-
if (absX >= absY) newDirection = dx >= 0 ? "right" : "left";
|
|
850
|
-
else newDirection = dy >= 0 ? "down" : "up";
|
|
851
|
-
const hysteresisThreshold = .2;
|
|
852
|
-
const ratio = absX > 0 || absY > 0 ? Math.min(absX, absY) / Math.max(absX, absY) : 0;
|
|
853
|
-
if (this.lastFacingDirection && ratio > 1 - hysteresisThreshold) {
|
|
854
|
-
if (!(this.lastFacingDirection === "left" && dx > 20 || this.lastFacingDirection === "right" && dx < -20 || this.lastFacingDirection === "up" && dy > 20 || this.lastFacingDirection === "down" && dy < -20)) return;
|
|
855
|
-
}
|
|
856
|
-
this.lastFacingDirection = newDirection;
|
|
857
|
-
this.event.changeDirection(newDirection);
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* Try to dodge
|
|
861
|
-
*/
|
|
862
|
-
tryDodge() {
|
|
863
|
-
const currentTime = Date.now();
|
|
864
|
-
if (currentTime - this.lastDodgeTime < this.dodgeCooldown) {
|
|
865
|
-
this.debugLog("dodge", `Dodge on cooldown (${this.dodgeCooldown - (currentTime - this.lastDodgeTime)}ms remaining)`);
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
if (Math.random() > this.dodgeChance) {
|
|
869
|
-
this.debugLog("dodge", `Dodge roll failed (chance=${(this.dodgeChance * 100).toFixed(0)}%)`);
|
|
870
|
-
return false;
|
|
871
|
-
}
|
|
872
|
-
if (!this.target) return false;
|
|
873
|
-
const dx = this.target.x() - this.event.x();
|
|
874
|
-
const dy = this.target.y() - this.event.y();
|
|
875
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
876
|
-
if (dist === 0) return false;
|
|
877
|
-
const dodgeDirX = -dy / dist;
|
|
878
|
-
const dodgeDirY = dx / dist;
|
|
879
|
-
const side = Math.random() > .5 ? 1 : -1;
|
|
880
|
-
this.debugLog("dodge", `Dodging (dir=${side > 0 ? "right" : "left"})`);
|
|
881
|
-
this.event.dash({
|
|
882
|
-
x: dodgeDirX * side,
|
|
883
|
-
y: dodgeDirY * side
|
|
884
|
-
}, 12, 300);
|
|
885
|
-
this.lastDodgeTime = currentTime;
|
|
886
|
-
if (this.enemyType === EnemyType.Defensive && Math.random() < .5) {
|
|
887
|
-
this.debugLog("dodge", "Counter-attack after dodge");
|
|
888
|
-
this.schedule(() => {
|
|
889
|
-
if (this.target && this.state === AiState.Combat) this.selectAndPerformAttack();
|
|
890
|
-
}, 400);
|
|
891
|
-
}
|
|
892
|
-
return true;
|
|
893
|
-
}
|
|
894
|
-
canDodge() {
|
|
895
|
-
if (this.dodgeChance === 0) return false;
|
|
896
|
-
return Date.now() - this.lastDodgeTime >= this.dodgeCooldown;
|
|
897
|
-
}
|
|
898
|
-
shouldDodge() {
|
|
899
|
-
if (!this.target) return false;
|
|
900
|
-
return this.getDistance(this.event, this.target) < this.attackRange * .8;
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Flee from target
|
|
904
|
-
*/
|
|
905
|
-
fleeFromTarget() {
|
|
906
|
-
if (!this.target) return;
|
|
907
|
-
const dx = this.event.x() - this.target.x();
|
|
908
|
-
const dy = this.event.y() - this.target.y();
|
|
909
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
910
|
-
if (dist === 0) return;
|
|
911
|
-
this.requestMoveTo({
|
|
912
|
-
x: () => this.event.x() + dx / dist * 200,
|
|
913
|
-
y: () => this.event.y() + dy / dist * 200
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Retreat from target (temporary)
|
|
918
|
-
*/
|
|
919
|
-
retreatFromTarget() {
|
|
920
|
-
if (!this.target) return;
|
|
921
|
-
const currentTime = Date.now();
|
|
922
|
-
if (currentTime - this.lastRetreatTime < this.retreatCooldown) return;
|
|
923
|
-
const dx = this.event.x() - this.target.x();
|
|
924
|
-
const dy = this.event.y() - this.target.y();
|
|
925
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
926
|
-
if (dist === 0) return;
|
|
927
|
-
this.event.dash({
|
|
928
|
-
x: dx / dist,
|
|
929
|
-
y: dy / dist
|
|
930
|
-
}, 8, 200);
|
|
931
|
-
this.lastRetreatTime = currentTime;
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Check damage taken for retreat decision
|
|
935
|
-
*/
|
|
936
|
-
checkDamageTaken() {
|
|
937
|
-
const currentTime = Date.now();
|
|
938
|
-
if (currentTime - this.lastHpCheck >= this.damageCheckInterval) {
|
|
939
|
-
this.recentDamageTaken = 0;
|
|
940
|
-
this.lastHpCheck = currentTime;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* Start patrol
|
|
945
|
-
*/
|
|
946
|
-
startPatrol() {
|
|
947
|
-
if (this.patrolWaypoints.length === 0) return;
|
|
948
|
-
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
949
|
-
this.requestMoveTo({
|
|
950
|
-
x: () => waypoint.x,
|
|
951
|
-
y: () => waypoint.y
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Update group behavior
|
|
956
|
-
*/
|
|
957
|
-
updateGroupBehavior() {
|
|
958
|
-
if (!this.groupBehavior) return;
|
|
959
|
-
this.groupUpdateInterval++;
|
|
960
|
-
if (this.groupUpdateInterval >= 20) {
|
|
961
|
-
this.groupUpdateInterval = 0;
|
|
962
|
-
this.findNearbyEnemies();
|
|
963
|
-
}
|
|
964
|
-
if (this.nearbyEnemies.length > 0 && this.target && this.state === AiState.Combat) this.applyFormation();
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Find nearby enemies
|
|
968
|
-
*/
|
|
969
|
-
findNearbyEnemies() {
|
|
970
|
-
this.nearbyEnemies = [];
|
|
971
|
-
const map = this.event.getCurrentMap();
|
|
972
|
-
if (!map) return;
|
|
973
|
-
const allEvents = Object.values(map.events());
|
|
974
|
-
const groupRadius = 150;
|
|
975
|
-
allEvents.forEach((event) => {
|
|
976
|
-
if (event === this.event) return;
|
|
977
|
-
const ai = event.battleAi;
|
|
978
|
-
if (ai && ai.groupBehavior) {
|
|
979
|
-
if (this.getDistance(this.event, event) <= groupRadius) this.nearbyEnemies.push(ai);
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Apply formation around target
|
|
985
|
-
*/
|
|
986
|
-
applyFormation() {
|
|
987
|
-
if (!this.target || this.nearbyEnemies.length === 0) return;
|
|
988
|
-
const totalEnemies = this.nearbyEnemies.length + 1;
|
|
989
|
-
const angleStep = 2 * Math.PI / totalEnemies;
|
|
990
|
-
let ourIndex = 0;
|
|
991
|
-
for (let i = 0; i < this.nearbyEnemies.length; i++) if (this.nearbyEnemies[i].event.id < this.event.id) ourIndex++;
|
|
992
|
-
const angle = angleStep * ourIndex;
|
|
993
|
-
const formationRadius = this.attackRange * 1.2;
|
|
994
|
-
const formationX = this.target.x() + Math.cos(angle) * formationRadius;
|
|
995
|
-
const formationY = this.target.y() + Math.sin(angle) * formationRadius;
|
|
996
|
-
if (Math.sqrt(Math.pow(this.event.x() - formationX, 2) + Math.pow(this.event.y() - formationY, 2)) > 20) this.requestMoveTo({
|
|
997
|
-
x: () => formationX,
|
|
998
|
-
y: () => formationY
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Handle player entering vision
|
|
1003
|
-
*/
|
|
1004
|
-
onDetectInShape(player, shape) {
|
|
1005
|
-
this.debugLog("vision", `Player ${player.id} entered vision (state=${this.state})`);
|
|
1006
|
-
this.target = player;
|
|
1007
|
-
if (this.state === AiState.Idle) this.changeState(AiState.Alert);
|
|
1008
|
-
else if (this.state === AiState.Alert) this.changeState(AiState.Combat);
|
|
1009
|
-
}
|
|
1010
|
-
/**
|
|
1011
|
-
* Handle player leaving vision
|
|
1012
|
-
*/
|
|
1013
|
-
onDetectOutShape(player, shape) {
|
|
1014
|
-
this.debugLog("vision", `Player ${player.id} left vision (wasTarget=${this.target === player})`);
|
|
1015
|
-
if (this.target === player) {
|
|
1016
|
-
this.target = null;
|
|
1017
|
-
this.isMovingToTarget = false;
|
|
1018
|
-
this.event.stopMoveTo();
|
|
1019
|
-
this.changeState(AiState.Idle);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
/**
|
|
1023
|
-
* Handle taking damage (called from server.ts)
|
|
1024
|
-
*
|
|
1025
|
-
* This triggers state changes like stun and flee check.
|
|
1026
|
-
* The actual damage is applied externally via RPGJS API.
|
|
1027
|
-
*/
|
|
1028
|
-
takeDamage(attacker) {
|
|
1029
|
-
const raw = this.event.applyDamage(attacker);
|
|
1030
|
-
return this.handleDamage(attacker, {
|
|
1031
|
-
damage: raw.damage ?? 0,
|
|
1032
|
-
defeated: this.event.hp <= 0,
|
|
1033
|
-
raw
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
handleDamage(attacker, damageResult) {
|
|
1037
|
-
const damage = damageResult.damage;
|
|
1038
|
-
this.debugLog("damage", `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || "?"})`);
|
|
1039
|
-
this.event.flash({
|
|
1040
|
-
type: "tint",
|
|
1041
|
-
tint: "red",
|
|
1042
|
-
duration: 200,
|
|
1043
|
-
cycles: 1
|
|
1044
|
-
});
|
|
1045
|
-
this.event.showHit(`-${damage}`);
|
|
1046
|
-
playActionBattleAnimation("hurt", this.event, this.animations, { attacker });
|
|
1047
|
-
this.recentDamageTaken += damage;
|
|
1048
|
-
if (this.state !== AiState.Stunned && this.state !== AiState.Flee) {
|
|
1049
|
-
this.debugLog("damage", "Stunned from damage");
|
|
1050
|
-
this.isMovingToTarget = false;
|
|
1051
|
-
this.stunnedUntil = Date.now() + 150;
|
|
1052
|
-
this.changeState(AiState.Stunned);
|
|
1053
|
-
}
|
|
1054
|
-
if (damageResult.defeated || this.event.hp <= 0) {
|
|
1055
|
-
this.debugLog("damage", "Defeated!");
|
|
1056
|
-
this.kill(attacker);
|
|
1057
|
-
return true;
|
|
1058
|
-
}
|
|
1059
|
-
return false;
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* Kill this AI
|
|
1063
|
-
*
|
|
1064
|
-
* Stops all movements, cleans up resources, calls the onDefeated hook,
|
|
1065
|
-
* and removes the event from the map.
|
|
1066
|
-
*/
|
|
1067
|
-
kill(attacker) {
|
|
1068
|
-
const removeDelay = getActionBattleAnimationRemovalDelay(playActionBattleAnimation("die", this.event, this.animations, { attacker }));
|
|
1069
|
-
if (this.onDefeatedCallback) this.onDefeatedCallback(this.event, attacker);
|
|
1070
|
-
this.destroy();
|
|
1071
|
-
if (removeDelay > 0) this.schedule(() => this.event.remove(), removeDelay);
|
|
1072
|
-
else this.event.remove();
|
|
1073
|
-
}
|
|
1074
|
-
/**
|
|
1075
|
-
* Get distance between entities
|
|
1076
|
-
*/
|
|
1077
|
-
getDistance(entity1, entity2) {
|
|
1078
|
-
const dx = entity1.x() - entity2.x();
|
|
1079
|
-
const dy = entity1.y() - entity2.y();
|
|
1080
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
1081
|
-
}
|
|
1082
|
-
updateBehavior(currentTime) {
|
|
1083
|
-
if (currentTime - this.behaviorLastUpdate < this.behaviorUpdateInterval) return;
|
|
1084
|
-
this.behaviorLastUpdate = currentTime;
|
|
1085
|
-
let score = this.behaviorScore;
|
|
1086
|
-
const maxHp = this.event.param[MAXHP];
|
|
1087
|
-
if (maxHp) {
|
|
1088
|
-
const hpPercent = this.event.hp / maxHp;
|
|
1089
|
-
score += (hpPercent - .5) * 40;
|
|
1090
|
-
}
|
|
1091
|
-
if (this.recentDamageTaken > 0) score -= Math.min(30, this.recentDamageTaken * .5);
|
|
1092
|
-
if (this.target) {
|
|
1093
|
-
const distance = this.getDistance(this.event, this.target);
|
|
1094
|
-
if (distance <= this.attackRange) score += 10;
|
|
1095
|
-
else if (distance > this.visionRange) score -= 10;
|
|
1096
|
-
}
|
|
1097
|
-
if (this.groupBehavior && this.nearbyEnemies.length > 0) score += Math.min(15, this.nearbyEnemies.length * 5);
|
|
1098
|
-
score = Math.max(0, Math.min(100, score));
|
|
1099
|
-
this.behaviorScore = score;
|
|
1100
|
-
const previousMode = this.behaviorMode;
|
|
1101
|
-
if (score >= this.behaviorAssaultThreshold) this.behaviorMode = "assault";
|
|
1102
|
-
else if (score <= this.behaviorRetreatThreshold) this.behaviorMode = "retreat";
|
|
1103
|
-
else this.behaviorMode = "tactical";
|
|
1104
|
-
if (previousMode !== this.behaviorMode) this.debugLog("state", `Behavior mode: ${previousMode} -> ${this.behaviorMode} (score=${score.toFixed(0)})`);
|
|
1105
|
-
if (this.behaviorMode === "retreat" && this.state === AiState.Combat) {
|
|
1106
|
-
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1107
|
-
this.isMovingToTarget = false;
|
|
1108
|
-
this.changeState(AiState.Flee);
|
|
1109
|
-
}
|
|
1110
|
-
} else if (this.behaviorMode === "assault" && this.state === AiState.Flee) {
|
|
1111
|
-
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) this.changeState(AiState.Combat);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
applyCustomBehavior(currentTime) {
|
|
1115
|
-
if (!this.behaviorKey) return;
|
|
1116
|
-
const behavior = getActionBattleSystems().ai.behaviors[this.behaviorKey];
|
|
1117
|
-
if (!behavior) return;
|
|
1118
|
-
const maxHp = this.event.param[MAXHP];
|
|
1119
|
-
const decision = behavior({
|
|
1120
|
-
event: this.event,
|
|
1121
|
-
target: this.target,
|
|
1122
|
-
state: this.state,
|
|
1123
|
-
enemyType: this.enemyType,
|
|
1124
|
-
distance: this.target ? this.getDistance(this.event, this.target) : null,
|
|
1125
|
-
hpPercent: maxHp ? this.event.hp / maxHp : null,
|
|
1126
|
-
now: currentTime
|
|
1127
|
-
});
|
|
1128
|
-
if (!decision) return;
|
|
1129
|
-
if (decision.attackCooldown !== void 0) this.attackCooldown = decision.attackCooldown;
|
|
1130
|
-
if (decision.moveToCooldown !== void 0) this.moveToCooldown = decision.moveToCooldown;
|
|
1131
|
-
if (decision.attackPatterns?.length) this.attackPatterns = decision.attackPatterns;
|
|
1132
|
-
if (decision.mode) {
|
|
1133
|
-
this.behaviorMode = decision.mode;
|
|
1134
|
-
this.behaviorEnabled = true;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
handleTacticalMovement(distance) {
|
|
1138
|
-
if (!this.target) return;
|
|
1139
|
-
const minRange = this.attackRange * .7;
|
|
1140
|
-
const maxRange = this.attackRange * 1.2;
|
|
1141
|
-
if (distance < minRange) {
|
|
1142
|
-
this.debugLog("movement", `Tactical retreat (dist=${distance.toFixed(1)}, minRange=${minRange.toFixed(1)})`);
|
|
1143
|
-
this.isMovingToTarget = false;
|
|
1144
|
-
this.retreatFromTarget();
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (distance > maxRange) {
|
|
1148
|
-
if (!this.isMovingToTarget) {
|
|
1149
|
-
this.debugLog("movement", `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
|
|
1150
|
-
this.isMovingToTarget = true;
|
|
1151
|
-
this.requestMoveTo(this.target);
|
|
1152
|
-
}
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (this.isMovingToTarget) {
|
|
1156
|
-
this.debugLog("movement", `Tactical hold (dist=${distance.toFixed(1)})`);
|
|
1157
|
-
this.isMovingToTarget = false;
|
|
1158
|
-
this.event.stopMoveTo();
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
handleAssaultMovement(distance) {
|
|
1162
|
-
if (!this.target) return;
|
|
1163
|
-
if (distance > this.attackRange) {
|
|
1164
|
-
if (!this.isMovingToTarget) {
|
|
1165
|
-
this.debugLog("movement", `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1166
|
-
this.isMovingToTarget = true;
|
|
1167
|
-
this.requestMoveTo(this.target);
|
|
1168
|
-
}
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
if (this.isMovingToTarget) {
|
|
1172
|
-
this.debugLog("movement", `Assault hold (dist=${distance.toFixed(1)})`);
|
|
1173
|
-
this.isMovingToTarget = false;
|
|
1174
|
-
this.event.stopMoveTo();
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
requestMoveTo(target) {
|
|
1178
|
-
const currentTime = Date.now();
|
|
1179
|
-
if (currentTime - this.lastMoveToTime < this.moveToCooldown) return false;
|
|
1180
|
-
this.event.moveTo(target);
|
|
1181
|
-
this.lastMoveToTime = currentTime;
|
|
1182
|
-
return true;
|
|
1183
|
-
}
|
|
1184
|
-
schedule(callback, delay) {
|
|
1185
|
-
const timer = setTimeout(() => {
|
|
1186
|
-
this.timers = this.timers.filter((entry) => entry !== timer);
|
|
1187
|
-
callback();
|
|
1188
|
-
}, delay);
|
|
1189
|
-
this.timers.push(timer);
|
|
1190
|
-
return timer;
|
|
1191
|
-
}
|
|
1192
|
-
getHealth() {
|
|
1193
|
-
return this.event.hp;
|
|
1194
|
-
}
|
|
1195
|
-
getMaxHealth() {
|
|
1196
|
-
return this.event.param[MAXHP];
|
|
1197
|
-
}
|
|
1198
|
-
getTarget() {
|
|
1199
|
-
return this.target;
|
|
1200
|
-
}
|
|
1201
|
-
getState() {
|
|
1202
|
-
return this.state;
|
|
1203
|
-
}
|
|
1204
|
-
getEnemyType() {
|
|
1205
|
-
return this.enemyType;
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Clean up
|
|
1209
|
-
*/
|
|
1210
|
-
destroy() {
|
|
1211
|
-
if (this.updateInterval) {
|
|
1212
|
-
clearInterval(this.updateInterval);
|
|
1213
|
-
this.updateInterval = void 0;
|
|
1214
|
-
}
|
|
1215
|
-
this.target = null;
|
|
1216
|
-
this.nearbyEnemies = [];
|
|
1217
|
-
this.timers.forEach((timer) => clearTimeout(timer));
|
|
1218
|
-
this.timers = [];
|
|
1219
|
-
}
|
|
99
|
+
playLocalPlayerAttackAnimation(player, normalized);
|
|
100
|
+
showLocalAttackPreview(player, normalized);
|
|
101
|
+
} }
|
|
102
|
+
});
|
|
1220
103
|
};
|
|
104
|
+
var client_default = createActionBattleClient();
|
|
1221
105
|
//#endregion
|
|
1222
|
-
export {
|
|
106
|
+
export { createActionBattleClient, client_default as default };
|