Files
slop-game-opus5.5/tests/js/config.test.js
T
2026-09-24 17:57:54 +02:00

115 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Plausibilitätsprüfungen der Balancing-Werte: fängt Tippfehler ab, bevor sie im
* Spiel als seltsames Verhalten auffallen.
*/
import { describe, expect, it } from 'vitest';
import {
ENEMY_TYPES,
FACTIONS,
HIGHSCORE,
ORBITAL,
PLAYER,
SCORING,
WAVES,
WEAPONS,
} from '../../app/static/js/config.js';
import { validateCommands } from '../../app/static/js/logic/comboInput.js';
/** Findet NaN, negative Zahlen und fehlende Werte in verschachtelten Objekten. */
function assertSaneNumbers(obj, path = '') {
for (const [key, value] of Object.entries(obj)) {
const here = `${path}.${key}`;
if (typeof value === 'number') {
expect(Number.isFinite(value), here).toBe(true);
expect(value, here).toBeGreaterThanOrEqual(0);
} else if (value && typeof value === 'object') {
assertSaneNumbers(value, here);
}
}
}
describe('config', () => {
it('enthält nur endliche, nicht-negative Zahlen', () => {
for (const [name, section] of Object.entries({ PLAYER, WEAPONS, ENEMY_TYPES, WAVES, SCORING, ORBITAL })) {
assertSaneNumbers(section, name);
}
});
it('hat je Fraktion genau einen kleinen, mittleren und Elite-Gegner', () => {
for (const faction of Object.keys(FACTIONS)) {
const tiers = Object.values(ENEMY_TYPES)
.filter((t) => t.faction === faction)
.map((t) => t.tier)
.sort();
expect(tiers, faction).toEqual(['elite', 'medium', 'small']);
}
});
it('verwendet konsistente IDs und gültige Gruppengrößen', () => {
for (const [key, type] of Object.entries(ENEMY_TYPES)) {
expect(type.id).toBe(key);
expect(type.groupSize.min).toBeLessThanOrEqual(type.groupSize.max);
expect(type.groupSize.min).toBeGreaterThanOrEqual(1);
}
for (const [key, weapon] of Object.entries(WEAPONS)) expect(weapon.id).toBe(key);
});
it('lässt Fernkämpfer nur schießen, wenn sie in ihrem Abstandsband sind', () => {
for (const type of Object.values(ENEMY_TYPES)) {
if (!type.range) continue;
expect(type.range.min, type.id).toBeLessThan(type.range.max);
expect(type.attack.maxDistance, type.id).toBeGreaterThanOrEqual(type.range.max);
}
});
it('verweist nur auf existierende Waffen und Brut-Typen', () => {
for (const slot of PLAYER.weaponSlots) expect(WEAPONS[slot]).toBeDefined();
expect(ENEMY_TYPES[ENEMY_TYPES.brutmutter.brood.childType]).toBeDefined();
});
it('hat mindestens drei gültige Orbital-Befehle mit Verzögerung und Abklingzeit', () => {
expect(ORBITAL.commands.length).toBeGreaterThanOrEqual(3);
expect(() => validateCommands(ORBITAL.commands)).not.toThrow();
for (const command of ORBITAL.commands) {
expect(command.delayMs, command.id).toBeGreaterThan(0);
expect(command.cooldownMs, command.id).toBeGreaterThan(command.delayMs);
}
const ids = ORBITAL.commands.map((c) => c.id);
expect(ids).toEqual(expect.arrayContaining(['strike', 'supply', 'turret']));
});
it('verwendet eigene Befehlscodes statt Codes aus bekannten Spielen', () => {
// Codes, die in einem bekannten Koop-Shooter mit ähnlicher Mechanik belegt sind
// (vom unabhängigen Review als zu nah beanstandet). Keiner unserer Codes darf gleich sein.
const taken = [
['down', 'down', 'up', 'right'],
['down', 'up', 'right', 'right', 'up'],
['up', 'down', 'right', 'left', 'up'],
['up', 'right', 'down', 'down', 'down'],
['right', 'right', 'up'],
['down', 'up', 'right', 'left'],
].map((s) => s.join(','));
for (const command of ORBITAL.commands) {
expect(taken, command.id).not.toContain(command.sequence.join(','));
}
// Jeder Code beginnt mit einer anderen Richtung – schon der erste Tastendruck ist eindeutig.
const starts = ORBITAL.commands.map((c) => c.sequence[0]);
expect(new Set(starts).size).toBe(starts.length);
});
it('nutzt als Halte-Taste weder Strg (Strg+W schließt den Tab) noch Shift (Einrastfunktion)', () => {
expect(['CTRL', 'CONTROL', 'SHIFT']).not.toContain(ORBITAL.holdKey.toUpperCase());
});
it('hält das Wellenbudget mindestens so hoch wie die teuerste Elite in Elite-Wellen', () => {
const maxEliteCost = Math.max(...Object.values(ENEMY_TYPES).filter((t) => t.tier === 'elite').map((t) => t.cost));
const firstEliteBudget = WAVES.firstBudget + WAVES.budgetGrowth * Math.pow(WAVES.eliteEvery - 1, WAVES.budgetExponent);
expect(firstEliteBudget).toBeGreaterThan(maxEliteCost);
});
it('stimmt die maximale Namenslänge mit dem Backend ab', () => {
expect(HIGHSCORE.nameMaxLength).toBe(16);
});
});