Files
slop-game-opus5.5/app/static/js/logic/steering.js
T
2026-09-24 17:57:54 +02:00

146 lines
5.0 KiB
JavaScript

/**
* 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<Vec2>} 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;
}