@rpgjs/action-battle 5.0.0-alpha.33 → 5.0.0-alpha.36
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/README.md +112 -24
- package/dist/ai.server.d.ts +1 -1
- package/dist/client/index.js +16 -6
- package/dist/client/index2.js +42 -8
- package/dist/client/index3.js +3 -3
- package/dist/client/index4.js +267 -71
- package/dist/client/index5.js +292 -0
- package/dist/client/index6.js +97 -0
- package/dist/client/index7.js +61 -0
- package/dist/client/index8.js +55 -0
- package/dist/client/index9.js +30 -0
- package/dist/client.d.ts +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/index.d.ts +9 -2
- package/dist/server/index.js +16 -6
- package/dist/server/index2.js +267 -71
- package/dist/server/index3.js +3 -3
- package/dist/server/index4.js +55 -0
- package/dist/server/index5.js +30 -0
- package/dist/server.d.ts +5 -0
- package/dist/targeting.d.ts +19 -0
- package/dist/ui/state.d.ts +18 -0
- package/package.json +7 -7
- package/src/ai.server.ts +5 -5
- package/src/client.ts +49 -8
- package/src/components/action-bar.ce +329 -0
- package/src/components/targeting-overlay.ce +99 -0
- package/src/config.ts +56 -0
- package/src/index.ts +32 -7
- package/src/server.ts +356 -87
- package/src/targeting.ts +45 -0
- package/src/types.ts +78 -0
- package/src/ui/state.ts +68 -0
package/dist/server/index2.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { RpgEvent } from "@rpgjs/server";
|
|
2
2
|
import { defineModule, Control } from "@rpgjs/common";
|
|
3
3
|
import { DEFAULT_KNOCKBACK } from "./index3.js";
|
|
4
|
+
import { normalizeActionBattleOptions } from "./index4.js";
|
|
5
|
+
import { manhattanDistance, parseAoeMask } from "./index5.js";
|
|
6
|
+
const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
4
7
|
const DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
5
8
|
up: { offsetX: -16, offsetY: -48, width: 32, height: 32 },
|
|
6
9
|
down: { offsetX: -16, offsetY: 16, width: 32, height: 32 },
|
|
@@ -56,83 +59,276 @@ function applyPlayerHitToEvent(player, target, hooks) {
|
|
|
56
59
|
}
|
|
57
60
|
return hitResult;
|
|
58
61
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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);
|
|
99
140
|
}
|
|
100
141
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @param player - The player leaving vision
|
|
125
|
-
* @param shape - The vision shape
|
|
126
|
-
*/
|
|
127
|
-
onDetectOutShape(event, player, shape) {
|
|
128
|
-
const ai = event.battleAi;
|
|
129
|
-
ai?.onDetectOutShape(player, shape);
|
|
130
|
-
}
|
|
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
|
+
});
|
|
131
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
|
|
132
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
|
+
const server = createActionBattleServer();
|
|
133
325
|
export {
|
|
326
|
+
ACTION_BATTLE_ACTION_BAR_GUI_ID,
|
|
134
327
|
DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
135
328
|
applyPlayerHitToEvent,
|
|
329
|
+
createActionBattleServer,
|
|
136
330
|
server as default,
|
|
137
|
-
getPlayerWeaponKnockbackForce
|
|
331
|
+
getPlayerWeaponKnockbackForce,
|
|
332
|
+
openActionBattleActionBar,
|
|
333
|
+
updateActionBattleActionBar
|
|
138
334
|
};
|
package/dist/server/index3.js
CHANGED
|
@@ -1121,7 +1121,7 @@ class BattleAi {
|
|
|
1121
1121
|
}
|
|
1122
1122
|
if (this.event.hp <= 0) {
|
|
1123
1123
|
this.debugLog("damage", "Defeated!");
|
|
1124
|
-
this.kill();
|
|
1124
|
+
this.kill(attacker);
|
|
1125
1125
|
return true;
|
|
1126
1126
|
}
|
|
1127
1127
|
return false;
|
|
@@ -1132,9 +1132,9 @@ class BattleAi {
|
|
|
1132
1132
|
* Stops all movements, cleans up resources, calls the onDefeated hook,
|
|
1133
1133
|
* and removes the event from the map.
|
|
1134
1134
|
*/
|
|
1135
|
-
kill() {
|
|
1135
|
+
kill(attacker) {
|
|
1136
1136
|
if (this.onDefeatedCallback) {
|
|
1137
|
-
this.onDefeatedCallback(this.event);
|
|
1137
|
+
this.onDefeatedCallback(this.event, attacker);
|
|
1138
1138
|
}
|
|
1139
1139
|
this.destroy();
|
|
1140
1140
|
this.event.remove();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_ACTION_BATTLE_OPTIONS = {
|
|
2
|
+
ui: {
|
|
3
|
+
actionBar: {
|
|
4
|
+
enabled: false,
|
|
5
|
+
autoOpen: false,
|
|
6
|
+
mode: "both"
|
|
7
|
+
},
|
|
8
|
+
targeting: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
showGrid: true,
|
|
11
|
+
colors: {
|
|
12
|
+
area: 3120887,
|
|
13
|
+
edge: 1796760,
|
|
14
|
+
cursor: 16765286
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
skills: {
|
|
19
|
+
defaultAoeMask: ["#"]
|
|
20
|
+
},
|
|
21
|
+
targeting: {
|
|
22
|
+
affects: "events",
|
|
23
|
+
allowEmptyTarget: true
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function normalizeActionBattleOptions(options = {}) {
|
|
27
|
+
return {
|
|
28
|
+
ui: {
|
|
29
|
+
actionBar: {
|
|
30
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.actionBar,
|
|
31
|
+
...options.ui?.actionBar
|
|
32
|
+
},
|
|
33
|
+
targeting: {
|
|
34
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting,
|
|
35
|
+
...options.ui?.targeting,
|
|
36
|
+
colors: {
|
|
37
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting?.colors,
|
|
38
|
+
...options.ui?.targeting?.colors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
skills: {
|
|
43
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.skills,
|
|
44
|
+
...options.skills
|
|
45
|
+
},
|
|
46
|
+
targeting: {
|
|
47
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.targeting,
|
|
48
|
+
...options.targeting
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
DEFAULT_ACTION_BATTLE_OPTIONS,
|
|
54
|
+
normalizeActionBattleOptions
|
|
55
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const normalizeMaskRows = (mask) => {
|
|
2
|
+
if (!mask) return ["#"];
|
|
3
|
+
if (Array.isArray(mask)) return mask;
|
|
4
|
+
return mask.trim().split("\n").map((row) => row.replace(/\r/g, ""));
|
|
5
|
+
};
|
|
6
|
+
const parseAoeMask = (mask) => {
|
|
7
|
+
const rows = normalizeMaskRows(mask);
|
|
8
|
+
const height = rows.length;
|
|
9
|
+
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
10
|
+
const centerX = Math.floor(width / 2);
|
|
11
|
+
const centerY = Math.floor(height / 2);
|
|
12
|
+
const cells = [];
|
|
13
|
+
rows.forEach((row, y) => {
|
|
14
|
+
for (let x = 0; x < row.length; x++) {
|
|
15
|
+
const char = row[x];
|
|
16
|
+
if (char && char !== "." && char !== " ") {
|
|
17
|
+
cells.push({ dx: x - centerX, dy: y - centerY });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (cells.length === 0) {
|
|
22
|
+
cells.push({ dx: 0, dy: 0 });
|
|
23
|
+
}
|
|
24
|
+
return { width, height, centerX, centerY, cells };
|
|
25
|
+
};
|
|
26
|
+
const manhattanDistance = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
27
|
+
export {
|
|
28
|
+
manhattanDistance,
|
|
29
|
+
parseAoeMask
|
|
30
|
+
};
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RpgEvent, RpgPlayer } from '@rpgjs/server';
|
|
2
2
|
import { HitResult, ApplyHitHooks } from './ai.server';
|
|
3
|
+
import { ActionBattleOptions } from './types';
|
|
4
|
+
export declare const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
3
5
|
/**
|
|
4
6
|
* Default player attack hitboxes offsets for each direction
|
|
5
7
|
*
|
|
@@ -89,5 +91,8 @@ export declare function getPlayerWeaponKnockbackForce(player: RpgPlayer): number
|
|
|
89
91
|
* ```
|
|
90
92
|
*/
|
|
91
93
|
export declare function applyPlayerHitToEvent(player: RpgPlayer, target: RpgEvent, hooks?: ApplyHitHooks): HitResult | undefined;
|
|
94
|
+
export declare const openActionBattleActionBar: (player: RpgPlayer, rawOptions?: ActionBattleOptions) => void;
|
|
95
|
+
export declare const updateActionBattleActionBar: (player: RpgPlayer, rawOptions?: ActionBattleOptions) => void;
|
|
96
|
+
export declare const createActionBattleServer: (rawOptions?: ActionBattleOptions) => any;
|
|
92
97
|
declare const _default: any;
|
|
93
98
|
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ActionBattleAoeMask } from './types';
|
|
2
|
+
export interface ParsedAoeMask {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
centerX: number;
|
|
6
|
+
centerY: number;
|
|
7
|
+
cells: Array<{
|
|
8
|
+
dx: number;
|
|
9
|
+
dy: number;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare const parseAoeMask: (mask: ActionBattleAoeMask | undefined) => ParsedAoeMask;
|
|
13
|
+
export declare const manhattanDistance: (a: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}, b: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}) => number;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ActionBattleActionBarSkill, ActionBattleOptions } from '../types';
|
|
2
|
+
export interface ActionBattleTargetingState {
|
|
3
|
+
active: boolean;
|
|
4
|
+
skill: ActionBattleActionBarSkill | null;
|
|
5
|
+
range: number;
|
|
6
|
+
offset: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
aoeMask: string[] | string;
|
|
11
|
+
}
|
|
12
|
+
export declare const actionBattleUiOptions: any;
|
|
13
|
+
export declare const actionBattleSkillOptions: any;
|
|
14
|
+
export declare const actionBattleTargetingState: any;
|
|
15
|
+
export declare const setActionBattleOptions: (options?: ActionBattleOptions) => void;
|
|
16
|
+
export declare const startTargeting: (skill: ActionBattleActionBarSkill) => void;
|
|
17
|
+
export declare const stopTargeting: () => void;
|
|
18
|
+
export declare const moveTargetingOffset: (dx: number, dy: number) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/action-battle",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.36",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@canvasengine/presets": "*",
|
|
26
|
-
"@rpgjs/client": "5.0.0-alpha.
|
|
27
|
-
"@rpgjs/common": "5.0.0-alpha.
|
|
28
|
-
"@rpgjs/server": "5.0.0-alpha.
|
|
29
|
-
"@rpgjs/vite": "5.0.0-alpha.
|
|
26
|
+
"@rpgjs/client": "5.0.0-alpha.36",
|
|
27
|
+
"@rpgjs/common": "5.0.0-alpha.36",
|
|
28
|
+
"@rpgjs/server": "5.0.0-alpha.36",
|
|
29
|
+
"@rpgjs/vite": "5.0.0-alpha.36",
|
|
30
30
|
"canvasengine": "*"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@canvasengine/compiler": "^2.0.0-beta.
|
|
37
|
-
"vite": "^7.3.
|
|
36
|
+
"@canvasengine/compiler": "^2.0.0-beta.52",
|
|
37
|
+
"vite": "^7.3.1",
|
|
38
38
|
"vite-plugin-dts": "^4.5.4"
|
|
39
39
|
},
|
|
40
40
|
"type": "module",
|
package/src/ai.server.ts
CHANGED
|
@@ -280,7 +280,7 @@ export class BattleAi {
|
|
|
280
280
|
private isMovingToTarget: boolean = false;
|
|
281
281
|
|
|
282
282
|
// Callback when AI is defeated
|
|
283
|
-
private onDefeatedCallback?: (event: RpgEvent) => void;
|
|
283
|
+
private onDefeatedCallback?: (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
284
284
|
|
|
285
285
|
// Direction hysteresis to prevent animation flickering
|
|
286
286
|
private lastFacingDirection: string | null = null;
|
|
@@ -349,7 +349,7 @@ export class BattleAi {
|
|
|
349
349
|
retreatThreshold?: number;
|
|
350
350
|
};
|
|
351
351
|
/** Callback called when the AI is defeated */
|
|
352
|
-
onDefeated?: (event: RpgEvent) => void;
|
|
352
|
+
onDefeated?: (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
353
353
|
} = {}
|
|
354
354
|
) {
|
|
355
355
|
event.battleAi = this;
|
|
@@ -1454,7 +1454,7 @@ export class BattleAi {
|
|
|
1454
1454
|
// Check death
|
|
1455
1455
|
if (this.event.hp <= 0) {
|
|
1456
1456
|
this.debugLog('damage', 'Defeated!');
|
|
1457
|
-
this.kill();
|
|
1457
|
+
this.kill(attacker);
|
|
1458
1458
|
return true;
|
|
1459
1459
|
}
|
|
1460
1460
|
|
|
@@ -1467,10 +1467,10 @@ export class BattleAi {
|
|
|
1467
1467
|
* Stops all movements, cleans up resources, calls the onDefeated hook,
|
|
1468
1468
|
* and removes the event from the map.
|
|
1469
1469
|
*/
|
|
1470
|
-
private kill() {
|
|
1470
|
+
private kill(attacker?: RpgPlayer) {
|
|
1471
1471
|
// Call onDefeated hook before cleanup
|
|
1472
1472
|
if (this.onDefeatedCallback) {
|
|
1473
|
-
this.onDefeatedCallback(this.event);
|
|
1473
|
+
this.onDefeatedCallback(this.event, attacker);
|
|
1474
1474
|
}
|
|
1475
1475
|
|
|
1476
1476
|
this.destroy();
|
package/src/client.ts
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
|
-
import {PrebuiltComponentAnimations, RpgClient } from "@rpgjs/client";
|
|
1
|
+
import { inject, PrebuiltComponentAnimations, RpgClient, RpgClientEngine, RpgGui } from "@rpgjs/client";
|
|
2
2
|
import { defineModule } from "@rpgjs/common";
|
|
3
|
+
import ActionBarComponent from "./components/action-bar.ce";
|
|
4
|
+
import TargetingOverlayComponent from "./components/targeting-overlay.ce";
|
|
5
|
+
import { setActionBattleOptions } from "./ui/state";
|
|
6
|
+
import { ActionBattleOptions } from "./types";
|
|
7
|
+
import { normalizeActionBattleOptions } from "./config";
|
|
3
8
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
export const createActionBattleClient = (
|
|
10
|
+
options: ActionBattleOptions = {}
|
|
11
|
+
) => {
|
|
12
|
+
const normalized = normalizeActionBattleOptions(options);
|
|
13
|
+
setActionBattleOptions(normalized);
|
|
14
|
+
const actionBarEnabled = normalized.ui?.actionBar?.enabled;
|
|
15
|
+
const targetingEnabled = normalized.ui?.targeting?.enabled;
|
|
16
|
+
const hitComponent = PrebuiltComponentAnimations?.Hit;
|
|
17
|
+
return defineModule<RpgClient>({
|
|
18
|
+
componentAnimations: hitComponent
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
id: "hit",
|
|
22
|
+
component: hitComponent,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
: [],
|
|
26
|
+
gui: actionBarEnabled
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
id: "action-battle-action-bar",
|
|
30
|
+
component: ActionBarComponent,
|
|
31
|
+
dependencies: () => {
|
|
32
|
+
const engine = inject(RpgClientEngine)
|
|
33
|
+
return [engine.scene.currentPlayer]
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
: [],
|
|
38
|
+
sprite: {
|
|
39
|
+
componentsInFront: targetingEnabled ? [TargetingOverlayComponent] : [],
|
|
40
|
+
},
|
|
41
|
+
sceneMap: {
|
|
42
|
+
onAfterLoading() {
|
|
43
|
+
if (actionBarEnabled && normalized.ui?.actionBar?.autoOpen) {
|
|
44
|
+
const gui = inject(RpgGui)
|
|
45
|
+
gui.display('action-battle-action-bar')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
9
48
|
}
|
|
10
|
-
|
|
11
|
-
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default createActionBattleClient();
|