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