@rpgjs/action-battle 5.0.0-alpha.44 → 5.0.0-beta.10

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 (103) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +19 -0
  3. package/README.md +392 -22
  4. package/dist/{ai.server.d.ts → client/ai.server.d.ts} +90 -28
  5. package/dist/client/animations.d.ts +16 -0
  6. package/dist/{client.d.ts → client/client.d.ts} +3 -2
  7. package/dist/{config.d.ts → client/config.d.ts} +2 -0
  8. package/dist/client/core/attack-profile.d.ts +9 -0
  9. package/dist/client/core/attack-runtime.d.ts +20 -0
  10. package/dist/client/core/context.d.ts +5 -0
  11. package/dist/client/core/defaults.d.ts +81 -0
  12. package/dist/client/core/enemy-attack-profiles.d.ts +6 -0
  13. package/dist/client/core/equipment.d.ts +2 -0
  14. package/dist/client/core/hit-reaction.d.ts +5 -0
  15. package/dist/client/core/hit.d.ts +2 -0
  16. package/dist/client/enemies/factory.d.ts +7 -0
  17. package/dist/client/index.d.ts +21 -0
  18. package/dist/client/index.js +24 -31
  19. package/dist/client/index10.js +61 -0
  20. package/dist/client/index11.js +55 -0
  21. package/dist/client/index12.js +106 -0
  22. package/dist/client/index13.js +143 -0
  23. package/dist/client/index14.js +25 -0
  24. package/dist/client/index15.js +72 -0
  25. package/dist/client/index16.js +1343 -0
  26. package/dist/client/index17.js +13 -0
  27. package/dist/client/index18.js +60 -0
  28. package/dist/client/index19.js +10 -0
  29. package/dist/client/index2.js +30 -45
  30. package/dist/client/index20.js +504 -0
  31. package/dist/client/index3.js +45 -1288
  32. package/dist/client/index4.js +105 -330
  33. package/dist/client/index5.js +84 -291
  34. package/dist/client/index6.js +309 -95
  35. package/dist/client/index7.js +35 -59
  36. package/dist/client/index8.js +101 -54
  37. package/dist/client/index9.js +79 -30
  38. package/dist/{server.d.ts → client/server.d.ts} +12 -4
  39. package/dist/client/ui/state.d.ts +35 -0
  40. package/dist/server/ai.server.d.ts +569 -0
  41. package/dist/server/animations.d.ts +16 -0
  42. package/dist/server/config.d.ts +5 -0
  43. package/dist/server/core/attack-profile.d.ts +9 -0
  44. package/dist/server/core/attack-runtime.d.ts +20 -0
  45. package/dist/server/core/context.d.ts +5 -0
  46. package/dist/server/core/defaults.d.ts +81 -0
  47. package/dist/server/core/enemy-attack-profiles.d.ts +6 -0
  48. package/dist/server/core/equipment.d.ts +2 -0
  49. package/dist/server/core/hit-reaction.d.ts +5 -0
  50. package/dist/server/core/hit.d.ts +2 -0
  51. package/dist/server/enemies/factory.d.ts +7 -0
  52. package/dist/server/index.d.ts +21 -0
  53. package/dist/server/index.js +23 -31
  54. package/dist/server/index10.js +1342 -0
  55. package/dist/server/index11.js +37 -0
  56. package/dist/server/index12.js +60 -0
  57. package/dist/server/index13.js +13 -0
  58. package/dist/server/index14.js +503 -0
  59. package/dist/server/index15.js +10 -0
  60. package/dist/server/index2.js +59 -332
  61. package/dist/server/index3.js +29 -1286
  62. package/dist/server/index4.js +45 -53
  63. package/dist/server/index5.js +107 -29
  64. package/dist/server/index6.js +143 -0
  65. package/dist/server/index7.js +25 -0
  66. package/dist/server/index8.js +72 -0
  67. package/dist/server/index9.js +55 -0
  68. package/dist/server/server.d.ts +106 -0
  69. package/dist/server/targeting.d.ts +19 -0
  70. package/package.json +12 -12
  71. package/src/ai.server.spec.ts +120 -0
  72. package/src/ai.server.ts +515 -91
  73. package/src/animations.ts +149 -0
  74. package/src/canvas-engine-shim.ts +4 -0
  75. package/src/client.ts +130 -2
  76. package/src/components/action-bar.ce +5 -3
  77. package/src/components/attack-preview.ce +90 -0
  78. package/src/config.ts +61 -0
  79. package/src/core/attack-profile.spec.ts +118 -0
  80. package/src/core/attack-profile.ts +100 -0
  81. package/src/core/attack-runtime.spec.ts +103 -0
  82. package/src/core/attack-runtime.ts +83 -0
  83. package/src/core/context.ts +35 -0
  84. package/src/core/contracts.ts +126 -0
  85. package/src/core/defaults.ts +162 -0
  86. package/src/core/enemy-attack-profiles.spec.ts +35 -0
  87. package/src/core/enemy-attack-profiles.ts +103 -0
  88. package/src/core/equipment.spec.ts +37 -0
  89. package/src/core/equipment.ts +17 -0
  90. package/src/core/hit-reaction.spec.ts +43 -0
  91. package/src/core/hit-reaction.ts +70 -0
  92. package/src/core/hit.spec.ts +111 -0
  93. package/src/core/hit.ts +92 -0
  94. package/src/enemies/factory.ts +25 -0
  95. package/src/index.ts +94 -1
  96. package/src/server.ts +427 -93
  97. package/src/targeting.spec.ts +24 -0
  98. package/src/types/canvas-engine.d.ts +4 -0
  99. package/src/types.ts +148 -0
  100. package/src/ui/state.ts +57 -0
  101. package/dist/index.d.ts +0 -11
  102. package/dist/ui/state.d.ts +0 -18
  103. /package/dist/{targeting.d.ts → client/targeting.d.ts} +0 -0
@@ -1,333 +1,108 @@
1
- import { defineModule, Control } from "@rpgjs/common";
2
- import { normalizeActionBattleOptions } from "./index8.js";
3
- import { manhattanDistance, parseAoeMask } from "./index9.js";
4
- const RpgEvent = null;
5
- const DEFAULT_KNOCKBACK = null;
6
- const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
7
- const DEFAULT_PLAYER_ATTACK_HITBOXES = {
8
- up: { offsetX: -16, offsetY: -48, width: 32, height: 32 },
9
- down: { offsetX: -16, offsetY: 16, width: 32, height: 32 },
10
- left: { offsetX: -48, offsetY: -16, width: 32, height: 32 },
11
- right: { offsetX: 16, offsetY: -16, width: 32, height: 32 },
12
- default: { offsetX: 0, offsetY: -32, width: 32, height: 32 }
1
+ import { normalizeActionBattleAttackProfile } from "./index3.js";
2
+ //#region src/config.ts
3
+ var DEFAULT_ACTION_BATTLE_OPTIONS = {
4
+ ui: {
5
+ actionBar: {
6
+ enabled: false,
7
+ autoOpen: false,
8
+ mode: "both"
9
+ },
10
+ targeting: {
11
+ enabled: true,
12
+ showGrid: true,
13
+ colors: {
14
+ area: 3120887,
15
+ edge: 1796760,
16
+ cursor: 16765286
17
+ }
18
+ }
19
+ },
20
+ skills: { defaultAoeMask: ["#"] },
21
+ targeting: {
22
+ affects: "events",
23
+ allowEmptyTarget: true
24
+ },
25
+ attack: {
26
+ lockMovement: true,
27
+ lockDurationMs: 350,
28
+ showPreview: true,
29
+ previewDurationMs: 180,
30
+ previewColor: 16774064,
31
+ previewAccentColor: 16777215
32
+ },
33
+ animations: {}
13
34
  };
14
- function getPlayerWeaponKnockbackForce(player) {
15
- try {
16
- const equipments = player.equipments?.() || [];
17
- for (const item of equipments) {
18
- const itemData = player.databaseById?.(item.id());
19
- if (itemData?._type === "weapon" && itemData.knockbackForce !== void 0) {
20
- return itemData.knockbackForce;
21
- }
22
- }
23
- } catch {
24
- }
25
- return DEFAULT_KNOCKBACK.force;
35
+ var currentActionBattleOptions = DEFAULT_ACTION_BATTLE_OPTIONS;
36
+ function normalizeActionBattleOptions(options = {}) {
37
+ const attack = {
38
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.attack,
39
+ ...options.attack
40
+ };
41
+ const attackProfile = normalizeActionBattleAttackProfile(attack.profile, {
42
+ lockMovement: attack.lockMovement,
43
+ lockDurationMs: attack.lockDurationMs,
44
+ hitboxes: attack.hitboxes
45
+ });
46
+ return {
47
+ ui: {
48
+ actionBar: {
49
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.actionBar,
50
+ ...options.ui?.actionBar
51
+ },
52
+ targeting: {
53
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting,
54
+ ...options.ui?.targeting,
55
+ colors: {
56
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting?.colors,
57
+ ...options.ui?.targeting?.colors
58
+ }
59
+ }
60
+ },
61
+ skills: {
62
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.skills,
63
+ ...options.skills
64
+ },
65
+ targeting: {
66
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.targeting,
67
+ ...options.targeting
68
+ },
69
+ debug: {
70
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.debug,
71
+ ...options.debug
72
+ },
73
+ attack: {
74
+ ...attack,
75
+ profile: attackProfile
76
+ },
77
+ animations: {
78
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.animations,
79
+ ...options.animations
80
+ },
81
+ systems: {
82
+ combat: {
83
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat,
84
+ ...options.systems?.combat,
85
+ hooks: {
86
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat?.hooks,
87
+ ...options.systems?.combat?.hooks
88
+ }
89
+ },
90
+ ai: {
91
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai,
92
+ ...options.systems?.ai,
93
+ behaviors: {
94
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai?.behaviors,
95
+ ...options.systems?.ai?.behaviors
96
+ }
97
+ }
98
+ }
99
+ };
26
100
  }
27
- function applyPlayerHitToEvent(player, target, hooks) {
28
- const ai = target.battleAi;
29
- if (!ai) return void 0;
30
- const knockbackForce = getPlayerWeaponKnockbackForce(player);
31
- const defeated = ai.takeDamage(player);
32
- const dx = target.x() - player.x();
33
- const dy = target.y() - player.y();
34
- const distance = Math.sqrt(dx * dx + dy * dy);
35
- let hitResult = {
36
- damage: 0,
37
- // Will be set by takeDamage internally
38
- knockbackForce,
39
- knockbackDuration: DEFAULT_KNOCKBACK.duration,
40
- defeated,
41
- attacker: player,
42
- target
43
- };
44
- if (hooks?.onBeforeHit) {
45
- const modified = hooks.onBeforeHit(hitResult);
46
- if (modified) {
47
- hitResult = modified;
48
- }
49
- }
50
- if (!hitResult.defeated && hitResult.knockbackForce > 0 && distance > 0) {
51
- const knockbackDirection = {
52
- x: dx / distance,
53
- y: dy / distance
54
- };
55
- target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
56
- }
57
- if (hooks?.onAfterHit) {
58
- hooks.onAfterHit(hitResult);
59
- }
60
- return hitResult;
101
+ function setActionBattleOptions(options) {
102
+ currentActionBattleOptions = options;
61
103
  }
62
- const resolveSignal = (value) => typeof value === "function" ? value() : value;
63
- const resolveItemData = (player, itemId) => {
64
- try {
65
- return player.databaseById?.(itemId);
66
- } catch {
67
- return null;
68
- }
69
- };
70
- const resolveSkillData = (player, skillId) => {
71
- try {
72
- return player.databaseById?.(skillId);
73
- } catch {
74
- return null;
75
- }
76
- };
77
- const resolveSkillTargeting = (player, skillId, options) => {
78
- const skillsOptions = options.skills;
79
- const skillData = resolveSkillData(player, skillId);
80
- if (skillsOptions?.getTargeting) {
81
- return skillsOptions.getTargeting(skillData);
82
- }
83
- const range = skillData?.range ?? skillData?.targeting?.range ?? skillData?.targeting?.distance;
84
- const aoeMask = skillData?.aoeMask ?? skillData?.targeting?.aoeMask ?? skillData?.targeting?.mask;
85
- if (range === void 0 && aoeMask === void 0) {
86
- return null;
87
- }
88
- return {
89
- range: range ?? 0,
90
- aoeMask
91
- };
92
- };
93
- const normalizeMaskRows = (mask) => {
94
- if (!mask) return [];
95
- if (Array.isArray(mask)) return mask;
96
- return mask.trim().split("\n").map((row) => row.replace(/\r/g, ""));
97
- };
98
- const buildActionBarData = (player, options) => {
99
- const items = (player.items?.() || []).map((item) => {
100
- const id = item.id?.() ?? item.id;
101
- const data = resolveItemData(player, id);
102
- const name = resolveSignal(data?.name) ?? resolveSignal(item.name) ?? id;
103
- const description = resolveSignal(data?.description) ?? resolveSignal(item.description) ?? "";
104
- const icon = resolveSignal(data?.icon) ?? resolveSignal(item.icon);
105
- const quantity = resolveSignal(item.quantity) ?? 1;
106
- const consumable = resolveSignal(data?.consumable);
107
- const itemType = resolveSignal(data?._type);
108
- const usable = quantity > 0 && consumable !== false && (itemType ? itemType === "item" : true);
109
- return {
110
- id,
111
- name,
112
- description,
113
- icon,
114
- quantity,
115
- usable
116
- };
117
- });
118
- const skills = (player.skills?.() || []).map((skill) => {
119
- const id = skill.id?.() ?? skill.id;
120
- const data = resolveSkillData(player, id) || skill;
121
- const name = resolveSignal(data?.name) ?? resolveSignal(skill.name) ?? id;
122
- const description = resolveSignal(data?.description) ?? resolveSignal(skill.description) ?? "";
123
- const icon = resolveSignal(data?.icon) ?? resolveSignal(skill.icon);
124
- const spCost = resolveSignal(data?.spCost) ?? resolveSignal(skill.spCost) ?? 0;
125
- const usable = spCost <= player.sp;
126
- const targeting = resolveSkillTargeting(player, id, options);
127
- const skillEntry = {
128
- id,
129
- name,
130
- description,
131
- icon,
132
- spCost,
133
- usable,
134
- range: targeting?.range ?? 0
135
- };
136
- if (targeting) {
137
- const mask = targeting.aoeMask ?? options.skills?.defaultAoeMask;
138
- if (mask) {
139
- skillEntry.aoeMask = normalizeMaskRows(mask);
140
- }
141
- }
142
- return skillEntry;
143
- });
144
- return { items, skills };
145
- };
146
- const ensureActionBarGui = (player, options) => {
147
- const existing = player.getGui?.(ACTION_BATTLE_ACTION_BAR_GUI_ID);
148
- const gui = existing || player.gui(ACTION_BATTLE_ACTION_BAR_GUI_ID);
149
- if (!gui.__actionBattleReady) {
150
- gui.__actionBattleReady = true;
151
- gui.on("useItem", ({ id }) => {
152
- try {
153
- player.useItem(id);
154
- } catch {
155
- }
156
- gui.update(buildActionBarData(player, options));
157
- });
158
- gui.on("useSkill", ({ id, target }) => {
159
- handleActionBattleSkillUse(player, id, target, options);
160
- gui.update(buildActionBarData(player, options));
161
- });
162
- gui.on("refresh", () => {
163
- gui.update(buildActionBarData(player, options));
164
- });
165
- }
166
- return gui;
167
- };
168
- const openActionBattleActionBar = (player, rawOptions = {}) => {
169
- const options = normalizeActionBattleOptions(rawOptions);
170
- const gui = ensureActionBarGui(player, options);
171
- gui.open(buildActionBarData(player, options));
172
- };
173
- const updateActionBattleActionBar = (player, rawOptions = {}) => {
174
- const options = normalizeActionBattleOptions(rawOptions);
175
- const gui = player.getGui?.(ACTION_BATTLE_ACTION_BAR_GUI_ID);
176
- if (gui) {
177
- gui.update(buildActionBarData(player, options));
178
- }
179
- };
180
- const getTileSize = (map) => ({
181
- width: map?.tileWidth ?? 32,
182
- height: map?.tileHeight ?? 32
183
- });
184
- const getEntityTile = (entity, tileSize) => {
185
- const hitbox = entity.hitbox?.() || { w: tileSize.width, h: tileSize.height };
186
- const x = Math.floor((entity.x() + hitbox.w / 2) / tileSize.width);
187
- const y = Math.floor((entity.y() + hitbox.h / 2) / tileSize.height);
188
- return { x, y };
189
- };
190
- const handleActionBattleSkillUse = (player, skillId, target, options) => {
191
- const map = player.getCurrentMap();
192
- if (!map) {
193
- player.useSkill(skillId);
194
- return;
195
- }
196
- const targeting = resolveSkillTargeting(player, skillId, options);
197
- if (!targeting || !target) {
198
- player.useSkill(skillId);
199
- return;
200
- }
201
- const tileSize = getTileSize(map);
202
- const origin = getEntityTile(player, tileSize);
203
- const targetTile = { x: target.x, y: target.y };
204
- if (manhattanDistance(origin, targetTile) > targeting.range) {
205
- return;
206
- }
207
- const mask = parseAoeMask(
208
- targeting.aoeMask || options.skills?.defaultAoeMask
209
- );
210
- const affected = /* @__PURE__ */ new Set();
211
- mask.cells.forEach((cell) => {
212
- const x = targetTile.x + cell.dx;
213
- const y = targetTile.y + cell.dy;
214
- affected.add(`${x},${y}`);
215
- });
216
- const targets = [];
217
- const affects = options.targeting?.affects || "events";
218
- if (affects === "events" || affects === "both") {
219
- map.getEvents().forEach((event) => {
220
- const tile = getEntityTile(event, tileSize);
221
- if (affected.has(`${tile.x},${tile.y}`)) {
222
- targets.push(event);
223
- }
224
- });
225
- }
226
- if (affects === "players" || affects === "both") {
227
- map.getPlayers().forEach((other) => {
228
- if (other.id === player.id) return;
229
- const tile = getEntityTile(other, tileSize);
230
- if (affected.has(`${tile.x},${tile.y}`)) {
231
- targets.push(other);
232
- }
233
- });
234
- }
235
- if (!options.targeting?.allowEmptyTarget && targets.length === 0) {
236
- return;
237
- }
238
- player.useSkill(skillId, targets);
239
- };
240
- const createActionBattleServer = (rawOptions = {}) => {
241
- const options = normalizeActionBattleOptions(rawOptions);
242
- return defineModule({
243
- player: {
244
- /**
245
- * Handle player input for combat actions
246
- *
247
- * When a player presses the action key, create an attack hitbox
248
- * that can damage AI enemies within range and knockback the event.
249
- * Knockback force is based on the player's equipped weapon.
250
- * Triggers attack animation and visual effects.
251
- *
252
- * @param player - The player performing the action
253
- * @param input - Input data containing pressed keys
254
- */
255
- onInput(player, input) {
256
- if (input.action == Control.Action) {
257
- player.setGraphicAnimation("attack", 1);
258
- const playerX = player.x();
259
- const playerY = player.y();
260
- const direction = player.getDirection();
261
- const directionKey = direction;
262
- const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[directionKey] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
263
- const hitboxes = [
264
- {
265
- x: playerX + hitboxConfig.offsetX,
266
- y: playerY + hitboxConfig.offsetY,
267
- width: hitboxConfig.width,
268
- height: hitboxConfig.height
269
- }
270
- ];
271
- const map = player.getCurrentMap();
272
- map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
273
- next(hits) {
274
- hits.forEach((hit) => {
275
- if (hit instanceof RpgEvent) {
276
- const result = applyPlayerHitToEvent(player, hit);
277
- if (result?.defeated) {
278
- console.log(`Player ${player.id} defeated AI ${hit.id}`);
279
- }
280
- }
281
- });
282
- }
283
- });
284
- }
285
- },
286
- onConnected(player) {
287
- if (options.ui?.actionBar?.enabled && options.ui?.actionBar?.autoOpen) {
288
- openActionBattleActionBar(player, options);
289
- }
290
- }
291
- },
292
- event: {
293
- /**
294
- * Handle player detection when entering AI vision
295
- *
296
- * Called when a player enters an AI event's vision range.
297
- * The AI will start pursuing and attacking the player.
298
- *
299
- * @param event - The AI event
300
- * @param player - The player entering vision
301
- * @param shape - The vision shape
302
- */
303
- onDetectInShape(event, player, shape) {
304
- const ai = event.battleAi;
305
- ai?.onDetectInShape(player, shape);
306
- },
307
- /**
308
- * Handle player leaving AI vision
309
- *
310
- * Called when a player leaves an AI event's vision range.
311
- * The AI will stop pursuing the player.
312
- *
313
- * @param event - The AI event
314
- * @param player - The player leaving vision
315
- * @param shape - The vision shape
316
- */
317
- onDetectOutShape(event, player, shape) {
318
- const ai = event.battleAi;
319
- ai?.onDetectOutShape(player, shape);
320
- }
321
- }
322
- });
323
- };
324
- createActionBattleServer();
325
- export {
326
- ACTION_BATTLE_ACTION_BAR_GUI_ID,
327
- DEFAULT_PLAYER_ATTACK_HITBOXES,
328
- applyPlayerHitToEvent,
329
- createActionBattleServer,
330
- getPlayerWeaponKnockbackForce,
331
- openActionBattleActionBar,
332
- updateActionBattleActionBar
333
- };
104
+ function getActionBattleOptions() {
105
+ return currentActionBattleOptions;
106
+ }
107
+ //#endregion
108
+ export { DEFAULT_ACTION_BATTLE_OPTIONS, getActionBattleOptions, normalizeActionBattleOptions, setActionBattleOptions };