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;
}