166 lines
6.2 KiB
JavaScript
166 lines
6.2 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { ENEMY_TYPES } from '../../app/static/js/config.js';
|
|
import {
|
|
attackStep,
|
|
broodStep,
|
|
createAttackState,
|
|
createBrain,
|
|
flankVelocity,
|
|
leaperStep,
|
|
swarmVelocity,
|
|
updateStrafe,
|
|
volleyAngles,
|
|
} from '../../app/static/js/logic/enemyAi.js';
|
|
import { degToRad, distance, length } from '../../app/static/js/logic/math.js';
|
|
import { createRng } from '../../app/static/js/logic/rng.js';
|
|
|
|
const player = { x: 0, y: 0 };
|
|
|
|
describe('createBrain', () => {
|
|
it('verzögert den ersten Angriff und ist reproduzierbar', () => {
|
|
const a = createBrain(createRng(1), 1000, { firstAttackDelayMs: 800 });
|
|
const b = createBrain(createRng(1), 1000, { firstAttackDelayMs: 800 });
|
|
expect(a).toEqual(b);
|
|
expect(a.attack.nextAttackAt).toBeGreaterThanOrEqual(1400);
|
|
expect(a.attack.nextAttackAt).toBeLessThan(2200);
|
|
});
|
|
});
|
|
|
|
describe('swarmVelocity', () => {
|
|
const type = ENEMY_TYPES.krabbler;
|
|
|
|
it('steuert aus der Distanz den eigenen Umzingelungsplatz an', () => {
|
|
const brain = { slotAngle: Math.PI / 2 }; // Platz unterhalb des Spielers
|
|
const v = swarmVelocity(brain, { x: 800, y: 0 }, player, 100, type);
|
|
// Ziel ist (0, encircleRadius): nach links UND nach unten.
|
|
expect(v.x).toBeLessThan(0);
|
|
expect(v.y).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('greift in Reichweite direkt an', () => {
|
|
const v = swarmVelocity({ slotAngle: Math.PI / 2 }, { x: 100, y: 0 }, player, 100, type);
|
|
expect(v.x).toBeCloseTo(-100);
|
|
expect(v.y).toBeCloseTo(0);
|
|
});
|
|
});
|
|
|
|
describe('leaperStep', () => {
|
|
const { leap } = ENEMY_TYPES.sprungkralle;
|
|
|
|
it('verfolgt außerhalb der Sprungreichweite', () => {
|
|
const brain = createBrain(createRng(1), 0);
|
|
const r = leaperStep(brain, { x: 1000, y: 0 }, player, 0, 120, leap);
|
|
expect(r.telegraph).toBe(false);
|
|
expect(r.velocity.x).toBeCloseTo(-120);
|
|
});
|
|
|
|
it('durchläuft Ansetzen → Sprung → Abklingzeit', () => {
|
|
const brain = createBrain(createRng(1), 0);
|
|
const self = { x: 200, y: 0 };
|
|
|
|
const windup = leaperStep(brain, self, player, 0, 120, leap);
|
|
expect(windup).toMatchObject({ telegraph: true, leaping: false, velocity: { x: 0, y: 0 } });
|
|
|
|
// Spieler bewegt sich während des Ansetzens: Die Sprungrichtung bleibt fest.
|
|
const moved = { x: 0, y: 500 };
|
|
const jump = leaperStep(brain, self, moved, leap.windupMs, 120, leap);
|
|
expect(jump.leaping).toBe(true);
|
|
expect(jump.velocity.x).toBeCloseTo(-leap.speed);
|
|
expect(jump.velocity.y).toBeCloseTo(0);
|
|
|
|
const landed = leaperStep(brain, self, player, leap.windupMs + leap.durationMs, 120, leap);
|
|
expect(landed.leaping).toBe(false);
|
|
expect(landed.telegraph).toBe(false);
|
|
|
|
// Während der Abklingzeit kein neuer Sprung, obwohl in Reichweite.
|
|
const cooling = leaperStep(brain, self, player, leap.windupMs + leap.durationMs + 10, 120, leap);
|
|
expect(cooling.telegraph).toBe(false);
|
|
const again = leaperStep(brain, self, player, leap.windupMs + leap.durationMs + leap.cooldownMs, 120, leap);
|
|
expect(again.telegraph).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('flankVelocity', () => {
|
|
it('schwenkt seitlich ein statt frontal anzulaufen', () => {
|
|
const type = ENEMY_TYPES.blechsoldat;
|
|
const self = { x: 600, y: 0 };
|
|
const left = flankVelocity({ flankSide: 1 }, self, player, 80, type);
|
|
const right = flankVelocity({ flankSide: -1 }, self, player, 80, type);
|
|
expect(Math.sign(left.y)).not.toBe(Math.sign(right.y));
|
|
expect(length(left)).toBeCloseTo(80);
|
|
});
|
|
|
|
it('hält an der Flankenposition Abstand zum Spieler', () => {
|
|
const type = ENEMY_TYPES.blechsoldat;
|
|
const angle = degToRad(type.flankAngleDeg);
|
|
// Genau an der Flankenposition relativ zu sich selbst gibt es keinen festen Punkt,
|
|
// daher: sehr nah am Spieler → muss zurückweichen oder seitlich ausweichen.
|
|
const self = { x: Math.cos(angle) * 100, y: Math.sin(angle) * 100 };
|
|
const v = flankVelocity({ flankSide: 1 }, self, player, 80, type);
|
|
expect(distance({ x: self.x + v.x, y: self.y + v.y }, player)).toBeGreaterThanOrEqual(distance(self, player) - 1);
|
|
});
|
|
});
|
|
|
|
describe('updateStrafe', () => {
|
|
it('wechselt die Richtung nach Ablauf des Intervalls', () => {
|
|
const brain = { strafeDirection: 1, nextStrafeSwitchAt: 100 };
|
|
updateStrafe(brain, 50, { min: 1000, max: 1000 }, createRng(1));
|
|
expect(brain.strafeDirection).toBe(1);
|
|
updateStrafe(brain, 100, { min: 1000, max: 1000 }, createRng(1));
|
|
expect(brain.strafeDirection).toBe(-1);
|
|
expect(brain.nextStrafeSwitchAt).toBe(1100);
|
|
});
|
|
});
|
|
|
|
describe('attackStep', () => {
|
|
const attack = { cooldownMs: 1000, burst: 3, burstIntervalMs: 100, maxDistance: 400 };
|
|
|
|
it('feuert nicht außerhalb der Reichweite', () => {
|
|
const state = createAttackState(0);
|
|
expect(attackStep(state, 0, 401, attack)).toBe(false);
|
|
});
|
|
|
|
it('feuert eine Salve mit Abständen und wartet dann die Abklingzeit ab', () => {
|
|
const state = createAttackState(0);
|
|
const shots = [];
|
|
for (let t = 0; t <= 1200; t += 50) {
|
|
if (attackStep(state, t, 100, attack)) shots.push(t);
|
|
}
|
|
expect(shots).toEqual([0, 100, 200, 1000, 1100, 1200]);
|
|
});
|
|
|
|
it('respektiert die Verzögerung des ersten Angriffs', () => {
|
|
const state = createAttackState(500);
|
|
expect(attackStep(state, 499, 100, attack)).toBe(false);
|
|
expect(attackStep(state, 500, 100, attack)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('volleyAngles', () => {
|
|
it('liefert bei einem Projektil genau die Zielrichtung', () => {
|
|
expect(volleyAngles(1.2, 1, 40)).toEqual([1.2]);
|
|
});
|
|
|
|
it('verteilt einen Fächer symmetrisch um die Zielrichtung', () => {
|
|
const angles = volleyAngles(0, 5, 40);
|
|
expect(angles).toHaveLength(5);
|
|
expect(angles[0]).toBeCloseTo(-degToRad(20));
|
|
expect(angles[2]).toBeCloseTo(0);
|
|
expect(angles[4]).toBeCloseTo(degToRad(20));
|
|
});
|
|
});
|
|
|
|
describe('broodStep', () => {
|
|
const brood = { intervalMs: 1000, spawnCount: 3, maxChildren: 5 };
|
|
|
|
it('brütet im Takt und respektiert die Obergrenze', () => {
|
|
const brain = { nextBroodAt: 0 };
|
|
expect(broodStep(brain, 0, 0, brood)).toBe(3);
|
|
expect(broodStep(brain, 500, 3, brood)).toBe(0);
|
|
expect(broodStep(brain, 1000, 3, brood)).toBe(2);
|
|
expect(broodStep(brain, 2000, 5, brood)).toBe(0);
|
|
expect(broodStep(brain, 3000, 9, brood)).toBe(0);
|
|
});
|
|
});
|