@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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +19 -0
- package/README.md +392 -22
- package/dist/{ai.server.d.ts → client/ai.server.d.ts} +90 -28
- package/dist/client/animations.d.ts +16 -0
- package/dist/{client.d.ts → client/client.d.ts} +3 -2
- package/dist/{config.d.ts → client/config.d.ts} +2 -0
- package/dist/client/core/attack-profile.d.ts +9 -0
- package/dist/client/core/attack-runtime.d.ts +20 -0
- package/dist/client/core/context.d.ts +5 -0
- package/dist/client/core/defaults.d.ts +81 -0
- package/dist/client/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/client/core/equipment.d.ts +2 -0
- package/dist/client/core/hit-reaction.d.ts +5 -0
- package/dist/client/core/hit.d.ts +2 -0
- package/dist/client/enemies/factory.d.ts +7 -0
- package/dist/client/index.d.ts +21 -0
- package/dist/client/index.js +24 -31
- package/dist/client/index10.js +61 -0
- package/dist/client/index11.js +55 -0
- package/dist/client/index12.js +106 -0
- package/dist/client/index13.js +143 -0
- package/dist/client/index14.js +25 -0
- package/dist/client/index15.js +72 -0
- package/dist/client/index16.js +1343 -0
- package/dist/client/index17.js +13 -0
- package/dist/client/index18.js +60 -0
- package/dist/client/index19.js +10 -0
- package/dist/client/index2.js +30 -45
- package/dist/client/index20.js +504 -0
- package/dist/client/index3.js +45 -1288
- package/dist/client/index4.js +105 -330
- package/dist/client/index5.js +84 -291
- package/dist/client/index6.js +309 -95
- package/dist/client/index7.js +35 -59
- package/dist/client/index8.js +101 -54
- package/dist/client/index9.js +79 -30
- package/dist/{server.d.ts → client/server.d.ts} +12 -4
- package/dist/client/ui/state.d.ts +35 -0
- package/dist/server/ai.server.d.ts +569 -0
- package/dist/server/animations.d.ts +16 -0
- package/dist/server/config.d.ts +5 -0
- package/dist/server/core/attack-profile.d.ts +9 -0
- package/dist/server/core/attack-runtime.d.ts +20 -0
- package/dist/server/core/context.d.ts +5 -0
- package/dist/server/core/defaults.d.ts +81 -0
- package/dist/server/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/server/core/equipment.d.ts +2 -0
- package/dist/server/core/hit-reaction.d.ts +5 -0
- package/dist/server/core/hit.d.ts +2 -0
- package/dist/server/enemies/factory.d.ts +7 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.js +23 -31
- package/dist/server/index10.js +1342 -0
- package/dist/server/index11.js +37 -0
- package/dist/server/index12.js +60 -0
- package/dist/server/index13.js +13 -0
- package/dist/server/index14.js +503 -0
- package/dist/server/index15.js +10 -0
- package/dist/server/index2.js +59 -332
- package/dist/server/index3.js +29 -1286
- package/dist/server/index4.js +45 -53
- package/dist/server/index5.js +107 -29
- package/dist/server/index6.js +143 -0
- package/dist/server/index7.js +25 -0
- package/dist/server/index8.js +72 -0
- package/dist/server/index9.js +55 -0
- package/dist/server/server.d.ts +106 -0
- package/dist/server/targeting.d.ts +19 -0
- package/package.json +12 -12
- package/src/ai.server.spec.ts +120 -0
- package/src/ai.server.ts +515 -91
- package/src/animations.ts +149 -0
- package/src/canvas-engine-shim.ts +4 -0
- package/src/client.ts +130 -2
- package/src/components/action-bar.ce +5 -3
- package/src/components/attack-preview.ce +90 -0
- package/src/config.ts +61 -0
- package/src/core/attack-profile.spec.ts +118 -0
- package/src/core/attack-profile.ts +100 -0
- package/src/core/attack-runtime.spec.ts +103 -0
- package/src/core/attack-runtime.ts +83 -0
- package/src/core/context.ts +35 -0
- package/src/core/contracts.ts +126 -0
- package/src/core/defaults.ts +162 -0
- package/src/core/enemy-attack-profiles.spec.ts +35 -0
- package/src/core/enemy-attack-profiles.ts +103 -0
- package/src/core/equipment.spec.ts +37 -0
- package/src/core/equipment.ts +17 -0
- package/src/core/hit-reaction.spec.ts +43 -0
- package/src/core/hit-reaction.ts +70 -0
- package/src/core/hit.spec.ts +111 -0
- package/src/core/hit.ts +92 -0
- package/src/enemies/factory.ts +25 -0
- package/src/index.ts +94 -1
- package/src/server.ts +427 -93
- package/src/targeting.spec.ts +24 -0
- package/src/types/canvas-engine.d.ts +4 -0
- package/src/types.ts +148 -0
- package/src/ui/state.ts +57 -0
- package/dist/index.d.ts +0 -11
- package/dist/ui/state.d.ts +0 -18
- /package/dist/{targeting.d.ts → client/targeting.d.ts} +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/targeting.ts
|
|
2
|
+
var normalizeMaskRows = (mask) => {
|
|
3
|
+
if (!mask) return ["#"];
|
|
4
|
+
if (Array.isArray(mask)) return mask;
|
|
5
|
+
return mask.trim().split("\n").map((row) => row.replace(/\r/g, ""));
|
|
6
|
+
};
|
|
7
|
+
var parseAoeMask = (mask) => {
|
|
8
|
+
const rows = normalizeMaskRows(mask);
|
|
9
|
+
const height = rows.length;
|
|
10
|
+
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
11
|
+
const centerX = Math.floor(width / 2);
|
|
12
|
+
const centerY = Math.floor(height / 2);
|
|
13
|
+
const cells = [];
|
|
14
|
+
rows.forEach((row, y) => {
|
|
15
|
+
for (let x = 0; x < row.length; x++) {
|
|
16
|
+
const char = row[x];
|
|
17
|
+
if (char && char !== "." && char !== " ") cells.push({
|
|
18
|
+
dx: x - centerX,
|
|
19
|
+
dy: y - centerY
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (cells.length === 0) cells.push({
|
|
24
|
+
dx: 0,
|
|
25
|
+
dy: 0
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
centerX,
|
|
31
|
+
centerY,
|
|
32
|
+
cells
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
var manhattanDistance = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
36
|
+
//#endregion
|
|
37
|
+
export { manhattanDistance, parseAoeMask };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isActionBattleEntityInvincible, setActionBattleInvincibility } from "./index3.js";
|
|
2
|
+
//#region src/core/hit.ts
|
|
3
|
+
var applyActionBattleHit = (system, context) => {
|
|
4
|
+
let hitContext = { ...context };
|
|
5
|
+
const before = system.hooks?.beforeHit?.(hitContext);
|
|
6
|
+
if (before === false) return {
|
|
7
|
+
damage: 0,
|
|
8
|
+
knockbackForce: 0,
|
|
9
|
+
knockbackDuration: 0,
|
|
10
|
+
defeated: false,
|
|
11
|
+
attacker: hitContext.attacker,
|
|
12
|
+
target: hitContext.target,
|
|
13
|
+
cancelled: true,
|
|
14
|
+
metadata: hitContext.metadata
|
|
15
|
+
};
|
|
16
|
+
if (before) hitContext = before;
|
|
17
|
+
if (isActionBattleEntityInvincible(hitContext.target)) return {
|
|
18
|
+
damage: 0,
|
|
19
|
+
knockbackForce: 0,
|
|
20
|
+
knockbackDuration: 0,
|
|
21
|
+
defeated: false,
|
|
22
|
+
attacker: hitContext.attacker,
|
|
23
|
+
target: hitContext.target,
|
|
24
|
+
cancelled: true,
|
|
25
|
+
metadata: hitContext.metadata,
|
|
26
|
+
reaction: hitContext.reaction
|
|
27
|
+
};
|
|
28
|
+
const damage = hitContext.damage ?? system.resolveDamage({
|
|
29
|
+
attacker: hitContext.attacker,
|
|
30
|
+
target: hitContext.target,
|
|
31
|
+
skill: hitContext.skill,
|
|
32
|
+
pattern: hitContext.pattern
|
|
33
|
+
});
|
|
34
|
+
hitContext.damage = damage;
|
|
35
|
+
const afterDamage = system.hooks?.afterDamage?.(hitContext);
|
|
36
|
+
if (afterDamage) hitContext = afterDamage;
|
|
37
|
+
const knockback = hitContext.knockback ?? system.resolveKnockback({
|
|
38
|
+
attacker: hitContext.attacker,
|
|
39
|
+
target: hitContext.target,
|
|
40
|
+
damage
|
|
41
|
+
});
|
|
42
|
+
hitContext.knockback = knockback;
|
|
43
|
+
if (!damage.defeated && knockback.force > 0 && knockback.direction) hitContext.target.knockback?.(knockback.direction, knockback.force, knockback.duration);
|
|
44
|
+
if (!damage.defeated && hitContext.reaction?.invincibilityMs) setActionBattleInvincibility(hitContext.target, hitContext.reaction.invincibilityMs);
|
|
45
|
+
const result = {
|
|
46
|
+
damage: damage.damage,
|
|
47
|
+
knockbackForce: knockback.force,
|
|
48
|
+
knockbackDuration: knockback.duration,
|
|
49
|
+
defeated: damage.defeated,
|
|
50
|
+
attacker: hitContext.attacker,
|
|
51
|
+
target: hitContext.target,
|
|
52
|
+
rawDamage: damage.raw,
|
|
53
|
+
reaction: hitContext.reaction,
|
|
54
|
+
metadata: hitContext.metadata
|
|
55
|
+
};
|
|
56
|
+
system.hooks?.afterHit?.(result);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { applyActionBattleHit };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/core/equipment.ts
|
|
2
|
+
var resolveItemId = (item) => item?.id?.() ?? item?.id;
|
|
3
|
+
function resolveActionBattleWeaponAttackProfile(entity) {
|
|
4
|
+
const equipments = entity?.equipments?.() || [];
|
|
5
|
+
for (const item of equipments) {
|
|
6
|
+
const itemId = resolveItemId(item);
|
|
7
|
+
const itemData = entity?.databaseById?.(itemId);
|
|
8
|
+
if (itemData?._type === "weapon" && itemData.attackProfile) return itemData.attackProfile;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { resolveActionBattleWeaponAttackProfile };
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { playActionBattleAnimation } from "./index2.js";
|
|
2
|
+
import "./index3.js";
|
|
3
|
+
import { normalizeActionBattleAttackProfile } from "./index4.js";
|
|
4
|
+
import { normalizeActionBattleOptions, setActionBattleOptions } from "./index5.js";
|
|
5
|
+
import { DEFAULT_ZELDA_PLAYER_HITBOXES } from "./index6.js";
|
|
6
|
+
import { getActionBattleSystems, setActionBattleSystems } from "./index7.js";
|
|
7
|
+
import "./index8.js";
|
|
8
|
+
import { ActionBattleHitTracker, createActionBattleAttackId, getNormalizedActionBattleAttackProfile, resolveActionBattleHitboxSpeed, scheduleActionBattleStartup } from "./index9.js";
|
|
9
|
+
import { DEFAULT_KNOCKBACK } from "./index10.js";
|
|
10
|
+
import { manhattanDistance, parseAoeMask } from "./index11.js";
|
|
11
|
+
import { applyActionBattleHit } from "./index12.js";
|
|
12
|
+
import { resolveActionBattleWeaponAttackProfile } from "./index13.js";
|
|
13
|
+
import { RpgEvent } from "@rpgjs/server";
|
|
14
|
+
import { Control, defineModule } from "@rpgjs/common";
|
|
15
|
+
//#region src/server.ts
|
|
16
|
+
var ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
17
|
+
var DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
|
|
18
|
+
/**
|
|
19
|
+
* Default player attack hitboxes offsets for each direction
|
|
20
|
+
*
|
|
21
|
+
* These hitboxes define the attack areas relative to the player's position
|
|
22
|
+
* for each cardinal direction. They are converted to absolute coordinates
|
|
23
|
+
* when creating the moving hitbox.
|
|
24
|
+
*/
|
|
25
|
+
var DEFAULT_PLAYER_ATTACK_HITBOXES = { ...DEFAULT_ZELDA_PLAYER_HITBOXES };
|
|
26
|
+
var beginPlayerAttackLock = (player, map, durationMs, locks) => {
|
|
27
|
+
if (durationMs <= 0) return true;
|
|
28
|
+
const runtimePlayer = player;
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (typeof runtimePlayer.__actionBattleAttackLockedUntil === "number" && runtimePlayer.__actionBattleAttackLockedUntil > now) return false;
|
|
31
|
+
const lockId = (runtimePlayer.__actionBattleAttackLockId ?? 0) + 1;
|
|
32
|
+
runtimePlayer.__actionBattleAttackLockId = lockId;
|
|
33
|
+
runtimePlayer.__actionBattleAttackLockedUntil = now + durationMs;
|
|
34
|
+
const previousCanMove = player.canMove;
|
|
35
|
+
const previousDirectionFixed = player.directionFixed;
|
|
36
|
+
const previousAnimationFixed = player.animationFixed;
|
|
37
|
+
if (locks.movement) {
|
|
38
|
+
player.pendingInputs = [];
|
|
39
|
+
player.lastProcessedInputTs = 0;
|
|
40
|
+
map?.stopMovement?.(player);
|
|
41
|
+
player.canMove = false;
|
|
42
|
+
}
|
|
43
|
+
if (locks.direction) player.directionFixed = true;
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
|
|
46
|
+
runtimePlayer.__actionBattleAttackLockedUntil = 0;
|
|
47
|
+
player.canMove = previousCanMove;
|
|
48
|
+
player.directionFixed = previousDirectionFixed;
|
|
49
|
+
player.animationFixed = previousAnimationFixed;
|
|
50
|
+
}, durationMs);
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
var isBattleEvent = (event) => !!event.battleAi;
|
|
54
|
+
var rectsOverlap = (a, b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
55
|
+
var eventRect = (event) => {
|
|
56
|
+
const hitbox = typeof event.hitbox === "function" ? event.hitbox() : event.hitbox;
|
|
57
|
+
return {
|
|
58
|
+
x: event.x(),
|
|
59
|
+
y: event.y(),
|
|
60
|
+
width: hitbox?.w ?? 32,
|
|
61
|
+
height: hitbox?.h ?? 32
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
var getVisibleActionEvents = (player, map, hitboxes) => {
|
|
65
|
+
if (!map) return [];
|
|
66
|
+
const eventsById = /* @__PURE__ */ new Map();
|
|
67
|
+
const addEvent = (event) => {
|
|
68
|
+
if (!event) return;
|
|
69
|
+
if (!(typeof map.isEventVisibleForPlayer === "function" ? map.isEventVisibleForPlayer(event, player) : true)) return;
|
|
70
|
+
eventsById.set(event.id, event);
|
|
71
|
+
};
|
|
72
|
+
const collisions = map.getCollisions?.(player.id);
|
|
73
|
+
if (Array.isArray(collisions)) collisions.forEach((id) => addEvent(map.getEvent(id)));
|
|
74
|
+
const direction = typeof player.getDirection === "function" ? player.getDirection() : void 0;
|
|
75
|
+
const interactionCollisions = map.getInteractionCollisions?.(player.id, direction);
|
|
76
|
+
if (Array.isArray(interactionCollisions)) interactionCollisions.forEach((id) => addEvent(map.getEvent(id)));
|
|
77
|
+
for (const event of map.getEvents()) {
|
|
78
|
+
const rect = eventRect(event);
|
|
79
|
+
if (hitboxes.some((hitbox) => rectsOverlap(hitbox, rect))) addEvent(event);
|
|
80
|
+
}
|
|
81
|
+
return Array.from(eventsById.values());
|
|
82
|
+
};
|
|
83
|
+
var isActionReservedForNormalEvent = (player, map, hitboxes) => {
|
|
84
|
+
const events = getVisibleActionEvents(player, map, hitboxes);
|
|
85
|
+
return events.length > 0 && !events.some(isBattleEvent);
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Get knockback force from player's equipped weapon
|
|
89
|
+
*
|
|
90
|
+
* Retrieves the knockbackForce property from the player's equipped weapon.
|
|
91
|
+
* Falls back to DEFAULT_KNOCKBACK.force if no weapon or property is set.
|
|
92
|
+
*
|
|
93
|
+
* @param player - The player to get weapon knockback from
|
|
94
|
+
* @returns Knockback force value
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* // Player with weapon having knockbackForce: 80
|
|
99
|
+
* const force = getPlayerWeaponKnockbackForce(player); // 80
|
|
100
|
+
*
|
|
101
|
+
* // No weapon equipped
|
|
102
|
+
* const force = getPlayerWeaponKnockbackForce(player); // 50 (default)
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
function getPlayerWeaponKnockbackForce(player) {
|
|
106
|
+
try {
|
|
107
|
+
const equipments = player.equipments?.() || [];
|
|
108
|
+
for (const item of equipments) {
|
|
109
|
+
const itemData = player.databaseById?.(item.id());
|
|
110
|
+
if (itemData?._type === "weapon" && itemData.knockbackForce !== void 0) return itemData.knockbackForce;
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
return DEFAULT_KNOCKBACK.force;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Apply hit from player to target (event with AI)
|
|
117
|
+
*
|
|
118
|
+
* Handles damage calculation, knockback based on weapon, and visual effects.
|
|
119
|
+
* Can be customized using hooks.
|
|
120
|
+
*
|
|
121
|
+
* @param player - The attacking player
|
|
122
|
+
* @param target - The event being hit
|
|
123
|
+
* @param hooks - Optional hooks for customizing hit behavior
|
|
124
|
+
* @returns Hit result if AI exists, undefined otherwise
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* // Basic hit
|
|
129
|
+
* const result = applyPlayerHitToEvent(player, event);
|
|
130
|
+
*
|
|
131
|
+
* // With custom hooks
|
|
132
|
+
* const result = applyPlayerHitToEvent(player, event, {
|
|
133
|
+
* onBeforeHit(result) {
|
|
134
|
+
* result.knockbackForce *= 2; // Double knockback
|
|
135
|
+
* return result;
|
|
136
|
+
* },
|
|
137
|
+
* onAfterHit(result) {
|
|
138
|
+
* if (result.defeated) {
|
|
139
|
+
* player.gold += 10;
|
|
140
|
+
* }
|
|
141
|
+
* }
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
function applyPlayerHitToEvent(player, target, hooks, metadata) {
|
|
146
|
+
const ai = target.battleAi;
|
|
147
|
+
if (!ai) return void 0;
|
|
148
|
+
const systems = getActionBattleSystems();
|
|
149
|
+
const result = applyActionBattleHit({
|
|
150
|
+
...systems.combat,
|
|
151
|
+
hooks: hooks ? {
|
|
152
|
+
...systems.combat.hooks,
|
|
153
|
+
beforeHit(context) {
|
|
154
|
+
const before = systems.combat.hooks?.beforeHit?.(context);
|
|
155
|
+
if (before === false) return false;
|
|
156
|
+
const nextContext = before || context;
|
|
157
|
+
const legacyResult = toLegacyHitResult(nextContext);
|
|
158
|
+
const modified = hooks.onBeforeHit?.(legacyResult);
|
|
159
|
+
if (!modified) return nextContext;
|
|
160
|
+
return {
|
|
161
|
+
...nextContext,
|
|
162
|
+
damage: {
|
|
163
|
+
damage: modified.damage,
|
|
164
|
+
defeated: modified.defeated,
|
|
165
|
+
raw: nextContext.damage?.raw
|
|
166
|
+
},
|
|
167
|
+
knockback: {
|
|
168
|
+
force: modified.knockbackForce,
|
|
169
|
+
duration: modified.knockbackDuration,
|
|
170
|
+
direction: nextContext.knockback?.direction
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
afterHit(result) {
|
|
175
|
+
systems.combat.hooks?.afterHit?.(result);
|
|
176
|
+
hooks.onAfterHit?.(result);
|
|
177
|
+
}
|
|
178
|
+
} : systems.combat.hooks
|
|
179
|
+
}, {
|
|
180
|
+
attacker: player,
|
|
181
|
+
target,
|
|
182
|
+
metadata,
|
|
183
|
+
reaction: metadata?.reaction
|
|
184
|
+
});
|
|
185
|
+
if (!result.cancelled) ai.handleDamage(player, {
|
|
186
|
+
damage: result.damage,
|
|
187
|
+
defeated: result.defeated,
|
|
188
|
+
raw: result.rawDamage,
|
|
189
|
+
reaction: result.reaction
|
|
190
|
+
});
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
var toLegacyHitResult = (context) => ({
|
|
194
|
+
damage: context.damage?.damage ?? 0,
|
|
195
|
+
knockbackForce: context.knockback?.force ?? getPlayerWeaponKnockbackForce(context.attacker),
|
|
196
|
+
knockbackDuration: context.knockback?.duration ?? DEFAULT_KNOCKBACK.duration,
|
|
197
|
+
defeated: context.damage?.defeated ?? false,
|
|
198
|
+
attacker: context.attacker,
|
|
199
|
+
target: context.target
|
|
200
|
+
});
|
|
201
|
+
var resolvePlayerAttackHitboxes = (player, directionKey, options, profile) => {
|
|
202
|
+
const configuredHitboxes = {
|
|
203
|
+
...DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
204
|
+
...options.attack?.hitboxes,
|
|
205
|
+
...profile.hitboxes
|
|
206
|
+
};
|
|
207
|
+
const hitboxConfig = configuredHitboxes[directionKey] || configuredHitboxes.default;
|
|
208
|
+
const defaultHitboxes = [{
|
|
209
|
+
x: player.x() + hitboxConfig.offsetX,
|
|
210
|
+
y: player.y() + hitboxConfig.offsetY,
|
|
211
|
+
width: hitboxConfig.width,
|
|
212
|
+
height: hitboxConfig.height
|
|
213
|
+
}];
|
|
214
|
+
return options.attack?.resolveHitboxes?.({
|
|
215
|
+
player,
|
|
216
|
+
direction: directionKey,
|
|
217
|
+
defaultHitboxes
|
|
218
|
+
}) ?? defaultHitboxes;
|
|
219
|
+
};
|
|
220
|
+
var mergeAttackProfileOverrides = (base, override) => ({
|
|
221
|
+
...base,
|
|
222
|
+
...override,
|
|
223
|
+
reaction: {
|
|
224
|
+
...base.reaction,
|
|
225
|
+
...override.reaction
|
|
226
|
+
},
|
|
227
|
+
hitboxes: {
|
|
228
|
+
...base.hitboxes,
|
|
229
|
+
...override.hitboxes
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
var resolvePlayerAttackProfile = (player, options) => {
|
|
233
|
+
const baseProfile = getNormalizedActionBattleAttackProfile(options);
|
|
234
|
+
const weaponProfile = resolveActionBattleWeaponAttackProfile(player);
|
|
235
|
+
if (!weaponProfile) return baseProfile;
|
|
236
|
+
return normalizeActionBattleAttackProfile(mergeAttackProfileOverrides(baseProfile, weaponProfile), {
|
|
237
|
+
lockMovement: options.attack?.lockMovement,
|
|
238
|
+
lockDurationMs: options.attack?.lockDurationMs,
|
|
239
|
+
hitboxes: options.attack?.hitboxes
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
var resolveSignal = (value) => typeof value === "function" ? value() : value;
|
|
243
|
+
var resolveItemData = (player, itemId) => {
|
|
244
|
+
try {
|
|
245
|
+
return player.databaseById?.(itemId);
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var resolveSkillData = (player, skillId) => {
|
|
251
|
+
try {
|
|
252
|
+
return player.databaseById?.(skillId);
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var resolveSkillTargeting = (player, skillId, options) => {
|
|
258
|
+
const skillsOptions = options.skills;
|
|
259
|
+
const skillData = resolveSkillData(player, skillId);
|
|
260
|
+
if (skillsOptions?.getTargeting) return skillsOptions.getTargeting(skillData);
|
|
261
|
+
const range = skillData?.range ?? skillData?.targeting?.range ?? skillData?.targeting?.distance;
|
|
262
|
+
const aoeMask = skillData?.aoeMask ?? skillData?.targeting?.aoeMask ?? skillData?.targeting?.mask;
|
|
263
|
+
if (range === void 0 && aoeMask === void 0) return null;
|
|
264
|
+
return {
|
|
265
|
+
range: range ?? 0,
|
|
266
|
+
aoeMask
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
var normalizeMaskRows = (mask) => {
|
|
270
|
+
if (!mask) return [];
|
|
271
|
+
if (Array.isArray(mask)) return mask;
|
|
272
|
+
return mask.trim().split("\n").map((row) => row.replace(/\r/g, ""));
|
|
273
|
+
};
|
|
274
|
+
var buildActionBarData = (player, options) => {
|
|
275
|
+
return {
|
|
276
|
+
items: (player.items?.() || []).map((item) => {
|
|
277
|
+
const id = item.id?.() ?? item.id;
|
|
278
|
+
const data = resolveItemData(player, id);
|
|
279
|
+
const name = resolveSignal(data?.name) ?? resolveSignal(item.name) ?? id;
|
|
280
|
+
const description = resolveSignal(data?.description) ?? resolveSignal(item.description) ?? "";
|
|
281
|
+
const icon = resolveSignal(data?.icon) ?? resolveSignal(item.icon);
|
|
282
|
+
const quantity = resolveSignal(item.quantity) ?? 1;
|
|
283
|
+
const consumable = resolveSignal(data?.consumable);
|
|
284
|
+
const itemType = resolveSignal(data?._type);
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
name,
|
|
288
|
+
description,
|
|
289
|
+
icon,
|
|
290
|
+
quantity,
|
|
291
|
+
usable: quantity > 0 && consumable !== false && (itemType ? itemType === "item" : true)
|
|
292
|
+
};
|
|
293
|
+
}),
|
|
294
|
+
skills: (player.skills?.() || []).map((skill) => {
|
|
295
|
+
const id = skill.id?.() ?? skill.id;
|
|
296
|
+
const data = resolveSkillData(player, id) || skill;
|
|
297
|
+
const name = resolveSignal(data?.name) ?? resolveSignal(skill.name) ?? id;
|
|
298
|
+
const description = resolveSignal(data?.description) ?? resolveSignal(skill.description) ?? "";
|
|
299
|
+
const icon = resolveSignal(data?.icon) ?? resolveSignal(skill.icon);
|
|
300
|
+
const spCost = resolveSignal(data?.spCost) ?? resolveSignal(skill.spCost) ?? 0;
|
|
301
|
+
const usable = spCost <= player.sp;
|
|
302
|
+
const targeting = resolveSkillTargeting(player, id, options);
|
|
303
|
+
const skillEntry = {
|
|
304
|
+
id,
|
|
305
|
+
name,
|
|
306
|
+
description,
|
|
307
|
+
icon,
|
|
308
|
+
spCost,
|
|
309
|
+
usable,
|
|
310
|
+
range: targeting?.range ?? 0
|
|
311
|
+
};
|
|
312
|
+
if (targeting) {
|
|
313
|
+
const mask = targeting.aoeMask ?? options.skills?.defaultAoeMask;
|
|
314
|
+
if (mask) skillEntry.aoeMask = normalizeMaskRows(mask);
|
|
315
|
+
}
|
|
316
|
+
return skillEntry;
|
|
317
|
+
})
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
var ensureActionBarGui = (player, options) => {
|
|
321
|
+
const gui = player.getGui?.("action-battle-action-bar") || player.gui("action-battle-action-bar");
|
|
322
|
+
if (!gui.__actionBattleReady) {
|
|
323
|
+
gui.__actionBattleReady = true;
|
|
324
|
+
gui.on("useItem", ({ id }) => {
|
|
325
|
+
try {
|
|
326
|
+
player.useItem(id);
|
|
327
|
+
} catch {}
|
|
328
|
+
gui.update(buildActionBarData(player, options));
|
|
329
|
+
});
|
|
330
|
+
gui.on("useSkill", ({ id, target }) => {
|
|
331
|
+
handleActionBattleSkillUse(player, id, target, options);
|
|
332
|
+
gui.update(buildActionBarData(player, options));
|
|
333
|
+
});
|
|
334
|
+
gui.on("refresh", () => {
|
|
335
|
+
gui.update(buildActionBarData(player, options));
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return gui;
|
|
339
|
+
};
|
|
340
|
+
var openActionBattleActionBar = (player, rawOptions = {}) => {
|
|
341
|
+
const options = normalizeActionBattleOptions(rawOptions);
|
|
342
|
+
ensureActionBarGui(player, options).open(buildActionBarData(player, options));
|
|
343
|
+
};
|
|
344
|
+
var updateActionBattleActionBar = (player, rawOptions = {}) => {
|
|
345
|
+
const options = normalizeActionBattleOptions(rawOptions);
|
|
346
|
+
const gui = player.getGui?.(ACTION_BATTLE_ACTION_BAR_GUI_ID);
|
|
347
|
+
if (gui) gui.update(buildActionBarData(player, options));
|
|
348
|
+
};
|
|
349
|
+
var getTileSize = (map) => ({
|
|
350
|
+
width: map?.tileWidth ?? 32,
|
|
351
|
+
height: map?.tileHeight ?? 32
|
|
352
|
+
});
|
|
353
|
+
var getEntityTile = (entity, tileSize) => {
|
|
354
|
+
const hitbox = entity.hitbox?.() || {
|
|
355
|
+
w: tileSize.width,
|
|
356
|
+
h: tileSize.height
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
x: Math.floor((entity.x() + hitbox.w / 2) / tileSize.width),
|
|
360
|
+
y: Math.floor((entity.y() + hitbox.h / 2) / tileSize.height)
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
var handleActionBattleSkillUse = (player, skillId, target, options) => {
|
|
364
|
+
const skillData = resolveSkillData(player, skillId);
|
|
365
|
+
const map = player.getCurrentMap();
|
|
366
|
+
if (!map) {
|
|
367
|
+
playActionBattleAnimation("castSkill", player, options.animations, { skill: skillData });
|
|
368
|
+
player.useSkill(skillId);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const targeting = resolveSkillTargeting(player, skillId, options);
|
|
372
|
+
if (!targeting || !target) {
|
|
373
|
+
playActionBattleAnimation("castSkill", player, options.animations, { skill: skillData });
|
|
374
|
+
player.useSkill(skillId);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const tileSize = getTileSize(map);
|
|
378
|
+
const origin = getEntityTile(player, tileSize);
|
|
379
|
+
const targetTile = {
|
|
380
|
+
x: target.x,
|
|
381
|
+
y: target.y
|
|
382
|
+
};
|
|
383
|
+
if (manhattanDistance(origin, targetTile) > targeting.range) return;
|
|
384
|
+
const mask = parseAoeMask(targeting.aoeMask || options.skills?.defaultAoeMask);
|
|
385
|
+
const affected = /* @__PURE__ */ new Set();
|
|
386
|
+
mask.cells.forEach((cell) => {
|
|
387
|
+
const x = targetTile.x + cell.dx;
|
|
388
|
+
const y = targetTile.y + cell.dy;
|
|
389
|
+
affected.add(`${x},${y}`);
|
|
390
|
+
});
|
|
391
|
+
const targets = [];
|
|
392
|
+
const affects = options.targeting?.affects || "events";
|
|
393
|
+
if (affects === "events" || affects === "both") map.getEvents().forEach((event) => {
|
|
394
|
+
const tile = getEntityTile(event, tileSize);
|
|
395
|
+
if (affected.has(`${tile.x},${tile.y}`)) targets.push(event);
|
|
396
|
+
});
|
|
397
|
+
if (affects === "players" || affects === "both") map.getPlayers().forEach((other) => {
|
|
398
|
+
if (other.id === player.id) return;
|
|
399
|
+
const tile = getEntityTile(other, tileSize);
|
|
400
|
+
if (affected.has(`${tile.x},${tile.y}`)) targets.push(other);
|
|
401
|
+
});
|
|
402
|
+
if (!options.targeting?.allowEmptyTarget && targets.length === 0) return;
|
|
403
|
+
playActionBattleAnimation("castSkill", player, options.animations, {
|
|
404
|
+
skill: skillData,
|
|
405
|
+
target: targets[0]
|
|
406
|
+
});
|
|
407
|
+
player.useSkill(skillId, targets);
|
|
408
|
+
};
|
|
409
|
+
var createActionBattleServer = (rawOptions = {}) => {
|
|
410
|
+
const options = normalizeActionBattleOptions(rawOptions);
|
|
411
|
+
setActionBattleOptions(options);
|
|
412
|
+
setActionBattleSystems(options);
|
|
413
|
+
return defineModule({
|
|
414
|
+
player: {
|
|
415
|
+
/**
|
|
416
|
+
* Handle player input for combat actions
|
|
417
|
+
*
|
|
418
|
+
* When a player presses the action key, create an attack hitbox
|
|
419
|
+
* that can damage AI enemies within range and knockback the event.
|
|
420
|
+
* Knockback force is based on the player's equipped weapon.
|
|
421
|
+
* Triggers attack animation and visual effects.
|
|
422
|
+
*
|
|
423
|
+
* @param player - The player performing the action
|
|
424
|
+
* @param input - Input data containing pressed keys
|
|
425
|
+
*/
|
|
426
|
+
onInput(player, input) {
|
|
427
|
+
if (input.action == Control.Action) {
|
|
428
|
+
const map = player.getCurrentMap();
|
|
429
|
+
const direction = player.getDirection();
|
|
430
|
+
const attackProfile = resolvePlayerAttackProfile(player, options);
|
|
431
|
+
const hitboxes = resolvePlayerAttackHitboxes(player, direction, options, attackProfile);
|
|
432
|
+
if (isActionReservedForNormalEvent(player, map, hitboxes)) return;
|
|
433
|
+
const lockMovement = attackProfile.movementLock;
|
|
434
|
+
const lockDirection = attackProfile.directionLock;
|
|
435
|
+
const lockDurationMs = attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
|
|
436
|
+
const actionLocked = (lockMovement || lockDirection) && lockDurationMs > 0;
|
|
437
|
+
if (actionLocked && !beginPlayerAttackLock(player, map, Math.max(0, lockDurationMs), {
|
|
438
|
+
movement: lockMovement,
|
|
439
|
+
direction: lockDirection
|
|
440
|
+
})) return;
|
|
441
|
+
playActionBattleAnimation("attack", player, options.animations);
|
|
442
|
+
if (actionLocked) player.animationFixed = true;
|
|
443
|
+
const attackId = createActionBattleAttackId(player.id, attackProfile.id);
|
|
444
|
+
const hitTracker = new ActionBattleHitTracker(attackProfile.hitPolicy);
|
|
445
|
+
if (options.debug?.attacks) console.log("[ActionBattle] player attack", {
|
|
446
|
+
attackId,
|
|
447
|
+
playerId: player.id,
|
|
448
|
+
profile: attackProfile.id,
|
|
449
|
+
hitboxes
|
|
450
|
+
});
|
|
451
|
+
scheduleActionBattleStartup(attackProfile, () => {
|
|
452
|
+
map?.createMovingHitbox(hitboxes, { speed: resolveActionBattleHitboxSpeed(attackProfile, hitboxes.length) }).subscribe({ next(hits) {
|
|
453
|
+
hits.forEach((hit) => {
|
|
454
|
+
if (hit instanceof RpgEvent) {
|
|
455
|
+
if (!hitTracker.tryHit(hit)) return;
|
|
456
|
+
if (applyPlayerHitToEvent(player, hit, void 0, {
|
|
457
|
+
attackId,
|
|
458
|
+
attackProfileId: attackProfile.id,
|
|
459
|
+
reaction: attackProfile.reaction
|
|
460
|
+
})?.defeated) console.log(`Player ${player.id} defeated AI ${hit.id}`);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
} });
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
onConnected(player) {
|
|
468
|
+
if (options.ui?.actionBar?.enabled && options.ui?.actionBar?.autoOpen) openActionBattleActionBar(player, options);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
event: {
|
|
472
|
+
/**
|
|
473
|
+
* Handle player detection when entering AI vision
|
|
474
|
+
*
|
|
475
|
+
* Called when a player enters an AI event's vision range.
|
|
476
|
+
* The AI will start pursuing and attacking the player.
|
|
477
|
+
*
|
|
478
|
+
* @param event - The AI event
|
|
479
|
+
* @param player - The player entering vision
|
|
480
|
+
* @param shape - The vision shape
|
|
481
|
+
*/
|
|
482
|
+
onDetectInShape(event, player, shape) {
|
|
483
|
+
event.battleAi?.onDetectInShape(player, shape);
|
|
484
|
+
},
|
|
485
|
+
/**
|
|
486
|
+
* Handle player leaving AI vision
|
|
487
|
+
*
|
|
488
|
+
* Called when a player leaves an AI event's vision range.
|
|
489
|
+
* The AI will stop pursuing the player.
|
|
490
|
+
*
|
|
491
|
+
* @param event - The AI event
|
|
492
|
+
* @param player - The player leaving vision
|
|
493
|
+
* @param shape - The vision shape
|
|
494
|
+
*/
|
|
495
|
+
onDetectOutShape(event, player, shape) {
|
|
496
|
+
event.battleAi?.onDetectOutShape(player, shape);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
var server_default = createActionBattleServer();
|
|
502
|
+
//#endregion
|
|
503
|
+
export { ACTION_BATTLE_ACTION_BAR_GUI_ID, DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, createActionBattleServer, server_default as default, getPlayerWeaponKnockbackForce, openActionBattleActionBar, updateActionBattleActionBar };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BattleAi } from "./index10.js";
|
|
2
|
+
//#region src/enemies/factory.ts
|
|
3
|
+
var createActionEnemy = (event, presetOrOptions, presets = {}) => {
|
|
4
|
+
const options = typeof presetOrOptions === "string" ? presets[presetOrOptions] : presetOrOptions;
|
|
5
|
+
if (!options) throw new Error(`Action battle enemy preset not found: ${presetOrOptions}`);
|
|
6
|
+
options.stats?.(event);
|
|
7
|
+
return new BattleAi(event, options);
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createActionEnemy };
|