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