@rpgjs/action-battle 5.0.0-beta.4 → 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
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: [
@@ -249,6 +267,11 @@ new BattleAi(event, {
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.
@@ -1,6 +1,7 @@
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
  };
@@ -14,6 +15,7 @@ export interface BattleAiOptions {
14
15
  fleeThreshold?: number;
15
16
  attackSkill?: any;
16
17
  attackPatterns?: AttackPattern[];
18
+ attackProfiles?: ActionBattleEnemyAttackProfileMap;
17
19
  patrolWaypoints?: Array<{
18
20
  x: number;
19
21
  y: number;
@@ -21,6 +23,9 @@ export interface BattleAiOptions {
21
23
  groupBehavior?: boolean;
22
24
  moveToCooldown?: number;
23
25
  retreatCooldown?: number;
26
+ poise?: number;
27
+ hitstunMs?: number;
28
+ invincibilityMs?: number;
24
29
  behavior?: {
25
30
  baseScore?: number;
26
31
  updateInterval?: number;
@@ -252,6 +257,7 @@ export declare class BattleAi {
252
257
  private fleeThreshold;
253
258
  private attackSkill;
254
259
  private attackPatterns;
260
+ private attackProfiles;
255
261
  private animations?;
256
262
  private comboCount;
257
263
  private comboMax;
@@ -281,6 +287,9 @@ export declare class BattleAi {
281
287
  private lastRetreatTime;
282
288
  private timers;
283
289
  private behaviorKey?;
290
+ private poise;
291
+ private hitstunMs;
292
+ private invincibilityMs;
284
293
  /**
285
294
  * Create a new Battle AI Controller
286
295
  *
@@ -362,6 +371,7 @@ export declare class BattleAi {
362
371
  * Uses skill if configured, otherwise creates hitbox
363
372
  */
364
373
  private performMeleeAttack;
374
+ private executeMeleeAttack;
365
375
  /**
366
376
  * Perform basic hitbox attack when no skill is set
367
377
  */
@@ -429,6 +439,9 @@ export declare class BattleAi {
429
439
  * Perform dash attack
430
440
  */
431
441
  private performDashAttack;
442
+ private getAttackProfile;
443
+ private telegraphAttack;
444
+ private scheduleAttackStartup;
432
445
  /**
433
446
  * Face the current target with hysteresis to prevent animation flickering
434
447
  *
@@ -487,7 +500,9 @@ export declare class BattleAi {
487
500
  * The actual damage is applied externally via RPGJS API.
488
501
  */
489
502
  takeDamage(attacker: RpgPlayer): boolean;
490
- handleDamage(attacker: RpgPlayer, damageResult: ActionBattleDamageResult): boolean;
503
+ handleDamage(attacker: RpgPlayer, damageResult: ActionBattleDamageResult & {
504
+ reaction?: NormalizedActionBattleHitReactionProfile;
505
+ }): boolean;
491
506
  /**
492
507
  * Kill this AI
493
508
  *
@@ -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 };
@@ -1,25 +1,54 @@
1
- import { defaultActionBattleSystems } from "./index10.js";
2
- //#region src/core/context.ts
3
- var mergeSystems = (options = {}) => ({
4
- combat: {
5
- ...defaultActionBattleSystems.combat,
6
- resolveDamage: options.systems?.combat?.damage ?? defaultActionBattleSystems.combat.resolveDamage,
7
- resolveKnockback: options.systems?.combat?.knockback ?? defaultActionBattleSystems.combat.resolveKnockback,
8
- hooks: {
9
- ...defaultActionBattleSystems.combat.hooks,
10
- ...options.systems?.combat?.hooks
11
- }
12
- },
13
- ai: { behaviors: {
14
- ...defaultActionBattleSystems.ai.behaviors,
15
- ...options.systems?.ai?.behaviors
16
- } }
17
- });
18
- var currentActionBattleSystems = mergeSystems();
19
- var setActionBattleSystems = (options = {}) => {
20
- currentActionBattleSystems = mergeSystems(options);
1
+ import { normalizeActionBattleAttackProfile } from "./index3.js";
2
+ //#region src/core/attack-runtime.ts
3
+ var ACTION_BATTLE_HITBOX_FRAME_MS = 16;
4
+ function getNormalizedActionBattleAttackProfile(options = {}) {
5
+ const attack = options.attack ?? {};
6
+ return normalizeActionBattleAttackProfile(attack.profile, {
7
+ lockMovement: attack.lockMovement,
8
+ lockDurationMs: attack.lockDurationMs,
9
+ hitboxes: attack.hitboxes
10
+ });
11
+ }
12
+ function resolveActionBattleHitboxSpeed(profile, hitboxCount) {
13
+ const positions = Math.max(1, Math.floor(hitboxCount));
14
+ const activeFrames = Math.max(1, Math.ceil(profile.activeMs / 16));
15
+ return Math.max(1, Math.ceil(activeFrames / positions));
16
+ }
17
+ function scheduleActionBattleStartup(profile, callback, scheduler = setTimeout) {
18
+ if (profile.startupMs <= 0) {
19
+ callback();
20
+ return null;
21
+ }
22
+ return scheduler(callback, profile.startupMs);
23
+ }
24
+ var attackIdCounter = 0;
25
+ function createActionBattleAttackId(attackerId, profileId) {
26
+ attackIdCounter++;
27
+ return `${attackerId ?? "unknown"}:${profileId}:${Date.now()}:${attackIdCounter}`;
28
+ }
29
+ var getTargetKey = (target) => {
30
+ if (!target || target.id === void 0 || target.id === null) return null;
31
+ return String(target.id);
32
+ };
33
+ var ActionBattleHitTracker = class {
34
+ hitTargets = /* @__PURE__ */ new Set();
35
+ constructor(hitPolicy) {
36
+ this.hitPolicy = hitPolicy;
37
+ }
38
+ canHit(target) {
39
+ if (this.hitPolicy === "allowRepeatHits") return true;
40
+ const key = getTargetKey(target);
41
+ return !key || !this.hitTargets.has(key);
42
+ }
43
+ recordHit(target) {
44
+ const key = getTargetKey(target);
45
+ if (key) this.hitTargets.add(key);
46
+ }
47
+ tryHit(target) {
48
+ if (!this.canHit(target)) return false;
49
+ this.recordHit(target);
50
+ return true;
51
+ }
21
52
  };
22
- var getActionBattleSystems = () => currentActionBattleSystems;
23
- var createActionBattleSystems = mergeSystems;
24
53
  //#endregion
25
- export { createActionBattleSystems, getActionBattleSystems, setActionBattleSystems };
54
+ export { ACTION_BATTLE_HITBOX_FRAME_MS, ActionBattleHitTracker, createActionBattleAttackId, getNormalizedActionBattleAttackProfile, resolveActionBattleHitboxSpeed, scheduleActionBattleStartup };