saumwacht-godot/scripts/luneraria/core/lune_content.gd
Martin Stickelbroeck f53859a298 M2: Helden, Items, Talente, Angriffschampions und Bossgrotte
Drei Helden mit Nähe-/Teilnahme-XP, Talenten und drei Itemplätzen,
drei Angriffschampions mit Buchung n -> n+1, Bossgrotte mit Streitfall,
ruhendem Boss, idempotenter Eroberung, einmaliger Entsendung für das
Siegerteam, hohem Kopfgeld und Sperre bis Sieg+4. HUD-Reiter, Porträt
aus echtem Modell, Grotten-Asset, Bots für alle Systeme. Regeln M2 73/73,
M1 91/91, App M2 15/15 und M1 38/38, Bossszenarien dokumentiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:33:11 +02:00

159 lines
9.3 KiB
GDScript
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.

class_name LuneContent
extends RefCounted
## M2 content: heroes, talents, items, attack champions and the grotto boss.
## All figures reuse existing models; hero and champion roles are provisional assignments
## of those models ("vorläufig"), not newly created peoples.
# ---------------------------------------------------------------- heroes
const HEROES: Dictionary = {
"held_ringborke": {"name": "Ringborke", "title": "Wächterin der Wurzeln", "model": "w04", "cost": 260, "role": "Front / Schutz",
"provisional": true, "def": {"max_hp": 2200.0, "damage": 34.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.4, "speed": 2.4, "ability": "w04"},
"summary": "Hält die Front, heilt sich selbst und schützt nahe Verbündete."},
"held_quellgeflecht": {"name": "Quellgeflecht", "title": "Hüterin der Quelle", "model": "w10", "cost": 260, "role": "Unterstützung / Heilung",
"provisional": true, "def": {"max_hp": 1300.0, "damage": 28.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.5, "speed": 2.0, "ability": "w10"},
"summary": "Heilt Verbündete in 4,5 m und stärkt die Armee im Gefecht."},
"held_funkenlanze": {"name": "Funkenlanze", "title": "Schützin des Glutpakts", "model": "g05", "cost": 260, "role": "Fernkampf / Schaden",
"provisional": true, "def": {"max_hp": 950.0, "damage": 46.0, "attack_type": 1, "armor_type": 0, "attack_range": 6.0, "speed": 2.0, "ability": "g05"},
"summary": "Hoher Einzelschaden aus der zweiten Reihe."},
}
## Cumulative XP needed for levels 1..10.
const LEVEL_XP: Array[int] = [0, 120, 280, 480, 720, 1000, 1320, 1680, 2080, 2520]
const HP_PER_LEVEL: float = 0.09
const DAMAGE_PER_LEVEL: float = 0.07
const XP_PARTICIPATION: int = 40
const XP_PER_ROUND: int = 6
const XP_RADIUS: float = 12.0
const TALENT_LEVELS: Array[int] = [3, 6]
## Two tiers of two mutually exclusive choices per hero; each changes the hero's job.
const TALENTS: Dictionary = {
"held_ringborke": [
[{"id": "borkenhaut", "name": "Borkenhaut", "text": "Eigenschutz: +15 % Schadensminderung für die Heldin."},
{"id": "schutzwurzeln", "name": "Schutzwurzeln", "text": "Gruppenschutz: alle 4 s erhalten bis zu 4 Verbündete in 4 m 10 % Schadensminderung für 4,5 s."}],
[{"id": "stammschlag", "name": "Stammschlag", "text": "Einzelziel: jeder 3. Treffer +60 % Schaden."},
{"id": "wurzelbeben", "name": "Wurzelbeben", "text": "Fläche: jeder 3. Treffer 50 % Schaden an bis zu 3 Gegner in 2 m."}]],
"held_quellgeflecht": [
[{"id": "tiefe_quelle", "name": "Tiefe Quelle", "text": "Einzelziel: heilt den schwächsten Verbündeten um 190 statt drei um je 80."},
{"id": "weite_quelle", "name": "Weite Quelle", "text": "Fläche: heilt bis zu 5 Verbündete in 5,5 m um je 65."}],
[{"id": "quellschild", "name": "Quellschild", "text": "Schutz: Geheilte erhalten 4 s lang einen Schild von 70."},
{"id": "keimsaat", "name": "Keimsaat", "text": "Druck: jeder 2. Treffer setzt Keimbrand, 9 Schaden/s für 4 s."}]],
"held_funkenlanze": [
[{"id": "durchschlag", "name": "Durchschlag", "text": "Einzelziel: jeder 4. Treffer ×2,1 statt ×1,5."},
{"id": "splitterschuss", "name": "Splitterschuss", "text": "Fläche: jeder 4. Treffer zusätzlich 45 % an bis zu 3 Gegner in 2 m."}],
[{"id": "fernsicht", "name": "Fernsicht", "text": "+1,5 m Reichweite."},
{"id": "glutsalve", "name": "Glutsalve", "text": "+20 % Angriffstempo."}]],
}
# ---------------------------------------------------------------- items
## Permanent, unique per hero (no two of the same item), three slots. Bonuses of different
## items add up within their category; caps: damage reduction 35 %, attack tempo +50 %.
## Order in combat: primary hit -> base ability -> talent -> item effect. Item effects never
## trigger other on-hit effects, heals never trigger anything, so no loops are possible.
const ITEMS: Dictionary = {
"basaltschild": {"name": "Basaltschild", "cost": 180, "text": "+350 Lebenspunkte", "hp_flat": 350.0},
"glutklinge": {"name": "Glutklinge", "cost": 180, "text": "+18 % Schaden", "damage": 0.18},
"rankenmantel": {"name": "Rankenmantel", "cost": 200, "text": "+10 % Schadensminderung", "reduction": 0.10},
"funkenfokus": {"name": "Funkenfokus", "cost": 200, "text": "+15 % Angriffstempo", "attack_speed": 0.15},
"quellamulett": {"name": "Quellamulett", "cost": 220, "text": "Regeneriert 1,5 % der Lebenspunkte pro Sekunde", "regen": 0.015},
"kristallsplitter": {"name": "Kristallsplitter", "cost": 260, "text": "Jeder Treffer: 25 % an bis zu 2 Gegner in 1,5 m · ab Stufe 3", "splash": 0.25, "min_level": 3},
}
const ITEM_SLOTS: int = 3
const ITEM_REFUND: float = 0.5
# ---------------------------------------------------------------- attack champions
## Bought in round n for one lane, fight once in round n+1, then vanish. Killing one pays gold.
const CHAMPIONS: Dictionary = {
"ch_schutz": {"name": "Rostschieber", "role": "Schutz", "model": "s02", "cost": 150, "base": "s02",
"text": "Schildträger: startet mit Schild, gibt alle 6 s bis zu 4 Verbündeten in 4 m einen Schild von 120.", "view_scale": 1.25},
"ch_druck": {"name": "Dunstsänger", "role": "Druck auf Unterstützer", "model": "s03", "cost": 150, "base": "s03",
"text": "Sucht Unterstützer und Fernkämpfer und verlangsamt deren Angriffe.", "priority": "support", "view_scale": 1.25},
"ch_belagerung": {"name": "Keilbrecher", "role": "Belagerung", "model": "s04", "cost": 180, "base": "s04",
"text": "Greift bevorzugt Gebäude an und verursacht dort ×2,5 Schaden.", "priority": "building", "siege_factor": 2.5, "view_scale": 1.3},
}
const CHAMPION_HP: float = 2.2
const CHAMPION_DAMAGE: float = 1.6
const CHAMPION_BOUNTY: int = 70
const CHAMPION_BOUNTY_PER_ROUND: int = 6
const MAX_BOOKINGS: int = 2
# ---------------------------------------------------------------- boss
const BOSS_MODEL: String = "e10"
const BOSS_GROTTO_HP: float = 4200.0
const BOSS_GROTTO_DAMAGE: float = 55.0
const BOSS_DEPLOY_HP: float = 5200.0
const BOSS_DEPLOY_DAMAGE: float = 70.0
const BOSS_BOUNTY: int = 520
const BOSS_BOUNTY_PER_ROUND: int = 60
## Victory in round n: deployment in n+1, locked in n+1..n+3, available again from n+4.
const BOSS_LOCK_ROUNDS: int = 3
const BOSS_XP: int = 220
const BUILDING_XP: int = 60
static func level_for(xp: int) -> int:
var level: int = 1
for index: int in range(LEVEL_XP.size()):
if xp >= LEVEL_XP[index]:
level = index + 1
return level
static func hero_definition(hero: Dictionary) -> Dictionary:
var spec: Dictionary = HEROES[hero.hero_id]
var definition: Dictionary = (spec.def as Dictionary).duplicate(true)
var level: int = level_for(int(hero.xp))
definition.id = spec.model
definition.hero_id = hero.hero_id
definition.display_name = spec.name
definition.max_hp = float(definition.max_hp) * (1.0 + HP_PER_LEVEL * float(level - 1))
definition.damage = float(definition.damage) * (1.0 + DAMAGE_PER_LEVEL * float(level - 1))
definition.level = level
definition.view_scale = 1.25
var damage_bonus: float = 0.0
var reduction: float = 0.0
var attack_speed: float = 0.0
for item_id: String in hero.items:
var item: Dictionary = ITEMS[item_id]
definition.max_hp = float(definition.max_hp) + float(item.get("hp_flat", 0.0))
damage_bonus += float(item.get("damage", 0.0))
reduction += float(item.get("reduction", 0.0))
attack_speed += float(item.get("attack_speed", 0.0))
definition.regen = float(definition.get("regen", 0.0)) + float(item.get("regen", 0.0))
definition.item_splash = float(definition.get("item_splash", 0.0)) + float(item.get("splash", 0.0))
var talents: Array = hero.get("talents", [])
if talents.has("borkenhaut"):
reduction += 0.15
if talents.has("fernsicht"):
definition.attack_range = float(definition.attack_range) + 1.5
if talents.has("glutsalve"):
attack_speed += 0.20
definition.talents = talents.duplicate()
definition.damage_bonus = damage_bonus
definition.reduction_bonus = reduction
definition.attack_speed_bonus = minf(0.5, attack_speed)
return definition
static func champion_definition(champion_id: String, round_number: int, balance: LuneBalance) -> Dictionary:
var spec: Dictionary = CHAMPIONS[champion_id]
var base: Dictionary = DataCatalog.SENDS[spec.base].duplicate(true)
base.id = spec.model
base.champion_id = champion_id
base.display_name = spec.name
base.max_hp = float(base.max_hp) * CHAMPION_HP * LuneWaves.strength(balance, round_number, balance.wave_hp_growth)
base.damage = float(base.damage) * CHAMPION_DAMAGE * LuneWaves.strength(balance, round_number, balance.wave_damage_growth)
base.bounty = CHAMPION_BOUNTY + CHAMPION_BOUNTY_PER_ROUND * round_number
base.priority = str(spec.get("priority", ""))
base.siege_factor = float(spec.get("siege_factor", 1.0))
base.view_scale = float(spec.view_scale)
if champion_id == "ch_schutz":
base.ability = "ch_schutz"
return base
static func boss_definition(round_number: int, balance: LuneBalance, deployed: bool) -> Dictionary:
var base: Dictionary = DataCatalog.ENEMIES[BOSS_MODEL].duplicate(true)
base.display_name = "Schlundkrone"
base.max_hp = (BOSS_DEPLOY_HP if deployed else BOSS_GROTTO_HP) * LuneWaves.strength(balance, round_number, balance.wave_hp_growth)
base.damage = (BOSS_DEPLOY_DAMAGE if deployed else BOSS_GROTTO_DAMAGE) * LuneWaves.strength(balance, round_number, balance.wave_damage_growth)
base.bounty = BOSS_BOUNTY + BOSS_BOUNTY_PER_ROUND * round_number if deployed else 0
base.is_boss = true
base.view_scale = 1.7
base.speed = 1.6
return base