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
+449
View File
@@ -0,0 +1,449 @@
/**
* Zentrale Spielkonfiguration: alle Balancing-Werte an einem Ort.
*
* Keine Magic Numbers im restlichen Code – wer das Spiel ausbalancieren will,
* ändert nur diese Datei. Einheiten: Pixel, Pixel pro Sekunde, Millisekunden.
* Die Werte sind in docs/GAME_DESIGN.md erläutert.
*
* Achtung: Die Punkte-Obergrenze im Backend (app/config.py, GAME_RULE_DEFAULTS)
* ist auf diese Werte abgestimmt. Wer Punkte stark erhöht, muss sie dort anpassen.
*/
/** Darstellung und Spielwelt. */
export const GAME = Object.freeze({
width: 1280,
height: 720,
backgroundColor: '#11151f',
/** Die Arena ist größer als der Bildschirm; die Kamera folgt dem Präsidenten. */
world: Object.freeze({ width: 2400, height: 1600 }),
/** Gegner erscheinen so weit außerhalb des sichtbaren Randes der Arena. */
spawnMargin: 40,
cameraLerp: 0.12,
/** Tiefe (Zeichenreihenfolge) der Ebenen. */
depth: Object.freeze({
ground: 0,
markers: 5,
pickups: 8,
enemies: 10,
player: 20,
projectiles: 25,
effects: 30,
hud: 100,
}),
});
/** Präsident Maximilian Valor. */
export const PLAYER = Object.freeze({
maxHp: 100,
speed: 240,
radius: 18,
/** Kurze Unverwundbarkeit nach einem Treffer, damit ein Schwarm nicht in einem Frame tötet. */
hitInvulnerabilityMs: 500,
/** Heilung nach jeder überstandenen Welle – belohnt Fortschritt ohne Nachschub. */
waveClearHeal: 15,
dodge: Object.freeze({
speed: 640,
durationMs: 320,
cooldownMs: 1500,
}),
/** Waffen in Slot-Reihenfolge (Tasten 1–3). */
weaponSlots: Object.freeze(['carbine', 'shotgun', 'pistol']),
});
/**
* Waffen. `reserveMax: null` bedeutet unbegrenzte Reserve (Dienstpistole als
* Rückfalloption, damit der Spieler ohne Munition nie wehrlos ist).
*/
export const WEAPONS = Object.freeze({
carbine: Object.freeze({
id: 'carbine',
name: 'Karabiner »Zuversicht«',
shortName: 'Karabiner',
magazineSize: 30,
reserveStart: 180,
reserveMax: 270,
fireIntervalMs: 110,
reloadMs: 1500,
damage: 14,
pellets: 1,
spreadDeg: 3,
bulletSpeed: 900,
rangePx: 820,
automatic: true,
}),
shotgun: Object.freeze({
id: 'shotgun',
name: 'Schrotflinte »Überzeugungskraft«',
shortName: 'Schrotflinte',
magazineSize: 6,
reserveStart: 24,
reserveMax: 48,
fireIntervalMs: 650,
reloadMs: 1900,
damage: 11,
pellets: 8,
spreadDeg: 24,
bulletSpeed: 820,
rangePx: 420,
automatic: false,
}),
pistol: Object.freeze({
id: 'pistol',
name: 'Dienstpistole »Anstand«',
shortName: 'Pistole',
magazineSize: 12,
reserveStart: null,
reserveMax: null,
fireIntervalMs: 260,
reloadMs: 1100,
damage: 16,
pellets: 1,
spreadDeg: 2,
bulletSpeed: 860,
rangePx: 700,
automatic: false,
}),
});
/** Faktionen (für Farben, Texte und Wellenzusammensetzung). */
export const FACTIONS = Object.freeze({
swarm: Object.freeze({ id: 'swarm', name: 'Die Schwärmer' }),
legion: Object.freeze({ id: 'legion', name: 'Die Blechlegion' }),
});
/**
* Gegnertypen. `cost` ist der Preis im Wellenbudget, `behavior` wählt die KI.
* Verhalten:
* - swarm: umzingeln (jeder Krabbler hat einen eigenen Anflugwinkel), nah dran direkt angreifen
* - leaper: verfolgen, dann mit Vorwarnung einen Sprungangriff ausführen
* - brood: langsam verfolgen und dabei Krabbler ausbrüten
* - kite: Abstand halten, seitlich ausweichen, aus der Distanz schießen
* - flank: seitlich versetzt zum Spieler positionieren (Flanke), Salven schießen
* - siege: großer Abstand, Fächersalven und Mörser mit Bodenmarkierung
*/
export const ENEMY_TYPES = Object.freeze({
krabbler: Object.freeze({
id: 'krabbler',
name: 'Krabbler',
faction: 'swarm',
tier: 'small',
behavior: 'swarm',
hp: 20,
speed: 175,
radius: 12,
contactDamage: 6,
contactCooldownMs: 700,
points: 10,
cost: 1,
groupSize: Object.freeze({ min: 4, max: 8 }),
/** Abstand, ab dem der Krabbler den Umzingelungspunkt verlässt und direkt angreift. */
engageRange: 170,
encircleRadius: 130,
}),
sprungkralle: Object.freeze({
id: 'sprungkralle',
name: 'Sprungkralle',
faction: 'swarm',
tier: 'medium',
behavior: 'leaper',
hp: 60,
speed: 125,
radius: 17,
contactDamage: 14,
contactCooldownMs: 800,
points: 30,
cost: 3,
groupSize: Object.freeze({ min: 1, max: 3 }),
leap: Object.freeze({
triggerRange: 260,
windupMs: 450,
speed: 560,
durationMs: 380,
cooldownMs: 2400,
}),
}),
brutmutter: Object.freeze({
id: 'brutmutter',
name: 'Brutmutter',
faction: 'swarm',
tier: 'elite',
behavior: 'brood',
hp: 450,
speed: 58,
radius: 34,
contactDamage: 28,
contactCooldownMs: 1000,
points: 250,
cost: 15,
groupSize: Object.freeze({ min: 1, max: 1 }),
brood: Object.freeze({
intervalMs: 5000,
spawnCount: 3,
/** Obergrenze lebender Nachkommen, damit eine ignorierte Brutmutter nicht eskaliert. */
maxChildren: 6,
childType: 'krabbler',
}),
}),
nieter: Object.freeze({
id: 'nieter',
name: 'Nieter-Drohne',
faction: 'legion',
tier: 'small',
behavior: 'kite',
hp: 30,
speed: 95,
radius: 13,
contactDamage: 5,
contactCooldownMs: 800,
points: 20,
cost: 2,
groupSize: Object.freeze({ min: 2, max: 3 }),
range: Object.freeze({ min: 220, max: 340 }),
attack: Object.freeze({
cooldownMs: 1500,
burst: 1,
burstIntervalMs: 0,
count: 1,
spreadDeg: 0,
damage: 7,
speed: 330,
/** Nur feuern, wenn der Spieler näher als das ist (etwas mehr als range.max). */
maxDistance: 480,
}),
}),
blechsoldat: Object.freeze({
id: 'blechsoldat',
name: 'Blechsoldat',
faction: 'legion',
tier: 'medium',
behavior: 'flank',
hp: 90,
speed: 78,
radius: 18,
contactDamage: 10,
contactCooldownMs: 900,
points: 50,
cost: 4,
groupSize: Object.freeze({ min: 1, max: 3 }),
range: Object.freeze({ min: 200, max: 380 }),
/** Winkelversatz der Flankenposition relativ zur Linie Spieler→Gegner. */
flankAngleDeg: 70,
flankDistance: 290,
attack: Object.freeze({
cooldownMs: 2300,
burst: 3,
burstIntervalMs: 140,
count: 1,
spreadDeg: 4,
damage: 8,
speed: 400,
maxDistance: 520,
}),
}),
walzmeister: Object.freeze({
id: 'walzmeister',
name: 'Walzmeister',
faction: 'legion',
tier: 'elite',
behavior: 'siege',
hp: 700,
speed: 44,
radius: 36,
contactDamage: 25,
contactCooldownMs: 1000,
points: 400,
cost: 18,
groupSize: Object.freeze({ min: 1, max: 1 }),
range: Object.freeze({ min: 260, max: 460 }),
attack: Object.freeze({
cooldownMs: 2700,
burst: 1,
burstIntervalMs: 0,
count: 7,
spreadDeg: 50,
damage: 10,
speed: 290,
maxDistance: 640,
}),
mortar: Object.freeze({
cooldownMs: 5200,
delayMs: 1400,
radius: 90,
damage: 26,
maxDistance: 700,
}),
}),
});
/** Allgemeine Steuerungswerte der Gegner-KI. */
export const AI = Object.freeze({
/** Abstand, unter dem sich Gegner gegenseitig wegschieben (verhindert Klumpen). */
separationRadius: 34,
separationWeight: 1.4,
/** Richtungswechsel beim seitlichen Ausweichen (Strafen) in diesem Zeitfenster. */
strafeSwitchMs: Object.freeze({ min: 1400, max: 3200 }),
/** Wie stark die Bewegung pro Frame zur Wunschrichtung nachgeführt wird (0..1). */
steeringResponsiveness: 0.18,
});
/** Wellen-Generator (siehe logic/waves.js). */
export const WAVES = Object.freeze({
/** Budget(n) = firstBudget + growth * (n - 1) ^ exponent */
firstBudget: 8,
budgetGrowth: 5,
budgetExponent: 1.15,
/** Wellen 1–2 nur Schwärmer, 3–4 nur Blechlegion, danach gemischt. */
legionFromWave: 3,
mixedFromWave: 5,
/** Jede n-te Welle bringt Elite-Gegner. */
eliteEvery: 5,
maxElitesPerWave: 4,
/** Anteil des Restbudgets, der in mittlere Gegner fließt – steigt mit der Welle. */
mediumShareBase: 0.1,
mediumSharePerWave: 0.04,
mediumShareMax: 0.5,
/** Ab dieser Welle werden Gegner zäher und schneller. */
scalingFromWave: 6,
hpScalePerWave: 0.07,
speedScalePerWave: 0.012,
speedScaleMax: 1.3,
/** Zeitlicher Abstand zwischen zwei Spawn-Gruppen. */
groupDelayMs: Object.freeze({ min: 1300, max: 3000 }),
/** Spätere Wellen spawnen schneller, aber nie schneller als dieser Faktor. */
groupDelayMinFactor: 0.45,
groupDelayFactorPerWave: 0.035,
/** Solange so viele Gegner leben, wartet der Spawner. */
maxAliveEnemies: 60,
intermissionMs: 8000,
firstWaveDelayMs: 1500,
});
/** Punkte und Serien-Multiplikator (siehe logic/scoring.js). */
export const SCORING = Object.freeze({
/** Abschüsse innerhalb dieses Fensters setzen die Serie fort. */
streakWindowMs: 3000,
/** Alle `stepKills` Abschüsse steigt der Multiplikator um `stepValue`. */
stepKills: 5,
stepValue: 0.5,
maxMultiplier: 4,
/** Treffer am Präsidenten beenden die Serie. */
damageBreaksStreak: true,
waveBonusPerWave: 100,
});
/**
* Orbital-Befehle: Taste `holdKey` gedrückt halten, dann die Richtungsfolge mit
* Pfeiltasten oder WASD eingeben. Ziel ist die Mausposition (auf maxTargetRange begrenzt).
* Keine Folge darf Anfang einer anderen sein (wird in den Tests geprüft). Die Codes sind
* eigene Erfindungen; jeder beginnt mit einer anderen Richtung, damit man sich nach
* dem ersten Tastendruck nicht mehr vertippen kann.
*/
export const ORBITAL = Object.freeze({
holdKey: 'Q',
maxTargetRange: 560,
commands: Object.freeze([
Object.freeze({
id: 'strike',
name: 'Orbitalschlag »Morgenröte«',
shortName: 'Orbitalschlag',
sequence: Object.freeze(['up', 'right', 'down', 'down']),
cooldownMs: 30000,
delayMs: 2500,
radius: 150,
damage: 260,
/** Auch der Präsident steht nicht über der Physik. */
playerDamage: 40,
}),
Object.freeze({
id: 'supply',
name: 'Nachschubkapsel »Fürsorgepaket«',
shortName: 'Nachschub',
sequence: Object.freeze(['left', 'left', 'down', 'right']),
cooldownMs: 45000,
delayMs: 3000,
radius: 70,
damage: 120,
playerDamage: 20,
heal: 50,
pickupRadius: 46,
lifetimeMs: 30000,
}),
Object.freeze({
id: 'turret',
name: 'Geschütz »Wachsamer Nachbar«',
shortName: 'Geschütz',
sequence: Object.freeze(['right', 'left', 'right', 'up', 'up']),
cooldownMs: 60000,
delayMs: 3000,
radius: 60,
damage: 80,
playerDamage: 15,
durationMs: 25000,
fireIntervalMs: 150,
range: 480,
bulletDamage: 12,
bulletSpeed: 950,
}),
]),
});
/** Darstellung von Kombinationspfeilen im HUD. */
export const DIRECTION_SYMBOLS = Object.freeze({ up: '↑', down: '↓', left: '←', right: '→' });
/** Farben (Platzhaltergrafiken, HUD, Effekte). */
export const COLORS = Object.freeze({
navy: 0x0d1321,
crimson: 0xc1121f,
gold: 0xf2c14e,
cream: 0xf4ecd8,
muted: 0xa9b1c4,
ground: 0x2a2f24,
groundAlt: 0x31372a,
player: 0xf4ecd8,
playerCape: 0xc1121f,
swarm: 0x8fbf3a,
swarmDark: 0x4f6d1e,
legion: 0x9aa4b5,
legionDark: 0x535c6b,
playerBullet: 0xffe28a,
enemyBullet: 0xff5a3c,
turretBullet: 0x9be7ff,
strike: 0xff7b00,
supply: 0x4fd1c5,
turret: 0x9be7ff,
mortar: 0xff3d3d,
hpBar: 0x5ad469,
hpBarLow: 0xe0453a,
});
/** Standardlautstärken (0..1) und Ton-Parameter. */
export const AUDIO = Object.freeze({
defaultVolumes: Object.freeze({ master: 0.8, music: 0.5, sfx: 0.8 }),
/** Mindestabstand zwischen gleichen Effekten, damit Dauerfeuer nicht übersteuert. */
sfxMinIntervalMs: 45,
});
/** Schlüssel für localStorage (versioniert, damit Formatänderungen alte Daten nicht brechen). */
export const STORAGE_KEYS = Object.freeze({
disclaimerAccepted: 'gloryDefense.disclaimerAccepted.v1',
volumes: 'gloryDefense.volumes.v1',
lastName: 'gloryDefense.lastName.v1',
});
/** Grenzen, die mit dem Backend übereinstimmen müssen (app/validation.py). */
export const HIGHSCORE = Object.freeze({
nameMaxLength: 16,
requestTimeoutMs: 8000,
});
/** Effekte und Rückmeldung. */
export const FX = Object.freeze({
screenShake: Object.freeze({
hit: Object.freeze({ durationMs: 120, intensity: 0.004 }),
explosion: Object.freeze({ durationMs: 350, intensity: 0.012 }),
}),
hitFlashMs: 80,
damageNumberMs: 600,
announcementMs: 3800,
deathDelayMs: 1600,
});