@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.
Files changed (63) hide show
  1. package/README.md +115 -0
  2. package/dist/ai.server.d.ts +17 -2
  3. package/dist/client/index.js +13 -8
  4. package/dist/client/index10.js +54 -136
  5. package/dist/client/index11.js +52 -23
  6. package/dist/client/index12.js +101 -1217
  7. package/dist/client/index13.js +139 -42
  8. package/dist/client/index14.js +23 -8
  9. package/dist/client/index15.js +68 -444
  10. package/dist/client/index16.js +1281 -0
  11. package/dist/client/index17.js +13 -0
  12. package/dist/client/index18.js +60 -0
  13. package/dist/client/index19.js +10 -0
  14. package/dist/client/index2.js +25 -87
  15. package/dist/client/index20.js +504 -0
  16. package/dist/client/index3.js +45 -83
  17. package/dist/client/index4.js +98 -297
  18. package/dist/client/index5.js +81 -33
  19. package/dist/client/index6.js +284 -78
  20. package/dist/client/index7.js +33 -74
  21. package/dist/client/index8.js +95 -55
  22. package/dist/client/index9.js +75 -96
  23. package/dist/core/attack-profile.d.ts +9 -0
  24. package/dist/core/attack-runtime.d.ts +20 -0
  25. package/dist/core/enemy-attack-profiles.d.ts +6 -0
  26. package/dist/core/equipment.d.ts +2 -0
  27. package/dist/core/hit-reaction.d.ts +5 -0
  28. package/dist/index.d.ts +6 -1
  29. package/dist/server/index.js +12 -7
  30. package/dist/server/index10.js +1278 -8
  31. package/dist/server/index11.js +37 -0
  32. package/dist/server/index12.js +60 -0
  33. package/dist/server/index13.js +13 -0
  34. package/dist/server/index14.js +503 -0
  35. package/dist/server/index15.js +10 -0
  36. package/dist/server/index3.js +25 -87
  37. package/dist/server/index4.js +45 -141
  38. package/dist/server/index5.js +104 -21
  39. package/dist/server/index6.js +137 -1215
  40. package/dist/server/index7.js +22 -34
  41. package/dist/server/index8.js +70 -44
  42. package/dist/server/index9.js +44 -437
  43. package/dist/server.d.ts +7 -1
  44. package/package.json +5 -5
  45. package/src/ai.server.ts +172 -43
  46. package/src/client.ts +21 -12
  47. package/src/config.ts +17 -2
  48. package/src/core/attack-profile.spec.ts +118 -0
  49. package/src/core/attack-profile.ts +100 -0
  50. package/src/core/attack-runtime.spec.ts +103 -0
  51. package/src/core/attack-runtime.ts +83 -0
  52. package/src/core/contracts.ts +3 -0
  53. package/src/core/enemy-attack-profiles.spec.ts +35 -0
  54. package/src/core/enemy-attack-profiles.ts +103 -0
  55. package/src/core/equipment.spec.ts +37 -0
  56. package/src/core/equipment.ts +17 -0
  57. package/src/core/hit-reaction.spec.ts +43 -0
  58. package/src/core/hit-reaction.ts +70 -0
  59. package/src/core/hit.spec.ts +54 -1
  60. package/src/core/hit.ts +26 -0
  61. package/src/index.ts +36 -0
  62. package/src/server.ts +180 -33
  63. package/src/types.ts +62 -6
@@ -1,1222 +1,106 @@
1
- import { getActionBattleOptions } from "./index2.js";
2
- import { getActionBattleAnimationRemovalDelay, playActionBattleAnimation } from "./index8.js";
3
- import { getActionBattleSystems } from "./index11.js";
4
- //#region src/ai.server.ts
5
- var MAXHP = null;
6
- var RpgPlayer = null;
7
- /**
8
- * AI Debug Logger
9
- *
10
- * Conditional logging utility for AI behavior debugging.
11
- * Enable by setting `AiDebug.enabled = true` or via environment variable `RPGJS_DEBUG_AI=1`
12
- *
13
- * @example
14
- * ```ts
15
- * // Enable debug logging
16
- * AiDebug.enabled = true;
17
- *
18
- * // Or filter by event ID
19
- * AiDebug.filterEventId = 'goblin-1';
20
- * ```
21
- */
22
- var AiDebug = {
23
- /** Enable/disable all AI debug logs */
24
- enabled: globalThis.process?.env?.RPGJS_DEBUG_AI === "1" || false,
25
- /** Filter logs to a specific event ID (null = all events) */
26
- filterEventId: null,
27
- /** Log categories to enable (empty = all) */
28
- categories: [],
29
- /**
30
- * Log an AI debug message
31
- *
32
- * @param category - Log category (e.g., 'state', 'attack', 'movement', 'damage')
33
- * @param eventId - Event ID for filtering
34
- * @param message - Log message
35
- * @param data - Optional additional data
36
- */
37
- log(category, eventId, message, data) {
38
- if (!this.enabled) return;
39
- if (this.filterEventId && eventId !== this.filterEventId) return;
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
- * AI State enumeration
48
- *
49
- * Defines the different states an AI can be in, each with its own behavior.
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
- * Advanced Battle AI Controller for events
99
- *
100
- * This class provides intelligent combat behavior control for events.
101
- * It uses the existing RPGJS API for stats, skills, items, etc.
102
- * The AI only manages behavior - the event's stats should be configured
103
- * in onInit using standard RPGJS methods.
104
- *
105
- * ## Usage with RPGJS API
106
- *
107
- * Configure the event stats using standard RPGJS methods:
108
- * - `this.hp = 100` - Set health
109
- * - `this.learnSkill(FireBall)` - Learn a skill
110
- * - `this.addItem(Potion, 3)` - Add items
111
- * - `this.equip(Sword)` - Equip items
112
- * - `this.setClass(WarriorClass)` - Set class
113
- * - `this.param[ATK] = 20` - Set parameters
114
- *
115
- * @example
116
- * ```ts
117
- * function GoblinEnemy() {
118
- * return {
119
- * name: "Goblin",
120
- * onInit() {
121
- * this.setGraphic("goblin");
122
- *
123
- * // Configure stats using RPGJS API
124
- * this.hp = 80;
125
- * this.param[ATK] = 15;
126
- * this.param[PDEF] = 5;
127
- * this.learnSkill(Slash);
128
- *
129
- * // Apply AI behavior
130
- * new BattleAi(this, {
131
- * enemyType: EnemyType.Aggressive,
132
- * attackSkill: Slash
133
- * });
134
- * }
135
- * };
136
- * }
137
- * ```
138
- */
139
- var BattleAi = class {
140
- event;
141
- target = null;
142
- lastAttackTime = 0;
143
- updateInterval;
144
- /**
145
- * Log AI debug message for this event
146
- */
147
- debugLog(category, message, data) {
148
- AiDebug.log(category, this.event.id, message, data);
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
- * Perform dash attack
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 { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType };
106
+ export { createActionBattleClient, client_default as default };