slop game oder so
This commit is contained in:
@@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user