slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
/**
* 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);
});
});