@rpgjs/action-battle 5.0.0-beta.11 → 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.
- package/CHANGELOG.md +11 -0
- package/dist/client/ai.server.d.ts +45 -8
- package/dist/client/attack-input.d.ts +3 -0
- package/dist/client/core/action-use.d.ts +18 -0
- package/dist/client/core/ai-behavior-tree.d.ts +99 -0
- package/dist/client/core/attack-runtime.d.ts +2 -0
- package/dist/client/core/defaults.d.ts +2 -1
- package/dist/client/core/equipment.d.ts +1 -0
- package/dist/client/core/targets.d.ts +15 -0
- package/dist/client/enemies/factory.d.ts +2 -0
- package/dist/client/index.d.ts +12 -7
- package/dist/client/index.js +16 -11
- package/dist/client/index10.js +32 -56
- package/dist/client/index11.js +99 -52
- package/dist/client/index12.js +76 -103
- package/dist/client/index13.js +72 -135
- package/dist/client/index14.js +67 -23
- package/dist/client/index15.js +197 -63
- package/dist/client/index16.js +112 -1337
- package/dist/client/index17.js +193 -7
- package/dist/client/index18.js +32 -58
- package/dist/client/index19.js +70 -8
- package/dist/client/index20.js +57 -501
- package/dist/client/index21.js +69 -0
- package/dist/client/index22.js +225 -0
- package/dist/client/index23.js +16 -0
- package/dist/client/index24.js +25 -0
- package/dist/client/index25.js +107 -0
- package/dist/client/index26.js +1707 -0
- package/dist/client/index27.js +12 -0
- package/dist/client/index28.js +589 -0
- package/dist/client/index4.js +79 -38
- package/dist/client/index6.js +65 -306
- package/dist/client/index7.js +33 -33
- package/dist/client/index8.js +24 -100
- package/dist/client/index9.js +293 -61
- package/dist/client/locomotion.d.ts +16 -0
- package/dist/client/movement.d.ts +14 -0
- package/dist/client/server.d.ts +7 -3
- package/dist/client/ui.d.ts +22 -0
- package/dist/client/visual.d.ts +15 -0
- package/dist/server/ai.server.d.ts +45 -8
- package/dist/server/attack-input.d.ts +3 -0
- package/dist/server/core/action-use.d.ts +18 -0
- package/dist/server/core/ai-behavior-tree.d.ts +99 -0
- package/dist/server/core/attack-runtime.d.ts +2 -0
- package/dist/server/core/defaults.d.ts +2 -1
- package/dist/server/core/equipment.d.ts +1 -0
- package/dist/server/core/targets.d.ts +15 -0
- package/dist/server/enemies/factory.d.ts +2 -0
- package/dist/server/index.d.ts +12 -7
- package/dist/server/index.js +14 -9
- package/dist/server/index10.js +64 -1336
- package/dist/server/index11.js +33 -33
- package/dist/server/index13.js +66 -11
- package/dist/server/index14.js +206 -484
- package/dist/server/index15.js +15 -9
- package/dist/server/index16.js +26 -0
- package/dist/server/index17.js +25 -0
- package/dist/server/index18.js +107 -0
- package/dist/server/index19.js +1707 -0
- package/dist/server/index2.js +10 -2
- package/dist/server/index20.js +37 -0
- package/dist/server/index21.js +588 -0
- package/dist/server/index22.js +78 -0
- package/dist/server/index23.js +12 -0
- package/dist/server/index5.js +79 -38
- package/dist/server/index6.js +192 -129
- package/dist/server/index7.js +198 -24
- package/dist/server/index8.js +28 -66
- package/dist/server/index9.js +68 -51
- package/dist/server/locomotion.d.ts +16 -0
- package/dist/server/movement.d.ts +14 -0
- package/dist/server/server.d.ts +7 -3
- package/dist/server/ui.d.ts +22 -0
- package/dist/server/visual.d.ts +15 -0
- package/package.json +5 -5
- package/src/ai.server.spec.ts +233 -0
- package/src/ai.server.ts +627 -108
- package/src/animations.spec.ts +40 -0
- package/src/animations.ts +31 -9
- package/src/attack-input.spec.ts +51 -0
- package/src/attack-input.ts +59 -0
- package/src/client.ts +75 -62
- package/src/config.ts +84 -37
- package/src/core/action-use.spec.ts +317 -0
- package/src/core/action-use.ts +386 -0
- package/src/core/ai-behavior-tree.spec.ts +116 -0
- package/src/core/ai-behavior-tree.ts +272 -0
- package/src/core/attack-profile.spec.ts +46 -0
- package/src/core/attack-runtime.spec.ts +35 -0
- package/src/core/attack-runtime.ts +32 -0
- package/src/core/context.ts +9 -0
- package/src/core/contracts.ts +146 -1
- package/src/core/defaults.ts +56 -0
- package/src/core/equipment.ts +9 -5
- package/src/core/targets.spec.ts +112 -0
- package/src/core/targets.ts +147 -0
- package/src/enemies/factory.ts +8 -0
- package/src/index.ts +111 -2
- package/src/locomotion.spec.ts +51 -0
- package/src/locomotion.ts +48 -0
- package/src/movement.spec.ts +78 -0
- package/src/movement.ts +46 -0
- package/src/server.ts +242 -66
- package/src/types.ts +105 -35
- package/src/ui.ts +113 -0
- package/src/visual.spec.ts +166 -0
- package/src/visual.ts +285 -0
- package/README.md +0 -1242
package/src/core/contracts.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/src/core/defaults.ts
CHANGED
|
@@ -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
|
|
package/src/core/equipment.ts
CHANGED
|
@@ -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
|
|
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"
|
|
13
|
-
return itemData
|
|
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
|
+
};
|
package/src/enemies/factory.ts
CHANGED
|
@@ -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,
|