@rpgjs/action-battle 5.0.0-beta.3 → 5.0.0-beta.5
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 +137 -0
- package/dist/ai.server.d.ts +8 -1
- package/dist/client/index.js +8 -4
- package/dist/client/index10.js +97 -330
- package/dist/client/index11.js +25 -0
- package/dist/client/index12.js +1222 -0
- package/dist/client/index13.js +46 -0
- package/dist/client/index14.js +10 -0
- package/dist/client/index15.js +448 -0
- package/dist/client/index2.js +30 -0
- package/dist/client/index3.js +33 -1
- package/dist/client/index4.js +7 -3
- package/dist/client/index7.js +76 -32
- package/dist/client/index8.js +24 -4
- package/dist/client/index9.js +94 -1165
- package/dist/core/context.d.ts +5 -0
- package/dist/core/defaults.d.ts +81 -0
- package/dist/core/hit.d.ts +2 -0
- package/dist/enemies/factory.d.ts +7 -0
- package/dist/index.d.ts +6 -1
- package/dist/server/index.js +7 -3
- package/dist/server/index10.js +10 -0
- package/dist/server/index2.js +23 -3
- package/dist/server/index3.js +30 -0
- package/dist/server/index4.js +137 -1163
- package/dist/server/index5.js +22 -34
- package/dist/server/index6.js +1190 -345
- package/dist/server/index7.js +37 -0
- package/dist/server/index8.js +46 -0
- package/dist/server/index9.js +447 -0
- package/dist/server.d.ts +2 -0
- package/dist/ui/state.d.ts +17 -0
- package/package.json +5 -5
- package/src/ai.server.ts +91 -24
- package/src/animations.ts +43 -4
- package/src/canvas-engine-shim.ts +4 -0
- package/src/client.ts +122 -2
- package/src/components/action-bar.ce +5 -3
- package/src/components/attack-preview.ce +90 -0
- package/src/config.ts +30 -0
- package/src/core/context.ts +35 -0
- package/src/core/contracts.ts +123 -0
- package/src/core/defaults.ts +162 -0
- package/src/core/hit.spec.ts +58 -0
- package/src/core/hit.ts +66 -0
- package/src/enemies/factory.ts +25 -0
- package/src/index.ts +40 -0
- package/src/server.ts +235 -71
- package/src/targeting.spec.ts +24 -0
- package/src/types/canvas-engine.d.ts +4 -0
- package/src/types.ts +46 -1
- package/src/ui/state.ts +57 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ The AI controller manages **behavior only** - all stats (HP, ATK, skills, items,
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **State Machine AI**: Enemies with dynamic behaviors (Idle, Alert, Combat, Flee, Stunned)
|
|
10
|
+
- **Plugin-first architecture**: Replace damage, hitboxes, knockback, hooks, and AI behaviors independently
|
|
10
11
|
- **Multiple Enemy Types**: Aggressive, Defensive, Ranged, Tank, Berserker
|
|
11
12
|
- **Attack Patterns**: Melee, Combo, Charged, Zone, Dash Attack
|
|
12
13
|
- **Skill Support**: AI can use any RPGJS skill
|
|
@@ -22,6 +23,90 @@ The AI controller manages **behavior only** - all stats (HP, ATK, skills, items,
|
|
|
22
23
|
npm install @rpgjs/action-battle
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
## Plugin-First Customization
|
|
27
|
+
|
|
28
|
+
`provideActionBattle()` ships with Zelda-like defaults, but each combat system
|
|
29
|
+
can be replaced without rewriting the module.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
33
|
+
|
|
34
|
+
export default provideActionBattle({
|
|
35
|
+
attack: {
|
|
36
|
+
lockMovement: true,
|
|
37
|
+
lockDurationMs: 280,
|
|
38
|
+
hitboxes: {
|
|
39
|
+
right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
systems: {
|
|
43
|
+
combat: {
|
|
44
|
+
damage({ attacker, target, skill }) {
|
|
45
|
+
const raw = target.applyDamage(attacker, skill);
|
|
46
|
+
return {
|
|
47
|
+
damage: raw.damage,
|
|
48
|
+
defeated: target.hp <= 0,
|
|
49
|
+
raw
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
knockback({ attacker, target }) {
|
|
53
|
+
const dx = target.x() - attacker.x();
|
|
54
|
+
const dy = target.y() - attacker.y();
|
|
55
|
+
const distance = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
56
|
+
return {
|
|
57
|
+
force: 70,
|
|
58
|
+
duration: 220,
|
|
59
|
+
direction: { x: dx / distance, y: dy / distance }
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
hooks: {
|
|
63
|
+
beforeHit(context) {
|
|
64
|
+
// Return false to cancel a hit, or return a modified context.
|
|
65
|
+
return context;
|
|
66
|
+
},
|
|
67
|
+
afterHit(result) {
|
|
68
|
+
console.log(`Damage: ${result.damage}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
ai: {
|
|
73
|
+
behaviors: {
|
|
74
|
+
slime({ hpPercent }) {
|
|
75
|
+
return {
|
|
76
|
+
mode: hpPercent !== null && hpPercent < 0.25 ? "retreat" : "assault",
|
|
77
|
+
attackCooldown: 900
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The main extension contracts are:
|
|
87
|
+
|
|
88
|
+
- `ActionBattleCombatSystem`: resolves hitboxes, damage, knockback, and hooks.
|
|
89
|
+
- `ActionBattleAiBehavior`: returns lightweight AI decisions from event state.
|
|
90
|
+
- `ActionBattleHitHooks`: `beforeHit`, `afterDamage`, and `afterHit`.
|
|
91
|
+
|
|
92
|
+
Use `createActionEnemy()` when you want data-driven enemy presets:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createActionEnemy, EnemyType } from "@rpgjs/action-battle/server";
|
|
96
|
+
|
|
97
|
+
const enemyPresets = {
|
|
98
|
+
slime: {
|
|
99
|
+
enemyType: EnemyType.Aggressive,
|
|
100
|
+
behaviorKey: "slime",
|
|
101
|
+
stats(event) {
|
|
102
|
+
event.hp = 40;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
createActionEnemy(this, "slime", enemyPresets);
|
|
108
|
+
```
|
|
109
|
+
|
|
25
110
|
## Quick Start
|
|
26
111
|
|
|
27
112
|
```typescript
|
|
@@ -496,6 +581,36 @@ The module handles player attacks via the `action` input:
|
|
|
496
581
|
// Knockback force is based on equipped weapon's knockbackForce property
|
|
497
582
|
```
|
|
498
583
|
|
|
584
|
+
By default, the player is locked in place for `350ms` when attacking, similar
|
|
585
|
+
to classic A-RPG combat where the attack resolves before movement resumes.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
provideActionBattle({
|
|
589
|
+
attack: {
|
|
590
|
+
lockMovement: true,
|
|
591
|
+
lockDurationMs: 350,
|
|
592
|
+
showPreview: true,
|
|
593
|
+
previewDurationMs: 180,
|
|
594
|
+
previewColor: 0xfff3b0,
|
|
595
|
+
previewAccentColor: 0xffffff
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Set `lockMovement` to `false` if your game should allow moving attacks.
|
|
601
|
+
The client also stops predicted movement immediately and shows a short slash
|
|
602
|
+
preview so the action feels responsive even before the server hit resolves. Set
|
|
603
|
+
`showPreview` to `false` if your project uses only spritesheet combat
|
|
604
|
+
animations.
|
|
605
|
+
|
|
606
|
+
Player attacks are resolved with `createMovingHitbox()` instead of a passive
|
|
607
|
+
contact collision. You can still customize the generated hitboxes with
|
|
608
|
+
`attack.hitboxes` or `attack.resolveHitboxes`.
|
|
609
|
+
|
|
610
|
+
When the action targets a normal event with no `BattleAi`, the server lets the
|
|
611
|
+
event handle `onAction` and does not create the combat hitbox. Enemy events
|
|
612
|
+
with `BattleAi` still trigger the A-RPG attack.
|
|
613
|
+
|
|
499
614
|
## Configurable Combat Animations
|
|
500
615
|
|
|
501
616
|
By default, player and AI attacks keep using the existing `attack` animation:
|
|
@@ -524,6 +639,28 @@ export default provideActionBattle({
|
|
|
524
639
|
});
|
|
525
640
|
```
|
|
526
641
|
|
|
642
|
+
RPGJS Studio stores combat animations as spritesheet media ids. If
|
|
643
|
+
`provideStudioGame()` is installed, `createStudioActionBattleAnimations()` can
|
|
644
|
+
read the project animations attached to the player at runtime. By default, the
|
|
645
|
+
helper plays Studio attack spritesheets with
|
|
646
|
+
`setGraphicAnimation("attack", graphic, 1)`:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
import { provideActionBattle } from "@rpgjs/action-battle/server";
|
|
650
|
+
import { createStudioActionBattleAnimations } from "@rpgjs/studio/server";
|
|
651
|
+
|
|
652
|
+
export default provideActionBattle({
|
|
653
|
+
animations: createStudioActionBattleAnimations()
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
You can also pass a static Studio animation object to override the media ids
|
|
658
|
+
manually. Animation values may be media ids or media objects returned by the
|
|
659
|
+
Studio game API.
|
|
660
|
+
|
|
661
|
+
The Studio field `castSpell` is accepted as an alias for action-battle's
|
|
662
|
+
`castSkill` animation key.
|
|
663
|
+
|
|
527
664
|
For data-driven spritesheets, use resolver functions:
|
|
528
665
|
|
|
529
666
|
```ts
|
package/dist/ai.server.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { RpgEvent, RpgPlayer } from '@rpgjs/server';
|
|
2
|
+
import { ActionBattleDamageResult } from './core/contracts';
|
|
2
3
|
import { ActionBattleAnimationOptions } from './types';
|
|
3
4
|
type RpgEventWithBattleAi = RpgEvent & {
|
|
4
|
-
battleAi
|
|
5
|
+
battleAi?: BattleAi;
|
|
5
6
|
};
|
|
6
7
|
export interface BattleAiOptions {
|
|
7
8
|
enemyType?: EnemyType;
|
|
@@ -27,6 +28,7 @@ export interface BattleAiOptions {
|
|
|
27
28
|
assaultThreshold?: number;
|
|
28
29
|
retreatThreshold?: number;
|
|
29
30
|
};
|
|
31
|
+
behaviorKey?: string;
|
|
30
32
|
animations?: ActionBattleAnimationOptions;
|
|
31
33
|
/** Callback called when the AI is defeated */
|
|
32
34
|
onDefeated?: (event: RpgEvent, attacker?: RpgPlayer) => void;
|
|
@@ -277,6 +279,8 @@ export declare class BattleAi {
|
|
|
277
279
|
private lastMoveToTime;
|
|
278
280
|
private retreatCooldown;
|
|
279
281
|
private lastRetreatTime;
|
|
282
|
+
private timers;
|
|
283
|
+
private behaviorKey?;
|
|
280
284
|
/**
|
|
281
285
|
* Create a new Battle AI Controller
|
|
282
286
|
*
|
|
@@ -483,6 +487,7 @@ export declare class BattleAi {
|
|
|
483
487
|
* The actual damage is applied externally via RPGJS API.
|
|
484
488
|
*/
|
|
485
489
|
takeDamage(attacker: RpgPlayer): boolean;
|
|
490
|
+
handleDamage(attacker: RpgPlayer, damageResult: ActionBattleDamageResult): boolean;
|
|
486
491
|
/**
|
|
487
492
|
* Kill this AI
|
|
488
493
|
*
|
|
@@ -495,9 +500,11 @@ export declare class BattleAi {
|
|
|
495
500
|
*/
|
|
496
501
|
private getDistance;
|
|
497
502
|
private updateBehavior;
|
|
503
|
+
private applyCustomBehavior;
|
|
498
504
|
private handleTacticalMovement;
|
|
499
505
|
private handleAssaultMovement;
|
|
500
506
|
private requestMoveTo;
|
|
507
|
+
private schedule;
|
|
501
508
|
getHealth(): number;
|
|
502
509
|
getMaxHealth(): number;
|
|
503
510
|
getTarget(): InstanceType<typeof RpgPlayer> | null;
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import client_default, { createActionBattleClient } from "./
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import client_default, { createActionBattleClient } from "./index9.js";
|
|
2
|
+
import { DEFAULT_ZELDA_PLAYER_HITBOXES, createDefaultPlayerHitboxResolver, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver } from "./index10.js";
|
|
3
|
+
import { createActionBattleSystems, getActionBattleSystems } from "./index11.js";
|
|
4
|
+
import { AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, EnemyType } from "./index12.js";
|
|
5
|
+
import { applyActionBattleHit } from "./index13.js";
|
|
6
|
+
import { createActionEnemy } from "./index14.js";
|
|
7
|
+
import { ACTION_BATTLE_ACTION_BAR_GUI_ID, DEFAULT_PLAYER_ATTACK_HITBOXES, applyPlayerHitToEvent, createActionBattleServer, getPlayerWeaponKnockbackForce, openActionBattleActionBar, updateActionBattleActionBar } from "./index15.js";
|
|
4
8
|
import { createModule } from "@rpgjs/common";
|
|
5
9
|
//#region src/index.ts
|
|
6
10
|
var server = null;
|
|
@@ -16,4 +20,4 @@ var src_default = {
|
|
|
16
20
|
client: client_default
|
|
17
21
|
};
|
|
18
22
|
//#endregion
|
|
19
|
-
export { ACTION_BATTLE_ACTION_BAR_GUI_ID, AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, DEFAULT_PLAYER_ATTACK_HITBOXES, EnemyType, applyPlayerHitToEvent, createActionBattleServer, src_default as default, getPlayerWeaponKnockbackForce, openActionBattleActionBar, provideActionBattle, updateActionBattleActionBar };
|
|
23
|
+
export { ACTION_BATTLE_ACTION_BAR_GUI_ID, AiDebug, AiState, AttackPattern, BattleAi, DEFAULT_KNOCKBACK, DEFAULT_PLAYER_ATTACK_HITBOXES, DEFAULT_ZELDA_PLAYER_HITBOXES, EnemyType, applyActionBattleHit, applyPlayerHitToEvent, createActionBattleServer, createActionBattleSystems, createActionEnemy, createDefaultPlayerHitboxResolver, src_default as default, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver, getActionBattleSystems, getPlayerWeaponKnockbackForce, openActionBattleActionBar, provideActionBattle, updateActionBattleActionBar };
|
package/dist/client/index10.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
//#region src/core/defaults.ts
|
|
2
|
+
var DEFAULT_CORE_KNOCKBACK = {
|
|
3
|
+
force: 50,
|
|
4
|
+
duration: 300
|
|
5
|
+
};
|
|
6
|
+
var CoreAttackPattern = {
|
|
7
|
+
Melee: "melee",
|
|
8
|
+
Combo: "combo",
|
|
9
|
+
Charged: "charged",
|
|
10
|
+
Zone: "zone",
|
|
11
|
+
DashAttack: "dashAttack"
|
|
12
|
+
};
|
|
13
|
+
var CoreEnemyType = {
|
|
14
|
+
Aggressive: "aggressive",
|
|
15
|
+
Defensive: "defensive",
|
|
16
|
+
Ranged: "ranged",
|
|
17
|
+
Tank: "tank",
|
|
18
|
+
Berserker: "berserker"
|
|
19
|
+
};
|
|
20
|
+
var DEFAULT_ZELDA_PLAYER_HITBOXES = {
|
|
17
21
|
up: {
|
|
18
22
|
offsetX: -16,
|
|
19
23
|
offsetY: -48,
|
|
@@ -45,332 +49,95 @@ var DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
|
45
49
|
height: 32
|
|
46
50
|
}
|
|
47
51
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @param player - The player to get weapon knockback from
|
|
55
|
-
* @returns Knockback force value
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* // Player with weapon having knockbackForce: 80
|
|
60
|
-
* const force = getPlayerWeaponKnockbackForce(player); // 80
|
|
61
|
-
*
|
|
62
|
-
* // No weapon equipped
|
|
63
|
-
* const force = getPlayerWeaponKnockbackForce(player); // 50 (default)
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
function getPlayerWeaponKnockbackForce(player) {
|
|
67
|
-
try {
|
|
68
|
-
const equipments = player.equipments?.() || [];
|
|
69
|
-
for (const item of equipments) {
|
|
70
|
-
const itemData = player.databaseById?.(item.id());
|
|
71
|
-
if (itemData?._type === "weapon" && itemData.knockbackForce !== void 0) return itemData.knockbackForce;
|
|
72
|
-
}
|
|
73
|
-
} catch {}
|
|
74
|
-
return DEFAULT_KNOCKBACK.force;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Apply hit from player to target (event with AI)
|
|
78
|
-
*
|
|
79
|
-
* Handles damage calculation, knockback based on weapon, and visual effects.
|
|
80
|
-
* Can be customized using hooks.
|
|
81
|
-
*
|
|
82
|
-
* @param player - The attacking player
|
|
83
|
-
* @param target - The event being hit
|
|
84
|
-
* @param hooks - Optional hooks for customizing hit behavior
|
|
85
|
-
* @returns Hit result if AI exists, undefined otherwise
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```ts
|
|
89
|
-
* // Basic hit
|
|
90
|
-
* const result = applyPlayerHitToEvent(player, event);
|
|
91
|
-
*
|
|
92
|
-
* // With custom hooks
|
|
93
|
-
* const result = applyPlayerHitToEvent(player, event, {
|
|
94
|
-
* onBeforeHit(result) {
|
|
95
|
-
* result.knockbackForce *= 2; // Double knockback
|
|
96
|
-
* return result;
|
|
97
|
-
* },
|
|
98
|
-
* onAfterHit(result) {
|
|
99
|
-
* if (result.defeated) {
|
|
100
|
-
* player.gold += 10;
|
|
101
|
-
* }
|
|
102
|
-
* }
|
|
103
|
-
* });
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
function applyPlayerHitToEvent(player, target, hooks) {
|
|
107
|
-
const ai = target.battleAi;
|
|
108
|
-
if (!ai) return void 0;
|
|
109
|
-
const knockbackForce = getPlayerWeaponKnockbackForce(player);
|
|
110
|
-
const defeated = ai.takeDamage(player);
|
|
111
|
-
const dx = target.x() - player.x();
|
|
112
|
-
const dy = target.y() - player.y();
|
|
113
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
114
|
-
let hitResult = {
|
|
115
|
-
damage: 0,
|
|
116
|
-
knockbackForce,
|
|
117
|
-
knockbackDuration: DEFAULT_KNOCKBACK.duration,
|
|
118
|
-
defeated,
|
|
119
|
-
attacker: player,
|
|
120
|
-
target
|
|
121
|
-
};
|
|
122
|
-
if (hooks?.onBeforeHit) {
|
|
123
|
-
const modified = hooks.onBeforeHit(hitResult);
|
|
124
|
-
if (modified) hitResult = modified;
|
|
125
|
-
}
|
|
126
|
-
if (!hitResult.defeated && hitResult.knockbackForce > 0 && distance > 0) {
|
|
127
|
-
const knockbackDirection = {
|
|
128
|
-
x: dx / distance,
|
|
129
|
-
y: dy / distance
|
|
130
|
-
};
|
|
131
|
-
target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
|
|
132
|
-
}
|
|
133
|
-
if (hooks?.onAfterHit) hooks.onAfterHit(hitResult);
|
|
134
|
-
return hitResult;
|
|
135
|
-
}
|
|
136
|
-
var resolveSignal = (value) => typeof value === "function" ? value() : value;
|
|
137
|
-
var resolveItemData = (player, itemId) => {
|
|
138
|
-
try {
|
|
139
|
-
return player.databaseById?.(itemId);
|
|
140
|
-
} catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
var resolveSkillData = (player, skillId) => {
|
|
145
|
-
try {
|
|
146
|
-
return player.databaseById?.(skillId);
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
52
|
+
var resolveEquippedWeapon = (entity) => {
|
|
53
|
+
const equipments = entity?.equipments?.() || [];
|
|
54
|
+
for (const item of equipments) {
|
|
55
|
+
const itemId = item?.id?.() ?? item?.id;
|
|
56
|
+
const itemData = entity?.databaseById?.(itemId);
|
|
57
|
+
if (itemData?._type === "weapon") return itemData;
|
|
149
58
|
}
|
|
59
|
+
return null;
|
|
150
60
|
};
|
|
151
|
-
var
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const aoeMask = skillData?.aoeMask ?? skillData?.targeting?.aoeMask ?? skillData?.targeting?.mask;
|
|
157
|
-
if (range === void 0 && aoeMask === void 0) return null;
|
|
61
|
+
var resolveDirection = (attacker, target) => {
|
|
62
|
+
const dx = target.x() - attacker.x();
|
|
63
|
+
const dy = target.y() - attacker.y();
|
|
64
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
65
|
+
if (distance <= 0) return void 0;
|
|
158
66
|
return {
|
|
159
|
-
|
|
160
|
-
|
|
67
|
+
x: dx / distance,
|
|
68
|
+
y: dy / distance
|
|
161
69
|
};
|
|
162
70
|
};
|
|
163
|
-
var
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return
|
|
71
|
+
var createDefaultPlayerHitboxResolver = (hitboxes = DEFAULT_ZELDA_PLAYER_HITBOXES) => (context) => {
|
|
72
|
+
const attacker = context.attacker;
|
|
73
|
+
const config = hitboxes[context.direction ?? (typeof attacker.getDirection === "function" ? attacker.getDirection() : "default")] || hitboxes.default;
|
|
74
|
+
return [{
|
|
75
|
+
x: attacker.x() + config.offsetX,
|
|
76
|
+
y: attacker.y() + config.offsetY,
|
|
77
|
+
width: config.width,
|
|
78
|
+
height: config.height
|
|
79
|
+
}];
|
|
167
80
|
};
|
|
168
|
-
var
|
|
81
|
+
var defaultRpgjsDamageResolver = (context) => {
|
|
82
|
+
const target = context.target;
|
|
83
|
+
const raw = target.applyDamage(context.attacker, context.skill);
|
|
169
84
|
return {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const name = resolveSignal(data?.name) ?? resolveSignal(item.name) ?? id;
|
|
174
|
-
const description = resolveSignal(data?.description) ?? resolveSignal(item.description) ?? "";
|
|
175
|
-
const icon = resolveSignal(data?.icon) ?? resolveSignal(item.icon);
|
|
176
|
-
const quantity = resolveSignal(item.quantity) ?? 1;
|
|
177
|
-
const consumable = resolveSignal(data?.consumable);
|
|
178
|
-
const itemType = resolveSignal(data?._type);
|
|
179
|
-
return {
|
|
180
|
-
id,
|
|
181
|
-
name,
|
|
182
|
-
description,
|
|
183
|
-
icon,
|
|
184
|
-
quantity,
|
|
185
|
-
usable: quantity > 0 && consumable !== false && (itemType ? itemType === "item" : true)
|
|
186
|
-
};
|
|
187
|
-
}),
|
|
188
|
-
skills: (player.skills?.() || []).map((skill) => {
|
|
189
|
-
const id = skill.id?.() ?? skill.id;
|
|
190
|
-
const data = resolveSkillData(player, id) || skill;
|
|
191
|
-
const name = resolveSignal(data?.name) ?? resolveSignal(skill.name) ?? id;
|
|
192
|
-
const description = resolveSignal(data?.description) ?? resolveSignal(skill.description) ?? "";
|
|
193
|
-
const icon = resolveSignal(data?.icon) ?? resolveSignal(skill.icon);
|
|
194
|
-
const spCost = resolveSignal(data?.spCost) ?? resolveSignal(skill.spCost) ?? 0;
|
|
195
|
-
const usable = spCost <= player.sp;
|
|
196
|
-
const targeting = resolveSkillTargeting(player, id, options);
|
|
197
|
-
const skillEntry = {
|
|
198
|
-
id,
|
|
199
|
-
name,
|
|
200
|
-
description,
|
|
201
|
-
icon,
|
|
202
|
-
spCost,
|
|
203
|
-
usable,
|
|
204
|
-
range: targeting?.range ?? 0
|
|
205
|
-
};
|
|
206
|
-
if (targeting) {
|
|
207
|
-
const mask = targeting.aoeMask ?? options.skills?.defaultAoeMask;
|
|
208
|
-
if (mask) skillEntry.aoeMask = normalizeMaskRows(mask);
|
|
209
|
-
}
|
|
210
|
-
return skillEntry;
|
|
211
|
-
})
|
|
85
|
+
damage: raw?.damage ?? 0,
|
|
86
|
+
defeated: target.hp <= 0,
|
|
87
|
+
raw
|
|
212
88
|
};
|
|
213
89
|
};
|
|
214
|
-
var
|
|
215
|
-
const
|
|
216
|
-
if (!gui.__actionBattleReady) {
|
|
217
|
-
gui.__actionBattleReady = true;
|
|
218
|
-
gui.on("useItem", ({ id }) => {
|
|
219
|
-
try {
|
|
220
|
-
player.useItem(id);
|
|
221
|
-
} catch {}
|
|
222
|
-
gui.update(buildActionBarData(player, options));
|
|
223
|
-
});
|
|
224
|
-
gui.on("useSkill", ({ id, target }) => {
|
|
225
|
-
handleActionBattleSkillUse(player, id, target, options);
|
|
226
|
-
gui.update(buildActionBarData(player, options));
|
|
227
|
-
});
|
|
228
|
-
gui.on("refresh", () => {
|
|
229
|
-
gui.update(buildActionBarData(player, options));
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return gui;
|
|
233
|
-
};
|
|
234
|
-
var openActionBattleActionBar = (player, rawOptions = {}) => {
|
|
235
|
-
const options = normalizeActionBattleOptions(rawOptions);
|
|
236
|
-
ensureActionBarGui(player, options).open(buildActionBarData(player, options));
|
|
237
|
-
};
|
|
238
|
-
var updateActionBattleActionBar = (player, rawOptions = {}) => {
|
|
239
|
-
const options = normalizeActionBattleOptions(rawOptions);
|
|
240
|
-
const gui = player.getGui?.(ACTION_BATTLE_ACTION_BAR_GUI_ID);
|
|
241
|
-
if (gui) gui.update(buildActionBarData(player, options));
|
|
242
|
-
};
|
|
243
|
-
var getTileSize = (map) => ({
|
|
244
|
-
width: map?.tileWidth ?? 32,
|
|
245
|
-
height: map?.tileHeight ?? 32
|
|
246
|
-
});
|
|
247
|
-
var getEntityTile = (entity, tileSize) => {
|
|
248
|
-
const hitbox = entity.hitbox?.() || {
|
|
249
|
-
w: tileSize.width,
|
|
250
|
-
h: tileSize.height
|
|
251
|
-
};
|
|
90
|
+
var defaultKnockbackResolver = (context) => {
|
|
91
|
+
const weapon = context.weapon ?? resolveEquippedWeapon(context.attacker);
|
|
252
92
|
return {
|
|
253
|
-
|
|
254
|
-
|
|
93
|
+
force: weapon?.knockbackForce ?? DEFAULT_CORE_KNOCKBACK.force,
|
|
94
|
+
duration: weapon?.knockbackDuration ?? DEFAULT_CORE_KNOCKBACK.duration,
|
|
95
|
+
direction: resolveDirection(context.attacker, context.target)
|
|
255
96
|
};
|
|
256
97
|
};
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
})
|
|
296
|
-
if (!options.targeting?.allowEmptyTarget && targets.length === 0) return;
|
|
297
|
-
playActionBattleAnimation("castSkill", player, options.animations, {
|
|
298
|
-
skill: skillData,
|
|
299
|
-
target: targets[0]
|
|
300
|
-
});
|
|
301
|
-
player.useSkill(skillId, targets);
|
|
98
|
+
var defaultCombatSystem = {
|
|
99
|
+
resolveHitboxes: createDefaultPlayerHitboxResolver(),
|
|
100
|
+
resolveDamage: defaultRpgjsDamageResolver,
|
|
101
|
+
resolveKnockback: defaultKnockbackResolver
|
|
102
|
+
};
|
|
103
|
+
var defaultEnemyBehaviors = {
|
|
104
|
+
[CoreEnemyType.Aggressive]: ({ hpPercent }) => ({
|
|
105
|
+
mode: hpPercent !== null && hpPercent < .15 ? "retreat" : "assault",
|
|
106
|
+
attackPatterns: [
|
|
107
|
+
CoreAttackPattern.Melee,
|
|
108
|
+
CoreAttackPattern.Combo,
|
|
109
|
+
CoreAttackPattern.DashAttack
|
|
110
|
+
]
|
|
111
|
+
}),
|
|
112
|
+
[CoreEnemyType.Defensive]: ({ hpPercent }) => ({
|
|
113
|
+
mode: hpPercent !== null && hpPercent < .3 ? "retreat" : "tactical",
|
|
114
|
+
attackPatterns: [CoreAttackPattern.Melee, CoreAttackPattern.Charged]
|
|
115
|
+
}),
|
|
116
|
+
[CoreEnemyType.Ranged]: ({ distance }) => ({
|
|
117
|
+
mode: distance !== null && distance < 80 ? "retreat" : "tactical",
|
|
118
|
+
attackPatterns: [CoreAttackPattern.Melee, CoreAttackPattern.Zone]
|
|
119
|
+
}),
|
|
120
|
+
[CoreEnemyType.Tank]: () => ({
|
|
121
|
+
mode: "assault",
|
|
122
|
+
attackPatterns: [
|
|
123
|
+
CoreAttackPattern.Melee,
|
|
124
|
+
CoreAttackPattern.Charged,
|
|
125
|
+
CoreAttackPattern.Zone
|
|
126
|
+
]
|
|
127
|
+
}),
|
|
128
|
+
[CoreEnemyType.Berserker]: ({ hpPercent }) => ({
|
|
129
|
+
mode: "assault",
|
|
130
|
+
attackCooldown: hpPercent === null ? void 0 : Math.max(250, 800 * Math.max(.3, hpPercent)),
|
|
131
|
+
attackPatterns: [
|
|
132
|
+
CoreAttackPattern.Melee,
|
|
133
|
+
CoreAttackPattern.Combo,
|
|
134
|
+
CoreAttackPattern.DashAttack
|
|
135
|
+
]
|
|
136
|
+
})
|
|
302
137
|
};
|
|
303
|
-
var
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return defineModule({
|
|
307
|
-
player: {
|
|
308
|
-
/**
|
|
309
|
-
* Handle player input for combat actions
|
|
310
|
-
*
|
|
311
|
-
* When a player presses the action key, create an attack hitbox
|
|
312
|
-
* that can damage AI enemies within range and knockback the event.
|
|
313
|
-
* Knockback force is based on the player's equipped weapon.
|
|
314
|
-
* Triggers attack animation and visual effects.
|
|
315
|
-
*
|
|
316
|
-
* @param player - The player performing the action
|
|
317
|
-
* @param input - Input data containing pressed keys
|
|
318
|
-
*/
|
|
319
|
-
onInput(player, input) {
|
|
320
|
-
if (input.action == Control.Action) {
|
|
321
|
-
playActionBattleAnimation("attack", player, options.animations);
|
|
322
|
-
const playerX = player.x();
|
|
323
|
-
const playerY = player.y();
|
|
324
|
-
const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[player.getDirection()] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
|
|
325
|
-
const hitboxes = [{
|
|
326
|
-
x: playerX + hitboxConfig.offsetX,
|
|
327
|
-
y: playerY + hitboxConfig.offsetY,
|
|
328
|
-
width: hitboxConfig.width,
|
|
329
|
-
height: hitboxConfig.height
|
|
330
|
-
}];
|
|
331
|
-
player.getCurrentMap()?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({ next(hits) {
|
|
332
|
-
hits.forEach((hit) => {
|
|
333
|
-
if (hit instanceof RpgEvent) {
|
|
334
|
-
if (applyPlayerHitToEvent(player, hit)?.defeated) console.log(`Player ${player.id} defeated AI ${hit.id}`);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
} });
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
onConnected(player) {
|
|
341
|
-
if (options.ui?.actionBar?.enabled && options.ui?.actionBar?.autoOpen) openActionBattleActionBar(player, options);
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
event: {
|
|
345
|
-
/**
|
|
346
|
-
* Handle player detection when entering AI vision
|
|
347
|
-
*
|
|
348
|
-
* Called when a player enters an AI event's vision range.
|
|
349
|
-
* The AI will start pursuing and attacking the player.
|
|
350
|
-
*
|
|
351
|
-
* @param event - The AI event
|
|
352
|
-
* @param player - The player entering vision
|
|
353
|
-
* @param shape - The vision shape
|
|
354
|
-
*/
|
|
355
|
-
onDetectInShape(event, player, shape) {
|
|
356
|
-
event.battleAi?.onDetectInShape(player, shape);
|
|
357
|
-
},
|
|
358
|
-
/**
|
|
359
|
-
* Handle player leaving AI vision
|
|
360
|
-
*
|
|
361
|
-
* Called when a player leaves an AI event's vision range.
|
|
362
|
-
* The AI will stop pursuing the player.
|
|
363
|
-
*
|
|
364
|
-
* @param event - The AI event
|
|
365
|
-
* @param player - The player leaving vision
|
|
366
|
-
* @param shape - The vision shape
|
|
367
|
-
*/
|
|
368
|
-
onDetectOutShape(event, player, shape) {
|
|
369
|
-
event.battleAi?.onDetectOutShape(player, shape);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
});
|
|
138
|
+
var defaultActionBattleSystems = {
|
|
139
|
+
combat: defaultCombatSystem,
|
|
140
|
+
ai: { behaviors: defaultEnemyBehaviors }
|
|
373
141
|
};
|
|
374
|
-
createActionBattleServer();
|
|
375
142
|
//#endregion
|
|
376
|
-
export {
|
|
143
|
+
export { DEFAULT_ZELDA_PLAYER_HITBOXES, createDefaultPlayerHitboxResolver, defaultActionBattleSystems, defaultCombatSystem, defaultEnemyBehaviors, defaultKnockbackResolver, defaultRpgjsDamageResolver };
|