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

450 lines
12 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.
/**
* 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,
});