@rpgjs/action-battle 5.0.0-alpha.32 → 5.0.0-alpha.35
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 +40 -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 +47 -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/README.md
CHANGED
|
@@ -157,8 +157,9 @@ new BattleAi(event, {
|
|
|
157
157
|
groupBehavior: true,
|
|
158
158
|
|
|
159
159
|
// Callback when AI is defeated
|
|
160
|
-
onDefeated: (event) => {
|
|
161
|
-
|
|
160
|
+
onDefeated: (event, attacker) => {
|
|
161
|
+
const name = attacker?.name?.() ?? "Unknown";
|
|
162
|
+
console.log(`${event.name()} was defeated by ${name}!`);
|
|
162
163
|
}
|
|
163
164
|
});
|
|
164
165
|
```
|
|
@@ -645,7 +646,7 @@ console.log(`Player knockback force: ${force}`);
|
|
|
645
646
|
|
|
646
647
|
## onDefeated Hook
|
|
647
648
|
|
|
648
|
-
The `onDefeated` callback is triggered when an AI enemy is killed. Use it to:
|
|
649
|
+
The `onDefeated` callback is triggered when an AI enemy is killed. It receives the defeated event and the player who landed the killing blow (if available). Use it to:
|
|
649
650
|
- Award experience, gold, or items to the player
|
|
650
651
|
- Spawn loot drops
|
|
651
652
|
- Trigger events or cutscenes
|
|
@@ -657,8 +658,9 @@ The `onDefeated` callback is triggered when an AI enemy is killed. Use it to:
|
|
|
657
658
|
```typescript
|
|
658
659
|
new BattleAi(this, {
|
|
659
660
|
enemyType: EnemyType.Aggressive,
|
|
660
|
-
onDefeated: (event) => {
|
|
661
|
-
|
|
661
|
+
onDefeated: (event, attacker) => {
|
|
662
|
+
const name = attacker?.name?.() ?? "Unknown";
|
|
663
|
+
console.log(`${event.name()} was defeated by ${name}!`);
|
|
662
664
|
}
|
|
663
665
|
});
|
|
664
666
|
```
|
|
@@ -677,23 +679,19 @@ function Goblin() {
|
|
|
677
679
|
|
|
678
680
|
new BattleAi(this, {
|
|
679
681
|
enemyType: EnemyType.Aggressive,
|
|
680
|
-
onDefeated: (event) => {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
682
|
+
onDefeated: (event, attacker) => {
|
|
683
|
+
if (!attacker) return;
|
|
684
|
+
|
|
685
|
+
// Award gold
|
|
686
|
+
attacker.gold += 25;
|
|
684
687
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// Random loot drop
|
|
693
|
-
if (Math.random() < 0.3) {
|
|
694
|
-
player.addItem(HealthPotion);
|
|
695
|
-
}
|
|
696
|
-
});
|
|
688
|
+
// Award experience
|
|
689
|
+
attacker.exp += 50;
|
|
690
|
+
|
|
691
|
+
// Random loot drop
|
|
692
|
+
if (Math.random() < 0.3) {
|
|
693
|
+
attacker.addItem(HealthPotion);
|
|
694
|
+
}
|
|
697
695
|
}
|
|
698
696
|
});
|
|
699
697
|
}
|
|
@@ -705,7 +703,7 @@ function Goblin() {
|
|
|
705
703
|
|
|
706
704
|
```typescript
|
|
707
705
|
new BattleAi(this, {
|
|
708
|
-
onDefeated: (event) => {
|
|
706
|
+
onDefeated: (event, attacker) => {
|
|
709
707
|
const map = event.getCurrentMap();
|
|
710
708
|
if (!map) return;
|
|
711
709
|
|
|
@@ -725,7 +723,7 @@ new BattleAi(this, {
|
|
|
725
723
|
let killCount = 0;
|
|
726
724
|
|
|
727
725
|
new BattleAi(this, {
|
|
728
|
-
onDefeated: (event) => {
|
|
726
|
+
onDefeated: (event, attacker) => {
|
|
729
727
|
killCount++;
|
|
730
728
|
|
|
731
729
|
// Check quest progress
|
|
@@ -749,7 +747,7 @@ function DragonBoss() {
|
|
|
749
747
|
|
|
750
748
|
new BattleAi(this, {
|
|
751
749
|
enemyType: EnemyType.Tank,
|
|
752
|
-
onDefeated: (event) => {
|
|
750
|
+
onDefeated: (event, attacker) => {
|
|
753
751
|
const map = event.getCurrentMap();
|
|
754
752
|
|
|
755
753
|
// Announce victory
|
|
@@ -782,3 +780,93 @@ Automatic feedback:
|
|
|
782
780
|
- **Damage Numbers**: Floating damage text
|
|
783
781
|
- **Attack Animation**: Triggers `attack` animation
|
|
784
782
|
- **Knockback**: Entities pushed back based on weapon `knockbackForce`
|
|
783
|
+
|
|
784
|
+
## Action Bar + AoE Targeting (client + server)
|
|
785
|
+
|
|
786
|
+
The action-battle package includes optional GUI components for an A-RPG action bar
|
|
787
|
+
and AoE skill targeting. They are disabled by default and are configured via
|
|
788
|
+
`provideActionBattle()`.
|
|
789
|
+
|
|
790
|
+
### Enable the Action Bar
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
import { provideActionBattle } from "@rpgjs/action-battle";
|
|
794
|
+
|
|
795
|
+
export default provideActionBattle({
|
|
796
|
+
ui: {
|
|
797
|
+
actionBar: {
|
|
798
|
+
enabled: true,
|
|
799
|
+
autoOpen: true,
|
|
800
|
+
mode: "both" // "items" | "skills" | "both"
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
You can open/close it manually on the server:
|
|
807
|
+
|
|
808
|
+
```ts
|
|
809
|
+
import { openActionBattleActionBar } from "@rpgjs/action-battle/server";
|
|
810
|
+
|
|
811
|
+
openActionBattleActionBar(player);
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### Skill Range + AoE Mask (ASCII)
|
|
815
|
+
|
|
816
|
+
Define range and AoE mask on the skill data (custom fields). The range uses
|
|
817
|
+
Manhattan distance, and the mask is centered on the target tile.
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
@Skill({
|
|
821
|
+
name: "Nova",
|
|
822
|
+
spCost: 12,
|
|
823
|
+
// Custom fields used by action-battle
|
|
824
|
+
range: 3,
|
|
825
|
+
aoeMask: [
|
|
826
|
+
".#.",
|
|
827
|
+
"###",
|
|
828
|
+
".#."
|
|
829
|
+
]
|
|
830
|
+
})
|
|
831
|
+
export class Nova {}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Targeting Options
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
export default provideActionBattle({
|
|
838
|
+
ui: {
|
|
839
|
+
actionBar: {
|
|
840
|
+
enabled: true,
|
|
841
|
+
autoOpen: false
|
|
842
|
+
},
|
|
843
|
+
targeting: {
|
|
844
|
+
enabled: true,
|
|
845
|
+
showGrid: true,
|
|
846
|
+
colors: {
|
|
847
|
+
area: 0x2f9ef7,
|
|
848
|
+
edge: 0x1b6a98,
|
|
849
|
+
cursor: 0xffd166
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
targeting: {
|
|
854
|
+
affects: "events", // "events" | "players" | "both"
|
|
855
|
+
allowEmptyTarget: true
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Custom Targeting Resolver (optional)
|
|
861
|
+
|
|
862
|
+
If you prefer to compute targeting from your own skill schema, use `getTargeting`:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
export default provideActionBattle({
|
|
866
|
+
skills: {
|
|
867
|
+
getTargeting(skill) {
|
|
868
|
+
return skill?.targeting;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
```
|
package/dist/ai.server.d.ts
CHANGED
|
@@ -296,7 +296,7 @@ export declare class BattleAi {
|
|
|
296
296
|
retreatThreshold?: number;
|
|
297
297
|
};
|
|
298
298
|
/** Callback called when the AI is defeated */
|
|
299
|
-
onDefeated?: (event: RpgEvent) => void;
|
|
299
|
+
onDefeated?: (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
300
300
|
});
|
|
301
301
|
/**
|
|
302
302
|
* Apply enemy type-specific behavior modifiers
|
package/dist/client/index.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import client from "./index2.js";
|
|
1
|
+
import client, { createActionBattleClient } from "./index2.js";
|
|
2
2
|
import { createModule } from "@rpgjs/common";
|
|
3
3
|
import { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType } from "./index3.js";
|
|
4
|
-
import { DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, getPlayerWeaponKnockbackForce } from "./index4.js";
|
|
4
|
+
import { ACTION_BATTLE_ACTION_BAR_GUI_ID, DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, createActionBattleServer, getPlayerWeaponKnockbackForce, openActionBattleActionBar, updateActionBattleActionBar } from "./index4.js";
|
|
5
5
|
const server = null;
|
|
6
|
-
|
|
6
|
+
const createActionBattleServer2 = null;
|
|
7
|
+
function provideActionBattle(options = {}) {
|
|
7
8
|
return createModule("ActionBattle", [
|
|
8
9
|
{
|
|
9
|
-
server,
|
|
10
|
-
client
|
|
10
|
+
server: createActionBattleServer2?.(options),
|
|
11
|
+
client: createActionBattleClient?.(options)
|
|
11
12
|
}
|
|
12
13
|
]);
|
|
13
14
|
}
|
|
15
|
+
const index = {
|
|
16
|
+
server,
|
|
17
|
+
client
|
|
18
|
+
};
|
|
14
19
|
export {
|
|
20
|
+
ACTION_BATTLE_ACTION_BAR_GUI_ID,
|
|
15
21
|
AiDebug,
|
|
16
22
|
AiState,
|
|
17
23
|
AttackPattern,
|
|
@@ -20,6 +26,10 @@ export {
|
|
|
20
26
|
DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
21
27
|
EnemyType,
|
|
22
28
|
applyPlayerHitToEvent,
|
|
29
|
+
createActionBattleServer,
|
|
30
|
+
index as default,
|
|
23
31
|
getPlayerWeaponKnockbackForce,
|
|
24
|
-
|
|
32
|
+
openActionBattleActionBar,
|
|
33
|
+
provideActionBattle,
|
|
34
|
+
updateActionBattleActionBar
|
|
25
35
|
};
|
package/dist/client/index2.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
|
-
import { PrebuiltComponentAnimations } from "@rpgjs/client";
|
|
1
|
+
import { PrebuiltComponentAnimations, inject, RpgGui, RpgClientEngine } from "@rpgjs/client";
|
|
2
2
|
import { defineModule } from "@rpgjs/common";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import component$1 from "./index5.js";
|
|
4
|
+
import component from "./index6.js";
|
|
5
|
+
import { setActionBattleOptions } from "./index7.js";
|
|
6
|
+
import { normalizeActionBattleOptions } from "./index8.js";
|
|
7
|
+
const createActionBattleClient = (options = {}) => {
|
|
8
|
+
const normalized = normalizeActionBattleOptions(options);
|
|
9
|
+
setActionBattleOptions(normalized);
|
|
10
|
+
const actionBarEnabled = normalized.ui?.actionBar?.enabled;
|
|
11
|
+
const targetingEnabled = normalized.ui?.targeting?.enabled;
|
|
12
|
+
const hitComponent = PrebuiltComponentAnimations?.Hit;
|
|
13
|
+
return defineModule({
|
|
14
|
+
componentAnimations: hitComponent ? [
|
|
15
|
+
{
|
|
16
|
+
id: "hit",
|
|
17
|
+
component: hitComponent
|
|
18
|
+
}
|
|
19
|
+
] : [],
|
|
20
|
+
gui: actionBarEnabled ? [
|
|
21
|
+
{
|
|
22
|
+
id: "action-battle-action-bar",
|
|
23
|
+
component: component$1,
|
|
24
|
+
dependencies: () => {
|
|
25
|
+
const engine = inject(RpgClientEngine);
|
|
26
|
+
return [engine.scene.currentPlayer];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
] : [],
|
|
30
|
+
sprite: {
|
|
31
|
+
componentsInFront: targetingEnabled ? [component] : []
|
|
32
|
+
},
|
|
33
|
+
sceneMap: {
|
|
34
|
+
onAfterLoading() {
|
|
35
|
+
const gui = inject(RpgGui);
|
|
36
|
+
gui.display("action-battle-action-bar");
|
|
37
|
+
}
|
|
8
38
|
}
|
|
9
|
-
|
|
10
|
-
}
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const client = createActionBattleClient();
|
|
11
42
|
export {
|
|
43
|
+
createActionBattleClient,
|
|
12
44
|
client as default
|
|
13
45
|
};
|
package/dist/client/index3.js
CHANGED
|
@@ -1122,7 +1122,7 @@ class BattleAi {
|
|
|
1122
1122
|
}
|
|
1123
1123
|
if (this.event.hp <= 0) {
|
|
1124
1124
|
this.debugLog("damage", "Defeated!");
|
|
1125
|
-
this.kill();
|
|
1125
|
+
this.kill(attacker);
|
|
1126
1126
|
return true;
|
|
1127
1127
|
}
|
|
1128
1128
|
return false;
|
|
@@ -1133,9 +1133,9 @@ class BattleAi {
|
|
|
1133
1133
|
* Stops all movements, cleans up resources, calls the onDefeated hook,
|
|
1134
1134
|
* and removes the event from the map.
|
|
1135
1135
|
*/
|
|
1136
|
-
kill() {
|
|
1136
|
+
kill(attacker) {
|
|
1137
1137
|
if (this.onDefeatedCallback) {
|
|
1138
|
-
this.onDefeatedCallback(this.event);
|
|
1138
|
+
this.onDefeatedCallback(this.event, attacker);
|
|
1139
1139
|
}
|
|
1140
1140
|
this.destroy();
|
|
1141
1141
|
this.event.remove();
|
package/dist/client/index4.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { defineModule, Control } from "@rpgjs/common";
|
|
2
|
+
import { normalizeActionBattleOptions } from "./index8.js";
|
|
3
|
+
import { manhattanDistance, parseAoeMask } from "./index9.js";
|
|
2
4
|
const RpgEvent = null;
|
|
3
5
|
const DEFAULT_KNOCKBACK = null;
|
|
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,82 +59,275 @@ function applyPlayerHitToEvent(player, target, hooks) {
|
|
|
56
59
|
}
|
|
57
60
|
return hitResult;
|
|
58
61
|
}
|
|
59
|
-
|
|
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
|
+
createActionBattleServer();
|
|
133
325
|
export {
|
|
326
|
+
ACTION_BATTLE_ACTION_BAR_GUI_ID,
|
|
134
327
|
DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
135
328
|
applyPlayerHitToEvent,
|
|
136
|
-
|
|
329
|
+
createActionBattleServer,
|
|
330
|
+
getPlayerWeaponKnockbackForce,
|
|
331
|
+
openActionBattleActionBar,
|
|
332
|
+
updateActionBattleActionBar
|
|
137
333
|
};
|