@rpgjs/action-battle 5.0.0-beta.3 → 5.0.0-beta.4

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