@rpgjs/action-battle 5.0.0-beta.10 → 5.0.0-beta.12

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 (111) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/client/ai.server.d.ts +45 -8
  3. package/dist/client/attack-input.d.ts +3 -0
  4. package/dist/client/core/action-use.d.ts +18 -0
  5. package/dist/client/core/ai-behavior-tree.d.ts +99 -0
  6. package/dist/client/core/attack-runtime.d.ts +2 -0
  7. package/dist/client/core/defaults.d.ts +2 -1
  8. package/dist/client/core/equipment.d.ts +1 -0
  9. package/dist/client/core/targets.d.ts +15 -0
  10. package/dist/client/enemies/factory.d.ts +2 -0
  11. package/dist/client/index.d.ts +12 -7
  12. package/dist/client/index.js +16 -11
  13. package/dist/client/index10.js +32 -56
  14. package/dist/client/index11.js +99 -52
  15. package/dist/client/index12.js +76 -103
  16. package/dist/client/index13.js +72 -135
  17. package/dist/client/index14.js +67 -23
  18. package/dist/client/index15.js +197 -63
  19. package/dist/client/index16.js +112 -1337
  20. package/dist/client/index17.js +193 -7
  21. package/dist/client/index18.js +32 -58
  22. package/dist/client/index19.js +70 -8
  23. package/dist/client/index20.js +57 -501
  24. package/dist/client/index21.js +69 -0
  25. package/dist/client/index22.js +225 -0
  26. package/dist/client/index23.js +16 -0
  27. package/dist/client/index24.js +25 -0
  28. package/dist/client/index25.js +107 -0
  29. package/dist/client/index26.js +1707 -0
  30. package/dist/client/index27.js +12 -0
  31. package/dist/client/index28.js +589 -0
  32. package/dist/client/index4.js +79 -38
  33. package/dist/client/index6.js +65 -306
  34. package/dist/client/index7.js +33 -33
  35. package/dist/client/index8.js +24 -100
  36. package/dist/client/index9.js +293 -61
  37. package/dist/client/locomotion.d.ts +16 -0
  38. package/dist/client/movement.d.ts +14 -0
  39. package/dist/client/server.d.ts +7 -3
  40. package/dist/client/ui.d.ts +22 -0
  41. package/dist/client/visual.d.ts +15 -0
  42. package/dist/server/ai.server.d.ts +45 -8
  43. package/dist/server/attack-input.d.ts +3 -0
  44. package/dist/server/core/action-use.d.ts +18 -0
  45. package/dist/server/core/ai-behavior-tree.d.ts +99 -0
  46. package/dist/server/core/attack-runtime.d.ts +2 -0
  47. package/dist/server/core/defaults.d.ts +2 -1
  48. package/dist/server/core/equipment.d.ts +1 -0
  49. package/dist/server/core/targets.d.ts +15 -0
  50. package/dist/server/enemies/factory.d.ts +2 -0
  51. package/dist/server/index.d.ts +12 -7
  52. package/dist/server/index.js +14 -9
  53. package/dist/server/index10.js +64 -1336
  54. package/dist/server/index11.js +33 -33
  55. package/dist/server/index13.js +66 -11
  56. package/dist/server/index14.js +206 -484
  57. package/dist/server/index15.js +15 -9
  58. package/dist/server/index16.js +26 -0
  59. package/dist/server/index17.js +25 -0
  60. package/dist/server/index18.js +107 -0
  61. package/dist/server/index19.js +1707 -0
  62. package/dist/server/index2.js +10 -2
  63. package/dist/server/index20.js +37 -0
  64. package/dist/server/index21.js +588 -0
  65. package/dist/server/index22.js +78 -0
  66. package/dist/server/index23.js +12 -0
  67. package/dist/server/index5.js +79 -38
  68. package/dist/server/index6.js +192 -129
  69. package/dist/server/index7.js +198 -24
  70. package/dist/server/index8.js +28 -66
  71. package/dist/server/index9.js +68 -51
  72. package/dist/server/locomotion.d.ts +16 -0
  73. package/dist/server/movement.d.ts +14 -0
  74. package/dist/server/server.d.ts +7 -3
  75. package/dist/server/ui.d.ts +22 -0
  76. package/dist/server/visual.d.ts +15 -0
  77. package/package.json +10 -10
  78. package/src/ai.server.spec.ts +233 -0
  79. package/src/ai.server.ts +627 -108
  80. package/src/animations.spec.ts +40 -0
  81. package/src/animations.ts +31 -9
  82. package/src/attack-input.spec.ts +51 -0
  83. package/src/attack-input.ts +59 -0
  84. package/src/client.ts +75 -62
  85. package/src/components/action-bar.ce +2 -2
  86. package/src/config.ts +84 -37
  87. package/src/core/action-use.spec.ts +317 -0
  88. package/src/core/action-use.ts +386 -0
  89. package/src/core/ai-behavior-tree.spec.ts +116 -0
  90. package/src/core/ai-behavior-tree.ts +272 -0
  91. package/src/core/attack-profile.spec.ts +46 -0
  92. package/src/core/attack-runtime.spec.ts +35 -0
  93. package/src/core/attack-runtime.ts +32 -0
  94. package/src/core/context.ts +9 -0
  95. package/src/core/contracts.ts +146 -1
  96. package/src/core/defaults.ts +56 -0
  97. package/src/core/equipment.ts +9 -5
  98. package/src/core/targets.spec.ts +112 -0
  99. package/src/core/targets.ts +147 -0
  100. package/src/enemies/factory.ts +8 -0
  101. package/src/index.ts +111 -2
  102. package/src/locomotion.spec.ts +51 -0
  103. package/src/locomotion.ts +48 -0
  104. package/src/movement.spec.ts +78 -0
  105. package/src/movement.ts +46 -0
  106. package/src/server.ts +242 -66
  107. package/src/types.ts +105 -35
  108. package/src/ui.ts +113 -0
  109. package/src/visual.spec.ts +166 -0
  110. package/src/visual.ts +285 -0
  111. package/README.md +0 -1242
@@ -1,9 +1,114 @@
1
1
  import type { RpgEvent, RpgPlayer } from "@rpgjs/server";
2
2
  import type { AttackPattern, EnemyType, AiState } from "../ai.server";
3
3
  import type { NormalizedActionBattleHitReactionProfile } from "../types";
4
+ import type {
5
+ ActionBattleAiIntent,
6
+ ActionBattleAiSimpleBehavior,
7
+ ActionBattleAiTreeInput,
8
+ } from "./ai-behavior-tree";
4
9
 
5
10
  export type ActionBattleEntity = RpgPlayer | RpgEvent;
6
11
 
12
+ export type ActionBattleTargetSelector =
13
+ | "players"
14
+ | "events"
15
+ | "all"
16
+ | "hostile"
17
+ | string[]
18
+ | ((context: ActionBattleTargetContext) => boolean);
19
+
20
+ export interface ActionBattleTargetContext {
21
+ attacker: ActionBattleEntity;
22
+ target: ActionBattleEntity;
23
+ attackerFaction?: string;
24
+ targetFaction?: string;
25
+ }
26
+
27
+ export interface ActionBattleTargetOptions {
28
+ faction?: string;
29
+ targets?: ActionBattleTargetSelector;
30
+ getFaction?: (entity: ActionBattleEntity) => string | undefined;
31
+ canTarget?: (context: ActionBattleTargetContext) => boolean;
32
+ }
33
+
34
+ export type ActionBattleActionMode = "instant" | "melee" | "projectile";
35
+
36
+ export type ActionBattleActionTarget = "enemy" | "ally" | "self" | "any";
37
+
38
+ export interface ActionBattleProjectileOptions {
39
+ type: string;
40
+ speed?: number;
41
+ range?: number;
42
+ /**
43
+ * Random direction offset in degrees, applied as +/- half this value.
44
+ * Useful for less accurate ranged attacks.
45
+ */
46
+ spreadDegrees?: number;
47
+ /**
48
+ * Convenience precision value from 0 to 1. Ignored when `spreadDegrees` is set.
49
+ * `1` is perfectly accurate, `0` can deviate up to 30 degrees.
50
+ */
51
+ accuracy?: number;
52
+ trajectory?: any;
53
+ direction?: { x: number; y: number };
54
+ origin?: { x: number; y: number };
55
+ collision?: any;
56
+ repeat?: any;
57
+ pattern?: any;
58
+ payload?: Record<string, unknown>;
59
+ params?: Record<string, unknown>;
60
+ onImpact?: (
61
+ context: ActionBattleProjectileImpactContext,
62
+ action: ActionBattleUseContext
63
+ ) => void;
64
+ }
65
+
66
+ export interface ActionBattleActionConfig {
67
+ target?: ActionBattleActionTarget;
68
+ range?: number;
69
+ cooldownMs?: number;
70
+ mode?: ActionBattleActionMode;
71
+ projectile?: Omit<ActionBattleProjectileOptions, "onImpact">;
72
+ }
73
+
74
+ export interface ActionBattleProjectileImpactContext {
75
+ attacker: ActionBattleEntity;
76
+ target?: ActionBattleEntity;
77
+ projectile: any;
78
+ hit: any;
79
+ map: any;
80
+ }
81
+
82
+ export interface ActionBattleUseContext {
83
+ attacker: ActionBattleEntity;
84
+ user: ActionBattleEntity;
85
+ target?: ActionBattleEntity | ActionBattleEntity[] | null;
86
+ usable: any;
87
+ skill?: any;
88
+ weapon?: any;
89
+ action?: ActionBattleActionConfig;
90
+ pattern?: AttackPattern | string;
91
+ defaultEffect(target?: ActionBattleEntity | ActionBattleEntity[] | null): any;
92
+ damage(target?: ActionBattleEntity | null): any;
93
+ heal(
94
+ target: ActionBattleEntity | ActionBattleEntity[] | null | undefined,
95
+ amount: number
96
+ ): number;
97
+ projectile(options?: ActionBattleProjectileOptions): any[];
98
+ }
99
+
100
+ export interface ActionBattleUsable {
101
+ id?: string;
102
+ _type?: string;
103
+ action?: ActionBattleActionConfig;
104
+ actionBattle?: ActionBattleActionConfig;
105
+ onUse?: (
106
+ user: ActionBattleEntity,
107
+ target: ActionBattleEntity | ActionBattleEntity[] | null | undefined,
108
+ action: ActionBattleUseContext
109
+ ) => any;
110
+ }
111
+
7
112
  export interface ActionBattleHitbox {
8
113
  x: number;
9
114
  y: number;
@@ -98,7 +203,7 @@ export interface ActionBattleCombatSystem {
98
203
 
99
204
  export interface ActionBattleAiContext {
100
205
  event: RpgEvent;
101
- target: RpgPlayer | null;
206
+ target: ActionBattleEntity | null;
102
207
  state: AiState;
103
208
  enemyType: EnemyType;
104
209
  distance: number | null;
@@ -111,6 +216,7 @@ export interface ActionBattleAiDecision {
111
216
  attackPatterns?: AttackPattern[];
112
217
  attackCooldown?: number;
113
218
  moveToCooldown?: number;
219
+ intent?: ActionBattleAiIntent | ActionBattleAiIntent[];
114
220
  metadata?: Record<string, any>;
115
221
  }
116
222
 
@@ -118,9 +224,48 @@ export type ActionBattleAiBehavior = (
118
224
  context: ActionBattleAiContext
119
225
  ) => ActionBattleAiDecision | void;
120
226
 
227
+ export interface ActionBattleAiPreset {
228
+ preset?: string | ActionBattleAiPreset;
229
+ faction?: string;
230
+ targets?: ActionBattleTargetSelector;
231
+ enemyType?: EnemyType;
232
+ attackCooldown?: number;
233
+ visionRange?: number;
234
+ attackRange?: number;
235
+ dodgeChance?: number;
236
+ dodgeCooldown?: number;
237
+ fleeThreshold?: number;
238
+ attackSkill?: any;
239
+ attackPatterns?: AttackPattern[];
240
+ attackProfiles?: any;
241
+ patrolWaypoints?: Array<{ x: number; y: number }>;
242
+ groupBehavior?: boolean;
243
+ moveToCooldown?: number;
244
+ retreatCooldown?: number;
245
+ poise?: number;
246
+ hitstunMs?: number;
247
+ invincibilityMs?: number;
248
+ behavior?: {
249
+ baseScore?: number;
250
+ updateInterval?: number;
251
+ minStateDuration?: number;
252
+ assaultThreshold?: number;
253
+ retreatThreshold?: number;
254
+ };
255
+ behaviorKey?: string;
256
+ tree?: ActionBattleAiTreeInput;
257
+ behaviorTree?: ActionBattleAiTreeInput;
258
+ simpleBehavior?: ActionBattleAiSimpleBehavior;
259
+ animations?: any;
260
+ rewards?: any;
261
+ autoAwardRewards?: boolean;
262
+ onDefeated?: any;
263
+ }
264
+
121
265
  export interface ActionBattleSystems {
122
266
  combat: ActionBattleCombatSystem;
123
267
  ai: {
124
268
  behaviors: Record<string, ActionBattleAiBehavior>;
269
+ presets: Record<string, ActionBattleAiPreset>;
125
270
  };
126
271
  }
@@ -1,6 +1,7 @@
1
1
  import type { RpgPlayer } from "@rpgjs/server";
2
2
  import type {
3
3
  ActionBattleAiBehavior,
4
+ ActionBattleAiPreset,
4
5
  ActionBattleAttackContext,
5
6
  ActionBattleCombatSystem,
6
7
  ActionBattleDamageContext,
@@ -146,10 +147,65 @@ export const defaultEnemyBehaviors: Record<string, ActionBattleAiBehavior> = {
146
147
  }),
147
148
  };
148
149
 
150
+ export const defaultEnemyPresets: Record<string, ActionBattleAiPreset> = {
151
+ [CoreEnemyType.Aggressive]: {
152
+ enemyType: CoreEnemyType.Aggressive as any,
153
+ attackCooldown: 600,
154
+ visionRange: 150,
155
+ attackRange: 50,
156
+ dodgeChance: 0.1,
157
+ dodgeCooldown: 3000,
158
+ fleeThreshold: 0.15,
159
+ behaviorKey: CoreEnemyType.Aggressive,
160
+ },
161
+ [CoreEnemyType.Defensive]: {
162
+ enemyType: CoreEnemyType.Defensive as any,
163
+ attackCooldown: 1500,
164
+ visionRange: 120,
165
+ attackRange: 60,
166
+ dodgeChance: 0.5,
167
+ dodgeCooldown: 1500,
168
+ fleeThreshold: 0.3,
169
+ behaviorKey: CoreEnemyType.Defensive,
170
+ },
171
+ [CoreEnemyType.Ranged]: {
172
+ enemyType: CoreEnemyType.Ranged as any,
173
+ attackCooldown: 1200,
174
+ visionRange: 200,
175
+ attackRange: 120,
176
+ dodgeChance: 0.4,
177
+ dodgeCooldown: 2000,
178
+ fleeThreshold: 0.25,
179
+ behaviorKey: CoreEnemyType.Ranged,
180
+ },
181
+ [CoreEnemyType.Tank]: {
182
+ enemyType: CoreEnemyType.Tank as any,
183
+ attackCooldown: 2000,
184
+ visionRange: 100,
185
+ attackRange: 50,
186
+ dodgeChance: 0,
187
+ dodgeCooldown: 5000,
188
+ fleeThreshold: 0.1,
189
+ poise: 2,
190
+ behaviorKey: CoreEnemyType.Tank,
191
+ },
192
+ [CoreEnemyType.Berserker]: {
193
+ enemyType: CoreEnemyType.Berserker as any,
194
+ attackCooldown: 800,
195
+ visionRange: 180,
196
+ attackRange: 55,
197
+ dodgeChance: 0.15,
198
+ dodgeCooldown: 2500,
199
+ fleeThreshold: 0.05,
200
+ behaviorKey: CoreEnemyType.Berserker,
201
+ },
202
+ };
203
+
149
204
  export const defaultActionBattleSystems: ActionBattleSystems = {
150
205
  combat: defaultCombatSystem,
151
206
  ai: {
152
207
  behaviors: defaultEnemyBehaviors,
208
+ presets: defaultEnemyPresets,
153
209
  },
154
210
  };
155
211
 
@@ -2,16 +2,20 @@ import type { ActionBattleAttackProfile } from "../types";
2
2
 
3
3
  const resolveItemId = (item: any) => item?.id?.() ?? item?.id;
4
4
 
5
- export function resolveActionBattleWeaponAttackProfile(
6
- entity: any
7
- ): ActionBattleAttackProfile | null {
5
+ export function resolveActionBattleWeapon(entity: any): any | null {
8
6
  const equipments = entity?.equipments?.() || [];
9
7
  for (const item of equipments) {
10
8
  const itemId = resolveItemId(item);
11
9
  const itemData = entity?.databaseById?.(itemId);
12
- if (itemData?._type === "weapon" && itemData.attackProfile) {
13
- return itemData.attackProfile;
10
+ if (itemData?._type === "weapon") {
11
+ return itemData;
14
12
  }
15
13
  }
16
14
  return null;
17
15
  }
16
+
17
+ export function resolveActionBattleWeaponAttackProfile(
18
+ entity: any
19
+ ): ActionBattleAttackProfile | null {
20
+ return resolveActionBattleWeapon(entity)?.attackProfile ?? null;
21
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { RpgEvent } from "@rpgjs/server";
3
+ import { BattleAi } from "../ai.server";
4
+ import {
5
+ canActionBattleTarget,
6
+ getActionBattleEntityKind,
7
+ getActionBattleFaction,
8
+ getActionBattleTargets,
9
+ } from "./targets";
10
+
11
+ const player = (id: string, faction?: string) =>
12
+ ({
13
+ id,
14
+ hp: 100,
15
+ actionBattleFaction: faction,
16
+ }) as any;
17
+
18
+ const battleEvent = (id: string, faction?: string) =>
19
+ ({
20
+ id,
21
+ hp: 100,
22
+ actionBattleFaction: faction,
23
+ battleAi: {
24
+ getFaction: () => faction,
25
+ getTargets: () => "players",
26
+ },
27
+ attachShape() {},
28
+ }) as any;
29
+
30
+ describe("action battle targets", () => {
31
+ test("keeps player default targets on action battle events", () => {
32
+ const attacker = player("player-1");
33
+ const target = battleEvent("enemy-1", "enemies");
34
+
35
+ expect(getActionBattleTargets(attacker, "events")).toBe("events");
36
+ expect(canActionBattleTarget(attacker, target, "events")).toBe(true);
37
+ expect(canActionBattleTarget(attacker, player("player-2"), "events")).toBe(false);
38
+ });
39
+
40
+ test("classifies runtime RpgEvent enemies before their RpgPlayer base class", () => {
41
+ const attacker = player("player-1");
42
+ const target = new RpgEvent() as any;
43
+ target.id = "enemy-1";
44
+ target.hp = 100;
45
+ target.battleAi = {
46
+ getFaction: () => "enemies",
47
+ getTargets: () => "players",
48
+ };
49
+
50
+ expect(getActionBattleEntityKind(target)).toBe("event");
51
+ expect(canActionBattleTarget(attacker, target, "events")).toBe(true);
52
+ expect(canActionBattleTarget(attacker, target, "players")).toBe(false);
53
+ });
54
+
55
+ test("supports player targets explicitly", () => {
56
+ const attacker = player("player-1");
57
+ const target = player("player-2");
58
+
59
+ expect(canActionBattleTarget(attacker, target, "players")).toBe(true);
60
+ });
61
+
62
+ test("supports all targets", () => {
63
+ const attacker = battleEvent("enemy-1", "enemies");
64
+
65
+ expect(canActionBattleTarget(attacker, player("player-1"), "all")).toBe(true);
66
+ expect(canActionBattleTarget(attacker, battleEvent("enemy-2", "enemies"), "all")).toBe(true);
67
+ });
68
+
69
+ test("supports hostile targets from different factions", () => {
70
+ const guard = battleEvent("guard-1", "guards");
71
+ const guardAlly = battleEvent("guard-2", "guards");
72
+ const bandit = battleEvent("bandit-1", "bandits");
73
+
74
+ expect(canActionBattleTarget(guard, guardAlly, "hostile")).toBe(false);
75
+ expect(canActionBattleTarget(guard, bandit, "hostile")).toBe(true);
76
+ });
77
+
78
+ test("supports explicit faction target lists", () => {
79
+ const guard = battleEvent("guard-1", "guards");
80
+ const bandit = battleEvent("bandit-1", "bandits");
81
+ const monster = battleEvent("monster-1", "monsters");
82
+
83
+ expect(canActionBattleTarget(guard, bandit, ["bandits"])).toBe(true);
84
+ expect(canActionBattleTarget(guard, monster, ["bandits"])).toBe(false);
85
+ });
86
+
87
+ test("BattleAi exposes runtime faction and targets", () => {
88
+ const event = {
89
+ id: "guard-1",
90
+ hp: 100,
91
+ param: {},
92
+ attachShape: () => ({ id: "vision_guard-1" }),
93
+ getCurrentMap: () => ({}),
94
+ stopMoveTo() {},
95
+ };
96
+ const ai = new BattleAi(event as any, {
97
+ faction: "guards",
98
+ targets: ["bandits"],
99
+ });
100
+
101
+ expect(getActionBattleFaction(event as any)).toBe("guards");
102
+ expect(getActionBattleTargets(event as any, "players")).toEqual(["bandits"]);
103
+
104
+ ai.setFaction("bandits");
105
+ ai.setTargets("hostile");
106
+
107
+ expect(getActionBattleFaction(event as any)).toBe("bandits");
108
+ expect(getActionBattleTargets(event as any, "players")).toBe("hostile");
109
+
110
+ ai.destroy();
111
+ });
112
+ });
@@ -0,0 +1,147 @@
1
+ import { RpgEvent, RpgPlayer } from "@rpgjs/server";
2
+ import type {
3
+ ActionBattleEntity,
4
+ ActionBattleTargetContext,
5
+ ActionBattleTargetOptions,
6
+ ActionBattleTargetSelector,
7
+ } from "./contracts";
8
+
9
+ export const ACTION_BATTLE_PLAYER_FACTION = "players";
10
+ export const ACTION_BATTLE_ENEMY_FACTION = "enemies";
11
+
12
+ type EntityKind = "player" | "event";
13
+
14
+ const getBattleAi = (entity: ActionBattleEntity) => (entity as any)?.battleAi;
15
+
16
+ export const getActionBattleEntityKind = (
17
+ entity: ActionBattleEntity
18
+ ): EntityKind => {
19
+ if (getBattleAi(entity)) return "event";
20
+ if ((entity as any) instanceof RpgEvent) return "event";
21
+ if (typeof (entity as any)?.attachShape === "function") return "event";
22
+ if ((entity as any) instanceof RpgPlayer) return "player";
23
+ return "player";
24
+ };
25
+
26
+ export const isActionBattlePlayer = (entity: ActionBattleEntity): boolean =>
27
+ getActionBattleEntityKind(entity) === "player";
28
+
29
+ export const isActionBattleEvent = (entity: ActionBattleEntity): boolean =>
30
+ getActionBattleEntityKind(entity) === "event";
31
+
32
+ export const isActionBattleCombatEntity = (
33
+ entity: ActionBattleEntity | undefined | null
34
+ ): entity is ActionBattleEntity => {
35
+ if (!entity) return false;
36
+ if (isActionBattlePlayer(entity as ActionBattleEntity)) return true;
37
+ return !!getBattleAi(entity as ActionBattleEntity);
38
+ };
39
+
40
+ export const getActionBattleFaction = (
41
+ entity: ActionBattleEntity,
42
+ options: ActionBattleTargetOptions = {}
43
+ ): string | undefined => {
44
+ const configured = options.getFaction?.(entity);
45
+ if (configured !== undefined) return configured;
46
+
47
+ const battleAi = getBattleAi(entity);
48
+ if (battleAi && typeof battleAi.getFaction === "function") {
49
+ const faction = battleAi.getFaction();
50
+ if (faction !== undefined) return faction;
51
+ }
52
+
53
+ const entityFaction =
54
+ (entity as any).actionBattleFaction ?? (entity as any).faction;
55
+ if (entityFaction !== undefined) return String(entityFaction);
56
+
57
+ if (isActionBattlePlayer(entity)) return ACTION_BATTLE_PLAYER_FACTION;
58
+ if (battleAi) return ACTION_BATTLE_ENEMY_FACTION;
59
+ return undefined;
60
+ };
61
+
62
+ export const getActionBattleTargets = (
63
+ entity: ActionBattleEntity,
64
+ fallback: ActionBattleTargetSelector
65
+ ): ActionBattleTargetSelector => {
66
+ const battleAi = getBattleAi(entity);
67
+ if (battleAi && typeof battleAi.getTargets === "function") {
68
+ return battleAi.getTargets();
69
+ }
70
+ return (entity as any).actionBattleTargets ?? fallback;
71
+ };
72
+
73
+ export const isActionBattleTargetDefeated = (
74
+ target: ActionBattleEntity | null | undefined
75
+ ): boolean => {
76
+ if (!target) return true;
77
+ const hp = (target as any).hp;
78
+ return typeof hp === "number" && hp <= 0;
79
+ };
80
+
81
+ export const matchesActionBattleTargetSelector = (
82
+ selector: ActionBattleTargetSelector | undefined,
83
+ context: ActionBattleTargetContext
84
+ ): boolean => {
85
+ if (!selector) return false;
86
+ if (typeof selector === "function") return selector(context);
87
+ if (selector === "all") return true;
88
+ if (selector === "players") return isActionBattlePlayer(context.target);
89
+ if (selector === "events") return isActionBattleEvent(context.target);
90
+ if (selector === "hostile") {
91
+ return (
92
+ !!context.attackerFaction &&
93
+ !!context.targetFaction &&
94
+ context.attackerFaction !== context.targetFaction
95
+ );
96
+ }
97
+ if (Array.isArray(selector)) {
98
+ return !!context.targetFaction && selector.includes(context.targetFaction);
99
+ }
100
+ return false;
101
+ };
102
+
103
+ export const canActionBattleTarget = (
104
+ attacker: ActionBattleEntity,
105
+ target: ActionBattleEntity,
106
+ selector: ActionBattleTargetSelector | undefined,
107
+ options: ActionBattleTargetOptions = {}
108
+ ): boolean => {
109
+ if (attacker === target) return false;
110
+ if (!isActionBattleCombatEntity(target)) return false;
111
+ if (isActionBattleTargetDefeated(target)) return false;
112
+
113
+ const context: ActionBattleTargetContext = {
114
+ attacker,
115
+ target,
116
+ attackerFaction: getActionBattleFaction(attacker, options),
117
+ targetFaction: getActionBattleFaction(target, options),
118
+ };
119
+
120
+ const allowed = options.canTarget?.(context);
121
+ if (allowed !== undefined) return allowed;
122
+
123
+ return matchesActionBattleTargetSelector(selector, context);
124
+ };
125
+
126
+ export const getActionBattleEntitiesInRange = (
127
+ attacker: ActionBattleEntity,
128
+ radius: number,
129
+ selector: ActionBattleTargetSelector | undefined,
130
+ options: ActionBattleTargetOptions = {}
131
+ ): ActionBattleEntity[] => {
132
+ const map = (attacker as any).getCurrentMap?.();
133
+ if (!map) return [];
134
+
135
+ const candidates: ActionBattleEntity[] = [];
136
+ map.getPlayers?.().forEach((player: RpgPlayer) => candidates.push(player));
137
+ map.getEvents?.().forEach((event: RpgEvent) => candidates.push(event));
138
+
139
+ return candidates.filter((candidate) => {
140
+ if (!canActionBattleTarget(attacker, candidate, selector, options)) {
141
+ return false;
142
+ }
143
+ const dx = (attacker as any).x() - (candidate as any).x();
144
+ const dy = (attacker as any).y() - (candidate as any).y();
145
+ return Math.sqrt(dx * dx + dy * dy) <= radius;
146
+ });
147
+ };
@@ -7,6 +7,14 @@ export interface ActionBattleEnemyPreset extends BattleAiOptions {
7
7
 
8
8
  export type ActionBattleEnemyPresetMap = Record<string, ActionBattleEnemyPreset>;
9
9
 
10
+ export const defineActionBattleEnemy = <T extends ActionBattleEnemyPreset>(
11
+ preset: T
12
+ ): T => preset;
13
+
14
+ export const defineActionBattleAiPreset = <T extends BattleAiOptions>(
15
+ preset: T
16
+ ): T => preset;
17
+
10
18
  export const createActionEnemy = (
11
19
  event: RpgEvent,
12
20
  presetOrOptions: string | BattleAiOptions,