import type { Grid } from './pathfinding';
import type { TileMapData } from './IsometricTileMap';
import type { Vec2 } from '../math/Vec2';
/**
* Круговые коллизии поверх тайловой сетки (чистая математика, без Pixi).
* Актор — круг радиуса r в мировых юнитах (тайл = 1×1); правило стиля:
* r < 0.5 — тело уже тайла, тогда путь A* по центрам тайлов остаётся
* проходимым и проверка сводится к соседним с центром тайлам.
* Проверка круга — точная (ближайшая точка квадрата тайла), не выборка
* точек: выборка пропускает угловой прокол Blocked-тайла по диагонали.
*/
/** Grid-адаптер над сырыми данными карты: blocked-иды + footprint пропов. */
export function gridOf(data: TileMapData): Grid {
const blockedSet = new Set(data.blocked);
const propBlocked = new Set<number>();
for (const p of data.props ?? []) {
const w = p.w ?? 1;
const h = p.h ?? 1;
for (let ty = p.y; ty < p.y + h; ty++) {
for (let tx = p.x; tx < p.x + w; tx++) propBlocked.add(ty * data.width + tx);
}
}
return {
width: data.width,
height: data.height,
isWalkable: (x, y) =>
x >= 0 && y >= 0 && x < data.width && y < data.height &&
!propBlocked.has(y * data.width + x) &&
!blockedSet.has(data.tiles[y * data.width + x]!)
};
}
/** Тайл блокирует движение (вне карты — тоже стена). */
function isBlocked(grid: Grid, tx: number, ty: number): boolean {
return tx < 0 || ty < 0 || tx >= grid.width || ty >= grid.height || !grid.isWalkable(tx, ty);
}
/** Помещается ли круг в позицию (точное пересечение с blocked-тайлами bbox). */
export function circleFits(grid: Grid, pos: Vec2, r: number): boolean {
if (r <= 0) {
return !isBlocked(grid, Math.floor(pos.x), Math.floor(pos.y));
}
// r < 0.5 — bbox накрывает не более 2×2 тайлов.
const x0 = Math.floor(pos.x - r);
const x1 = Math.floor(pos.x + r);
const y0 = Math.floor(pos.y - r);
const y1 = Math.floor(pos.y + r);
for (let ty = y0; ty <= y1; ty++) {
for (let tx = x0; tx <= x1; tx++) {
if (!isBlocked(grid, tx, ty)) continue;
// Ближайшая к центру круга точка квадрата тайла.
const nx = Math.max(tx, Math.min(pos.x, tx + 1));
const ny = Math.max(ty, Math.min(pos.y, ty + 1));
const dx = pos.x - nx;
const dy = pos.y - ny;
if (dx * dx + dy * dy < r * r) return false; // касание стеной разрешено
}
}
return true;
}
/**
* Сдвинуть круг на delta со скольжением вдоль стен: оси проверяются
* раздельно (X -> Y), прижатая к стене ось не блокирует движение по другой.
* Мутирует pos. Возвращает true, если хоть одна ось сместилась.
*/
export function moveCircle(grid: Grid, pos: Vec2, delta: Vec2, r: number): boolean {
let moved = false;
const nx = pos.x + delta.x;
if (circleFits(grid, { x: nx, y: pos.y }, r)) {
pos.x = nx;
moved = moved || delta.x !== 0;
}
const ny = pos.y + delta.y;
if (circleFits(grid, { x: pos.x, y: ny }, r)) {
pos.y = ny;
moved = moved || delta.y !== 0;
}
return moved;
}
/**
* Вытолкнуть круг из стены (после отброса/телепорта): ищет ближайший по
* спирали тайл, в чей центр круг помещается, и ставит pos в его центр.
* Свободного в радиусе 3 тайлов нет — позиция не меняется.
*/
export function pushOutOfWalls(grid: Grid, pos: Vec2, r: number): void {
if (circleFits(grid, pos, r)) return;
const cx = Math.floor(pos.x);
const cy = Math.floor(pos.y);
for (let ring = 0; ring <= 3; ring++) {
for (let ty = cy - ring; ty <= cy + ring; ty++) {
for (let tx = cx - ring; tx <= cx + ring; tx++) {
// Кольцо: периметр квадрата (внутренние уже проверены).
const onRing = Math.max(Math.abs(tx - cx), Math.abs(ty - cy)) === ring;
if (!onRing) continue;
const center = { x: tx + 0.5, y: ty + 0.5 };
if (circleFits(grid, center, r)) {
pos.x = center.x;
pos.y = center.y;
return;
}
}
}
}
}
/**
* Растолкать круг a из круга b: a сдвигается на глубину перекрытия
* (двигаем только a — каждый актор выталкивает себя сам). Слившиеся
* в точку круги расходятся по +X (детерминированный фолбэк).
*/
export function separateCircles(a: Vec2, ra: number, b: Vec2, rb: number): void {
const dx = a.x - b.x;
const dy = a.y - b.y;
const d = Math.hypot(dx, dy);
const overlap = ra + rb - d;
if (overlap <= 0) return;
if (d < 1e-6) {
a.x += overlap;
return;
}
const k = overlap / d;
a.x += dx * k;
a.y += dy * k;
}