@rpgjs/action-battle 5.0.0-alpha.28

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/src/client.ts ADDED
@@ -0,0 +1,11 @@
1
+ import {PrebuiltComponentAnimations, RpgClient } from "@rpgjs/client";
2
+ import { defineModule } from "@rpgjs/common";
3
+
4
+ export default defineModule<RpgClient>({
5
+ componentAnimations: [
6
+ {
7
+ id: 'hit',
8
+ component: PrebuiltComponentAnimations.Hit
9
+ }
10
+ ]
11
+ })
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ import server from "./server";
2
+ import client from "./client";
3
+ import { createModule } from "@rpgjs/common";
4
+
5
+ // AI exports
6
+ export { BattleAi, AiState, EnemyType, AttackPattern, AiDebug, DEFAULT_KNOCKBACK } from "./ai.server";
7
+
8
+ // Types exports
9
+ export type { HitResult, ApplyHitHooks } from "./ai.server";
10
+
11
+ // Server exports
12
+ export { DEFAULT_PLAYER_ATTACK_HITBOXES, getPlayerWeaponKnockbackForce, applyPlayerHitToEvent } from "./server";
13
+
14
+ export function provideActionBattle() {
15
+ return createModule("ActionBattle", [
16
+ {
17
+ server,
18
+ client,
19
+ },
20
+ ]);
21
+ }
package/src/server.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { RpgEvent, RpgPlayer, type RpgServer } from "@rpgjs/server";
2
+ import { defineModule } from "@rpgjs/common";
3
+ import { BattleAi, HitResult, ApplyHitHooks, DEFAULT_KNOCKBACK } from "./ai.server";
4
+
5
+ /**
6
+ * Default player attack hitboxes offsets for each direction
7
+ *
8
+ * These hitboxes define the attack areas relative to the player's position
9
+ * for each cardinal direction. They are converted to absolute coordinates
10
+ * when creating the moving hitbox.
11
+ */
12
+ export const DEFAULT_PLAYER_ATTACK_HITBOXES = {
13
+ up: { offsetX: -16, offsetY: -48, width: 32, height: 32 },
14
+ down: { offsetX: -16, offsetY: 16, width: 32, height: 32 },
15
+ left: { offsetX: -48, offsetY: -16, width: 32, height: 32 },
16
+ right: { offsetX: 16, offsetY: -16, width: 32, height: 32 },
17
+ default: { offsetX: 0, offsetY: -32, width: 32, height: 32 }
18
+ };
19
+
20
+ /**
21
+ * Get knockback force from player's equipped weapon
22
+ *
23
+ * Retrieves the knockbackForce property from the player's equipped weapon.
24
+ * Falls back to DEFAULT_KNOCKBACK.force if no weapon or property is set.
25
+ *
26
+ * @param player - The player to get weapon knockback from
27
+ * @returns Knockback force value
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // Player with weapon having knockbackForce: 80
32
+ * const force = getPlayerWeaponKnockbackForce(player); // 80
33
+ *
34
+ * // No weapon equipped
35
+ * const force = getPlayerWeaponKnockbackForce(player); // 50 (default)
36
+ * ```
37
+ */
38
+ export function getPlayerWeaponKnockbackForce(player: RpgPlayer): number {
39
+ try {
40
+ const equipments = player.equipments?.() || [];
41
+ for (const item of equipments) {
42
+ const itemData = (player as any).databaseById?.(item.id());
43
+ if (itemData?._type === 'weapon' && itemData.knockbackForce !== undefined) {
44
+ return itemData.knockbackForce;
45
+ }
46
+ }
47
+ } catch {
48
+ // If error, return default
49
+ }
50
+ return DEFAULT_KNOCKBACK.force;
51
+ }
52
+
53
+ /**
54
+ * Apply hit from player to target (event with AI)
55
+ *
56
+ * Handles damage calculation, knockback based on weapon, and visual effects.
57
+ * Can be customized using hooks.
58
+ *
59
+ * @param player - The attacking player
60
+ * @param target - The event being hit
61
+ * @param hooks - Optional hooks for customizing hit behavior
62
+ * @returns Hit result if AI exists, undefined otherwise
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // Basic hit
67
+ * const result = applyPlayerHitToEvent(player, event);
68
+ *
69
+ * // With custom hooks
70
+ * const result = applyPlayerHitToEvent(player, event, {
71
+ * onBeforeHit(result) {
72
+ * result.knockbackForce *= 2; // Double knockback
73
+ * return result;
74
+ * },
75
+ * onAfterHit(result) {
76
+ * if (result.defeated) {
77
+ * player.gold += 10;
78
+ * }
79
+ * }
80
+ * });
81
+ * ```
82
+ */
83
+ export function applyPlayerHitToEvent(
84
+ player: RpgPlayer,
85
+ target: RpgEvent,
86
+ hooks?: ApplyHitHooks
87
+ ): HitResult | undefined {
88
+ const ai = (target as any).battleAi as BattleAi;
89
+ if (!ai) return undefined;
90
+
91
+ // Get knockback force from player's weapon
92
+ const knockbackForce = getPlayerWeaponKnockbackForce(player);
93
+
94
+ // Apply damage to AI
95
+ const defeated = ai.takeDamage(player);
96
+
97
+ // Calculate knockback direction (away from player)
98
+ const dx = target.x() - player.x();
99
+ const dy = target.y() - player.y();
100
+ const distance = Math.sqrt(dx * dx + dy * dy);
101
+
102
+ // Create hit result
103
+ let hitResult: HitResult = {
104
+ damage: 0, // Will be set by takeDamage internally
105
+ knockbackForce,
106
+ knockbackDuration: DEFAULT_KNOCKBACK.duration,
107
+ defeated,
108
+ attacker: player,
109
+ target
110
+ };
111
+
112
+ // Call onBeforeHit hook
113
+ if (hooks?.onBeforeHit) {
114
+ const modified = hooks.onBeforeHit(hitResult);
115
+ if (modified) {
116
+ hitResult = modified;
117
+ }
118
+ }
119
+
120
+ // Apply knockback only if not defeated (entity still exists)
121
+ if (!hitResult.defeated && hitResult.knockbackForce > 0 && distance > 0) {
122
+ const knockbackDirection = {
123
+ x: dx / distance,
124
+ y: dy / distance
125
+ };
126
+ target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
127
+ }
128
+
129
+ // Call onAfterHit hook
130
+ if (hooks?.onAfterHit) {
131
+ hooks.onAfterHit(hitResult);
132
+ }
133
+
134
+ return hitResult;
135
+ }
136
+
137
+ export default defineModule<RpgServer>({
138
+ player: {
139
+ /**
140
+ * Handle player input for combat actions
141
+ *
142
+ * When a player presses the action key, create an attack hitbox
143
+ * that can damage AI enemies within range and knockback the event.
144
+ * Knockback force is based on the player's equipped weapon.
145
+ * Triggers attack animation and visual effects.
146
+ *
147
+ * @param player - The player performing the action
148
+ * @param input - Input data containing pressed keys
149
+ */
150
+ onInput(player: RpgPlayer, input: any) {
151
+ if (input.action) {
152
+ // Trigger attack animation
153
+ player.setAnimation('attack', 1);
154
+
155
+ // Get player position
156
+ const playerX = player.x();
157
+ const playerY = player.y();
158
+ const direction = player.getDirection();
159
+
160
+ // Convert Direction enum to string key
161
+ const directionKey = direction as string;
162
+
163
+ // Get hitbox configuration for the direction
164
+ const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[directionKey as keyof typeof DEFAULT_PLAYER_ATTACK_HITBOXES] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
165
+
166
+ // Convert relative hitbox to absolute coordinates
167
+ const hitboxes: Array<{
168
+ x: number;
169
+ y: number;
170
+ width: number;
171
+ height: number;
172
+ }> = [{
173
+ x: playerX + hitboxConfig.offsetX,
174
+ y: playerY + hitboxConfig.offsetY,
175
+ width: hitboxConfig.width,
176
+ height: hitboxConfig.height
177
+ }];
178
+
179
+ const map = player.getCurrentMap();
180
+
181
+ map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
182
+ next(hits) {
183
+ hits.forEach((hit) => {
184
+ if (hit instanceof RpgEvent) {
185
+ const result = applyPlayerHitToEvent(player, hit);
186
+ if (result?.defeated) {
187
+ console.log(`Player ${player.id} defeated AI ${hit.id}`);
188
+ }
189
+ }
190
+ });
191
+ },
192
+ });
193
+ }
194
+ },
195
+ },
196
+ event: {
197
+ /**
198
+ * Handle player detection when entering AI vision
199
+ *
200
+ * Called when a player enters an AI event's vision range.
201
+ * The AI will start pursuing and attacking the player.
202
+ *
203
+ * @param event - The AI event
204
+ * @param player - The player entering vision
205
+ * @param shape - The vision shape
206
+ */
207
+ onDetectInShape(event: RpgEvent, player: RpgPlayer, shape: any) {
208
+ const ai = (event as any).battleAi as BattleAi;
209
+ ai?.onDetectInShape(player, shape);
210
+ },
211
+
212
+ /**
213
+ * Handle player leaving AI vision
214
+ *
215
+ * Called when a player leaves an AI event's vision range.
216
+ * The AI will stop pursuing the player.
217
+ *
218
+ * @param event - The AI event
219
+ * @param player - The player leaving vision
220
+ * @param shape - The vision shape
221
+ */
222
+ onDetectOutShape(event: RpgEvent, player: RpgPlayer, shape: any) {
223
+ const ai = (event as any).battleAi as BattleAi;
224
+ ai?.onDetectOutShape(player, shape);
225
+ },
226
+ },
227
+ });
package/vite.config.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { rpgjsModuleViteConfig } from "@rpgjs/vite";
2
+
3
+ export default rpgjsModuleViteConfig();