97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import { type World, type Vec2, type EntityId } from "../../core/types";
|
|
import { isBlocked } from "../world/world-logic";
|
|
import { raycast } from "../../core/math";
|
|
import { EntityManager } from "../EntityManager";
|
|
|
|
export interface ProjectileResult {
|
|
path: Vec2[];
|
|
blockedPos: Vec2;
|
|
hitActorId?: EntityId;
|
|
}
|
|
|
|
/**
|
|
* Calculates the path and impact of a projectile.
|
|
*/
|
|
export function traceProjectile(
|
|
world: World,
|
|
start: Vec2,
|
|
target: Vec2,
|
|
entityManager: EntityManager,
|
|
shooterId?: EntityId
|
|
): ProjectileResult {
|
|
const points = raycast(start.x, start.y, target.x, target.y);
|
|
let blockedPos = target;
|
|
let hitActorId: EntityId | undefined;
|
|
|
|
// Iterate points (skip start)
|
|
for (let i = 1; i < points.length; i++) {
|
|
const p = points[i];
|
|
|
|
// Check for blocking
|
|
if (isBlocked(world, p.x, p.y, entityManager)) {
|
|
// Check if we hit a combatant
|
|
const actors = entityManager.getActorsAt(p.x, p.y);
|
|
const enemy = actors.find(a => a.category === "combatant" && a.id !== shooterId);
|
|
|
|
if (enemy) {
|
|
hitActorId = enemy.id;
|
|
blockedPos = p;
|
|
} else {
|
|
// Hit wall or other obstacle
|
|
blockedPos = p;
|
|
}
|
|
break;
|
|
}
|
|
blockedPos = p;
|
|
}
|
|
|
|
return {
|
|
path: points,
|
|
blockedPos,
|
|
hitActorId
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Finds the closest visible enemy to a given position.
|
|
*/
|
|
export function getClosestVisibleEnemy(
|
|
world: World,
|
|
origin: Vec2,
|
|
seenTiles: Set<string> | boolean[] | Uint8Array, // Support various visibility structures
|
|
width?: number // Required if seenTiles is a flat array
|
|
): Vec2 | null {
|
|
let closestDistSq = Infinity;
|
|
let closestPos: Vec2 | null = null;
|
|
|
|
// Helper to check visibility
|
|
const isVisible = (x: number, y: number) => {
|
|
if (Array.isArray(seenTiles) || seenTiles instanceof Uint8Array || seenTiles instanceof Int8Array) {
|
|
// Flat array
|
|
if (!width) return false;
|
|
return (seenTiles as any)[y * width + x];
|
|
} else {
|
|
// Set<string>
|
|
return (seenTiles as Set<string>).has(`${x},${y}`);
|
|
}
|
|
};
|
|
|
|
for (const actor of world.actors.values()) {
|
|
if (actor.category !== "combatant" || actor.isPlayer) continue;
|
|
|
|
// Check visibility
|
|
if (!isVisible(actor.pos.x, actor.pos.y)) continue;
|
|
|
|
const dx = actor.pos.x - origin.x;
|
|
const dy = actor.pos.y - origin.y;
|
|
const distSq = dx*dx + dy*dy;
|
|
|
|
if (distSq < closestDistSq) {
|
|
closestDistSq = distSq;
|
|
closestPos = { x: actor.pos.x, y: actor.pos.y };
|
|
}
|
|
}
|
|
|
|
return closestPos;
|
|
}
|