@rpgjs/action-battle 5.0.0-beta.5 → 5.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +161 -22
  2. package/dist/ai.server.d.ts +55 -4
  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 +1343 -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 +7 -2
  29. package/dist/server/index.js +12 -7
  30. package/dist/server/index10.js +1340 -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/index2.js +1 -1
  37. package/dist/server/index3.js +25 -87
  38. package/dist/server/index4.js +45 -141
  39. package/dist/server/index5.js +104 -21
  40. package/dist/server/index6.js +137 -1215
  41. package/dist/server/index7.js +22 -34
  42. package/dist/server/index8.js +70 -44
  43. package/dist/server/index9.js +44 -437
  44. package/dist/server.d.ts +8 -2
  45. package/dist/ui/state.d.ts +5 -5
  46. package/package.json +5 -5
  47. package/src/ai.server.spec.ts +120 -0
  48. package/src/ai.server.ts +362 -56
  49. package/src/client.ts +21 -12
  50. package/src/config.ts +17 -2
  51. package/src/core/attack-profile.spec.ts +118 -0
  52. package/src/core/attack-profile.ts +100 -0
  53. package/src/core/attack-runtime.spec.ts +103 -0
  54. package/src/core/attack-runtime.ts +83 -0
  55. package/src/core/contracts.ts +3 -0
  56. package/src/core/enemy-attack-profiles.spec.ts +35 -0
  57. package/src/core/enemy-attack-profiles.ts +103 -0
  58. package/src/core/equipment.spec.ts +37 -0
  59. package/src/core/equipment.ts +17 -0
  60. package/src/core/hit-reaction.spec.ts +43 -0
  61. package/src/core/hit-reaction.ts +70 -0
  62. package/src/core/hit.spec.ts +54 -1
  63. package/src/core/hit.ts +26 -0
  64. package/src/index.ts +48 -1
  65. package/src/server.ts +192 -34
  66. package/src/types.ts +62 -6
package/README.md CHANGED
@@ -231,6 +231,24 @@ new BattleAi(event, {
231
231
  AttackPattern.Combo,
232
232
  AttackPattern.DashAttack
233
233
  ],
234
+
235
+ // Per-pattern enemy attack timing and reactions
236
+ attackProfiles: {
237
+ charged: {
238
+ startupMs: 900,
239
+ activeMs: 140,
240
+ recoveryMs: 300,
241
+ reaction: {
242
+ hitstunMs: 240,
243
+ staggerPower: 2
244
+ }
245
+ }
246
+ },
247
+
248
+ // Hit reaction tuning
249
+ poise: 1,
250
+ hitstunMs: 150,
251
+ invincibilityMs: 250,
234
252
 
235
253
  // Patrol waypoints (for idle state)
236
254
  patrolWaypoints: [
@@ -242,13 +260,18 @@ new BattleAi(event, {
242
260
  groupBehavior: true,
243
261
 
244
262
  // Callback when AI is defeated
245
- onDefeated: (event, attacker) => {
263
+ onDefeated: ({ event, attacker }) => {
246
264
  const name = attacker?.name?.() ?? "Unknown";
247
265
  console.log(`${event.name()} was defeated by ${name}!`);
248
266
  }
249
267
  });
250
268
  ```
251
269
 
270
+ `attackProfiles` lets enemies telegraph attacks with `startupMs`, keep hitboxes
271
+ active for `activeMs`, and apply hit reactions. `poise` controls interruption:
272
+ an incoming hit only stuns the enemy when its `reaction.staggerPower` is greater
273
+ than or equal to the enemy's `poise`.
274
+
252
275
  ## Enemy Types
253
276
 
254
277
  Types modify AI **behavior** (cooldowns, ranges, dodge), not stats:
@@ -607,6 +630,98 @@ Player attacks are resolved with `createMovingHitbox()` instead of a passive
607
630
  contact collision. You can still customize the generated hitboxes with
608
631
  `attack.hitboxes` or `attack.resolveHitboxes`.
609
632
 
633
+ ### Attack profile model
634
+
635
+ Use `attack.profile` to describe the timing model of a player attack in one
636
+ typed object. A profile separates the attack into startup, active, and recovery
637
+ phases so combat systems can share the same vocabulary.
638
+
639
+ ```ts
640
+ import { provideActionBattle } from "@rpgjs/action-battle/server";
641
+
642
+ export default provideActionBattle({
643
+ attack: {
644
+ profile: {
645
+ id: "iron-sword",
646
+ startupMs: 80,
647
+ activeMs: 120,
648
+ recoveryMs: 180,
649
+ cooldownMs: 380,
650
+ movementLock: true,
651
+ directionLock: true,
652
+ animationKey: "attack",
653
+ hitPolicy: "oncePerTarget",
654
+ reaction: {
655
+ invincibilityMs: 250,
656
+ hitstunMs: 150,
657
+ staggerPower: 1
658
+ },
659
+ hitboxes: {
660
+ right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
661
+ }
662
+ },
663
+ lockDurationMs: 380
664
+ }
665
+ });
666
+ ```
667
+
668
+ The default profile mirrors the legacy attack lock: no startup, a short active
669
+ window, and recovery that totals `350ms`. The player attack runtime uses
670
+ `startupMs` before creating the hitbox, `activeMs` to keep the hitbox active,
671
+ and `totalDurationMs` for movement and direction locks. `hitPolicy:
672
+ "oncePerTarget"` prevents the same attack window from damaging the same target
673
+ multiple times.
674
+
675
+ `reaction` describes what happens after the hit connects:
676
+
677
+ - `invincibilityMs`: temporary invincibility after damage.
678
+ - `hitstunMs`: stun duration requested by the hit.
679
+ - `staggerPower`: value compared against enemy `poise`.
680
+
681
+ ```ts
682
+ import {
683
+ normalizeActionBattleAttackProfile,
684
+ type ActionBattleAttackProfile
685
+ } from "@rpgjs/action-battle/server";
686
+
687
+ const sword: ActionBattleAttackProfile = {
688
+ id: "sword",
689
+ startupMs: 70,
690
+ activeMs: 110,
691
+ recoveryMs: 170
692
+ };
693
+
694
+ const normalized = normalizeActionBattleAttackProfile(sword);
695
+ ```
696
+
697
+ Equipped weapons can override the player attack profile:
698
+
699
+ ```ts
700
+ const Dagger = {
701
+ id: "dagger",
702
+ name: "Dagger",
703
+ _type: "weapon" as const,
704
+ atk: 8,
705
+ knockbackForce: 20,
706
+ attackProfile: {
707
+ id: "dagger",
708
+ startupMs: 40,
709
+ activeMs: 70,
710
+ recoveryMs: 110
711
+ }
712
+ };
713
+ ```
714
+
715
+ Enable lightweight attack logs while tuning profiles:
716
+
717
+ ```ts
718
+ provideActionBattle({
719
+ debug: {
720
+ attacks: true
721
+ }
722
+ });
723
+ ```
724
+
610
725
  When the action targets a normal event with no `BattleAi`, the server lets the
611
726
  event handle `onAction` and does not create the combat hitbox. Enemy events
612
727
  with `BattleAi` still trigger the A-RPG attack.
@@ -724,8 +839,9 @@ new BattleAi(this, {
724
839
  });
725
840
  ```
726
841
 
727
- `waitEnd: true` delays event removal for defeated AI with the default delay used
728
- by action-battle. Use `delayMs` when you need an exact duration.
842
+ `waitEnd: true` uses the default defeated transition timeout. Use `delayMs`
843
+ when you need an exact duration. The visual transition itself is handled by the
844
+ client `sprite.onBeforeRemove` hook.
729
845
 
730
846
  ## Knockback System
731
847
 
@@ -877,25 +993,57 @@ console.log(`Player knockback force: ${force}`);
877
993
 
878
994
  ## onDefeated Hook
879
995
 
880
- The `onDefeated` callback is triggered when an AI enemy is killed. It receives the defeated event and the player who landed the killing blow (if available). Use it to:
996
+ The `onDefeated` callback is triggered when an AI enemy is killed. The simplest
997
+ reward flow is configured directly on `BattleAi`; the reward is given to the
998
+ player who landed the killing blow.
999
+
1000
+ ```typescript
1001
+ new BattleAi(this, {
1002
+ enemyType: EnemyType.Aggressive,
1003
+ animations: {
1004
+ die: {
1005
+ animationName: "die",
1006
+ graphic: "goblin_die",
1007
+ repeat: 1,
1008
+ delayMs: 700
1009
+ }
1010
+ },
1011
+ rewards: {
1012
+ exp: 50,
1013
+ gold: 25,
1014
+ items: [{ itemId: "health_potion", amount: 1, chance: 30 }],
1015
+ showNotification: true
1016
+ }
1017
+ });
1018
+ ```
1019
+
1020
+ On defeat, `BattleAi` stops the AI, awards configured rewards once, and calls
1021
+ `event.remove({ reason: "defeated", transition })`. The client can use
1022
+ `sprite.onBeforeRemove` to play the `die` transition before the sprite
1023
+ disappears.
1024
+
1025
+ `onDefeated` receives a context object in new code:
881
1026
  - Award experience, gold, or items to the player
882
1027
  - Spawn loot drops
883
1028
  - Trigger events or cutscenes
884
1029
  - Update quest progress
885
- - Play death animations or sounds
1030
+ - Play death sounds
886
1031
 
887
1032
  ### Basic Usage
888
1033
 
889
1034
  ```typescript
890
1035
  new BattleAi(this, {
891
1036
  enemyType: EnemyType.Aggressive,
892
- onDefeated: (event, attacker) => {
1037
+ onDefeated: ({ event, attacker }) => {
893
1038
  const name = attacker?.name?.() ?? "Unknown";
894
1039
  console.log(`${event.name()} was defeated by ${name}!`);
895
1040
  }
896
1041
  });
897
1042
  ```
898
1043
 
1044
+ The legacy `(event, attacker)` callback signature is still supported for
1045
+ two-argument callbacks.
1046
+
899
1047
  ### Award Rewards on Kill
900
1048
 
901
1049
  ```typescript
@@ -910,19 +1058,10 @@ function Goblin() {
910
1058
 
911
1059
  new BattleAi(this, {
912
1060
  enemyType: EnemyType.Aggressive,
913
- onDefeated: (event, attacker) => {
914
- if (!attacker) return;
915
-
916
- // Award gold
917
- attacker.gold += 25;
918
-
919
- // Award experience
920
- attacker.exp += 50;
921
-
922
- // Random loot drop
923
- if (Math.random() < 0.3) {
924
- attacker.addItem(HealthPotion);
925
- }
1061
+ rewards: {
1062
+ gold: 25,
1063
+ exp: 50,
1064
+ items: [{ item: HealthPotion, amount: 1, chance: 30 }]
926
1065
  }
927
1066
  });
928
1067
  }
@@ -934,7 +1073,7 @@ function Goblin() {
934
1073
 
935
1074
  ```typescript
936
1075
  new BattleAi(this, {
937
- onDefeated: (event, attacker) => {
1076
+ onDefeated: ({ event }) => {
938
1077
  const map = event.getCurrentMap();
939
1078
  if (!map) return;
940
1079
 
@@ -954,7 +1093,7 @@ new BattleAi(this, {
954
1093
  let killCount = 0;
955
1094
 
956
1095
  new BattleAi(this, {
957
- onDefeated: (event, attacker) => {
1096
+ onDefeated: () => {
958
1097
  killCount++;
959
1098
 
960
1099
  // Check quest progress
@@ -978,7 +1117,7 @@ function DragonBoss() {
978
1117
 
979
1118
  new BattleAi(this, {
980
1119
  enemyType: EnemyType.Tank,
981
- onDefeated: (event, attacker) => {
1120
+ onDefeated: ({ event }) => {
982
1121
  const map = event.getCurrentMap();
983
1122
 
984
1123
  // Announce victory
@@ -1,10 +1,35 @@
1
1
  import { RpgEvent, RpgPlayer } from '@rpgjs/server';
2
+ import { ActionBattleEnemyAttackProfileMap } from './core/enemy-attack-profiles';
2
3
  import { ActionBattleDamageResult } from './core/contracts';
3
- import { ActionBattleAnimationOptions } from './types';
4
+ import { NormalizedActionBattleHitReactionProfile, ActionBattleAnimationOptions } from './types';
4
5
  type RpgEventWithBattleAi = RpgEvent & {
5
6
  battleAi?: BattleAi;
6
7
  };
7
- export interface BattleAiOptions {
8
+ export interface BattleAiRewardItem {
9
+ item?: any;
10
+ itemId?: string;
11
+ amount?: number;
12
+ chance?: number;
13
+ }
14
+ export interface BattleAiRewards {
15
+ exp?: number;
16
+ gold?: number;
17
+ items?: Array<BattleAiRewardItem | string>;
18
+ showNotification?: boolean;
19
+ }
20
+ export interface BattleAiDefeatReward {
21
+ readonly awarded: boolean;
22
+ giveTo(player?: RpgPlayer | null): void;
23
+ }
24
+ export interface BattleAiDefeatedContext {
25
+ event: RpgEvent;
26
+ attacker?: RpgPlayer;
27
+ reward: BattleAiDefeatReward;
28
+ remove: () => void;
29
+ }
30
+ export type BattleAiDefeatedCallback = (context: BattleAiDefeatedContext) => void;
31
+ export type BattleAiLegacyDefeatedCallback = (event: RpgEvent, attacker?: RpgPlayer) => void;
32
+ export interface BattleAiBaseOptions {
8
33
  enemyType?: EnemyType;
9
34
  attackCooldown?: number;
10
35
  visionRange?: number;
@@ -14,6 +39,7 @@ export interface BattleAiOptions {
14
39
  fleeThreshold?: number;
15
40
  attackSkill?: any;
16
41
  attackPatterns?: AttackPattern[];
42
+ attackProfiles?: ActionBattleEnemyAttackProfileMap;
17
43
  patrolWaypoints?: Array<{
18
44
  x: number;
19
45
  y: number;
@@ -21,6 +47,9 @@ export interface BattleAiOptions {
21
47
  groupBehavior?: boolean;
22
48
  moveToCooldown?: number;
23
49
  retreatCooldown?: number;
50
+ poise?: number;
51
+ hitstunMs?: number;
52
+ invincibilityMs?: number;
24
53
  behavior?: {
25
54
  baseScore?: number;
26
55
  updateInterval?: number;
@@ -30,8 +59,16 @@ export interface BattleAiOptions {
30
59
  };
31
60
  behaviorKey?: string;
32
61
  animations?: ActionBattleAnimationOptions;
62
+ rewards?: BattleAiRewards;
63
+ autoAwardRewards?: boolean;
64
+ }
65
+ export interface BattleAiOptions extends BattleAiBaseOptions {
33
66
  /** Callback called when the AI is defeated */
34
- onDefeated?: (event: RpgEvent, attacker?: RpgPlayer) => void;
67
+ onDefeated?: BattleAiDefeatedCallback;
68
+ }
69
+ export interface BattleAiLegacyOptions extends BattleAiBaseOptions {
70
+ /** @deprecated Use the context callback signature instead. */
71
+ onDefeated?: BattleAiLegacyDefeatedCallback;
35
72
  }
36
73
  /**
37
74
  * Hit result data returned after applying damage
@@ -252,6 +289,7 @@ export declare class BattleAi {
252
289
  private fleeThreshold;
253
290
  private attackSkill;
254
291
  private attackPatterns;
292
+ private attackProfiles;
255
293
  private animations?;
256
294
  private comboCount;
257
295
  private comboMax;
@@ -266,6 +304,9 @@ export declare class BattleAi {
266
304
  private damageCheckInterval;
267
305
  private isMovingToTarget;
268
306
  private onDefeatedCallback?;
307
+ private rewards?;
308
+ private autoAwardRewards;
309
+ private defeated;
269
310
  private lastFacingDirection;
270
311
  private behaviorScore;
271
312
  private behaviorMode;
@@ -281,6 +322,9 @@ export declare class BattleAi {
281
322
  private lastRetreatTime;
282
323
  private timers;
283
324
  private behaviorKey?;
325
+ private poise;
326
+ private hitstunMs;
327
+ private invincibilityMs;
284
328
  /**
285
329
  * Create a new Battle AI Controller
286
330
  *
@@ -306,6 +350,7 @@ export declare class BattleAi {
306
350
  * ```
307
351
  */
308
352
  constructor(event: RpgEventWithBattleAi, options?: BattleAiOptions);
353
+ constructor(event: RpgEventWithBattleAi, options?: BattleAiLegacyOptions);
309
354
  /**
310
355
  * Apply enemy type-specific behavior modifiers
311
356
  *
@@ -362,6 +407,7 @@ export declare class BattleAi {
362
407
  * Uses skill if configured, otherwise creates hitbox
363
408
  */
364
409
  private performMeleeAttack;
410
+ private executeMeleeAttack;
365
411
  /**
366
412
  * Perform basic hitbox attack when no skill is set
367
413
  */
@@ -429,6 +475,9 @@ export declare class BattleAi {
429
475
  * Perform dash attack
430
476
  */
431
477
  private performDashAttack;
478
+ private getAttackProfile;
479
+ private telegraphAttack;
480
+ private scheduleAttackStartup;
432
481
  /**
433
482
  * Face the current target with hysteresis to prevent animation flickering
434
483
  *
@@ -487,7 +536,9 @@ export declare class BattleAi {
487
536
  * The actual damage is applied externally via RPGJS API.
488
537
  */
489
538
  takeDamage(attacker: RpgPlayer): boolean;
490
- handleDamage(attacker: RpgPlayer, damageResult: ActionBattleDamageResult): boolean;
539
+ handleDamage(attacker: RpgPlayer, damageResult: ActionBattleDamageResult & {
540
+ reaction?: NormalizedActionBattleHitReactionProfile;
541
+ }): boolean;
491
542
  /**
492
543
  * Kill this AI
493
544
  *
@@ -1,10 +1,15 @@
1
- import client_default, { createActionBattleClient } from "./index9.js";
2
- import { DEFAULT_ZELDA_PLAYER_HITBOXES, createDefaultPlayerHitboxResolver, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver } from "./index10.js";
3
- import { createActionBattleSystems, getActionBattleSystems } from "./index11.js";
4
- import { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType } from "./index12.js";
5
- import { applyActionBattleHit } from "./index13.js";
6
- import { createActionEnemy } from "./index14.js";
7
- import { ACTION_BATTLE_ACTION_BAR_GUI_ID, DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, createActionBattleServer, getPlayerWeaponKnockbackForce, openActionBattleActionBar, updateActionBattleActionBar } from "./index15.js";
1
+ import { DEFAULT_ACTION_BATTLE_HIT_REACTION, isActionBattleEntityInvincible, normalizeActionBattleHitReaction, setActionBattleInvincibility } from "./index2.js";
2
+ import { DEFAULT_ACTION_BATTLE_ATTACK_PROFILE, normalizeActionBattleAttackProfile } from "./index3.js";
3
+ import { ACTION_BATTLE_HITBOX_FRAME_MS, ActionBattleHitTracker, createActionBattleAttackId, getNormalizedActionBattleAttackProfile, resolveActionBattleHitboxSpeed, scheduleActionBattleStartup } from "./index11.js";
4
+ import client_default, { createActionBattleClient } from "./index12.js";
5
+ import { DEFAULT_ZELDA_PLAYER_HITBOXES, createDefaultPlayerHitboxResolver, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver } from "./index13.js";
6
+ import { createActionBattleSystems, getActionBattleSystems } from "./index14.js";
7
+ import { DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES, normalizeActionBattleEnemyAttackProfiles } from "./index15.js";
8
+ import { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType } from "./index16.js";
9
+ import { resolveActionBattleWeaponAttackProfile } from "./index17.js";
10
+ import { applyActionBattleHit } from "./index18.js";
11
+ import { createActionEnemy } from "./index19.js";
12
+ import { ACTION_BATTLE_ACTION_BAR_GUI_ID, DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, createActionBattleServer, getPlayerWeaponKnockbackForce, openActionBattleActionBar, updateActionBattleActionBar } from "./index20.js";
8
13
  import { createModule } from "@rpgjs/common";
9
14
  //#region src/index.ts
10
15
  var server = null;
@@ -20,4 +25,4 @@ var src_default = {
20
25
  client: client_default
21
26
  };
22
27
  //#endregion
23
- export { ACTION_BATTLE_ACTION_BAR_GUI_ID, AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, DEFAULT_PLAYER_ATTACK_HITBOXES, DEFAULT_ZELDA_PLAYER_HITBOXES, EnemyType, applyActionBattleHit, applyPlayerHitToEvent, createActionBattleServer, createActionBattleSystems, createActionEnemy, createDefaultPlayerHitboxResolver, src_default as default, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver, getActionBattleSystems, getPlayerWeaponKnockbackForce, openActionBattleActionBar, provideActionBattle, updateActionBattleActionBar };
28
+ export { ACTION_BATTLE_ACTION_BAR_GUI_ID, ACTION_BATTLE_HITBOX_FRAME_MS, ActionBattleHitTracker, AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_ACTION_BATTLE_ATTACK_PROFILE, DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES, DEFAULT_ACTION_BATTLE_HIT_REACTION, DEFAULT_KNOCKBACK, DEFAULT_PLAYER_ATTACK_HITBOXES, DEFAULT_ZELDA_PLAYER_HITBOXES, EnemyType, applyActionBattleHit, applyPlayerHitToEvent, createActionBattleAttackId, createActionBattleServer, createActionBattleSystems, createActionEnemy, createDefaultPlayerHitboxResolver, src_default as default, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver, getActionBattleSystems, getNormalizedActionBattleAttackProfile, getPlayerWeaponKnockbackForce, isActionBattleEntityInvincible, normalizeActionBattleAttackProfile, normalizeActionBattleEnemyAttackProfiles, normalizeActionBattleHitReaction, openActionBattleActionBar, provideActionBattle, resolveActionBattleHitboxSpeed, resolveActionBattleWeaponAttackProfile, scheduleActionBattleStartup, setActionBattleInvincibility, updateActionBattleActionBar };
@@ -1,143 +1,61 @@
1
- //#region src/core/defaults.ts
2
- var DEFAULT_CORE_KNOCKBACK = {
3
- force: 50,
4
- duration: 300
5
- };
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
50
- }
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
1
+ var DEFAULT_ANIMATION_BY_KEY = {
2
+ attack: "attack",
3
+ hurt: "hurt",
4
+ die: "die",
5
+ castSkill: "skill",
6
+ castSpell: "skill"
7
+ };
8
+ var getConfiguredAnimation = (key, animations) => {
9
+ if (!animations) return {
10
+ hasConfiguredAnimation: false,
11
+ configured: void 0
12
+ };
13
+ const hasConfiguredAnimation = Object.prototype.hasOwnProperty.call(animations, key);
14
+ if (hasConfiguredAnimation) return {
15
+ hasConfiguredAnimation,
16
+ configured: animations[key]
17
+ };
18
+ if (key === "castSkill") return {
19
+ hasConfiguredAnimation: Object.prototype.hasOwnProperty.call(animations, "castSpell"),
20
+ configured: animations.castSpell
69
21
  };
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
22
  return {
85
- damage: raw?.damage ?? 0,
86
- defeated: target.hp <= 0,
87
- raw
23
+ hasConfiguredAnimation: false,
24
+ configured: void 0
88
25
  };
89
26
  };
90
- var defaultKnockbackResolver = (context) => {
91
- const weapon = context.weapon ?? resolveEquippedWeapon(context.attacker);
27
+ function resolveActionBattleAnimation(key, entity, animations, context, defaults = {}) {
28
+ const defaultAnimationName = defaults.animationName ?? DEFAULT_ANIMATION_BY_KEY[key];
29
+ const defaultRepeat = defaults.repeat ?? 1;
30
+ const { hasConfiguredAnimation, configured: configuredAnimation } = getConfiguredAnimation(key, animations);
31
+ if (!hasConfiguredAnimation && key !== "attack") return null;
32
+ const configured = hasConfiguredAnimation ? configuredAnimation : defaultAnimationName;
33
+ const result = typeof configured === "function" ? configured(entity, context) : configured;
34
+ if (result == null) return null;
35
+ if (typeof result === "string") return {
36
+ animationName: result,
37
+ repeat: defaultRepeat,
38
+ waitEnd: false
39
+ };
92
40
  return {
93
- force: weapon?.knockbackForce ?? DEFAULT_CORE_KNOCKBACK.force,
94
- duration: weapon?.knockbackDuration ?? DEFAULT_CORE_KNOCKBACK.duration,
95
- direction: resolveDirection(context.attacker, context.target)
41
+ animationName: result.animationName ?? defaultAnimationName,
42
+ graphic: result.graphic,
43
+ repeat: result.repeat ?? defaultRepeat,
44
+ waitEnd: result.waitEnd ?? false,
45
+ delayMs: result.delayMs
96
46
  };
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
- };
47
+ }
48
+ function playActionBattleAnimation(key, entity, animations, context, defaults = {}) {
49
+ const animation = resolveActionBattleAnimation(key, entity, animations, context, defaults);
50
+ if (!animation) return null;
51
+ if (animation.graphic !== void 0) entity.setGraphicAnimation(animation.animationName, animation.graphic, animation.repeat);
52
+ else entity.setGraphicAnimation(animation.animationName, animation.repeat);
53
+ return animation;
54
+ }
55
+ function getActionBattleAnimationRemovalDelay(animation) {
56
+ if (!animation) return 0;
57
+ if (animation.delayMs !== void 0) return animation.delayMs;
58
+ return animation.waitEnd ? 500 : 0;
59
+ }
142
60
  //#endregion
143
- export { DEFAULT_ZELDA_PLAYER_HITBOXES, createDefaultPlayerHitboxResolver, defaultActionBattleSystems, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver };
61
+ export { getActionBattleAnimationRemovalDelay, playActionBattleAnimation, resolveActionBattleAnimation };