@rpgjs/action-battle 5.0.0-beta.12 → 5.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/client/ai.server.d.ts +12 -0
- package/dist/client/core/defaults.d.ts +1 -1
- package/dist/client/index17.js +11 -1
- package/dist/client/index21.js +2 -1
- package/dist/client/index22.js +2 -1
- package/dist/client/index26.js +316 -74
- package/dist/server/ai.server.d.ts +12 -0
- package/dist/server/core/defaults.d.ts +1 -1
- package/dist/server/index13.js +2 -1
- package/dist/server/index14.js +2 -1
- package/dist/server/index19.js +316 -74
- package/dist/server/index7.js +11 -1
- package/package.json +5 -5
- package/src/ai.server.spec.ts +147 -1
- package/src/ai.server.ts +375 -68
- package/src/core/action-use.ts +2 -1
- package/src/core/defaults.ts +16 -1
- package/src/core/hit.spec.ts +21 -0
- package/src/core/targets.spec.ts +12 -0
- package/src/core/targets.ts +4 -1
- package/src/visual.ts +1 -1
package/src/ai.server.ts
CHANGED
|
@@ -60,6 +60,31 @@ type RpgEventWithBattleAi = RpgEvent & {
|
|
|
60
60
|
battleAi?: BattleAi;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
type ResolvedMoveTarget =
|
|
64
|
+
| {
|
|
65
|
+
kind: "entity";
|
|
66
|
+
target: ActionBattleEntity;
|
|
67
|
+
id?: string;
|
|
68
|
+
x?: number;
|
|
69
|
+
y?: number;
|
|
70
|
+
signature: string;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
kind: "position";
|
|
74
|
+
target: { x: number; y: number };
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
signature: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const resolveMoveCoordinate = (value: unknown): number | undefined => {
|
|
81
|
+
const raw = typeof value === "function" ? (value as () => unknown)() : value;
|
|
82
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const createMoveSignature = (x: number, y: number): string =>
|
|
86
|
+
`position:${Math.round(x)}:${Math.round(y)}`;
|
|
87
|
+
|
|
63
88
|
export interface BattleAiRewardItem {
|
|
64
89
|
item?: any;
|
|
65
90
|
itemId?: string;
|
|
@@ -334,16 +359,10 @@ export const AiDebug = {
|
|
|
334
359
|
* @param data - Optional additional data
|
|
335
360
|
*/
|
|
336
361
|
log(category: string, eventId: string | undefined, message: string, data?: any): void {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const prefix = `[AI:${category}]${eventId ? ` [${eventId.substring(0, 8)}]` : ''}`;
|
|
342
|
-
if (data !== undefined) {
|
|
343
|
-
console.log(prefix, message, data);
|
|
344
|
-
} else {
|
|
345
|
-
console.log(prefix, message);
|
|
346
|
-
}
|
|
362
|
+
void category;
|
|
363
|
+
void eventId;
|
|
364
|
+
void message;
|
|
365
|
+
void data;
|
|
347
366
|
}
|
|
348
367
|
};
|
|
349
368
|
|
|
@@ -497,6 +516,37 @@ export class BattleAi {
|
|
|
497
516
|
AiDebug.log(category, this.event.id, message, data);
|
|
498
517
|
}
|
|
499
518
|
|
|
519
|
+
private traceLog(category: string, message: string, data?: any): void {
|
|
520
|
+
void category;
|
|
521
|
+
void message;
|
|
522
|
+
void data;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private lockActionUntil(until: number, reason: string, data?: any): void {
|
|
526
|
+
if (until <= this.actionLockedUntil) return;
|
|
527
|
+
this.actionLockedUntil = until;
|
|
528
|
+
this.traceLog("state", "action locked", {
|
|
529
|
+
reason,
|
|
530
|
+
lockedMs: Math.max(0, until - Date.now()),
|
|
531
|
+
...data,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private lockForAttack(
|
|
536
|
+
profile: NormalizedActionBattleAttackProfile,
|
|
537
|
+
pattern: AttackPattern
|
|
538
|
+
): void {
|
|
539
|
+
this.isMovingToTarget = false;
|
|
540
|
+
this.event.stopMoveTo();
|
|
541
|
+
this.lockActionUntil(Date.now() + profile.totalDurationMs, "attack", {
|
|
542
|
+
pattern,
|
|
543
|
+
totalDurationMs: profile.totalDurationMs,
|
|
544
|
+
startupMs: profile.startupMs,
|
|
545
|
+
activeMs: profile.activeMs,
|
|
546
|
+
recoveryMs: profile.recoveryMs,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
500
550
|
// State machine
|
|
501
551
|
private state: AiState = AiState.Idle;
|
|
502
552
|
private stateStartTime: number = 0;
|
|
@@ -570,6 +620,11 @@ export class BattleAi {
|
|
|
570
620
|
private lastMoveToTime: number = 0;
|
|
571
621
|
private retreatCooldown: number = 600;
|
|
572
622
|
private lastRetreatTime: number = 0;
|
|
623
|
+
private actionLockedUntil: number = 0;
|
|
624
|
+
private lastActionLockTraceTime: number = 0;
|
|
625
|
+
private lastMoveToCooldownTraceTime: number = 0;
|
|
626
|
+
private lastMoveToCooldownTraceSignature: string | null = null;
|
|
627
|
+
private lastTargetMovementSkipTraceTime: number = 0;
|
|
573
628
|
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
574
629
|
private behaviorKey?: string;
|
|
575
630
|
private behaviorTree?: ActionBattleAiTreeNode;
|
|
@@ -581,6 +636,7 @@ export class BattleAi {
|
|
|
581
636
|
private visionSetupRetries: number = 0;
|
|
582
637
|
private maxVisionSetupRetries: number = 20;
|
|
583
638
|
private destroyed: boolean = false;
|
|
639
|
+
private lastNoTargetTraceTime: number = 0;
|
|
584
640
|
|
|
585
641
|
/**
|
|
586
642
|
* Create a new Battle AI Controller
|
|
@@ -790,6 +846,10 @@ export class BattleAi {
|
|
|
790
846
|
map?.physic?.getEntityByUUID &&
|
|
791
847
|
!map.physic.getEntityByUUID(this.event.id)
|
|
792
848
|
) {
|
|
849
|
+
this.traceLog("vision", "physics body not ready", {
|
|
850
|
+
retries: this.visionSetupRetries,
|
|
851
|
+
hasMap: !!map,
|
|
852
|
+
});
|
|
793
853
|
return false;
|
|
794
854
|
}
|
|
795
855
|
|
|
@@ -800,14 +860,29 @@ export class BattleAi {
|
|
|
800
860
|
height: diameter,
|
|
801
861
|
angle: 360,
|
|
802
862
|
});
|
|
803
|
-
if (!shape)
|
|
863
|
+
if (!shape) {
|
|
864
|
+
this.traceLog("vision", "attachShape returned no shape", {
|
|
865
|
+
retries: this.visionSetupRetries,
|
|
866
|
+
visionRange: this.visionRange,
|
|
867
|
+
});
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
804
870
|
this.visionShape = shape;
|
|
871
|
+
this.traceLog("vision", "vision attached", {
|
|
872
|
+
shapeId: (shape as any)?.id,
|
|
873
|
+
visionRange: this.visionRange,
|
|
874
|
+
});
|
|
805
875
|
return true;
|
|
806
876
|
}
|
|
807
877
|
|
|
808
878
|
private scheduleVisionSetup() {
|
|
809
879
|
if (this.destroyed || this.setupVision()) return;
|
|
810
|
-
if (this.visionSetupRetries >= this.maxVisionSetupRetries)
|
|
880
|
+
if (this.visionSetupRetries >= this.maxVisionSetupRetries) {
|
|
881
|
+
this.traceLog("vision", "vision setup gave up", {
|
|
882
|
+
retries: this.visionSetupRetries,
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
811
886
|
|
|
812
887
|
this.visionSetupRetries++;
|
|
813
888
|
this.schedule(() => {
|
|
@@ -848,10 +923,19 @@ export class BattleAi {
|
|
|
848
923
|
|
|
849
924
|
if (!validTransitions[this.state].includes(newState)) {
|
|
850
925
|
this.debugLog('state', `INVALID transition ${this.state} -> ${newState}`);
|
|
926
|
+
this.traceLog("state", "invalid transition", {
|
|
927
|
+
from: this.state,
|
|
928
|
+
to: newState,
|
|
929
|
+
});
|
|
851
930
|
return;
|
|
852
931
|
}
|
|
853
932
|
|
|
854
933
|
this.debugLog('state', `STATE change: ${this.state} -> ${newState}`);
|
|
934
|
+
this.traceLog("state", "state change", {
|
|
935
|
+
from: this.state,
|
|
936
|
+
to: newState,
|
|
937
|
+
targetId: this.target?.id,
|
|
938
|
+
});
|
|
855
939
|
this.state = newState;
|
|
856
940
|
this.stateStartTime = Date.now();
|
|
857
941
|
|
|
@@ -905,6 +989,20 @@ export class BattleAi {
|
|
|
905
989
|
return;
|
|
906
990
|
}
|
|
907
991
|
|
|
992
|
+
if (currentTime < this.actionLockedUntil) {
|
|
993
|
+
if (this.target) this.faceTarget();
|
|
994
|
+
if (currentTime - this.lastActionLockTraceTime > 250) {
|
|
995
|
+
this.lastActionLockTraceTime = currentTime;
|
|
996
|
+
this.traceLog("state", "waiting action recovery", {
|
|
997
|
+
remainingMs: this.actionLockedUntil - currentTime,
|
|
998
|
+
state: this.state,
|
|
999
|
+
targetId: this.target?.id,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
this.checkDamageTaken();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
908
1006
|
// Berserker: faster attacks when HP is low
|
|
909
1007
|
if (this.enemyType === EnemyType.Berserker && this.event.param[MAXHP]) {
|
|
910
1008
|
const hpPercent = this.event.hp / this.event.param[MAXHP];
|
|
@@ -982,8 +1080,28 @@ export class BattleAi {
|
|
|
982
1080
|
this.faceTarget();
|
|
983
1081
|
|
|
984
1082
|
const distance = this.getDistance(this.event, this.target);
|
|
1083
|
+
this.traceLog("movement", "alert update", {
|
|
1084
|
+
targetId: this.target.id,
|
|
1085
|
+
distance,
|
|
1086
|
+
attackRange: this.attackRange,
|
|
1087
|
+
visionRange: this.visionRange,
|
|
1088
|
+
isMovingToTarget: this.isMovingToTarget,
|
|
1089
|
+
});
|
|
985
1090
|
if (distance <= this.attackRange * 1.5) {
|
|
1091
|
+
if (this.isMovingToTarget) {
|
|
1092
|
+
this.isMovingToTarget = false;
|
|
1093
|
+
this.event.stopMoveTo();
|
|
1094
|
+
}
|
|
986
1095
|
this.changeState(AiState.Combat);
|
|
1096
|
+
} else if (distance <= this.visionRange * 1.5) {
|
|
1097
|
+
if (!this.isMovingToTarget) {
|
|
1098
|
+
this.debugLog('movement', `Alert approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1099
|
+
this.requestTargetMovement();
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
this.debugLog('combat', `Alert target out of range (dist=${distance.toFixed(1)})`);
|
|
1103
|
+
this.clearTarget();
|
|
1104
|
+
this.changeState(AiState.Idle);
|
|
987
1105
|
}
|
|
988
1106
|
} else {
|
|
989
1107
|
this.changeState(AiState.Idle);
|
|
@@ -1001,6 +1119,15 @@ export class BattleAi {
|
|
|
1001
1119
|
}
|
|
1002
1120
|
|
|
1003
1121
|
const distance = this.getDistance(this.event, this.target);
|
|
1122
|
+
this.traceLog("combat", "combat update", {
|
|
1123
|
+
targetId: this.target.id,
|
|
1124
|
+
distance,
|
|
1125
|
+
attackRange: this.attackRange,
|
|
1126
|
+
visionRange: this.visionRange,
|
|
1127
|
+
isMovingToTarget: this.isMovingToTarget,
|
|
1128
|
+
behaviorEnabled: this.behaviorEnabled,
|
|
1129
|
+
behaviorMode: this.behaviorMode,
|
|
1130
|
+
});
|
|
1004
1131
|
|
|
1005
1132
|
// Check if target is still in range
|
|
1006
1133
|
if (distance > this.visionRange * 1.5) {
|
|
@@ -1055,8 +1182,7 @@ export class BattleAi {
|
|
|
1055
1182
|
} else if (distance > this.attackRange) {
|
|
1056
1183
|
if (!this.isMovingToTarget) {
|
|
1057
1184
|
this.debugLog('movement', `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1058
|
-
this.
|
|
1059
|
-
this.requestMoveTo(this.target);
|
|
1185
|
+
this.requestTargetMovement();
|
|
1060
1186
|
}
|
|
1061
1187
|
} else {
|
|
1062
1188
|
if (this.isMovingToTarget) {
|
|
@@ -1069,8 +1195,7 @@ export class BattleAi {
|
|
|
1069
1195
|
if (distance > this.attackRange) {
|
|
1070
1196
|
if (!this.isMovingToTarget) {
|
|
1071
1197
|
this.debugLog('movement', `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
1072
|
-
this.
|
|
1073
|
-
this.requestMoveTo(this.target);
|
|
1198
|
+
this.requestTargetMovement();
|
|
1074
1199
|
}
|
|
1075
1200
|
} else {
|
|
1076
1201
|
if (this.isMovingToTarget) {
|
|
@@ -1217,15 +1342,9 @@ export class BattleAi {
|
|
|
1217
1342
|
const profile = this.getAttackProfile(AttackPattern.Melee);
|
|
1218
1343
|
|
|
1219
1344
|
this.faceTarget({ force: true });
|
|
1345
|
+
this.lockForAttack(profile, AttackPattern.Melee);
|
|
1220
1346
|
this.telegraphAttack(profile);
|
|
1221
|
-
|
|
1222
|
-
emitActionBattleClientVisual({
|
|
1223
|
-
moment: "attack",
|
|
1224
|
-
entity: this.event,
|
|
1225
|
-
target: this.target,
|
|
1226
|
-
animations: this.animations,
|
|
1227
|
-
});
|
|
1228
|
-
});
|
|
1347
|
+
this.playAttackVisual(profile, AttackPattern.Melee);
|
|
1229
1348
|
|
|
1230
1349
|
this.scheduleAttackStartup(profile, () => {
|
|
1231
1350
|
this.executeMeleeAttack(profile, AttackPattern.Melee);
|
|
@@ -1439,12 +1558,12 @@ export class BattleAi {
|
|
|
1439
1558
|
// Visual feedback
|
|
1440
1559
|
withActionBattleAnimationUnlocked(target, () => {
|
|
1441
1560
|
emitActionBattleClientVisual({
|
|
1442
|
-
moment: "
|
|
1561
|
+
moment: "hurt",
|
|
1443
1562
|
entity: this.event,
|
|
1444
1563
|
target,
|
|
1564
|
+
attacker: this.event,
|
|
1445
1565
|
damage: hitResult.damage,
|
|
1446
1566
|
result: hitResult,
|
|
1447
|
-
animations: this.animations,
|
|
1448
1567
|
});
|
|
1449
1568
|
});
|
|
1450
1569
|
setActionBattleInvincibility(
|
|
@@ -1526,15 +1645,9 @@ export class BattleAi {
|
|
|
1526
1645
|
this.comboCount++;
|
|
1527
1646
|
const profile = this.getAttackProfile(AttackPattern.Combo);
|
|
1528
1647
|
this.faceTarget({ force: true });
|
|
1648
|
+
this.lockForAttack(profile, AttackPattern.Combo);
|
|
1529
1649
|
this.telegraphAttack(profile);
|
|
1530
|
-
|
|
1531
|
-
emitActionBattleClientVisual({
|
|
1532
|
-
moment: "attack",
|
|
1533
|
-
entity: this.event,
|
|
1534
|
-
target: this.target,
|
|
1535
|
-
animations: this.animations,
|
|
1536
|
-
});
|
|
1537
|
-
});
|
|
1650
|
+
this.playAttackVisual(profile, AttackPattern.Combo);
|
|
1538
1651
|
this.scheduleAttackStartup(profile, () => {
|
|
1539
1652
|
this.executeMeleeAttack(profile, AttackPattern.Combo);
|
|
1540
1653
|
});
|
|
@@ -1561,16 +1674,9 @@ export class BattleAi {
|
|
|
1561
1674
|
|
|
1562
1675
|
this.chargingAttack = true;
|
|
1563
1676
|
this.faceTarget({ force: true });
|
|
1677
|
+
this.lockForAttack(profile, AttackPattern.Charged);
|
|
1564
1678
|
this.telegraphAttack(profile);
|
|
1565
|
-
|
|
1566
|
-
emitActionBattleClientVisual({
|
|
1567
|
-
moment: "attack",
|
|
1568
|
-
entity: this.event,
|
|
1569
|
-
target: this.target,
|
|
1570
|
-
animations: this.animations,
|
|
1571
|
-
animationDefaults: { repeat: 2 },
|
|
1572
|
-
});
|
|
1573
|
-
});
|
|
1679
|
+
this.playAttackVisual(profile, AttackPattern.Charged, { repeat: 2 });
|
|
1574
1680
|
|
|
1575
1681
|
this.scheduleAttackStartup(profile, () => {
|
|
1576
1682
|
if (!this.target || this.state !== AiState.Combat) {
|
|
@@ -1589,15 +1695,9 @@ export class BattleAi {
|
|
|
1589
1695
|
*/
|
|
1590
1696
|
private performZoneAttack() {
|
|
1591
1697
|
const profile = this.getAttackProfile(AttackPattern.Zone);
|
|
1698
|
+
this.lockForAttack(profile, AttackPattern.Zone);
|
|
1592
1699
|
this.telegraphAttack(profile);
|
|
1593
|
-
|
|
1594
|
-
emitActionBattleClientVisual({
|
|
1595
|
-
moment: "attack",
|
|
1596
|
-
entity: this.event,
|
|
1597
|
-
target: this.target ?? undefined,
|
|
1598
|
-
animations: this.animations,
|
|
1599
|
-
});
|
|
1600
|
-
});
|
|
1700
|
+
this.playAttackVisual(profile, AttackPattern.Zone);
|
|
1601
1701
|
|
|
1602
1702
|
const eventX = this.event.x();
|
|
1603
1703
|
const eventY = this.event.y();
|
|
@@ -1651,7 +1751,9 @@ export class BattleAi {
|
|
|
1651
1751
|
const dirY = dy / dist;
|
|
1652
1752
|
|
|
1653
1753
|
this.faceTarget({ force: true });
|
|
1754
|
+
this.lockForAttack(profile, AttackPattern.DashAttack);
|
|
1654
1755
|
this.telegraphAttack(profile);
|
|
1756
|
+
this.playAttackVisual(profile, AttackPattern.DashAttack);
|
|
1655
1757
|
|
|
1656
1758
|
this.scheduleAttackStartup(profile, () => {
|
|
1657
1759
|
if (!this.target || this.state !== AiState.Combat) return;
|
|
@@ -1671,6 +1773,28 @@ export class BattleAi {
|
|
|
1671
1773
|
] ?? this.attackProfiles.melee;
|
|
1672
1774
|
}
|
|
1673
1775
|
|
|
1776
|
+
private playAttackVisual(
|
|
1777
|
+
profile: NormalizedActionBattleAttackProfile,
|
|
1778
|
+
pattern: AttackPattern,
|
|
1779
|
+
animationDefaults?: { animationName?: string; repeat?: number }
|
|
1780
|
+
): void {
|
|
1781
|
+
const moment =
|
|
1782
|
+
profile.animationKey === "castSkill" || profile.animationKey === "castSpell"
|
|
1783
|
+
? "castSkill"
|
|
1784
|
+
: "attack";
|
|
1785
|
+
|
|
1786
|
+
withActionBattleAnimationUnlocked(this.event, () => {
|
|
1787
|
+
emitActionBattleClientVisual({
|
|
1788
|
+
moment,
|
|
1789
|
+
entity: this.event,
|
|
1790
|
+
target: this.target ?? undefined,
|
|
1791
|
+
pattern,
|
|
1792
|
+
animations: this.animations,
|
|
1793
|
+
animationDefaults,
|
|
1794
|
+
});
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1674
1798
|
private telegraphAttack(profile: NormalizedActionBattleAttackProfile) {
|
|
1675
1799
|
if (profile.startupMs <= 0) return;
|
|
1676
1800
|
this.event.flash({
|
|
@@ -1820,8 +1944,8 @@ export class BattleAi {
|
|
|
1820
1944
|
if (dist === 0) return;
|
|
1821
1945
|
|
|
1822
1946
|
const fleeTarget = {
|
|
1823
|
-
x:
|
|
1824
|
-
y:
|
|
1947
|
+
x: this.event.x() + (dx / dist) * 200,
|
|
1948
|
+
y: this.event.y() + (dy / dist) * 200
|
|
1825
1949
|
};
|
|
1826
1950
|
|
|
1827
1951
|
this.requestMoveTo(fleeTarget as any);
|
|
@@ -1868,7 +1992,7 @@ export class BattleAi {
|
|
|
1868
1992
|
if (this.patrolWaypoints.length === 0) return;
|
|
1869
1993
|
|
|
1870
1994
|
const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
|
|
1871
|
-
this.requestMoveTo({ x:
|
|
1995
|
+
this.requestMoveTo({ x: waypoint.x, y: waypoint.y } as any);
|
|
1872
1996
|
}
|
|
1873
1997
|
|
|
1874
1998
|
/**
|
|
@@ -1940,7 +2064,7 @@ export class BattleAi {
|
|
|
1940
2064
|
);
|
|
1941
2065
|
|
|
1942
2066
|
if (distanceToFormation > 20) {
|
|
1943
|
-
this.requestMoveTo({ x:
|
|
2067
|
+
this.requestMoveTo({ x: formationX, y: formationY } as any);
|
|
1944
2068
|
}
|
|
1945
2069
|
}
|
|
1946
2070
|
|
|
@@ -1948,12 +2072,28 @@ export class BattleAi {
|
|
|
1948
2072
|
* Handle player entering vision
|
|
1949
2073
|
*/
|
|
1950
2074
|
onDetectInShape(target: ActionBattleEntity, shape: any) {
|
|
1951
|
-
|
|
2075
|
+
const canTarget = this.canTarget(target);
|
|
2076
|
+
const defeated = this.isTargetDefeated(target);
|
|
2077
|
+
this.traceLog("vision", "detect in shape", {
|
|
2078
|
+
targetId: target?.id,
|
|
2079
|
+
shapeId: shape?.id,
|
|
2080
|
+
canTarget,
|
|
2081
|
+
defeated,
|
|
2082
|
+
state: this.state,
|
|
2083
|
+
targetHp: (target as any)?.hp,
|
|
2084
|
+
});
|
|
2085
|
+
if (!canTarget || defeated) return;
|
|
1952
2086
|
this.debugLog('vision', `Target ${target.id} entered vision (state=${this.state})`);
|
|
1953
2087
|
this.engageTarget(target);
|
|
1954
2088
|
}
|
|
1955
2089
|
|
|
1956
2090
|
private engageTarget(target: ActionBattleEntity) {
|
|
2091
|
+
this.traceLog("target", "engage target", {
|
|
2092
|
+
targetId: target.id,
|
|
2093
|
+
previousTargetId: this.target?.id,
|
|
2094
|
+
state: this.state,
|
|
2095
|
+
distance: this.getDistance(this.event, target),
|
|
2096
|
+
});
|
|
1957
2097
|
this.target = target;
|
|
1958
2098
|
|
|
1959
2099
|
if (this.state === AiState.Idle) {
|
|
@@ -1968,6 +2108,12 @@ export class BattleAi {
|
|
|
1968
2108
|
*/
|
|
1969
2109
|
onDetectOutShape(target: ActionBattleEntity, shape: any) {
|
|
1970
2110
|
this.debugLog('vision', `Target ${target.id} left vision (wasTarget=${this.target === target})`);
|
|
2111
|
+
this.traceLog("vision", "detect out shape", {
|
|
2112
|
+
targetId: target?.id,
|
|
2113
|
+
shapeId: shape?.id,
|
|
2114
|
+
wasTarget: this.target === target,
|
|
2115
|
+
state: this.state,
|
|
2116
|
+
});
|
|
1971
2117
|
if (this.target === target) {
|
|
1972
2118
|
this.clearTarget();
|
|
1973
2119
|
this.changeState(AiState.Idle);
|
|
@@ -1998,8 +2144,21 @@ export class BattleAi {
|
|
|
1998
2144
|
}
|
|
1999
2145
|
): boolean {
|
|
2000
2146
|
if (this.defeated) return true;
|
|
2001
|
-
const damage = damageResult.damage;
|
|
2147
|
+
const damage = Number.isFinite(damageResult.damage) ? damageResult.damage : 0;
|
|
2002
2148
|
this.debugLog('damage', `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || '?'})`);
|
|
2149
|
+
const canRetaliate = attacker ? this.canTarget(attacker) : false;
|
|
2150
|
+
const attackerDefeated = this.isTargetDefeated(attacker);
|
|
2151
|
+
this.traceLog("damage", "handle damage", {
|
|
2152
|
+
attackerId: attacker?.id,
|
|
2153
|
+
damage,
|
|
2154
|
+
defeated: damageResult.defeated,
|
|
2155
|
+
eventHp: this.event.hp,
|
|
2156
|
+
maxHp: this.event.param[MAXHP],
|
|
2157
|
+
state: this.state,
|
|
2158
|
+
canRetaliate,
|
|
2159
|
+
attackerDefeated,
|
|
2160
|
+
currentTargetId: this.target?.id,
|
|
2161
|
+
});
|
|
2003
2162
|
|
|
2004
2163
|
// Visual feedback
|
|
2005
2164
|
withActionBattleAnimationUnlocked(this.event, () => {
|
|
@@ -2018,10 +2177,36 @@ export class BattleAi {
|
|
|
2018
2177
|
// Track damage
|
|
2019
2178
|
this.recentDamageTaken += damage;
|
|
2020
2179
|
|
|
2180
|
+
if (
|
|
2181
|
+
attacker &&
|
|
2182
|
+
this.canTarget(attacker) &&
|
|
2183
|
+
!this.isTargetDefeated(attacker) &&
|
|
2184
|
+
this.state !== AiState.Flee
|
|
2185
|
+
) {
|
|
2186
|
+
this.traceLog("target", "retaliate against attacker", {
|
|
2187
|
+
attackerId: attacker.id,
|
|
2188
|
+
previousTargetId: this.target?.id,
|
|
2189
|
+
state: this.state,
|
|
2190
|
+
});
|
|
2191
|
+
this.target = attacker;
|
|
2192
|
+
if (this.state === AiState.Idle || this.state === AiState.Alert) {
|
|
2193
|
+
this.isMovingToTarget = false;
|
|
2194
|
+
this.changeState(AiState.Combat);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2021
2198
|
const reaction = damageResult.reaction;
|
|
2022
2199
|
const staggerPower = reaction?.staggerPower ?? damage;
|
|
2023
2200
|
const hitstunMs = reaction?.hitstunMs ?? this.hitstunMs;
|
|
2024
|
-
const shouldStun =
|
|
2201
|
+
const shouldStun =
|
|
2202
|
+
(damage > 0 || (reaction?.staggerPower ?? 0) > 0) &&
|
|
2203
|
+
staggerPower >= this.poise &&
|
|
2204
|
+
hitstunMs > 0;
|
|
2205
|
+
this.lockActionUntil(Date.now() + Math.max(220, hitstunMs + 120), "damage recovery", {
|
|
2206
|
+
attackerId: attacker?.id,
|
|
2207
|
+
damage,
|
|
2208
|
+
hitstunMs,
|
|
2209
|
+
});
|
|
2025
2210
|
setActionBattleInvincibility(
|
|
2026
2211
|
this.event,
|
|
2027
2212
|
reaction?.invincibilityMs ?? this.invincibilityMs
|
|
@@ -2157,7 +2342,10 @@ export class BattleAi {
|
|
|
2157
2342
|
|
|
2158
2343
|
private findNearestTarget(): ActionBattleEntity | null {
|
|
2159
2344
|
const map = this.event.getCurrentMap();
|
|
2160
|
-
if (!map)
|
|
2345
|
+
if (!map) {
|
|
2346
|
+
this.traceLog("target", "find nearest skipped: no map");
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2161
2349
|
|
|
2162
2350
|
const candidates: ActionBattleEntity[] = [];
|
|
2163
2351
|
map.getPlayers?.().forEach((player: RpgPlayer) => candidates.push(player));
|
|
@@ -2175,6 +2363,31 @@ export class BattleAi {
|
|
|
2175
2363
|
}
|
|
2176
2364
|
}
|
|
2177
2365
|
|
|
2366
|
+
const now = Date.now();
|
|
2367
|
+
if (nearest) {
|
|
2368
|
+
this.traceLog("target", "nearest target found", {
|
|
2369
|
+
targetId: nearest.id,
|
|
2370
|
+
distance: nearestDistance,
|
|
2371
|
+
visionRange: this.visionRange,
|
|
2372
|
+
candidates: candidates.length,
|
|
2373
|
+
});
|
|
2374
|
+
} else if (now - this.lastNoTargetTraceTime > 1000) {
|
|
2375
|
+
this.lastNoTargetTraceTime = now;
|
|
2376
|
+
this.traceLog("target", "no target found", {
|
|
2377
|
+
visionRange: this.visionRange,
|
|
2378
|
+
candidates: candidates.map((candidate) => {
|
|
2379
|
+
const distance = this.getDistance(this.event, candidate);
|
|
2380
|
+
return {
|
|
2381
|
+
id: candidate.id,
|
|
2382
|
+
hp: (candidate as any).hp,
|
|
2383
|
+
canTarget: this.canTarget(candidate),
|
|
2384
|
+
defeated: this.isTargetDefeated(candidate),
|
|
2385
|
+
distance,
|
|
2386
|
+
};
|
|
2387
|
+
}),
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2178
2391
|
return nearest;
|
|
2179
2392
|
}
|
|
2180
2393
|
|
|
@@ -2370,8 +2583,7 @@ export class BattleAi {
|
|
|
2370
2583
|
return consumes;
|
|
2371
2584
|
case "moveToTarget":
|
|
2372
2585
|
if (!this.target) return false;
|
|
2373
|
-
this.
|
|
2374
|
-
this.requestMoveTo(this.target);
|
|
2586
|
+
this.requestTargetMovement();
|
|
2375
2587
|
return consumes;
|
|
2376
2588
|
case "fleeFromTarget":
|
|
2377
2589
|
if (!this.target) return false;
|
|
@@ -2404,8 +2616,7 @@ export class BattleAi {
|
|
|
2404
2616
|
return consumes;
|
|
2405
2617
|
}
|
|
2406
2618
|
if (distance > intent.distance + tolerance) {
|
|
2407
|
-
this.
|
|
2408
|
-
this.requestMoveTo(this.target);
|
|
2619
|
+
this.requestTargetMovement();
|
|
2409
2620
|
return consumes;
|
|
2410
2621
|
}
|
|
2411
2622
|
if (this.isMovingToTarget) {
|
|
@@ -2473,8 +2684,7 @@ export class BattleAi {
|
|
|
2473
2684
|
if (distance > maxRange) {
|
|
2474
2685
|
if (!this.isMovingToTarget) {
|
|
2475
2686
|
this.debugLog('movement', `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
|
|
2476
|
-
this.
|
|
2477
|
-
this.requestMoveTo(this.target);
|
|
2687
|
+
this.requestTargetMovement();
|
|
2478
2688
|
}
|
|
2479
2689
|
return;
|
|
2480
2690
|
}
|
|
@@ -2491,8 +2701,7 @@ export class BattleAi {
|
|
|
2491
2701
|
if (distance > this.attackRange) {
|
|
2492
2702
|
if (!this.isMovingToTarget) {
|
|
2493
2703
|
this.debugLog('movement', `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
|
|
2494
|
-
this.
|
|
2495
|
-
this.requestMoveTo(this.target);
|
|
2704
|
+
this.requestTargetMovement();
|
|
2496
2705
|
}
|
|
2497
2706
|
return;
|
|
2498
2707
|
}
|
|
@@ -2504,16 +2713,114 @@ export class BattleAi {
|
|
|
2504
2713
|
}
|
|
2505
2714
|
}
|
|
2506
2715
|
|
|
2716
|
+
private resolveMoveTarget(target: any): ResolvedMoveTarget | null {
|
|
2717
|
+
if (!target) return null;
|
|
2718
|
+
|
|
2719
|
+
const targetId =
|
|
2720
|
+
target.id !== undefined && target.id !== null ? String(target.id) : undefined;
|
|
2721
|
+
const x = resolveMoveCoordinate(target.x);
|
|
2722
|
+
const y = resolveMoveCoordinate(target.y);
|
|
2723
|
+
|
|
2724
|
+
if (targetId) {
|
|
2725
|
+
return {
|
|
2726
|
+
kind: "entity",
|
|
2727
|
+
target,
|
|
2728
|
+
id: targetId,
|
|
2729
|
+
x,
|
|
2730
|
+
y,
|
|
2731
|
+
signature: `entity:${targetId}`,
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
if (x === undefined || y === undefined) {
|
|
2736
|
+
return null;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
return {
|
|
2740
|
+
kind: "position",
|
|
2741
|
+
target: { x, y },
|
|
2742
|
+
x,
|
|
2743
|
+
y,
|
|
2744
|
+
signature: createMoveSignature(x, y),
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2507
2748
|
private requestMoveTo(target: any): boolean {
|
|
2508
2749
|
const currentTime = Date.now();
|
|
2750
|
+
const resolvedTarget = this.resolveMoveTarget(target);
|
|
2751
|
+
if (!resolvedTarget) {
|
|
2752
|
+
this.traceLog("movement", "moveTo skipped: invalid target", {
|
|
2753
|
+
target,
|
|
2754
|
+
});
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2509
2758
|
if (currentTime - this.lastMoveToTime < this.moveToCooldown) {
|
|
2759
|
+
if (
|
|
2760
|
+
this.lastMoveToCooldownTraceSignature !== resolvedTarget.signature ||
|
|
2761
|
+
currentTime - this.lastMoveToCooldownTraceTime > 1000
|
|
2762
|
+
) {
|
|
2763
|
+
this.lastMoveToCooldownTraceTime = currentTime;
|
|
2764
|
+
this.lastMoveToCooldownTraceSignature = resolvedTarget.signature;
|
|
2765
|
+
this.traceLog("movement", "moveTo skipped: cooldown", {
|
|
2766
|
+
targetKind: resolvedTarget.kind,
|
|
2767
|
+
targetId:
|
|
2768
|
+
resolvedTarget.kind === "entity" ? resolvedTarget.id : undefined,
|
|
2769
|
+
targetPosition: {
|
|
2770
|
+
x: resolvedTarget.x,
|
|
2771
|
+
y: resolvedTarget.y,
|
|
2772
|
+
},
|
|
2773
|
+
elapsed: currentTime - this.lastMoveToTime,
|
|
2774
|
+
moveToCooldown: this.moveToCooldown,
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2510
2777
|
return false;
|
|
2511
2778
|
}
|
|
2512
|
-
this.event.
|
|
2779
|
+
const map = this.event.getCurrentMap?.() as any;
|
|
2780
|
+
const hasBody =
|
|
2781
|
+
!!map?.physic?.getEntityByUUID?.(this.event.id) ||
|
|
2782
|
+
!!map?.getBody?.(this.event.id);
|
|
2783
|
+
this.traceLog("movement", "moveTo requested", {
|
|
2784
|
+
targetKind: resolvedTarget.kind,
|
|
2785
|
+
targetId:
|
|
2786
|
+
resolvedTarget.kind === "entity" ? resolvedTarget.id : undefined,
|
|
2787
|
+
eventPosition: {
|
|
2788
|
+
x: this.event.x?.(),
|
|
2789
|
+
y: this.event.y?.(),
|
|
2790
|
+
},
|
|
2791
|
+
targetPosition: {
|
|
2792
|
+
x: resolvedTarget.x,
|
|
2793
|
+
y: resolvedTarget.y,
|
|
2794
|
+
},
|
|
2795
|
+
hasMap: !!map,
|
|
2796
|
+
hasMovementBody: hasBody,
|
|
2797
|
+
});
|
|
2798
|
+
this.event.moveTo(resolvedTarget.target as any);
|
|
2513
2799
|
this.lastMoveToTime = currentTime;
|
|
2514
2800
|
return true;
|
|
2515
2801
|
}
|
|
2516
2802
|
|
|
2803
|
+
private requestTargetMovement(target: ActionBattleEntity | null = this.target): boolean {
|
|
2804
|
+
if (!target) {
|
|
2805
|
+
this.traceLog("movement", "target movement skipped: no target");
|
|
2806
|
+
return false;
|
|
2807
|
+
}
|
|
2808
|
+
const started = this.requestMoveTo(target);
|
|
2809
|
+
if (started) {
|
|
2810
|
+
this.isMovingToTarget = true;
|
|
2811
|
+
} else {
|
|
2812
|
+
const now = Date.now();
|
|
2813
|
+
if (now - this.lastTargetMovementSkipTraceTime > 1000) {
|
|
2814
|
+
this.lastTargetMovementSkipTraceTime = now;
|
|
2815
|
+
this.traceLog("movement", "target movement did not start", {
|
|
2816
|
+
targetId: target.id,
|
|
2817
|
+
isMovingToTarget: this.isMovingToTarget,
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return started;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2517
2824
|
private schedule(callback: () => void, delay: number) {
|
|
2518
2825
|
const timer = setTimeout(() => {
|
|
2519
2826
|
this.timers = this.timers.filter((entry) => entry !== timer);
|