/** * Steuerungsverhalten („Steering Behaviors“) für die Gegner-KI, ohne Phaser. * * Jede Funktion liefert eine Wunschgeschwindigkeit. Die Entities mischen mehrere * davon (z. B. Verfolgen + Abstand zu Nachbarn) und begrenzen das Ergebnis auf ihr * Tempo. Die drei Grundmuster aus dem Design: * - Verfolgen → seek * - Abstand → keepDistance (mit seitlichem Ausweichen im Wohlfühlbereich) * - Flankieren → flankPoint / encirclePoint als Ziel für seek * @module logic/steering */ import { add, angleTo, clampLength, distance, fromAngle, length, normalize, scale, sub } from './math.js'; /** @typedef {import('./math.js').Vec2} Vec2 */ /** Anteil der Grundgeschwindigkeit beim seitlichen Ausweichen. */ export const DEFAULT_STRAFE_FACTOR = 0.6; /** * Direkt auf ein Ziel zu. * @param {Vec2} position * @param {Vec2} target * @param {number} speed * @returns {Vec2} */ export function seek(position, target, speed) { return scale(normalize(sub(target, position)), speed); } /** * Direkt von einer Bedrohung weg. * @param {Vec2} position * @param {Vec2} threat * @param {number} speed * @returns {Vec2} */ export function flee(position, threat, speed) { return scale(seek(position, threat, speed), -1); } /** * Abstoßung von Nachbarn, damit Gruppen nicht zu einem Klumpen verschmelzen. * Nähere Nachbarn stoßen stärker ab (linear bis 0 am Rand des Radius). * @param {Vec2} position * @param {Iterable} neighbors Andere Gegner (der eigene Eintrag darf enthalten sein). * @param {number} radius * @returns {Vec2} Unskalierter Richtungsvektor (Länge ≈ Anzahl/Stärke der Überlappungen). */ export function separation(position, neighbors, radius) { let push = { x: 0, y: 0 }; for (const other of neighbors) { if (other === position) continue; const d = distance(position, other); if (d >= radius) continue; // Exakt übereinander: feste Richtung statt NaN, damit sich die beiden trennen. const away = d > 0 ? normalize(sub(position, other)) : { x: 1, y: 0 }; push = add(push, scale(away, 1 - d / radius)); } return push; } /** * Hält einen Abstand zwischen `band.min` und `band.max` zum Ziel ein. * Zu weit weg → annähern, zu nah → zurückweichen, dazwischen → seitlich ausweichen, * damit Fernkämpfer kein stehendes Ziel sind. * @param {Vec2} position * @param {Vec2} target * @param {{min: number, max: number}} band * @param {number} speed * @param {1|-1} strafeDirection 1 = im Uhrzeigersinn, -1 = dagegen. * @param {number} [strafeFactor] * @returns {Vec2} */ export function keepDistance(position, target, band, speed, strafeDirection, strafeFactor = DEFAULT_STRAFE_FACTOR) { const d = distance(position, target); const toTarget = normalize(sub(target, position)); if (d > band.max) return scale(toTarget, speed); if (d < band.min) return scale(toTarget, -speed); const tangent = { x: -toTarget.y * strafeDirection, y: toTarget.x * strafeDirection }; return scale(tangent, speed * strafeFactor); } /** * Flankenposition: Punkt auf einem Kreis um das Ziel, gegenüber der aktuellen * Blickachse Ziel→Gegner um `angleOffset` gedreht. Der Gegner läuft also nicht * frontal an, sondern schwenkt seitlich ein. * @param {Vec2} target * @param {Vec2} position * @param {number} angleOffset Bogenmaß; Vorzeichen bestimmt die Seite. * @param {number} radius * @returns {Vec2} */ export function flankPoint(target, position, angleOffset, radius) { const baseAngle = angleTo(target, position); return add(target, scale(fromAngle(baseAngle + angleOffset), radius)); } /** * Umzingelungspunkt: fester Winkel („Platz im Kreis“) um das Ziel. Verteilt man * die Winkel auf einen Schwarm, greift er von allen Seiten an. * @param {Vec2} target * @param {number} slotAngle Bogenmaß. * @param {number} radius * @returns {Vec2} */ export function encirclePoint(target, slotAngle, radius) { return add(target, scale(fromAngle(slotAngle), radius)); } /** * Gewichtete Summe mehrerer Steuerungsvektoren, begrenzt auf `maxSpeed`. * @param {{vector: Vec2, weight: number}[]} parts * @param {number} maxSpeed * @returns {Vec2} */ export function combine(parts, maxSpeed) { const sum = parts.reduce((acc, p) => add(acc, scale(p.vector, p.weight)), { x: 0, y: 0 }); return clampLength(sum, maxSpeed); } /** * Nähert die aktuelle Geschwindigkeit der Wunschgeschwindigkeit an. Ohne diese * Glättung würden Gegner bei jedem Richtungswechsel ruckartig umspringen. * @param {Vec2} current * @param {Vec2} desired * @param {number} responsiveness 0..1 (1 = sofort). * @returns {Vec2} */ export function steerTowards(current, desired, responsiveness) { return { x: current.x + (desired.x - current.x) * responsiveness, y: current.y + (desired.y - current.y) * responsiveness, }; } /** * Ist ein Vektor (fast) null? Nützlich für Animationen („steht still“). * @param {Vec2} v * @param {number} [epsilon] * @returns {boolean} */ export function isNearlyZero(v, epsilon = 1e-3) { return length(v) < epsilon; }