pieces/agents/targeting.js

import {
  ADJ_DIRECTIONS,
  EAST,
  NORTH,
  NORTHEAST,
  NORTHWEST,
  SOUTH,
  SOUTHEAST,
  SOUTHWEST,
  WEST,
} from "../../core/direction.js";
import { PLAYER } from "../../core/flags.js";

/**
 * Targeting — describes how an agent chooses a movement or attack direction.
 * Mirrors the Java Targeting parameter object / builder pattern.
 */
export class Targeting {
  constructor() {
    this._range = 1;
    this._distance = -1;
    this._attackPlayer = false;
    this._fleePlayer = false;
    this._moveRandom = false;
    this._trackPlayer = false;
    this._dodgeBullets = 0;
  }

  attackPlayer(range) {
    this._attackPlayer = true;
    this._range = range;
    return this;
  }
  fleePlayer(range) {
    this._fleePlayer = true;
    this._range = range;
    return this;
  }
  moveRandomly() {
    this._moveRandom = true;
    return this;
  }
  trackPlayer() {
    this._trackPlayer = true;
    return this;
  }
  keepDistance(d) {
    this._distance = d;
    return this;
  }
  dodgeBullets(pct) {
    this._dodgeBullets = pct;
    return this;
  }
}

// ── Geometry helpers ──────────────────────────────────────────────────────────

export function getDistance(cell1, cell2) {
  const dx = cell2.x - cell1.x;
  const dy = cell2.y - cell1.y;
  return Math.sqrt(dx * dx + dy * dy);
}

export function getDirectionToCell(origin, target) {
  if (!target) return null;
  const dx = target.x - origin.x;
  const dy = target.y - origin.y;
  if (dx === 0 && dy === 0) return null;
  // Mirrors Java AgentUtils.getDirectionToCell — compare signs of dx/dy
  if (dx === 0 && dy < 0) return NORTH;
  if (dx === 0 && dy > 0) return SOUTH;
  if (dx > 0 && dy === 0) return EAST;
  if (dx < 0 && dy === 0) return WEST;
  if (dx < 0 && dy < 0) return NORTHWEST;
  if (dx > 0 && dy > 0) return SOUTHEAST;
  if (dx > 0 && dy < 0) return NORTHEAST;
  return SOUTHWEST;
}

function isStraightPath(cell1, cell2) {
  const d = getDistance(cell1, cell2);
  if (d <= 1) return false;
  return (
    cell1.x === cell2.x ||
    cell1.y === cell2.y ||
    cell1.x - cell2.x === cell1.y - cell2.y
  );
}

function getDirectionAtOffset(dir, offset) {
  const idx = ADJ_DIRECTIONS.indexOf(dir);
  if (idx === -1) return null;
  const n = ADJ_DIRECTIONS.length;
  return ADJ_DIRECTIONS[(((idx + offset) % n) + n) % n];
}

// ── Pathfinding ───────────────────────────────────────────────────────────────

function isMoveViable(board, from, agent, targetCell, dir, targeting) {
  const next = board.getAdjacentCell(from.x, from.y, dir);
  if (!next) return false;

  const isAttackingPlayer = targeting._attackPlayer && next.agent?.is?.(PLAYER);
  if (!next.canEnter(from, agent, dir, isAttackingPlayer)) return false;

  if (targeting._dodgeBullets > 0) {
    const playerCell = board.getCurrentCell();
    if (
      playerCell &&
      isStraightPath(next, playerCell) &&
      Math.random() * 100 < targeting._dodgeBullets
    ) {
      return false;
    }
  }

  return true;
}

export function findPathInDirection(
  board,
  cell,
  agent,
  targetCell,
  direction,
  targeting,
) {
  if (isMoveViable(board, cell, agent, targetCell, direction, targeting))
    return direction;

  const adjustments =
    Math.random() < 0.5 ? [-1, 1, -2, 2, -3, 3] : [1, -1, 2, -2, 3, -3];
  for (const adj of adjustments) {
    const sideways = getDirectionAtOffset(direction, adj);
    if (
      sideways &&
      isMoveViable(board, cell, agent, targetCell, sideways, targeting)
    ) {
      return sideways;
    }
  }
  return null;
}

export function getRandomDirection() {
  return ADJ_DIRECTIONS[Math.floor(Math.random() * ADJ_DIRECTIONS.length)];
}

/**
 * Determine the direction an agent should move given its targeting spec.
 * Returns a Direction or null if no move is possible.
 */
export function findPathToTarget(board, agentCell, agent, targeting) {
  const playerCell = board.getCurrentCell();
  let targetCell = null;

  if ((targeting._attackPlayer || targeting._fleePlayer) && playerCell) {
    const d = getDistance(agentCell, playerCell);
    if (d <= targeting._range) targetCell = playerCell;
  }

  if (targetCell != null) {
    let dir = getDirectionToCell(agentCell, targetCell);
    // Flee: reverse direction; keepDistance: also reverse when too close
    if (
      dir &&
      (targeting._fleePlayer ||
        (targeting._distance >= 0 &&
          getDistance(agentCell, targetCell) <= targeting._distance))
    ) {
      dir = getDirectionAtOffset(dir, 4); // +4 = opposite direction in 8-compass
    }
    if (dir) {
      const found = findPathInDirection(
        board,
        agentCell,
        agent,
        targetCell,
        dir,
        targeting,
      );
      if (found) return found;
    }
  }

  if (targeting._moveRandom) {
    for (let i = 0; i < 6; i++) {
      const d = getRandomDirection();
      if (isMoveViable(board, agentCell, agent, null, d, targeting)) return d;
    }
  }

  return null;
}