Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
18 KiB
GDScript
184 lines
18 KiB
GDScript
extends Node
|
||
## Immutable design data. Callers receive fresh combat dictionaries.
|
||
|
||
const SHORT_WAVE_COUNT: int = 8
|
||
const EXPEDITION_WAVE_COUNT: int = 20
|
||
const BASE_IDS: Array[String] = ["g01", "g03", "g05", "g07", "g09"]
|
||
const DAMAGE_NAMES: Array[String] = ["Klinge", "Durchschlag", "Impuls"]
|
||
const ARMOR_NAMES: Array[String] = ["Leicht", "Schwer", "Ätherisch"]
|
||
const UNITS: Dictionary = {
|
||
"g01": {"id": "g01", "display_name": "Kohlenbeißer", "cost": 45, "upgrade_id": "g02", "max_hp": 260.0, "damage": 17.0, "attack_type": 0, "armor_type": 0, "attack_range": 1.1, "speed": 2.4, "ability": "g01", "leak_value": 0, "bounty": 0, "color": Color("d76e36"), "ability_description": "Jeder 3. Treffer: Brand, 4 Schaden/s für 3 s."},
|
||
"g02": {"id": "g02", "display_name": "Schlackenklaue", "cost": 90, "upgrade_id": "", "max_hp": 690.0, "damage": 44.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.2, "speed": 2.4, "ability": "g02", "leak_value": 0, "bounty": 0, "color": Color("ef9849"), "ability_description": "Jeder 3. Treffer: Brand, 12 Schaden/s für 3 s."},
|
||
"g03": {"id": "g03", "display_name": "Schlackenwacht", "cost": 70, "upgrade_id": "g04", "max_hp": 650.0, "damage": 11.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.2, "speed": 2.4, "ability": "g03", "leak_value": 0, "bounty": 0, "color": Color("8c5544"), "ability_description": "Erste 8 s: 15 % weniger erlittener Schaden."},
|
||
"g04": {"id": "g04", "display_name": "Basaltbastion", "cost": 150, "upgrade_id": "", "max_hp": 1650.0, "damage": 27.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.4, "speed": 2.4, "ability": "g04", "leak_value": 0, "bounty": 0, "color": Color("ba6c42"), "ability_description": "Erste 10 s: 25 % weniger Schaden, danach 10 %."},
|
||
"g05": {"id": "g05", "display_name": "Funkenlanze", "cost": 65, "upgrade_id": "g06", "max_hp": 240.0, "damage": 24.0, "attack_type": 1, "armor_type": 0, "attack_range": 5.5, "speed": 2.0, "ability": "g05", "leak_value": 0, "bounty": 0, "color": Color("edb354"), "ability_description": "Jeder 4. Treffer: 50 % zusätzlicher Grundschaden."},
|
||
"g06": {"id": "g06", "display_name": "Glasballiste", "cost": 140, "upgrade_id": "", "max_hp": 640.0, "damage": 65.0, "attack_type": 1, "armor_type": 0, "attack_range": 6.5, "speed": 2.0, "ability": "g06", "leak_value": 0, "bounty": 0, "color": Color("ffd579"), "ability_description": "Jeder 4. Treffer: 75 % zusätzlicher Grundschaden."},
|
||
"g07": {"id": "g07", "display_name": "Aschenwerfer", "cost": 95, "upgrade_id": "g08", "max_hp": 310.0, "damage": 23.0, "attack_type": 2, "armor_type": 0, "attack_range": 4.5, "speed": 2.0, "ability": "g07", "leak_value": 0, "bounty": 0, "color": Color("cb7867"), "ability_description": "Jeder Treffer: 30 % Flächenschaden an bis zu 3 weiteren Zielen in 1,5 m."},
|
||
"g08": {"id": "g08", "display_name": "Kraterrufer", "cost": 190, "upgrade_id": "", "max_hp": 820.0, "damage": 58.0, "attack_type": 2, "armor_type": 1, "attack_range": 5.0, "speed": 2.0, "ability": "g08", "leak_value": 0, "bounty": 0, "color": Color("f6956e"), "ability_description": "Jeder Treffer: 40 % Flächenschaden an bis zu 4 weiteren Zielen in 2 m."},
|
||
"g09": {"id": "g09", "display_name": "Herdhüter", "cost": 85, "upgrade_id": "g10", "max_hp": 420.0, "damage": 12.0, "attack_type": 2, "armor_type": 2, "attack_range": 3.5, "speed": 2.0, "ability": "g09", "leak_value": 0, "bounty": 0, "color": Color("e9ae80"), "ability_description": "Alle 6 s: bis zu 3 Verbündete in 3 m erhalten für 4 s +10 % Grundangriffsschaden."},
|
||
"g10": {"id": "g10", "display_name": "Sonnenesse", "cost": 170, "upgrade_id": "", "max_hp": 1080.0, "damage": 30.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.0, "speed": 2.0, "ability": "g10", "leak_value": 0, "bounty": 0, "color": Color("ffe0a4"), "ability_description": "Alle 6 s: bis zu 4 Verbündete in 4 m erhalten für 4 s +15 % Grundangriffsschaden und 45 Heilung."},
|
||
"w01": {"id": "w01", "display_name": "Dornhopser", "cost": 50, "upgrade_id": "w02", "max_hp": 290.0, "damage": 18.0, "attack_type": 0, "armor_type": 0, "attack_range": 1.1, "speed": 2.4, "ability": "w01", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 3. Treffer: −20 % Bewegung für 2 s.", "color": Color("a7ba6c")},
|
||
"w02": {"id": "w02", "display_name": "Rankenschnapper", "cost": 100, "upgrade_id": "", "max_hp": 800.0, "damage": 47.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.3, "speed": 2.4, "ability": "w02", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 3. Treffer: −30 % Bewegung und −10 % Angriffstempo für 3 s.", "color": Color("a7ba6c")},
|
||
"w03": {"id": "w03", "display_name": "Korkrücken", "cost": 75, "upgrade_id": "w04", "max_hp": 620.0, "damage": 12.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.2, "speed": 2.4, "ability": "w03", "leak_value": 0, "bounty": 0, "ability_description": "Heilt sich alle 5 s um 35 HP, sobald er verwundet ist.", "color": Color("a7ba6c")},
|
||
"w04": {"id": "w04", "display_name": "Ringborke", "cost": 155, "upgrade_id": "", "max_hp": 1560.0, "damage": 31.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.4, "speed": 2.4, "ability": "w04", "leak_value": 0, "bounty": 0, "ability_description": "Heilt sich alle 5 s um 100 HP, sobald er verwundet ist.", "color": Color("a7ba6c")},
|
||
"w05": {"id": "w05", "display_name": "Samenschleuder", "cost": 60, "upgrade_id": "w06", "max_hp": 230.0, "damage": 23.0, "attack_type": 1, "armor_type": 0, "attack_range": 5.5, "speed": 2.0, "ability": "w05", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 4. Treffer: Keimbrand, 3 Schaden/s für 4 s.", "color": Color("a7ba6c")},
|
||
"w06": {"id": "w06", "display_name": "Zapfenkrone", "cost": 135, "upgrade_id": "", "max_hp": 630.0, "damage": 61.0, "attack_type": 1, "armor_type": 0, "attack_range": 6.0, "speed": 2.0, "ability": "w06", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 4. Treffer: Keimbrand, 10 Schaden/s für 4 s.", "color": Color("a7ba6c")},
|
||
"w07": {"id": "w07", "display_name": "Sporenkelch", "cost": 100, "upgrade_id": "w08", "max_hp": 370.0, "damage": 22.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.0, "speed": 2.0, "ability": "w07", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 3. Treffer: 60 % Grundschaden an bis zu 3 weiteren Zielen in 2 m.", "color": Color("a7ba6c")},
|
||
"w08": {"id": "w08", "display_name": "Pilzkathedrale", "cost": 195, "upgrade_id": "", "max_hp": 940.0, "damage": 59.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.5, "speed": 2.0, "ability": "w08", "leak_value": 0, "bounty": 0, "ability_description": "Jeder 3. Treffer: 80 % Grundschaden an bis zu 4 weiteren Zielen in 2,5 m.", "color": Color("a7ba6c")},
|
||
"w09": {"id": "w09", "display_name": "Tauhüter", "cost": 90, "upgrade_id": "w10", "max_hp": 390.0, "damage": 10.0, "attack_type": 2, "armor_type": 0, "attack_range": 4.0, "speed": 2.0, "ability": "w09", "leak_value": 0, "bounty": 0, "ability_description": "Alle 4 s: heilt den am stärksten verwundeten Verbündeten in 4 m um 65 HP.", "color": Color("a7ba6c")},
|
||
"w10": {"id": "w10", "display_name": "Quellgeflecht", "cost": 180, "upgrade_id": "", "max_hp": 1000.0, "damage": 27.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.5, "speed": 2.0, "ability": "w10", "leak_value": 0, "bounty": 0, "ability_description": "Alle 4 s: heilt bis zu 3 verwundete Verbündete in 4,5 m um je 80 HP.", "color": Color("a7ba6c")},
|
||
}
|
||
const ENEMIES: Dictionary = {
|
||
"e01": {"id": "e01", "display_name": "Splitterling", "max_hp": 90.0, "damage": 6.0, "attack_type": 0, "armor_type": 0, "attack_range": 1.0, "speed": 3.0, "ability": "e01", "leak_value": 25, "bounty": 3},
|
||
"e02": {"id": "e02", "display_name": "Schalenläufer", "max_hp": 240.0, "damage": 9.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.2, "speed": 2.0, "ability": "e02", "leak_value": 55, "bounty": 6},
|
||
"e03": {"id": "e03", "display_name": "Nebelrest", "max_hp": 150.0, "damage": 11.0, "attack_type": 2, "armor_type": 2, "attack_range": 3.5, "speed": 2.2, "ability": "e03", "leak_value": 40, "bounty": 5},
|
||
"e04": {"id": "e04", "display_name": "Fadenschütze", "max_hp": 130.0, "damage": 12.0, "attack_type": 1, "armor_type": 0, "attack_range": 6.0, "speed": 2.4, "ability": "e04", "leak_value": 35, "bounty": 5},
|
||
"e05": {"id": "e05", "display_name": "Bruchstampfer", "max_hp": 480.0, "damage": 16.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.4, "speed": 1.6, "ability": "e05", "leak_value": 90, "bounty": 10},
|
||
"e06": {"id": "e06", "display_name": "Knotenrufer", "max_hp": 190.0, "damage": 7.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.0, "speed": 2.0, "ability": "e06", "leak_value": 45, "bounty": 6},
|
||
"e07": {"id": "e07", "display_name": "Bruchhirsch", "max_hp": 1100.0, "damage": 28.0, "attack_type": 0, "armor_type": 0, "attack_range": 1.4, "speed": 1.8, "ability": "e07", "leak_value": 250, "bounty": 30, "is_boss": true},
|
||
"e08": {"id": "e08", "display_name": "Riegelkoloss", "max_hp": 1400.0, "damage": 30.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.5, "speed": 1.5, "ability": "e08", "leak_value": 330, "bounty": 40, "is_boss": true},
|
||
"e09": {"id": "e09", "display_name": "Maskenrichter", "max_hp": 1800.0, "damage": 34.0, "attack_type": 2, "armor_type": 2, "attack_range": 5.0, "speed": 1.8, "ability": "e09", "leak_value": 400, "bounty": 50, "is_boss": true},
|
||
"e10": {"id": "e10", "display_name": "Schlundkrone", "max_hp": 3000.0, "damage": 50.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.5, "speed": 1.3, "ability": "e10", "leak_value": 600, "bounty": 70, "is_boss": true},
|
||
}
|
||
const SENDS: Dictionary = {
|
||
"s01": {"id": "s01", "display_name": "Nadelspäher", "cost": 20, "income_gain": 3, "max_hp": 100.0, "damage": 8.0, "attack_type": 1, "armor_type": 0, "attack_range": 4.5, "speed": 2.8, "ability": "s01", "leak_value": 30, "bounty": 3},
|
||
"s02": {"id": "s02", "display_name": "Rostschieber", "cost": 40, "income_gain": 5, "max_hp": 240.0, "damage": 8.0, "attack_type": 0, "armor_type": 1, "attack_range": 1.2, "speed": 1.8, "ability": "s02", "leak_value": 55, "bounty": 6},
|
||
"s03": {"id": "s03", "display_name": "Dunstsänger", "cost": 80, "income_gain": 9, "max_hp": 320.0, "damage": 10.0, "attack_type": 2, "armor_type": 2, "attack_range": 5.0, "speed": 2.0, "ability": "s03", "leak_value": 70, "bounty": 9},
|
||
"s04": {"id": "s04", "display_name": "Keilbrecher", "cost": 130, "income_gain": 12, "max_hp": 650.0, "damage": 20.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.3, "speed": 1.6, "ability": "s04", "leak_value": 120, "bounty": 14},
|
||
"s05": {"id": "s05", "display_name": "Risszehrer", "cost": 200, "income_gain": 17, "max_hp": 1000.0, "damage": 30.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.0, "speed": 1.7, "ability": "s05", "leak_value": 180, "bounty": 20},
|
||
}
|
||
# Glutpakt vertical slice: a readable eight-wave progression with two bosses.
|
||
const WAVES: Array[Dictionary] = [
|
||
{"e01": 8}, {"e01": 10}, {"e01": 6, "e02": 4},
|
||
{"e01": 8, "e04": 4}, {"e07": 1, "e01": 6},
|
||
{"e02": 6, "e01": 6}, {"e03": 8, "e01": 4},
|
||
{"e08": 1, "e02": 2, "e04": 3},
|
||
]
|
||
const WAVE_TITLES: Array[String] = [
|
||
"Erste Risse", "Splitterstrom", "Harte Schalen", "Fäden im Rücken",
|
||
"Der Bruchhirsch", "Gepanzerter Vorstoß", "Stimmen aus dem Nebel", "Der letzte Riegel"
|
||
]
|
||
const WAVE_HINTS: Array[String] = [
|
||
"Stelle Schlackenwacht und Kohlenbeißer nach vorn, die Funkenlanze dahinter.",
|
||
"Ergänze deine Verteidigung. Arbeiter produzieren erst ab der nächsten Bauphase.",
|
||
"Durchschlag wirkt gegen schwere Schalen. Söldner sind jetzt verfügbar.",
|
||
"Schützen stehen hinten. Flächenschaden hilft gegen die Splitterlinge davor.",
|
||
"BOSS · Der Bruchhirsch trifft bei jedem vierten Angriff mehrere Ziele. Werte deine Front auf.",
|
||
"Schwere Rüstung: Funkenlanze und Glasballiste verursachen 30 % mehr Schaden.",
|
||
"Ätherische Gegner: Aschenwerfer und Herdhüter nutzen Impulsschaden. Halte die Gruppe zusammen.",
|
||
"FINALE · Riegelkoloss mit Startschutz. Starke Front, Durchschlag und alle übrigen Funken einsetzen."
|
||
]
|
||
|
||
# Full expedition from the design dossier; the proven short match stays separate.
|
||
const EXPEDITION_WAVES: Array[Dictionary] = [
|
||
{"e01":8}, {"e01":10}, {"e01":6,"e02":4}, {"e01":8,"e04":4},
|
||
{"e07":1,"e01":6}, {"e02":6,"e01":6}, {"e03":8,"e01":4},
|
||
{"e02":6,"e04":6,"e01":4}, {"e05":4,"e02":6,"e06":4},
|
||
{"e08":1,"e03":8,"e04":4}, {"e01":16,"e04":6},
|
||
{"e02":8,"e05":4,"e06":4}, {"e03":12,"e04":6},
|
||
{"e05":6,"e02":6,"e01":6}, {"e09":1,"e03":6,"e06":4},
|
||
{"e01":18,"e04":6,"e03":4}, {"e05":8,"e02":6,"e06":4},
|
||
{"e03":10,"e04":8,"e06":4}, {"e05":6,"e02":8,"e01":8,"e04":4},
|
||
{"e10":1,"e05":6,"e03":6,"e06":4},
|
||
]
|
||
const EXPEDITION_TITLES: Array[String] = [
|
||
"Erste Risse", "Splitterstrom", "Harte Schalen", "Fäden im Rücken", "Der Bruchhirsch",
|
||
"Gepanzerter Vorstoß", "Stimmen aus dem Nebel", "Gedeckter Vormarsch", "Die Knotenrufer",
|
||
"Der Riegelkoloss", "Splitterflut", "Heilende Front", "Nebel und Fäden", "Bruchlinie",
|
||
"Der Maskenrichter", "Der große Schwarm", "Stampfende Reihen", "Rissgesang", "Vor dem Schlund", "Die Schlundkrone"
|
||
]
|
||
const EXPEDITION_HINTS: Array[String] = [
|
||
"", "", "", "", "", "", "",
|
||
"Eine breite Panzerfront schützt sechs Schützen. Kombiniere Durchschlag mit Flächenschaden.",
|
||
"NEU · Bruchstampfer treffen mehrere Ziele. Knotenrufer heilen verwundete Angreifer; konzentriere deinen Schaden.",
|
||
"BOSS · Riegelkoloss mit Startschild und ätherischer Begleitung. Durchschlag und Impuls ergänzen sich.",
|
||
"Ein großer Schwarm. Kraterrufer treffen mehrere Gegner; schütze deine Artillerie.",
|
||
"Schwere Front mit Heilern. Aufgewertete Glasballisten helfen gegen hohe Einzelziel-HP.",
|
||
"Nebelreste tragen ätherische Rüstung. Impulsschaden ist hier besonders wirksam.",
|
||
"Breite Nahkampfwelle: mehrere Frontkämpfer verteilen den Druck.",
|
||
"BOSS · Der Maskenrichter verlangsamt alle sechs Sekunden bis zu drei Wächter. Halte Schaden und Unterstützung bereit.",
|
||
"28 Angreifer: Flächenschaden und eine belastbare Front sind entscheidend.",
|
||
"Acht Bruchstampfer und vier Heiler. Werte deine Front und den Durchschlagschaden auf.",
|
||
"Ätherische Gegner und Fernkämpfer greifen gemeinsam an. Prüfe deine Schadensmischung.",
|
||
"Letzte Auszahlung vor dem Finale. Investiere Gold in die Formation und plane deine letzten Sends.",
|
||
"FINALE · Die Schlundkrone trifft bei jedem vierten Angriff mehrere Wächter. Nutze Gold und übrige Funken."
|
||
]
|
||
|
||
func wave_count() -> int:
|
||
return EXPEDITION_WAVE_COUNT if GameState.match_mode == GameState.MODE_EXPEDITION else SHORT_WAVE_COUNT
|
||
|
||
func get_unit(id: String) -> Dictionary:
|
||
return UNITS.get(id, {}).duplicate(true)
|
||
|
||
func get_wave(wave: int) -> Dictionary:
|
||
if wave < 1 or wave > wave_count():
|
||
return {}
|
||
var x: float = float(wave - 1)
|
||
var full: bool = GameState.match_mode == GameState.MODE_EXPEDITION
|
||
var groups: Dictionary = (EXPEDITION_WAVES if full else WAVES)[wave - 1].duplicate(true)
|
||
var count: int = 0
|
||
var names: PackedStringArray = []
|
||
for id: String in groups:
|
||
count += int(groups[id])
|
||
names.append("%d × %s" % [groups[id], ENEMIES[id]["display_name"]])
|
||
return {"number": wave, "groups": groups, "count": count,
|
||
"title": EXPEDITION_TITLES[wave - 1] if full else WAVE_TITLES[wave - 1],
|
||
"hint": EXPEDITION_HINTS[wave - 1] if full and wave > 7 else WAVE_HINTS[wave - 1],
|
||
"hp_factor": 1.0 + 0.10*x + 0.004*x*x if full else 1.0 + 0.14*x + 0.010*x*x,
|
||
"damage_factor": 1.0 + 0.06*x + 0.002*x*x if full else 1.0 + 0.08*x + 0.004*x*x,
|
||
"bounty_factor": 1.0 + 0.04 * x, "description": ", ".join(names)}
|
||
|
||
func get_attackers(wave: int) -> Array:
|
||
var result: Array = []
|
||
var info: Dictionary = get_wave(wave)
|
||
if info.is_empty():
|
||
return result
|
||
for id: String in info["groups"]:
|
||
for _index: int in range(int(info["groups"][id])):
|
||
result.append(_scaled(ENEMIES[id], info, true))
|
||
return result
|
||
|
||
func get_send(id: String, wave: int) -> Dictionary:
|
||
var info: Dictionary = get_wave(wave)
|
||
if not SENDS.has(id) or info.is_empty():
|
||
return {}
|
||
return _scaled(SENDS[id], info, false)
|
||
|
||
func _scaled(source: Dictionary, wave: Dictionary, natural: bool) -> Dictionary:
|
||
var result: Dictionary = source.duplicate(true)
|
||
result["max_hp"] = ceil(float(source["max_hp"]) * float(wave["hp_factor"]))
|
||
result["damage"] = float(source["damage"]) * float(wave["damage_factor"])
|
||
result["bounty"] = floori(float(source["bounty"]) * float(wave["bounty_factor"]))
|
||
result["hp_factor"] = wave["hp_factor"]
|
||
result["natural"] = natural
|
||
result["is_boss"] = source.get("is_boss", false)
|
||
result["cost"] = source.get("cost", 0)
|
||
result["upgrade_id"] = ""
|
||
result["color"] = Color("c4d2cd") if natural else Color("bc87bd")
|
||
return result
|
||
|
||
const FACTIONS: Array[String] = ["glutpakt", "wurzelbund"]
|
||
const FACTION_NAMES: Dictionary = {"glutpakt": "Glutpakt", "wurzelbund": "Wurzelbund"}
|
||
const ROLES: Array[String] = ["Nahkampf", "Front / Tank", "Fernkampf", "Flächenschaden", "Unterstützung"]
|
||
|
||
func base_ids(team: int = 0) -> Array[String]:
|
||
var prefix: String = "w" if GameState.factions[team] == "wurzelbund" else "g"
|
||
return [prefix+"01", prefix+"03", prefix+"05", prefix+"07", prefix+"09"]
|
||
|
||
func faction_ids(faction: String) -> Array[String]:
|
||
var prefix: String = "w" if faction == "wurzelbund" else "g"
|
||
return [prefix+"01", prefix+"03", prefix+"05", prefix+"07", prefix+"09"]
|
||
|
||
func for_team(id: String, team: int) -> String:
|
||
return ("w" + id.substr(1)) if id.begins_with("g") and GameState.factions[team] == "wurzelbund" else id
|
||
|
||
func starter_cost(team: int = 0) -> int:
|
||
var ids := base_ids(team)
|
||
return int(UNITS[ids[0]].cost + UNITS[ids[1]].cost + UNITS[ids[2]].cost)
|
||
|
||
func wave_hint(wave: int, team: int = 0) -> String:
|
||
var hint: String = str(get_wave(wave).get("hint", ""))
|
||
if GameState.factions[team] == "wurzelbund":
|
||
for number: int in range(1,11):
|
||
hint = hint.replace(UNITS["g%02d" % number].display_name, UNITS["w%02d" % number].display_name)
|
||
return hint
|