@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.
@@ -5,9 +5,10 @@ var ACTION_BATTLE_ENEMY_FACTION = "enemies";
5
5
  var getBattleAi = (entity) => entity?.battleAi;
6
6
  var getActionBattleEntityKind = (entity) => {
7
7
  if (getBattleAi(entity)) return "event";
8
+ if (typeof entity?.isEvent === "function") return entity.isEvent() ? "event" : "player";
8
9
  if (entity instanceof RpgEvent) return "event";
9
- if (typeof entity?.attachShape === "function") return "event";
10
10
  if (entity instanceof RpgPlayer) return "player";
11
+ if (typeof entity?.attachShape === "function") return "event";
11
12
  return "player";
12
13
  };
13
14
  var isActionBattlePlayer = (entity) => getActionBattleEntityKind(entity) === "player";
@@ -62,9 +62,10 @@ var applyDamageEffect = (attacker, target, skill, reaction, metadata) => {
62
62
  });
63
63
  if (!result.cancelled) {
64
64
  emitActionBattleClientVisual({
65
- moment: "hit",
65
+ moment: "hurt",
66
66
  entity: attacker,
67
67
  target,
68
+ attacker,
68
69
  damage: result.damage,
69
70
  result,
70
71
  skill
@@ -14,6 +14,11 @@ import { safeActionBattleDash } from "./index17.js";
14
14
  import { defineAiBehavior, defineAiTree } from "./index18.js";
15
15
  import { MAXHP } from "@rpgjs/server";
16
16
  //#region src/ai.server.ts
17
+ var resolveMoveCoordinate = (value) => {
18
+ const raw = typeof value === "function" ? value() : value;
19
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
20
+ };
21
+ var createMoveSignature = (x, y) => `position:${Math.round(x)}:${Math.round(y)}`;
17
22
  var normalizeRewardItem = (item) => {
18
23
  if (typeof item === "string") return {
19
24
  itemId: item,
@@ -97,14 +102,7 @@ var AiDebug = {
97
102
  * @param message - Log message
98
103
  * @param data - Optional additional data
99
104
  */
100
- log(category, eventId, message, data) {
101
- if (!this.enabled) return;
102
- if (this.filterEventId && eventId !== this.filterEventId) return;
103
- if (this.categories.length > 0 && !this.categories.includes(category)) return;
104
- const prefix = `[AI:${category}]${eventId ? ` [${eventId.substring(0, 8)}]` : ""}`;
105
- if (data !== void 0) console.log(prefix, message, data);
106
- else console.log(prefix, message);
107
- }
105
+ log(category, eventId, message, data) {}
108
106
  };
109
107
  /**
110
108
  * AI State enumeration
@@ -237,6 +235,27 @@ var BattleAi = class {
237
235
  debugLog(category, message, data) {
238
236
  AiDebug.log(category, this.event.id, message, data);
239
237
  }
238
+ traceLog(category, message, data) {}
239
+ lockActionUntil(until, reason, data) {
240
+ if (until <= this.actionLockedUntil) return;
241
+ this.actionLockedUntil = until;
242
+ this.traceLog("state", "action locked", {
243
+ reason,
244
+ lockedMs: Math.max(0, until - Date.now()),
245
+ ...data
246
+ });
247
+ }
248
+ lockForAttack(profile, pattern) {
249
+ this.isMovingToTarget = false;
250
+ this.event.stopMoveTo();
251
+ this.lockActionUntil(Date.now() + profile.totalDurationMs, "attack", {
252
+ pattern,
253
+ totalDurationMs: profile.totalDurationMs,
254
+ startupMs: profile.startupMs,
255
+ activeMs: profile.activeMs,
256
+ recoveryMs: profile.recoveryMs
257
+ });
258
+ }
240
259
  state = AiState.Idle;
241
260
  stateStartTime = 0;
242
261
  stunnedUntil = 0;
@@ -283,6 +302,11 @@ var BattleAi = class {
283
302
  lastMoveToTime = 0;
284
303
  retreatCooldown = 600;
285
304
  lastRetreatTime = 0;
305
+ actionLockedUntil = 0;
306
+ lastActionLockTraceTime = 0;
307
+ lastMoveToCooldownTraceTime = 0;
308
+ lastMoveToCooldownTraceSignature = null;
309
+ lastTargetMovementSkipTraceTime = 0;
286
310
  timers = [];
287
311
  behaviorKey;
288
312
  behaviorTree;
@@ -294,6 +318,7 @@ var BattleAi = class {
294
318
  visionSetupRetries = 0;
295
319
  maxVisionSetupRetries = 20;
296
320
  destroyed = false;
321
+ lastNoTargetTraceTime = 0;
297
322
  constructor(event, options = {}) {
298
323
  options = mergeBattleAiPresetOptions(options);
299
324
  event.battleAi = this;
@@ -407,7 +432,13 @@ var BattleAi = class {
407
432
  setupVision() {
408
433
  if (this.visionShape) return true;
409
434
  const map = this.event.getCurrentMap?.();
410
- if (map?.physic?.getEntityByUUID && !map.physic.getEntityByUUID(this.event.id)) return false;
435
+ if (map?.physic?.getEntityByUUID && !map.physic.getEntityByUUID(this.event.id)) {
436
+ this.traceLog("vision", "physics body not ready", {
437
+ retries: this.visionSetupRetries,
438
+ hasMap: !!map
439
+ });
440
+ return false;
441
+ }
411
442
  const diameter = this.visionRange * 2;
412
443
  const shape = this.event.attachShape(`vision_${this.event.id}`, {
413
444
  radius: this.visionRange,
@@ -415,13 +446,26 @@ var BattleAi = class {
415
446
  height: diameter,
416
447
  angle: 360
417
448
  });
418
- if (!shape) return false;
449
+ if (!shape) {
450
+ this.traceLog("vision", "attachShape returned no shape", {
451
+ retries: this.visionSetupRetries,
452
+ visionRange: this.visionRange
453
+ });
454
+ return false;
455
+ }
419
456
  this.visionShape = shape;
457
+ this.traceLog("vision", "vision attached", {
458
+ shapeId: shape?.id,
459
+ visionRange: this.visionRange
460
+ });
420
461
  return true;
421
462
  }
422
463
  scheduleVisionSetup() {
423
464
  if (this.destroyed || this.setupVision()) return;
424
- if (this.visionSetupRetries >= this.maxVisionSetupRetries) return;
465
+ if (this.visionSetupRetries >= this.maxVisionSetupRetries) {
466
+ this.traceLog("vision", "vision setup gave up", { retries: this.visionSetupRetries });
467
+ return;
468
+ }
425
469
  this.visionSetupRetries++;
426
470
  this.schedule(() => {
427
471
  if (this.destroyed || !this.event.getCurrentMap()) return;
@@ -458,9 +502,18 @@ var BattleAi = class {
458
502
  [AiState.Stunned]: [AiState.Combat, AiState.Idle]
459
503
  }[this.state].includes(newState)) {
460
504
  this.debugLog("state", `INVALID transition ${this.state} -> ${newState}`);
505
+ this.traceLog("state", "invalid transition", {
506
+ from: this.state,
507
+ to: newState
508
+ });
461
509
  return;
462
510
  }
463
511
  this.debugLog("state", `STATE change: ${this.state} -> ${newState}`);
512
+ this.traceLog("state", "state change", {
513
+ from: this.state,
514
+ to: newState,
515
+ targetId: this.target?.id
516
+ });
464
517
  this.state = newState;
465
518
  this.stateStartTime = Date.now();
466
519
  switch (newState) {
@@ -498,6 +551,19 @@ var BattleAi = class {
498
551
  if (currentTime >= this.stunnedUntil) this.changeState(AiState.Combat);
499
552
  return;
500
553
  }
554
+ if (currentTime < this.actionLockedUntil) {
555
+ if (this.target) this.faceTarget();
556
+ if (currentTime - this.lastActionLockTraceTime > 250) {
557
+ this.lastActionLockTraceTime = currentTime;
558
+ this.traceLog("state", "waiting action recovery", {
559
+ remainingMs: this.actionLockedUntil - currentTime,
560
+ state: this.state,
561
+ targetId: this.target?.id
562
+ });
563
+ }
564
+ this.checkDamageTaken();
565
+ return;
566
+ }
501
567
  if (this.enemyType === EnemyType.Berserker && this.event.param[MAXHP]) {
502
568
  const hpPercent = this.event.hp / this.event.param[MAXHP];
503
569
  const berserkerModifier = Math.max(.3, hpPercent);
@@ -554,7 +620,30 @@ var BattleAi = class {
554
620
  updateAlertBehavior() {
555
621
  if (this.target) {
556
622
  this.faceTarget();
557
- if (this.getDistance(this.event, this.target) <= this.attackRange * 1.5) this.changeState(AiState.Combat);
623
+ const distance = this.getDistance(this.event, this.target);
624
+ this.traceLog("movement", "alert update", {
625
+ targetId: this.target.id,
626
+ distance,
627
+ attackRange: this.attackRange,
628
+ visionRange: this.visionRange,
629
+ isMovingToTarget: this.isMovingToTarget
630
+ });
631
+ if (distance <= this.attackRange * 1.5) {
632
+ if (this.isMovingToTarget) {
633
+ this.isMovingToTarget = false;
634
+ this.event.stopMoveTo();
635
+ }
636
+ this.changeState(AiState.Combat);
637
+ } else if (distance <= this.visionRange * 1.5) {
638
+ if (!this.isMovingToTarget) {
639
+ this.debugLog("movement", `Alert approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
640
+ this.requestTargetMovement();
641
+ }
642
+ } else {
643
+ this.debugLog("combat", `Alert target out of range (dist=${distance.toFixed(1)})`);
644
+ this.clearTarget();
645
+ this.changeState(AiState.Idle);
646
+ }
558
647
  } else this.changeState(AiState.Idle);
559
648
  }
560
649
  /**
@@ -567,6 +656,15 @@ var BattleAi = class {
567
656
  return;
568
657
  }
569
658
  const distance = this.getDistance(this.event, this.target);
659
+ this.traceLog("combat", "combat update", {
660
+ targetId: this.target.id,
661
+ distance,
662
+ attackRange: this.attackRange,
663
+ visionRange: this.visionRange,
664
+ isMovingToTarget: this.isMovingToTarget,
665
+ behaviorEnabled: this.behaviorEnabled,
666
+ behaviorMode: this.behaviorMode
667
+ });
570
668
  if (distance > this.visionRange * 1.5) {
571
669
  this.debugLog("combat", `Target out of range (dist=${distance.toFixed(1)}, maxRange=${(this.visionRange * 1.5).toFixed(1)})`);
572
670
  this.clearTarget();
@@ -606,8 +704,7 @@ var BattleAi = class {
606
704
  } else if (distance > this.attackRange) {
607
705
  if (!this.isMovingToTarget) {
608
706
  this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
609
- this.isMovingToTarget = true;
610
- this.requestMoveTo(this.target);
707
+ this.requestTargetMovement();
611
708
  }
612
709
  } else if (this.isMovingToTarget) {
613
710
  this.debugLog("movement", `In range, stopping (dist=${distance.toFixed(1)})`);
@@ -617,8 +714,7 @@ var BattleAi = class {
617
714
  } else if (distance > this.attackRange) {
618
715
  if (!this.isMovingToTarget) {
619
716
  this.debugLog("movement", `Moving to target (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
620
- this.isMovingToTarget = true;
621
- this.requestMoveTo(this.target);
717
+ this.requestTargetMovement();
622
718
  }
623
719
  } else if (this.isMovingToTarget) {
624
720
  this.debugLog("movement", `In range, stopping (dist=${distance.toFixed(1)})`);
@@ -741,15 +837,9 @@ var BattleAi = class {
741
837
  if (!this.target) return;
742
838
  const profile = this.getAttackProfile(AttackPattern.Melee);
743
839
  this.faceTarget({ force: true });
840
+ this.lockForAttack(profile, AttackPattern.Melee);
744
841
  this.telegraphAttack(profile);
745
- withActionBattleAnimationUnlocked(this.event, () => {
746
- emitActionBattleClientVisual({
747
- moment: "attack",
748
- entity: this.event,
749
- target: this.target,
750
- animations: this.animations
751
- });
752
- });
842
+ this.playAttackVisual(profile, AttackPattern.Melee);
753
843
  this.scheduleAttackStartup(profile, () => {
754
844
  this.executeMeleeAttack(profile, AttackPattern.Melee);
755
845
  });
@@ -887,12 +977,12 @@ var BattleAi = class {
887
977
  }
888
978
  withActionBattleAnimationUnlocked(target, () => {
889
979
  emitActionBattleClientVisual({
890
- moment: "hit",
980
+ moment: "hurt",
891
981
  entity: this.event,
892
982
  target,
983
+ attacker: this.event,
893
984
  damage: hitResult.damage,
894
- result: hitResult,
895
- animations: this.animations
985
+ result: hitResult
896
986
  });
897
987
  });
898
988
  setActionBattleInvincibility(target, profile.reaction.invincibilityMs);
@@ -953,15 +1043,9 @@ var BattleAi = class {
953
1043
  this.comboCount++;
954
1044
  const profile = this.getAttackProfile(AttackPattern.Combo);
955
1045
  this.faceTarget({ force: true });
1046
+ this.lockForAttack(profile, AttackPattern.Combo);
956
1047
  this.telegraphAttack(profile);
957
- withActionBattleAnimationUnlocked(this.event, () => {
958
- emitActionBattleClientVisual({
959
- moment: "attack",
960
- entity: this.event,
961
- target: this.target,
962
- animations: this.animations
963
- });
964
- });
1048
+ this.playAttackVisual(profile, AttackPattern.Combo);
965
1049
  this.scheduleAttackStartup(profile, () => {
966
1050
  this.executeMeleeAttack(profile, AttackPattern.Combo);
967
1051
  });
@@ -979,16 +1063,9 @@ var BattleAi = class {
979
1063
  const profile = this.getAttackProfile(AttackPattern.Charged);
980
1064
  this.chargingAttack = true;
981
1065
  this.faceTarget({ force: true });
1066
+ this.lockForAttack(profile, AttackPattern.Charged);
982
1067
  this.telegraphAttack(profile);
983
- withActionBattleAnimationUnlocked(this.event, () => {
984
- emitActionBattleClientVisual({
985
- moment: "attack",
986
- entity: this.event,
987
- target: this.target,
988
- animations: this.animations,
989
- animationDefaults: { repeat: 2 }
990
- });
991
- });
1068
+ this.playAttackVisual(profile, AttackPattern.Charged, { repeat: 2 });
992
1069
  this.scheduleAttackStartup(profile, () => {
993
1070
  if (!this.target || this.state !== AiState.Combat) {
994
1071
  this.chargingAttack = false;
@@ -1005,15 +1082,9 @@ var BattleAi = class {
1005
1082
  */
1006
1083
  performZoneAttack() {
1007
1084
  const profile = this.getAttackProfile(AttackPattern.Zone);
1085
+ this.lockForAttack(profile, AttackPattern.Zone);
1008
1086
  this.telegraphAttack(profile);
1009
- withActionBattleAnimationUnlocked(this.event, () => {
1010
- emitActionBattleClientVisual({
1011
- moment: "attack",
1012
- entity: this.event,
1013
- target: this.target ?? void 0,
1014
- animations: this.animations
1015
- });
1016
- });
1087
+ this.playAttackVisual(profile, AttackPattern.Zone);
1017
1088
  const eventX = this.event.x();
1018
1089
  const eventY = this.event.y();
1019
1090
  const radius = 50;
@@ -1055,7 +1126,9 @@ var BattleAi = class {
1055
1126
  const dirX = dx / dist;
1056
1127
  const dirY = dy / dist;
1057
1128
  this.faceTarget({ force: true });
1129
+ this.lockForAttack(profile, AttackPattern.DashAttack);
1058
1130
  this.telegraphAttack(profile);
1131
+ this.playAttackVisual(profile, AttackPattern.DashAttack);
1059
1132
  this.scheduleAttackStartup(profile, () => {
1060
1133
  if (!this.target || this.state !== AiState.Combat) return;
1061
1134
  safeActionBattleDash(this.event, {
@@ -1071,6 +1144,19 @@ var BattleAi = class {
1071
1144
  getAttackProfile(pattern) {
1072
1145
  return this.attackProfiles[pattern] ?? this.attackProfiles.melee;
1073
1146
  }
1147
+ playAttackVisual(profile, pattern, animationDefaults) {
1148
+ const moment = profile.animationKey === "castSkill" || profile.animationKey === "castSpell" ? "castSkill" : "attack";
1149
+ withActionBattleAnimationUnlocked(this.event, () => {
1150
+ emitActionBattleClientVisual({
1151
+ moment,
1152
+ entity: this.event,
1153
+ target: this.target ?? void 0,
1154
+ pattern,
1155
+ animations: this.animations,
1156
+ animationDefaults
1157
+ });
1158
+ });
1159
+ }
1074
1160
  telegraphAttack(profile) {
1075
1161
  if (profile.startupMs <= 0) return;
1076
1162
  this.event.flash({
@@ -1163,10 +1249,11 @@ var BattleAi = class {
1163
1249
  const dy = this.event.y() - this.target.y();
1164
1250
  const dist = Math.sqrt(dx * dx + dy * dy);
1165
1251
  if (dist === 0) return;
1166
- this.requestMoveTo({
1167
- x: () => this.event.x() + dx / dist * 200,
1168
- y: () => this.event.y() + dy / dist * 200
1169
- });
1252
+ const fleeTarget = {
1253
+ x: this.event.x() + dx / dist * 200,
1254
+ y: this.event.y() + dy / dist * 200
1255
+ };
1256
+ this.requestMoveTo(fleeTarget);
1170
1257
  }
1171
1258
  /**
1172
1259
  * Retreat from target (temporary)
@@ -1202,8 +1289,8 @@ var BattleAi = class {
1202
1289
  if (this.patrolWaypoints.length === 0) return;
1203
1290
  const waypoint = this.patrolWaypoints[this.currentPatrolIndex];
1204
1291
  this.requestMoveTo({
1205
- x: () => waypoint.x,
1206
- y: () => waypoint.y
1292
+ x: waypoint.x,
1293
+ y: waypoint.y
1207
1294
  });
1208
1295
  }
1209
1296
  /**
@@ -1249,19 +1336,35 @@ var BattleAi = class {
1249
1336
  const formationX = this.target.x() + Math.cos(angle) * formationRadius;
1250
1337
  const formationY = this.target.y() + Math.sin(angle) * formationRadius;
1251
1338
  if (Math.sqrt(Math.pow(this.event.x() - formationX, 2) + Math.pow(this.event.y() - formationY, 2)) > 20) this.requestMoveTo({
1252
- x: () => formationX,
1253
- y: () => formationY
1339
+ x: formationX,
1340
+ y: formationY
1254
1341
  });
1255
1342
  }
1256
1343
  /**
1257
1344
  * Handle player entering vision
1258
1345
  */
1259
1346
  onDetectInShape(target, shape) {
1260
- if (!this.canTarget(target) || this.isTargetDefeated(target)) return;
1347
+ const canTarget = this.canTarget(target);
1348
+ const defeated = this.isTargetDefeated(target);
1349
+ this.traceLog("vision", "detect in shape", {
1350
+ targetId: target?.id,
1351
+ shapeId: shape?.id,
1352
+ canTarget,
1353
+ defeated,
1354
+ state: this.state,
1355
+ targetHp: target?.hp
1356
+ });
1357
+ if (!canTarget || defeated) return;
1261
1358
  this.debugLog("vision", `Target ${target.id} entered vision (state=${this.state})`);
1262
1359
  this.engageTarget(target);
1263
1360
  }
1264
1361
  engageTarget(target) {
1362
+ this.traceLog("target", "engage target", {
1363
+ targetId: target.id,
1364
+ previousTargetId: this.target?.id,
1365
+ state: this.state,
1366
+ distance: this.getDistance(this.event, target)
1367
+ });
1265
1368
  this.target = target;
1266
1369
  if (this.state === AiState.Idle) this.changeState(AiState.Alert);
1267
1370
  else if (this.state === AiState.Alert) this.changeState(AiState.Combat);
@@ -1271,6 +1374,12 @@ var BattleAi = class {
1271
1374
  */
1272
1375
  onDetectOutShape(target, shape) {
1273
1376
  this.debugLog("vision", `Target ${target.id} left vision (wasTarget=${this.target === target})`);
1377
+ this.traceLog("vision", "detect out shape", {
1378
+ targetId: target?.id,
1379
+ shapeId: shape?.id,
1380
+ wasTarget: this.target === target,
1381
+ state: this.state
1382
+ });
1274
1383
  if (this.target === target) {
1275
1384
  this.clearTarget();
1276
1385
  this.changeState(AiState.Idle);
@@ -1293,8 +1402,21 @@ var BattleAi = class {
1293
1402
  }
1294
1403
  handleDamage(attacker, damageResult) {
1295
1404
  if (this.defeated) return true;
1296
- const damage = damageResult.damage;
1405
+ const damage = Number.isFinite(damageResult.damage) ? damageResult.damage : 0;
1297
1406
  this.debugLog("damage", `Took ${damage} damage from ${attacker.id} (HP: ${this.event.hp}/${this.event.param[MAXHP] || "?"})`);
1407
+ const canRetaliate = attacker ? this.canTarget(attacker) : false;
1408
+ const attackerDefeated = this.isTargetDefeated(attacker);
1409
+ this.traceLog("damage", "handle damage", {
1410
+ attackerId: attacker?.id,
1411
+ damage,
1412
+ defeated: damageResult.defeated,
1413
+ eventHp: this.event.hp,
1414
+ maxHp: this.event.param[MAXHP],
1415
+ state: this.state,
1416
+ canRetaliate,
1417
+ attackerDefeated,
1418
+ currentTargetId: this.target?.id
1419
+ });
1298
1420
  withActionBattleAnimationUnlocked(this.event, () => {
1299
1421
  emitActionBattleClientVisual({
1300
1422
  moment: "hurt",
@@ -1308,10 +1430,27 @@ var BattleAi = class {
1308
1430
  });
1309
1431
  });
1310
1432
  this.recentDamageTaken += damage;
1433
+ if (attacker && this.canTarget(attacker) && !this.isTargetDefeated(attacker) && this.state !== AiState.Flee) {
1434
+ this.traceLog("target", "retaliate against attacker", {
1435
+ attackerId: attacker.id,
1436
+ previousTargetId: this.target?.id,
1437
+ state: this.state
1438
+ });
1439
+ this.target = attacker;
1440
+ if (this.state === AiState.Idle || this.state === AiState.Alert) {
1441
+ this.isMovingToTarget = false;
1442
+ this.changeState(AiState.Combat);
1443
+ }
1444
+ }
1311
1445
  const reaction = damageResult.reaction;
1312
1446
  const staggerPower = reaction?.staggerPower ?? damage;
1313
1447
  const hitstunMs = reaction?.hitstunMs ?? this.hitstunMs;
1314
- const shouldStun = staggerPower >= this.poise && hitstunMs > 0;
1448
+ const shouldStun = (damage > 0 || (reaction?.staggerPower ?? 0) > 0) && staggerPower >= this.poise && hitstunMs > 0;
1449
+ this.lockActionUntil(Date.now() + Math.max(220, hitstunMs + 120), "damage recovery", {
1450
+ attackerId: attacker?.id,
1451
+ damage,
1452
+ hitstunMs
1453
+ });
1315
1454
  setActionBattleInvincibility(this.event, reaction?.invincibilityMs ?? this.invincibilityMs);
1316
1455
  if (shouldStun && this.state !== AiState.Stunned && this.state !== AiState.Flee) {
1317
1456
  this.debugLog("damage", "Stunned from damage");
@@ -1394,7 +1533,10 @@ var BattleAi = class {
1394
1533
  }
1395
1534
  findNearestTarget() {
1396
1535
  const map = this.event.getCurrentMap();
1397
- if (!map) return null;
1536
+ if (!map) {
1537
+ this.traceLog("target", "find nearest skipped: no map");
1538
+ return null;
1539
+ }
1398
1540
  const candidates = [];
1399
1541
  map.getPlayers?.().forEach((player) => candidates.push(player));
1400
1542
  map.getEvents?.().forEach((event) => candidates.push(event));
@@ -1409,6 +1551,29 @@ var BattleAi = class {
1409
1551
  nearestDistance = distance;
1410
1552
  }
1411
1553
  }
1554
+ const now = Date.now();
1555
+ if (nearest) this.traceLog("target", "nearest target found", {
1556
+ targetId: nearest.id,
1557
+ distance: nearestDistance,
1558
+ visionRange: this.visionRange,
1559
+ candidates: candidates.length
1560
+ });
1561
+ else if (now - this.lastNoTargetTraceTime > 1e3) {
1562
+ this.lastNoTargetTraceTime = now;
1563
+ this.traceLog("target", "no target found", {
1564
+ visionRange: this.visionRange,
1565
+ candidates: candidates.map((candidate) => {
1566
+ const distance = this.getDistance(this.event, candidate);
1567
+ return {
1568
+ id: candidate.id,
1569
+ hp: candidate.hp,
1570
+ canTarget: this.canTarget(candidate),
1571
+ defeated: this.isTargetDefeated(candidate),
1572
+ distance
1573
+ };
1574
+ })
1575
+ });
1576
+ }
1412
1577
  return nearest;
1413
1578
  }
1414
1579
  isTargetDefeated(target) {
@@ -1540,8 +1705,7 @@ var BattleAi = class {
1540
1705
  return consumes;
1541
1706
  case "moveToTarget":
1542
1707
  if (!this.target) return false;
1543
- this.isMovingToTarget = true;
1544
- this.requestMoveTo(this.target);
1708
+ this.requestTargetMovement();
1545
1709
  return consumes;
1546
1710
  case "fleeFromTarget":
1547
1711
  if (!this.target) return false;
@@ -1564,8 +1728,7 @@ var BattleAi = class {
1564
1728
  return consumes;
1565
1729
  }
1566
1730
  if (distance > intent.distance + tolerance) {
1567
- this.isMovingToTarget = true;
1568
- this.requestMoveTo(this.target);
1731
+ this.requestTargetMovement();
1569
1732
  return consumes;
1570
1733
  }
1571
1734
  if (this.isMovingToTarget) {
@@ -1614,8 +1777,7 @@ var BattleAi = class {
1614
1777
  if (distance > maxRange) {
1615
1778
  if (!this.isMovingToTarget) {
1616
1779
  this.debugLog("movement", `Tactical approach (dist=${distance.toFixed(1)}, maxRange=${maxRange.toFixed(1)})`);
1617
- this.isMovingToTarget = true;
1618
- this.requestMoveTo(this.target);
1780
+ this.requestTargetMovement();
1619
1781
  }
1620
1782
  return;
1621
1783
  }
@@ -1630,8 +1792,7 @@ var BattleAi = class {
1630
1792
  if (distance > this.attackRange) {
1631
1793
  if (!this.isMovingToTarget) {
1632
1794
  this.debugLog("movement", `Assault approach (dist=${distance.toFixed(1)}, attackRange=${this.attackRange})`);
1633
- this.isMovingToTarget = true;
1634
- this.requestMoveTo(this.target);
1795
+ this.requestTargetMovement();
1635
1796
  }
1636
1797
  return;
1637
1798
  }
@@ -1641,13 +1802,94 @@ var BattleAi = class {
1641
1802
  this.event.stopMoveTo();
1642
1803
  }
1643
1804
  }
1805
+ resolveMoveTarget(target) {
1806
+ if (!target) return null;
1807
+ const targetId = target.id !== void 0 && target.id !== null ? String(target.id) : void 0;
1808
+ const x = resolveMoveCoordinate(target.x);
1809
+ const y = resolveMoveCoordinate(target.y);
1810
+ if (targetId) return {
1811
+ kind: "entity",
1812
+ target,
1813
+ id: targetId,
1814
+ x,
1815
+ y,
1816
+ signature: `entity:${targetId}`
1817
+ };
1818
+ if (x === void 0 || y === void 0) return null;
1819
+ return {
1820
+ kind: "position",
1821
+ target: {
1822
+ x,
1823
+ y
1824
+ },
1825
+ x,
1826
+ y,
1827
+ signature: createMoveSignature(x, y)
1828
+ };
1829
+ }
1644
1830
  requestMoveTo(target) {
1645
1831
  const currentTime = Date.now();
1646
- if (currentTime - this.lastMoveToTime < this.moveToCooldown) return false;
1647
- this.event.moveTo(target);
1832
+ const resolvedTarget = this.resolveMoveTarget(target);
1833
+ if (!resolvedTarget) {
1834
+ this.traceLog("movement", "moveTo skipped: invalid target", { target });
1835
+ return false;
1836
+ }
1837
+ if (currentTime - this.lastMoveToTime < this.moveToCooldown) {
1838
+ if (this.lastMoveToCooldownTraceSignature !== resolvedTarget.signature || currentTime - this.lastMoveToCooldownTraceTime > 1e3) {
1839
+ this.lastMoveToCooldownTraceTime = currentTime;
1840
+ this.lastMoveToCooldownTraceSignature = resolvedTarget.signature;
1841
+ this.traceLog("movement", "moveTo skipped: cooldown", {
1842
+ targetKind: resolvedTarget.kind,
1843
+ targetId: resolvedTarget.kind === "entity" ? resolvedTarget.id : void 0,
1844
+ targetPosition: {
1845
+ x: resolvedTarget.x,
1846
+ y: resolvedTarget.y
1847
+ },
1848
+ elapsed: currentTime - this.lastMoveToTime,
1849
+ moveToCooldown: this.moveToCooldown
1850
+ });
1851
+ }
1852
+ return false;
1853
+ }
1854
+ const map = this.event.getCurrentMap?.();
1855
+ const hasBody = !!map?.physic?.getEntityByUUID?.(this.event.id) || !!map?.getBody?.(this.event.id);
1856
+ this.traceLog("movement", "moveTo requested", {
1857
+ targetKind: resolvedTarget.kind,
1858
+ targetId: resolvedTarget.kind === "entity" ? resolvedTarget.id : void 0,
1859
+ eventPosition: {
1860
+ x: this.event.x?.(),
1861
+ y: this.event.y?.()
1862
+ },
1863
+ targetPosition: {
1864
+ x: resolvedTarget.x,
1865
+ y: resolvedTarget.y
1866
+ },
1867
+ hasMap: !!map,
1868
+ hasMovementBody: hasBody
1869
+ });
1870
+ this.event.moveTo(resolvedTarget.target);
1648
1871
  this.lastMoveToTime = currentTime;
1649
1872
  return true;
1650
1873
  }
1874
+ requestTargetMovement(target = this.target) {
1875
+ if (!target) {
1876
+ this.traceLog("movement", "target movement skipped: no target");
1877
+ return false;
1878
+ }
1879
+ const started = this.requestMoveTo(target);
1880
+ if (started) this.isMovingToTarget = true;
1881
+ else {
1882
+ const now = Date.now();
1883
+ if (now - this.lastTargetMovementSkipTraceTime > 1e3) {
1884
+ this.lastTargetMovementSkipTraceTime = now;
1885
+ this.traceLog("movement", "target movement did not start", {
1886
+ targetId: target.id,
1887
+ isMovingToTarget: this.isMovingToTarget
1888
+ });
1889
+ }
1890
+ }
1891
+ return started;
1892
+ }
1651
1893
  schedule(callback, delay) {
1652
1894
  const timer = setTimeout(() => {
1653
1895
  this.timers = this.timers.filter((entry) => entry !== timer);
@@ -80,9 +80,19 @@ var createDefaultPlayerHitboxResolver = (hitboxes = DEFAULT_ZELDA_PLAYER_HITBOXE
80
80
  };
81
81
  var defaultRpgjsDamageResolver = (context) => {
82
82
  const target = context.target;
83
+ const previousHp = typeof target.hp === "number" && Number.isFinite(target.hp) ? target.hp : void 0;
83
84
  const raw = target.applyDamage(context.attacker, context.skill);
85
+ const resolvedDamage = Number(raw?.damage ?? 0);
86
+ if (!Number.isFinite(resolvedDamage)) {
87
+ if (previousHp !== void 0) target.hp = previousHp;
88
+ return {
89
+ damage: 0,
90
+ defeated: false,
91
+ raw
92
+ };
93
+ }
84
94
  return {
85
- damage: raw?.damage ?? 0,
95
+ damage: resolvedDamage,
86
96
  defeated: target.hp <= 0,
87
97
  raw
88
98
  };