115 lines
4.5 KiB
JavaScript
115 lines
4.5 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|
||
});
|