/**
* Точки взаимодействия (перенос идеи v1 как чистых данных): реестр
* «интерактивов» {id, pos, radius, label} + чистая функция поиска ближайшей
* в радиусе. Регистрация/удаление — игра; движок только считает ближайшую
* точку к герою и говорит, можно ли взаимодействовать.
*/
/** Точка, около которой герою доступно взаимодействие (E). */
export interface Interactable {
/** Идентификатор (стабильный, для моста и диалогов). */
id: string;
/** Мировая позиция центра (x, y — не учитывается, z) — горизонтальная дистанция. */
pos: readonly [number, number, number];
/** Радиус доступности (в вокселях). */
radius: number;
/** Подсказка для игрока/агента («Поговорить», «Осмотреть»). */
label: string;
/** Граф диалога, если точка говорит (связка точка ↔ диалог — данные). */
dialogueId?: string;
}
/** Расстояние до точки и сама точка (для сортировки/подсказки «какая ближе»). */
export interface InteractableHit<T> {
item: T;
dist: number;
}
/** Горизонтальное (x-z) расстояние от позиции до точки. */
function horizontalDist(
pos: readonly [number, number, number],
it: { pos: readonly [number, number, number] },
): number {
const dx = it.pos[0] - pos[0];
const dz = it.pos[2] - pos[2];
return Math.hypot(dx, dz);
}
/** Ближайшая доступная точка (в радиусе) или null. Y не учитывается. */
export function nearestInteractable<T extends Interactable>(
registry: readonly T[],
pos: readonly [number, number, number],
): T | null {
let best: T | null = null;
let bestDist = Infinity;
for (const it of registry) {
const d = horizontalDist(pos, it);
if (d <= it.radius && d < bestDist) {
best = it;
bestDist = d;
}
}
return best;
}
/** Все доступные точки с расстояниями, ближе — раньше (для отладки/моста). */
export function interactablesInRadius<T extends Interactable>(
registry: readonly T[],
pos: readonly [number, number, number],
): InteractableHit<T>[] {
return registry
.map((item) => ({ item, dist: horizontalDist(pos, item) }))
.filter((h) => h.dist <= h.item.radius)
.sort((a, b) => a.dist - b.dist);
}