@rpgjs/action-battle 5.0.0-alpha.30 → 5.0.0-alpha.32
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 +1 -1
- package/dist/ai.server.d.ts +25 -0
- package/dist/client/index3.js +170 -9
- package/dist/client/index4.js +3 -3
- package/dist/server/index2.js +3 -3
- package/dist/server/index3.js +170 -9
- package/package.json +5 -5
- package/src/ai.server.ts +200 -9
- package/src/server.ts +3 -3
package/README.md
CHANGED
|
@@ -601,7 +601,7 @@ import {
|
|
|
601
601
|
|
|
602
602
|
// Custom attack with hooks
|
|
603
603
|
function customAttack(player: RpgPlayer) {
|
|
604
|
-
player.
|
|
604
|
+
player.setGraphicAnimation('attack', 1);
|
|
605
605
|
|
|
606
606
|
const direction = player.getDirection();
|
|
607
607
|
const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[direction] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
|
package/dist/ai.server.d.ts
CHANGED
|
@@ -235,6 +235,18 @@ export declare class BattleAi {
|
|
|
235
235
|
private isMovingToTarget;
|
|
236
236
|
private onDefeatedCallback?;
|
|
237
237
|
private lastFacingDirection;
|
|
238
|
+
private behaviorScore;
|
|
239
|
+
private behaviorMode;
|
|
240
|
+
private behaviorLastUpdate;
|
|
241
|
+
private behaviorUpdateInterval;
|
|
242
|
+
private behaviorAssaultThreshold;
|
|
243
|
+
private behaviorRetreatThreshold;
|
|
244
|
+
private behaviorMinStateDuration;
|
|
245
|
+
private behaviorEnabled;
|
|
246
|
+
private moveToCooldown;
|
|
247
|
+
private lastMoveToTime;
|
|
248
|
+
private retreatCooldown;
|
|
249
|
+
private lastRetreatTime;
|
|
238
250
|
/**
|
|
239
251
|
* Create a new Battle AI Controller
|
|
240
252
|
*
|
|
@@ -274,6 +286,15 @@ export declare class BattleAi {
|
|
|
274
286
|
y: number;
|
|
275
287
|
}>;
|
|
276
288
|
groupBehavior?: boolean;
|
|
289
|
+
moveToCooldown?: number;
|
|
290
|
+
retreatCooldown?: number;
|
|
291
|
+
behavior?: {
|
|
292
|
+
baseScore?: number;
|
|
293
|
+
updateInterval?: number;
|
|
294
|
+
minStateDuration?: number;
|
|
295
|
+
assaultThreshold?: number;
|
|
296
|
+
retreatThreshold?: number;
|
|
297
|
+
};
|
|
277
298
|
/** Callback called when the AI is defeated */
|
|
278
299
|
onDefeated?: (event: RpgEvent) => void;
|
|
279
300
|
});
|
|
@@ -469,6 +490,10 @@ export declare class BattleAi {
|
|
|
469
490
|
* Get distance between entities
|
|
470
491
|
*/
|
|
471
492
|
private getDistance;
|
|
493
|
+
private updateBehavior;
|
|
494
|
+
private handleTacticalMovement;
|
|
495
|
+
private handleAssaultMovement;
|
|
496
|
+
private requestMoveTo;
|
|
472
497
|
getHealth(): number;
|
|
473
498
|
getMaxHealth(): number;
|
|
474
499
|
getTarget(): InstanceType<typeof RpgPlayer> | null;
|
package/dist/client/index3.js
CHANGED
|
@@ -101,6 +101,18 @@ class BattleAi {
|
|
|
101
101
|
this.damageCheckInterval = 2e3;
|
|
102
102
|
this.isMovingToTarget = false;
|
|
103
103
|
this.lastFacingDirection = null;
|
|
104
|
+
this.behaviorScore = 50;
|
|
105
|
+
this.behaviorMode = "tactical";
|
|
106
|
+
this.behaviorLastUpdate = 0;
|
|
107
|
+
this.behaviorUpdateInterval = 400;
|
|
108
|
+
this.behaviorAssaultThreshold = 65;
|
|
109
|
+
this.behaviorRetreatThreshold = 35;
|
|
110
|
+
this.behaviorMinStateDuration = 600;
|
|
111
|
+
this.behaviorEnabled = false;
|
|
112
|
+
this.moveToCooldown = 400;
|
|
113
|
+
this.lastMoveToTime = 0;
|
|
114
|
+
this.retreatCooldown = 600;
|
|
115
|
+
this.lastRetreatTime = 0;
|
|
104
116
|
event.battleAi = this;
|
|
105
117
|
this.event = event;
|
|
106
118
|
this.enemyType = options.enemyType || "aggressive";
|
|
@@ -116,6 +128,30 @@ class BattleAi {
|
|
|
116
128
|
this.patrolWaypoints = options.patrolWaypoints || [];
|
|
117
129
|
this.currentPatrolIndex = 0;
|
|
118
130
|
this.onDefeatedCallback = options.onDefeated;
|
|
131
|
+
if (options.behavior) {
|
|
132
|
+
this.behaviorEnabled = true;
|
|
133
|
+
if (options.behavior.baseScore !== void 0) {
|
|
134
|
+
this.behaviorScore = options.behavior.baseScore;
|
|
135
|
+
}
|
|
136
|
+
if (options.behavior.updateInterval !== void 0) {
|
|
137
|
+
this.behaviorUpdateInterval = options.behavior.updateInterval;
|
|
138
|
+
}
|
|
139
|
+
if (options.behavior.minStateDuration !== void 0) {
|
|
140
|
+
this.behaviorMinStateDuration = options.behavior.minStateDuration;
|
|
141
|
+
}
|
|
142
|
+
if (options.behavior.assaultThreshold !== void 0) {
|
|
143
|
+
this.behaviorAssaultThreshold = options.behavior.assaultThreshold;
|
|
144
|
+
}
|
|
145
|
+
if (options.behavior.retreatThreshold !== void 0) {
|
|
146
|
+
this.behaviorRetreatThreshold = options.behavior.retreatThreshold;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (options.moveToCooldown !== void 0) {
|
|
150
|
+
this.moveToCooldown = options.moveToCooldown;
|
|
151
|
+
}
|
|
152
|
+
if (options.retreatCooldown !== void 0) {
|
|
153
|
+
this.retreatCooldown = options.retreatCooldown;
|
|
154
|
+
}
|
|
119
155
|
this.setupVision();
|
|
120
156
|
this.startAiBehaviorLoop();
|
|
121
157
|
this.changeState(
|
|
@@ -310,6 +346,9 @@ class BattleAi {
|
|
|
310
346
|
const berserkerModifier = Math.max(0.3, hpPercent);
|
|
311
347
|
this.attackCooldown = 800 * berserkerModifier;
|
|
312
348
|
}
|
|
349
|
+
if (this.behaviorEnabled) {
|
|
350
|
+
this.updateBehavior(currentTime);
|
|
351
|
+
}
|
|
313
352
|
switch (this.state) {
|
|
314
353
|
case "idle":
|
|
315
354
|
this.updateIdleBehavior();
|
|
@@ -403,7 +442,20 @@ class BattleAi {
|
|
|
403
442
|
this.tryDodge();
|
|
404
443
|
return;
|
|
405
444
|
}
|
|
406
|
-
if (this.
|
|
445
|
+
if (this.behaviorEnabled) {
|
|
446
|
+
if (this.behaviorMode === "tactical") {
|
|
447
|
+
this.handleTacticalMovement(distance);
|
|
448
|
+
} else if (this.behaviorMode === "assault") {
|
|
449
|
+
this.handleAssaultMovement(distance);
|
|
450
|
+
} else if (this.behaviorMode === "retreat") {
|
|
451
|
+
this.isMovingToTarget = false;
|
|
452
|
+
this.fleeFromTarget();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (this.behaviorEnabled && this.behaviorMode === "assault") ;
|
|
457
|
+
else if (this.behaviorEnabled && this.behaviorMode === "tactical") ;
|
|
458
|
+
else if (this.enemyType === "ranged") {
|
|
407
459
|
if (distance < this.attackRange * 0.6) {
|
|
408
460
|
this.debugLog("movement", `Retreating (dist=${distance.toFixed(1)}, minRange=${(this.attackRange * 0.6).toFixed(1)})`);
|
|
409
461
|
this.isMovingToTarget = false;
|
|
@@ -412,7 +464,7 @@ class BattleAi {
|
|
|
412
464
|
if (!this.isMovingToTarget) {
|
|
413
465
|
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
414
466
|
this.isMovingToTarget = true;
|
|
415
|
-
this.
|
|
467
|
+
this.requestMoveTo(this.target);
|
|
416
468
|
}
|
|
417
469
|
} else {
|
|
418
470
|
if (this.isMovingToTarget) {
|
|
@@ -426,7 +478,7 @@ class BattleAi {
|
|
|
426
478
|
if (!this.isMovingToTarget) {
|
|
427
479
|
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
428
480
|
this.isMovingToTarget = true;
|
|
429
|
-
this.
|
|
481
|
+
this.requestMoveTo(this.target);
|
|
430
482
|
}
|
|
431
483
|
} else {
|
|
432
484
|
if (this.isMovingToTarget) {
|
|
@@ -591,7 +643,7 @@ class BattleAi {
|
|
|
591
643
|
performMeleeAttack() {
|
|
592
644
|
if (!this.target) return;
|
|
593
645
|
this.faceTarget();
|
|
594
|
-
this.event.
|
|
646
|
+
this.event.setGraphicAnimation("attack", 1);
|
|
595
647
|
if (this.attackSkill) {
|
|
596
648
|
try {
|
|
597
649
|
this.event.useSkill(this.attackSkill, this.target);
|
|
@@ -758,7 +810,7 @@ class BattleAi {
|
|
|
758
810
|
if (!this.target) return;
|
|
759
811
|
this.chargingAttack = true;
|
|
760
812
|
this.faceTarget();
|
|
761
|
-
this.event.
|
|
813
|
+
this.event.setGraphicAnimation("attack", 2);
|
|
762
814
|
setTimeout(() => {
|
|
763
815
|
if (!this.target || this.state !== "combat") {
|
|
764
816
|
this.chargingAttack = false;
|
|
@@ -780,7 +832,7 @@ class BattleAi {
|
|
|
780
832
|
* Perform zone attack (360 degrees)
|
|
781
833
|
*/
|
|
782
834
|
performZoneAttack() {
|
|
783
|
-
this.event.
|
|
835
|
+
this.event.setGraphicAnimation("attack", 1);
|
|
784
836
|
const eventX = this.event.x();
|
|
785
837
|
const eventY = this.event.y();
|
|
786
838
|
const radius = 50;
|
|
@@ -915,18 +967,23 @@ class BattleAi {
|
|
|
915
967
|
x: () => this.event.x() + dx / dist * 200,
|
|
916
968
|
y: () => this.event.y() + dy / dist * 200
|
|
917
969
|
};
|
|
918
|
-
this.
|
|
970
|
+
this.requestMoveTo(fleeTarget);
|
|
919
971
|
}
|
|
920
972
|
/**
|
|
921
973
|
* Retreat from target (temporary)
|
|
922
974
|
*/
|
|
923
975
|
retreatFromTarget() {
|
|
924
976
|
if (!this.target) return;
|
|
977
|
+
const currentTime = Date.now();
|
|
978
|
+
if (currentTime - this.lastRetreatTime < this.retreatCooldown) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
925
981
|
const dx = this.event.x() - this.target.x();
|
|
926
982
|
const dy = this.event.y() - this.target.y();
|
|
927
983
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
928
984
|
if (dist === 0) return;
|
|
929
985
|
this.event.dash({ x: dx / dist, y: dy / dist }, 8, 200);
|
|
986
|
+
this.lastRetreatTime = currentTime;
|
|
930
987
|
}
|
|
931
988
|
/**
|
|
932
989
|
* Check damage taken for retreat decision
|
|
@@ -944,7 +1001,7 @@ class BattleAi {
|
|
|
944
1001
|
startPatrol() {
|
|
945
1002
|
if (this.patrolWaypoints.length === 0) return;
|
|
946
1003
|
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
947
|
-
this.
|
|
1004
|
+
this.requestMoveTo({ x: () => waypoint.x, y: () => waypoint.y });
|
|
948
1005
|
}
|
|
949
1006
|
/**
|
|
950
1007
|
* Update group behavior
|
|
@@ -1001,7 +1058,7 @@ class BattleAi {
|
|
|
1001
1058
|
Math.pow(this.event.x() - formationX, 2) + Math.pow(this.event.y() - formationY, 2)
|
|
1002
1059
|
);
|
|
1003
1060
|
if (distanceToFormation > 20) {
|
|
1004
|
-
this.
|
|
1061
|
+
this.requestMoveTo({ x: () => formationX, y: () => formationY });
|
|
1005
1062
|
}
|
|
1006
1063
|
}
|
|
1007
1064
|
/**
|
|
@@ -1091,6 +1148,110 @@ class BattleAi {
|
|
|
1091
1148
|
const dy = entity1.y() - entity2.y();
|
|
1092
1149
|
return Math.sqrt(dx * dx + dy * dy);
|
|
1093
1150
|
}
|
|
1151
|
+
updateBehavior(currentTime) {
|
|
1152
|
+
if (currentTime - this.behaviorLastUpdate < this.behaviorUpdateInterval) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
this.behaviorLastUpdate = currentTime;
|
|
1156
|
+
let score = this.behaviorScore;
|
|
1157
|
+
const maxHp = this.event.param[MAXHP];
|
|
1158
|
+
if (maxHp) {
|
|
1159
|
+
const hpPercent = this.event.hp / maxHp;
|
|
1160
|
+
score += (hpPercent - 0.5) * 40;
|
|
1161
|
+
}
|
|
1162
|
+
if (this.recentDamageTaken > 0) {
|
|
1163
|
+
score -= Math.min(30, this.recentDamageTaken * 0.5);
|
|
1164
|
+
}
|
|
1165
|
+
if (this.target) {
|
|
1166
|
+
const distance = this.getDistance(this.event, this.target);
|
|
1167
|
+
if (distance <= this.attackRange) {
|
|
1168
|
+
score += 10;
|
|
1169
|
+
} else if (distance > this.visionRange) {
|
|
1170
|
+
score -= 10;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (this.groupBehavior && this.nearbyEnemies.length > 0) {
|
|
1174
|
+
score += Math.min(15, this.nearbyEnemies.length * 5);
|
|
1175
|
+
}
|
|
1176
|
+
score = Math.max(0, Math.min(100, score));
|
|
1177
|
+
this.behaviorScore = score;
|
|
1178
|
+
const previousMode = this.behaviorMode;
|
|
1179
|
+
if (score >= this.behaviorAssaultThreshold) {
|
|
1180
|
+
this.behaviorMode = "assault";
|
|
1181
|
+
} else if (score <= this.behaviorRetreatThreshold) {
|
|
1182
|
+
this.behaviorMode = "retreat";
|
|
1183
|
+
} else {
|
|
1184
|
+
this.behaviorMode = "tactical";
|
|
1185
|
+
}
|
|
1186
|
+
if (previousMode !== this.behaviorMode) {
|
|
1187
|
+
this.debugLog("state", `Behavior mode: ${previousMode} -> ${this.behaviorMode} (score=${score.toFixed(0)})`);
|
|
1188
|
+
}
|
|
1189
|
+
if (this.behaviorMode === "retreat" && this.state === "combat") {
|
|
1190
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1191
|
+
this.isMovingToTarget = false;
|
|
1192
|
+
this.changeState(
|
|
1193
|
+
"flee"
|
|
1194
|
+
/* Flee */
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
} else if (this.behaviorMode === "assault" && this.state === "flee") {
|
|
1198
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1199
|
+
this.changeState(
|
|
1200
|
+
"combat"
|
|
1201
|
+
/* Combat */
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
handleTacticalMovement(distance) {
|
|
1207
|
+
if (!this.target) return;
|
|
1208
|
+
const minRange = this.attackRange * 0.7;
|
|
1209
|
+
const maxRange = this.attackRange * 1.2;
|
|
1210
|
+
if (distance < minRange) {
|
|
1211
|
+
this.debugLog("movement", `Tactical retreat (dist=${distance.toFixed(1)}, minRange=${minRange.toFixed(1)})`);
|
|
1212
|
+
this.isMovingToTarget = false;
|
|
1213
|
+
this.retreatFromTarget();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (distance > maxRange) {
|
|
1217
|
+
if (!this.isMovingToTarget) {
|
|
1218
|
+
this.debugLog("movement", `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
|
|
1219
|
+
this.isMovingToTarget = true;
|
|
1220
|
+
this.requestMoveTo(this.target);
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (this.isMovingToTarget) {
|
|
1225
|
+
this.debugLog("movement", `Tactical hold (dist=${distance.toFixed(1)})`);
|
|
1226
|
+
this.isMovingToTarget = false;
|
|
1227
|
+
this.event.stopMoveTo();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
handleAssaultMovement(distance) {
|
|
1231
|
+
if (!this.target) return;
|
|
1232
|
+
if (distance > this.attackRange) {
|
|
1233
|
+
if (!this.isMovingToTarget) {
|
|
1234
|
+
this.debugLog("movement", `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1235
|
+
this.isMovingToTarget = true;
|
|
1236
|
+
this.requestMoveTo(this.target);
|
|
1237
|
+
}
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (this.isMovingToTarget) {
|
|
1241
|
+
this.debugLog("movement", `Assault hold (dist=${distance.toFixed(1)})`);
|
|
1242
|
+
this.isMovingToTarget = false;
|
|
1243
|
+
this.event.stopMoveTo();
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
requestMoveTo(target) {
|
|
1247
|
+
const currentTime = Date.now();
|
|
1248
|
+
if (currentTime - this.lastMoveToTime < this.moveToCooldown) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
this.event.moveTo(target);
|
|
1252
|
+
this.lastMoveToTime = currentTime;
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1094
1255
|
// Public getters
|
|
1095
1256
|
getHealth() {
|
|
1096
1257
|
return this.event.hp;
|
package/dist/client/index4.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineModule } from "@rpgjs/common";
|
|
1
|
+
import { defineModule, Control } from "@rpgjs/common";
|
|
2
2
|
const RpgEvent = null;
|
|
3
3
|
const DEFAULT_KNOCKBACK = null;
|
|
4
4
|
const DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
@@ -70,8 +70,8 @@ defineModule({
|
|
|
70
70
|
* @param input - Input data containing pressed keys
|
|
71
71
|
*/
|
|
72
72
|
onInput(player, input) {
|
|
73
|
-
if (input.action) {
|
|
74
|
-
player.
|
|
73
|
+
if (input.action == Control.Action) {
|
|
74
|
+
player.setGraphicAnimation("attack", 1);
|
|
75
75
|
const playerX = player.x();
|
|
76
76
|
const playerY = player.y();
|
|
77
77
|
const direction = player.getDirection();
|
package/dist/server/index2.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpgEvent } from "@rpgjs/server";
|
|
2
|
-
import { defineModule } from "@rpgjs/common";
|
|
2
|
+
import { defineModule, Control } from "@rpgjs/common";
|
|
3
3
|
import { DEFAULT_KNOCKBACK } from "./index3.js";
|
|
4
4
|
const DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
5
5
|
up: { offsetX: -16, offsetY: -48, width: 32, height: 32 },
|
|
@@ -70,8 +70,8 @@ const server = defineModule({
|
|
|
70
70
|
* @param input - Input data containing pressed keys
|
|
71
71
|
*/
|
|
72
72
|
onInput(player, input) {
|
|
73
|
-
if (input.action) {
|
|
74
|
-
player.
|
|
73
|
+
if (input.action == Control.Action) {
|
|
74
|
+
player.setGraphicAnimation("attack", 1);
|
|
75
75
|
const playerX = player.x();
|
|
76
76
|
const playerY = player.y();
|
|
77
77
|
const direction = player.getDirection();
|
package/dist/server/index3.js
CHANGED
|
@@ -100,6 +100,18 @@ class BattleAi {
|
|
|
100
100
|
this.damageCheckInterval = 2e3;
|
|
101
101
|
this.isMovingToTarget = false;
|
|
102
102
|
this.lastFacingDirection = null;
|
|
103
|
+
this.behaviorScore = 50;
|
|
104
|
+
this.behaviorMode = "tactical";
|
|
105
|
+
this.behaviorLastUpdate = 0;
|
|
106
|
+
this.behaviorUpdateInterval = 400;
|
|
107
|
+
this.behaviorAssaultThreshold = 65;
|
|
108
|
+
this.behaviorRetreatThreshold = 35;
|
|
109
|
+
this.behaviorMinStateDuration = 600;
|
|
110
|
+
this.behaviorEnabled = false;
|
|
111
|
+
this.moveToCooldown = 400;
|
|
112
|
+
this.lastMoveToTime = 0;
|
|
113
|
+
this.retreatCooldown = 600;
|
|
114
|
+
this.lastRetreatTime = 0;
|
|
103
115
|
event.battleAi = this;
|
|
104
116
|
this.event = event;
|
|
105
117
|
this.enemyType = options.enemyType || "aggressive";
|
|
@@ -115,6 +127,30 @@ class BattleAi {
|
|
|
115
127
|
this.patrolWaypoints = options.patrolWaypoints || [];
|
|
116
128
|
this.currentPatrolIndex = 0;
|
|
117
129
|
this.onDefeatedCallback = options.onDefeated;
|
|
130
|
+
if (options.behavior) {
|
|
131
|
+
this.behaviorEnabled = true;
|
|
132
|
+
if (options.behavior.baseScore !== void 0) {
|
|
133
|
+
this.behaviorScore = options.behavior.baseScore;
|
|
134
|
+
}
|
|
135
|
+
if (options.behavior.updateInterval !== void 0) {
|
|
136
|
+
this.behaviorUpdateInterval = options.behavior.updateInterval;
|
|
137
|
+
}
|
|
138
|
+
if (options.behavior.minStateDuration !== void 0) {
|
|
139
|
+
this.behaviorMinStateDuration = options.behavior.minStateDuration;
|
|
140
|
+
}
|
|
141
|
+
if (options.behavior.assaultThreshold !== void 0) {
|
|
142
|
+
this.behaviorAssaultThreshold = options.behavior.assaultThreshold;
|
|
143
|
+
}
|
|
144
|
+
if (options.behavior.retreatThreshold !== void 0) {
|
|
145
|
+
this.behaviorRetreatThreshold = options.behavior.retreatThreshold;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (options.moveToCooldown !== void 0) {
|
|
149
|
+
this.moveToCooldown = options.moveToCooldown;
|
|
150
|
+
}
|
|
151
|
+
if (options.retreatCooldown !== void 0) {
|
|
152
|
+
this.retreatCooldown = options.retreatCooldown;
|
|
153
|
+
}
|
|
118
154
|
this.setupVision();
|
|
119
155
|
this.startAiBehaviorLoop();
|
|
120
156
|
this.changeState(
|
|
@@ -309,6 +345,9 @@ class BattleAi {
|
|
|
309
345
|
const berserkerModifier = Math.max(0.3, hpPercent);
|
|
310
346
|
this.attackCooldown = 800 * berserkerModifier;
|
|
311
347
|
}
|
|
348
|
+
if (this.behaviorEnabled) {
|
|
349
|
+
this.updateBehavior(currentTime);
|
|
350
|
+
}
|
|
312
351
|
switch (this.state) {
|
|
313
352
|
case "idle":
|
|
314
353
|
this.updateIdleBehavior();
|
|
@@ -402,7 +441,20 @@ class BattleAi {
|
|
|
402
441
|
this.tryDodge();
|
|
403
442
|
return;
|
|
404
443
|
}
|
|
405
|
-
if (this.
|
|
444
|
+
if (this.behaviorEnabled) {
|
|
445
|
+
if (this.behaviorMode === "tactical") {
|
|
446
|
+
this.handleTacticalMovement(distance);
|
|
447
|
+
} else if (this.behaviorMode === "assault") {
|
|
448
|
+
this.handleAssaultMovement(distance);
|
|
449
|
+
} else if (this.behaviorMode === "retreat") {
|
|
450
|
+
this.isMovingToTarget = false;
|
|
451
|
+
this.fleeFromTarget();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (this.behaviorEnabled && this.behaviorMode === "assault") ;
|
|
456
|
+
else if (this.behaviorEnabled && this.behaviorMode === "tactical") ;
|
|
457
|
+
else if (this.enemyType === "ranged") {
|
|
406
458
|
if (distance < this.attackRange * 0.6) {
|
|
407
459
|
this.debugLog("movement", `Retreating (dist=${distance.toFixed(1)}, minRange=${(this.attackRange * 0.6).toFixed(1)})`);
|
|
408
460
|
this.isMovingToTarget = false;
|
|
@@ -411,7 +463,7 @@ class BattleAi {
|
|
|
411
463
|
if (!this.isMovingToTarget) {
|
|
412
464
|
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
413
465
|
this.isMovingToTarget = true;
|
|
414
|
-
this.
|
|
466
|
+
this.requestMoveTo(this.target);
|
|
415
467
|
}
|
|
416
468
|
} else {
|
|
417
469
|
if (this.isMovingToTarget) {
|
|
@@ -425,7 +477,7 @@ class BattleAi {
|
|
|
425
477
|
if (!this.isMovingToTarget) {
|
|
426
478
|
this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
427
479
|
this.isMovingToTarget = true;
|
|
428
|
-
this.
|
|
480
|
+
this.requestMoveTo(this.target);
|
|
429
481
|
}
|
|
430
482
|
} else {
|
|
431
483
|
if (this.isMovingToTarget) {
|
|
@@ -590,7 +642,7 @@ class BattleAi {
|
|
|
590
642
|
performMeleeAttack() {
|
|
591
643
|
if (!this.target) return;
|
|
592
644
|
this.faceTarget();
|
|
593
|
-
this.event.
|
|
645
|
+
this.event.setGraphicAnimation("attack", 1);
|
|
594
646
|
if (this.attackSkill) {
|
|
595
647
|
try {
|
|
596
648
|
this.event.useSkill(this.attackSkill, this.target);
|
|
@@ -757,7 +809,7 @@ class BattleAi {
|
|
|
757
809
|
if (!this.target) return;
|
|
758
810
|
this.chargingAttack = true;
|
|
759
811
|
this.faceTarget();
|
|
760
|
-
this.event.
|
|
812
|
+
this.event.setGraphicAnimation("attack", 2);
|
|
761
813
|
setTimeout(() => {
|
|
762
814
|
if (!this.target || this.state !== "combat") {
|
|
763
815
|
this.chargingAttack = false;
|
|
@@ -779,7 +831,7 @@ class BattleAi {
|
|
|
779
831
|
* Perform zone attack (360 degrees)
|
|
780
832
|
*/
|
|
781
833
|
performZoneAttack() {
|
|
782
|
-
this.event.
|
|
834
|
+
this.event.setGraphicAnimation("attack", 1);
|
|
783
835
|
const eventX = this.event.x();
|
|
784
836
|
const eventY = this.event.y();
|
|
785
837
|
const radius = 50;
|
|
@@ -914,18 +966,23 @@ class BattleAi {
|
|
|
914
966
|
x: () => this.event.x() + dx / dist * 200,
|
|
915
967
|
y: () => this.event.y() + dy / dist * 200
|
|
916
968
|
};
|
|
917
|
-
this.
|
|
969
|
+
this.requestMoveTo(fleeTarget);
|
|
918
970
|
}
|
|
919
971
|
/**
|
|
920
972
|
* Retreat from target (temporary)
|
|
921
973
|
*/
|
|
922
974
|
retreatFromTarget() {
|
|
923
975
|
if (!this.target) return;
|
|
976
|
+
const currentTime = Date.now();
|
|
977
|
+
if (currentTime - this.lastRetreatTime < this.retreatCooldown) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
924
980
|
const dx = this.event.x() - this.target.x();
|
|
925
981
|
const dy = this.event.y() - this.target.y();
|
|
926
982
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
927
983
|
if (dist === 0) return;
|
|
928
984
|
this.event.dash({ x: dx / dist, y: dy / dist }, 8, 200);
|
|
985
|
+
this.lastRetreatTime = currentTime;
|
|
929
986
|
}
|
|
930
987
|
/**
|
|
931
988
|
* Check damage taken for retreat decision
|
|
@@ -943,7 +1000,7 @@ class BattleAi {
|
|
|
943
1000
|
startPatrol() {
|
|
944
1001
|
if (this.patrolWaypoints.length === 0) return;
|
|
945
1002
|
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
946
|
-
this.
|
|
1003
|
+
this.requestMoveTo({ x: () => waypoint.x, y: () => waypoint.y });
|
|
947
1004
|
}
|
|
948
1005
|
/**
|
|
949
1006
|
* Update group behavior
|
|
@@ -1000,7 +1057,7 @@ class BattleAi {
|
|
|
1000
1057
|
Math.pow(this.event.x() - formationX, 2) + Math.pow(this.event.y() - formationY, 2)
|
|
1001
1058
|
);
|
|
1002
1059
|
if (distanceToFormation > 20) {
|
|
1003
|
-
this.
|
|
1060
|
+
this.requestMoveTo({ x: () => formationX, y: () => formationY });
|
|
1004
1061
|
}
|
|
1005
1062
|
}
|
|
1006
1063
|
/**
|
|
@@ -1090,6 +1147,110 @@ class BattleAi {
|
|
|
1090
1147
|
const dy = entity1.y() - entity2.y();
|
|
1091
1148
|
return Math.sqrt(dx * dx + dy * dy);
|
|
1092
1149
|
}
|
|
1150
|
+
updateBehavior(currentTime) {
|
|
1151
|
+
if (currentTime - this.behaviorLastUpdate < this.behaviorUpdateInterval) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
this.behaviorLastUpdate = currentTime;
|
|
1155
|
+
let score = this.behaviorScore;
|
|
1156
|
+
const maxHp = this.event.param[MAXHP];
|
|
1157
|
+
if (maxHp) {
|
|
1158
|
+
const hpPercent = this.event.hp / maxHp;
|
|
1159
|
+
score += (hpPercent - 0.5) * 40;
|
|
1160
|
+
}
|
|
1161
|
+
if (this.recentDamageTaken > 0) {
|
|
1162
|
+
score -= Math.min(30, this.recentDamageTaken * 0.5);
|
|
1163
|
+
}
|
|
1164
|
+
if (this.target) {
|
|
1165
|
+
const distance = this.getDistance(this.event, this.target);
|
|
1166
|
+
if (distance <= this.attackRange) {
|
|
1167
|
+
score += 10;
|
|
1168
|
+
} else if (distance > this.visionRange) {
|
|
1169
|
+
score -= 10;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (this.groupBehavior && this.nearbyEnemies.length > 0) {
|
|
1173
|
+
score += Math.min(15, this.nearbyEnemies.length * 5);
|
|
1174
|
+
}
|
|
1175
|
+
score = Math.max(0, Math.min(100, score));
|
|
1176
|
+
this.behaviorScore = score;
|
|
1177
|
+
const previousMode = this.behaviorMode;
|
|
1178
|
+
if (score >= this.behaviorAssaultThreshold) {
|
|
1179
|
+
this.behaviorMode = "assault";
|
|
1180
|
+
} else if (score <= this.behaviorRetreatThreshold) {
|
|
1181
|
+
this.behaviorMode = "retreat";
|
|
1182
|
+
} else {
|
|
1183
|
+
this.behaviorMode = "tactical";
|
|
1184
|
+
}
|
|
1185
|
+
if (previousMode !== this.behaviorMode) {
|
|
1186
|
+
this.debugLog("state", `Behavior mode: ${previousMode} -> ${this.behaviorMode} (score=${score.toFixed(0)})`);
|
|
1187
|
+
}
|
|
1188
|
+
if (this.behaviorMode === "retreat" && this.state === "combat") {
|
|
1189
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1190
|
+
this.isMovingToTarget = false;
|
|
1191
|
+
this.changeState(
|
|
1192
|
+
"flee"
|
|
1193
|
+
/* Flee */
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
} else if (this.behaviorMode === "assault" && this.state === "flee") {
|
|
1197
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1198
|
+
this.changeState(
|
|
1199
|
+
"combat"
|
|
1200
|
+
/* Combat */
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
handleTacticalMovement(distance) {
|
|
1206
|
+
if (!this.target) return;
|
|
1207
|
+
const minRange = this.attackRange * 0.7;
|
|
1208
|
+
const maxRange = this.attackRange * 1.2;
|
|
1209
|
+
if (distance < minRange) {
|
|
1210
|
+
this.debugLog("movement", `Tactical retreat (dist=${distance.toFixed(1)}, minRange=${minRange.toFixed(1)})`);
|
|
1211
|
+
this.isMovingToTarget = false;
|
|
1212
|
+
this.retreatFromTarget();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (distance > maxRange) {
|
|
1216
|
+
if (!this.isMovingToTarget) {
|
|
1217
|
+
this.debugLog("movement", `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
|
|
1218
|
+
this.isMovingToTarget = true;
|
|
1219
|
+
this.requestMoveTo(this.target);
|
|
1220
|
+
}
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (this.isMovingToTarget) {
|
|
1224
|
+
this.debugLog("movement", `Tactical hold (dist=${distance.toFixed(1)})`);
|
|
1225
|
+
this.isMovingToTarget = false;
|
|
1226
|
+
this.event.stopMoveTo();
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
handleAssaultMovement(distance) {
|
|
1230
|
+
if (!this.target) return;
|
|
1231
|
+
if (distance > this.attackRange) {
|
|
1232
|
+
if (!this.isMovingToTarget) {
|
|
1233
|
+
this.debugLog("movement", `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1234
|
+
this.isMovingToTarget = true;
|
|
1235
|
+
this.requestMoveTo(this.target);
|
|
1236
|
+
}
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (this.isMovingToTarget) {
|
|
1240
|
+
this.debugLog("movement", `Assault hold (dist=${distance.toFixed(1)})`);
|
|
1241
|
+
this.isMovingToTarget = false;
|
|
1242
|
+
this.event.stopMoveTo();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
requestMoveTo(target) {
|
|
1246
|
+
const currentTime = Date.now();
|
|
1247
|
+
if (currentTime - this.lastMoveToTime < this.moveToCooldown) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
this.event.moveTo(target);
|
|
1251
|
+
this.lastMoveToTime = currentTime;
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1093
1254
|
// Public getters
|
|
1094
1255
|
getHealth() {
|
|
1095
1256
|
return this.event.hp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/action-battle",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.32",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@canvasengine/presets": "*",
|
|
26
|
-
"@rpgjs/client": "5.0.0-alpha.
|
|
27
|
-
"@rpgjs/common": "5.0.0-alpha.
|
|
28
|
-
"@rpgjs/server": "5.0.0-alpha.
|
|
29
|
-
"@rpgjs/vite": "5.0.0-alpha.
|
|
26
|
+
"@rpgjs/client": "5.0.0-alpha.32",
|
|
27
|
+
"@rpgjs/common": "5.0.0-alpha.32",
|
|
28
|
+
"@rpgjs/server": "5.0.0-alpha.32",
|
|
29
|
+
"@rpgjs/vite": "5.0.0-alpha.32",
|
|
30
30
|
"canvasengine": "*"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
package/src/ai.server.ts
CHANGED
|
@@ -285,6 +285,21 @@ export class BattleAi {
|
|
|
285
285
|
// Direction hysteresis to prevent animation flickering
|
|
286
286
|
private lastFacingDirection: string | null = null;
|
|
287
287
|
|
|
288
|
+
// Behavior gauge (0-100)
|
|
289
|
+
private behaviorScore: number = 50;
|
|
290
|
+
private behaviorMode: 'assault' | 'tactical' | 'retreat' = 'tactical';
|
|
291
|
+
private behaviorLastUpdate: number = 0;
|
|
292
|
+
private behaviorUpdateInterval: number = 400;
|
|
293
|
+
private behaviorAssaultThreshold: number = 65;
|
|
294
|
+
private behaviorRetreatThreshold: number = 35;
|
|
295
|
+
private behaviorMinStateDuration: number = 600;
|
|
296
|
+
private behaviorEnabled: boolean = false;
|
|
297
|
+
|
|
298
|
+
// Movement throttling
|
|
299
|
+
private moveToCooldown: number = 400;
|
|
300
|
+
private lastMoveToTime: number = 0;
|
|
301
|
+
private retreatCooldown: number = 600;
|
|
302
|
+
private lastRetreatTime: number = 0;
|
|
288
303
|
|
|
289
304
|
/**
|
|
290
305
|
* Create a new Battle AI Controller
|
|
@@ -324,6 +339,15 @@ export class BattleAi {
|
|
|
324
339
|
attackPatterns?: AttackPattern[];
|
|
325
340
|
patrolWaypoints?: Array<{ x: number; y: number }>;
|
|
326
341
|
groupBehavior?: boolean;
|
|
342
|
+
moveToCooldown?: number;
|
|
343
|
+
retreatCooldown?: number;
|
|
344
|
+
behavior?: {
|
|
345
|
+
baseScore?: number;
|
|
346
|
+
updateInterval?: number;
|
|
347
|
+
minStateDuration?: number;
|
|
348
|
+
assaultThreshold?: number;
|
|
349
|
+
retreatThreshold?: number;
|
|
350
|
+
};
|
|
327
351
|
/** Callback called when the AI is defeated */
|
|
328
352
|
onDefeated?: (event: RpgEvent) => void;
|
|
329
353
|
} = {}
|
|
@@ -355,6 +379,33 @@ export class BattleAi {
|
|
|
355
379
|
// Initialize defeat callback
|
|
356
380
|
this.onDefeatedCallback = options.onDefeated;
|
|
357
381
|
|
|
382
|
+
// Behavior gauge settings
|
|
383
|
+
if (options.behavior) {
|
|
384
|
+
this.behaviorEnabled = true;
|
|
385
|
+
if (options.behavior.baseScore !== undefined) {
|
|
386
|
+
this.behaviorScore = options.behavior.baseScore;
|
|
387
|
+
}
|
|
388
|
+
if (options.behavior.updateInterval !== undefined) {
|
|
389
|
+
this.behaviorUpdateInterval = options.behavior.updateInterval;
|
|
390
|
+
}
|
|
391
|
+
if (options.behavior.minStateDuration !== undefined) {
|
|
392
|
+
this.behaviorMinStateDuration = options.behavior.minStateDuration;
|
|
393
|
+
}
|
|
394
|
+
if (options.behavior.assaultThreshold !== undefined) {
|
|
395
|
+
this.behaviorAssaultThreshold = options.behavior.assaultThreshold;
|
|
396
|
+
}
|
|
397
|
+
if (options.behavior.retreatThreshold !== undefined) {
|
|
398
|
+
this.behaviorRetreatThreshold = options.behavior.retreatThreshold;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (options.moveToCooldown !== undefined) {
|
|
403
|
+
this.moveToCooldown = options.moveToCooldown;
|
|
404
|
+
}
|
|
405
|
+
if (options.retreatCooldown !== undefined) {
|
|
406
|
+
this.retreatCooldown = options.retreatCooldown;
|
|
407
|
+
}
|
|
408
|
+
|
|
358
409
|
// Setup AI systems
|
|
359
410
|
this.setupVision();
|
|
360
411
|
this.startAiBehaviorLoop();
|
|
@@ -531,6 +582,11 @@ export class BattleAi {
|
|
|
531
582
|
this.attackCooldown = 800 * berserkerModifier;
|
|
532
583
|
}
|
|
533
584
|
|
|
585
|
+
// Update behavior gauge and state decision
|
|
586
|
+
if (this.behaviorEnabled) {
|
|
587
|
+
this.updateBehavior(currentTime);
|
|
588
|
+
}
|
|
589
|
+
|
|
534
590
|
// State-specific behavior
|
|
535
591
|
switch (this.state) {
|
|
536
592
|
case AiState.Idle:
|
|
@@ -625,8 +681,24 @@ export class BattleAi {
|
|
|
625
681
|
return;
|
|
626
682
|
}
|
|
627
683
|
|
|
684
|
+
if (this.behaviorEnabled) {
|
|
685
|
+
if (this.behaviorMode === 'tactical') {
|
|
686
|
+
this.handleTacticalMovement(distance);
|
|
687
|
+
} else if (this.behaviorMode === 'assault') {
|
|
688
|
+
this.handleAssaultMovement(distance);
|
|
689
|
+
} else if (this.behaviorMode === 'retreat') {
|
|
690
|
+
this.isMovingToTarget = false;
|
|
691
|
+
this.fleeFromTarget();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
628
696
|
// Movement based on enemy type
|
|
629
|
-
if (this.
|
|
697
|
+
if (this.behaviorEnabled && this.behaviorMode === 'assault') {
|
|
698
|
+
// Assault mode already handled movement
|
|
699
|
+
} else if (this.behaviorEnabled && this.behaviorMode === 'tactical') {
|
|
700
|
+
// Tactical mode already handled movement
|
|
701
|
+
} else if (this.enemyType === EnemyType.Ranged) {
|
|
630
702
|
if (distance < this.attackRange * 0.6) {
|
|
631
703
|
this.debugLog('movement', `Retreating (dist=${distance.toFixed(1)}, minRange=${(this.attackRange * 0.6).toFixed(1)})`);
|
|
632
704
|
this.isMovingToTarget = false;
|
|
@@ -635,7 +707,7 @@ export class BattleAi {
|
|
|
635
707
|
if (!this.isMovingToTarget) {
|
|
636
708
|
this.debugLog('movement', `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
637
709
|
this.isMovingToTarget = true;
|
|
638
|
-
this.
|
|
710
|
+
this.requestMoveTo(this.target);
|
|
639
711
|
}
|
|
640
712
|
} else {
|
|
641
713
|
if (this.isMovingToTarget) {
|
|
@@ -649,7 +721,7 @@ export class BattleAi {
|
|
|
649
721
|
if (!this.isMovingToTarget) {
|
|
650
722
|
this.debugLog('movement', `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
651
723
|
this.isMovingToTarget = true;
|
|
652
|
-
this.
|
|
724
|
+
this.requestMoveTo(this.target);
|
|
653
725
|
}
|
|
654
726
|
} else {
|
|
655
727
|
if (this.isMovingToTarget) {
|
|
@@ -795,7 +867,7 @@ export class BattleAi {
|
|
|
795
867
|
if (!this.target) return;
|
|
796
868
|
|
|
797
869
|
this.faceTarget();
|
|
798
|
-
this.event.
|
|
870
|
+
this.event.setGraphicAnimation('attack', 1);
|
|
799
871
|
|
|
800
872
|
// Use skill if available
|
|
801
873
|
if (this.attackSkill) {
|
|
@@ -994,7 +1066,7 @@ export class BattleAi {
|
|
|
994
1066
|
|
|
995
1067
|
this.chargingAttack = true;
|
|
996
1068
|
this.faceTarget();
|
|
997
|
-
this.event.
|
|
1069
|
+
this.event.setGraphicAnimation('attack', 2);
|
|
998
1070
|
|
|
999
1071
|
setTimeout(() => {
|
|
1000
1072
|
if (!this.target || this.state !== AiState.Combat) {
|
|
@@ -1021,7 +1093,7 @@ export class BattleAi {
|
|
|
1021
1093
|
* Perform zone attack (360 degrees)
|
|
1022
1094
|
*/
|
|
1023
1095
|
private performZoneAttack() {
|
|
1024
|
-
this.event.
|
|
1096
|
+
this.event.setGraphicAnimation('attack', 1);
|
|
1025
1097
|
|
|
1026
1098
|
const eventX = this.event.x();
|
|
1027
1099
|
const eventY = this.event.y();
|
|
@@ -1202,7 +1274,7 @@ export class BattleAi {
|
|
|
1202
1274
|
y: () => this.event.y() + (dy / dist) * 200
|
|
1203
1275
|
};
|
|
1204
1276
|
|
|
1205
|
-
this.
|
|
1277
|
+
this.requestMoveTo(fleeTarget as any);
|
|
1206
1278
|
}
|
|
1207
1279
|
|
|
1208
1280
|
/**
|
|
@@ -1210,6 +1282,10 @@ export class BattleAi {
|
|
|
1210
1282
|
*/
|
|
1211
1283
|
private retreatFromTarget() {
|
|
1212
1284
|
if (!this.target) return;
|
|
1285
|
+
const currentTime = Date.now();
|
|
1286
|
+
if (currentTime - this.lastRetreatTime < this.retreatCooldown) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1213
1289
|
|
|
1214
1290
|
const dx = this.event.x() - this.target.x();
|
|
1215
1291
|
const dy = this.event.y() - this.target.y();
|
|
@@ -1218,6 +1294,7 @@ export class BattleAi {
|
|
|
1218
1294
|
if (dist === 0) return;
|
|
1219
1295
|
|
|
1220
1296
|
this.event.dash({ x: dx / dist, y: dy / dist }, 8, 200);
|
|
1297
|
+
this.lastRetreatTime = currentTime;
|
|
1221
1298
|
}
|
|
1222
1299
|
|
|
1223
1300
|
/**
|
|
@@ -1239,7 +1316,7 @@ export class BattleAi {
|
|
|
1239
1316
|
if (this.patrolWaypoints.length === 0) return;
|
|
1240
1317
|
|
|
1241
1318
|
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
1242
|
-
this.
|
|
1319
|
+
this.requestMoveTo({ x: () => waypoint.x, y: () => waypoint.y } as any);
|
|
1243
1320
|
}
|
|
1244
1321
|
|
|
1245
1322
|
/**
|
|
@@ -1311,7 +1388,7 @@ export class BattleAi {
|
|
|
1311
1388
|
);
|
|
1312
1389
|
|
|
1313
1390
|
if (distanceToFormation > 20) {
|
|
1314
|
-
this.
|
|
1391
|
+
this.requestMoveTo({ x: () => formationX, y: () => formationY } as any);
|
|
1315
1392
|
}
|
|
1316
1393
|
}
|
|
1317
1394
|
|
|
@@ -1409,6 +1486,120 @@ export class BattleAi {
|
|
|
1409
1486
|
return Math.sqrt(dx * dx + dy * dy);
|
|
1410
1487
|
}
|
|
1411
1488
|
|
|
1489
|
+
private updateBehavior(currentTime: number) {
|
|
1490
|
+
if (currentTime - this.behaviorLastUpdate < this.behaviorUpdateInterval) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
this.behaviorLastUpdate = currentTime;
|
|
1494
|
+
|
|
1495
|
+
let score = this.behaviorScore;
|
|
1496
|
+
const maxHp = this.event.param[MAXHP];
|
|
1497
|
+
if (maxHp) {
|
|
1498
|
+
const hpPercent = this.event.hp / maxHp;
|
|
1499
|
+
score += (hpPercent - 0.5) * 40;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (this.recentDamageTaken > 0) {
|
|
1503
|
+
score -= Math.min(30, this.recentDamageTaken * 0.5);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (this.target) {
|
|
1507
|
+
const distance = this.getDistance(this.event, this.target);
|
|
1508
|
+
if (distance <= this.attackRange) {
|
|
1509
|
+
score += 10;
|
|
1510
|
+
} else if (distance > this.visionRange) {
|
|
1511
|
+
score -= 10;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (this.groupBehavior && this.nearbyEnemies.length > 0) {
|
|
1516
|
+
score += Math.min(15, this.nearbyEnemies.length * 5);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
score = Math.max(0, Math.min(100, score));
|
|
1520
|
+
this.behaviorScore = score;
|
|
1521
|
+
|
|
1522
|
+
const previousMode = this.behaviorMode;
|
|
1523
|
+
if (score >= this.behaviorAssaultThreshold) {
|
|
1524
|
+
this.behaviorMode = 'assault';
|
|
1525
|
+
} else if (score <= this.behaviorRetreatThreshold) {
|
|
1526
|
+
this.behaviorMode = 'retreat';
|
|
1527
|
+
} else {
|
|
1528
|
+
this.behaviorMode = 'tactical';
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (previousMode !== this.behaviorMode) {
|
|
1532
|
+
this.debugLog('state', `Behavior mode: ${previousMode} -> ${this.behaviorMode} (score=${score.toFixed(0)})`);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (this.behaviorMode === 'retreat' && this.state === AiState.Combat) {
|
|
1536
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1537
|
+
this.isMovingToTarget = false;
|
|
1538
|
+
this.changeState(AiState.Flee);
|
|
1539
|
+
}
|
|
1540
|
+
} else if (this.behaviorMode === 'assault' && this.state === AiState.Flee) {
|
|
1541
|
+
if (currentTime - this.stateStartTime >= this.behaviorMinStateDuration) {
|
|
1542
|
+
this.changeState(AiState.Combat);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
private handleTacticalMovement(distance: number) {
|
|
1548
|
+
if (!this.target) return;
|
|
1549
|
+
const minRange = this.attackRange * 0.7;
|
|
1550
|
+
const maxRange = this.attackRange * 1.2;
|
|
1551
|
+
|
|
1552
|
+
if (distance < minRange) {
|
|
1553
|
+
this.debugLog('movement', `Tactical retreat (dist=${distance.toFixed(1)}, minRange=${minRange.toFixed(1)})`);
|
|
1554
|
+
this.isMovingToTarget = false;
|
|
1555
|
+
this.retreatFromTarget();
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (distance > maxRange) {
|
|
1560
|
+
if (!this.isMovingToTarget) {
|
|
1561
|
+
this.debugLog('movement', `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
|
|
1562
|
+
this.isMovingToTarget = true;
|
|
1563
|
+
this.requestMoveTo(this.target);
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (this.isMovingToTarget) {
|
|
1569
|
+
this.debugLog('movement', `Tactical hold (dist=${distance.toFixed(1)})`);
|
|
1570
|
+
this.isMovingToTarget = false;
|
|
1571
|
+
this.event.stopMoveTo();
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private handleAssaultMovement(distance: number) {
|
|
1576
|
+
if (!this.target) return;
|
|
1577
|
+
if (distance > this.attackRange) {
|
|
1578
|
+
if (!this.isMovingToTarget) {
|
|
1579
|
+
this.debugLog('movement', `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1580
|
+
this.isMovingToTarget = true;
|
|
1581
|
+
this.requestMoveTo(this.target);
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (this.isMovingToTarget) {
|
|
1587
|
+
this.debugLog('movement', `Assault hold (dist=${distance.toFixed(1)})`);
|
|
1588
|
+
this.isMovingToTarget = false;
|
|
1589
|
+
this.event.stopMoveTo();
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private requestMoveTo(target: any): boolean {
|
|
1594
|
+
const currentTime = Date.now();
|
|
1595
|
+
if (currentTime - this.lastMoveToTime < this.moveToCooldown) {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
this.event.moveTo(target as any);
|
|
1599
|
+
this.lastMoveToTime = currentTime;
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1412
1603
|
// Public getters
|
|
1413
1604
|
getHealth(): number { return this.event.hp; }
|
|
1414
1605
|
getMaxHealth(): number { return this.event.param[MAXHP]; }
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpgEvent, RpgPlayer, type RpgServer } from "@rpgjs/server";
|
|
2
|
-
import { defineModule } from "@rpgjs/common";
|
|
2
|
+
import { Control, defineModule } from "@rpgjs/common";
|
|
3
3
|
import { BattleAi, HitResult, ApplyHitHooks, DEFAULT_KNOCKBACK } from "./ai.server";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -148,9 +148,9 @@ export default defineModule<RpgServer>({
|
|
|
148
148
|
* @param input - Input data containing pressed keys
|
|
149
149
|
*/
|
|
150
150
|
onInput(player: RpgPlayer, input: any) {
|
|
151
|
-
if (input.action) {
|
|
151
|
+
if (input.action == Control.Action) {
|
|
152
152
|
// Trigger attack animation
|
|
153
|
-
player.
|
|
153
|
+
player.setGraphicAnimation('attack', 1);
|
|
154
154
|
|
|
155
155
|
// Get player position
|
|
156
156
|
const playerX = player.x();
|