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 ×1,8 Schaden.", "priority": "building", "siege_factor": 1.8, "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 = 3600.0 const BOSS_GROTTO_DAMAGE: float = 55.0 const BOSS_DEPLOY_HP: float = 6800.0 const BOSS_DEPLOY_DAMAGE: float = 88.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