class_name Data extends RefCounted ## Statische Spieldaten: Elemente, Rollen, Basiswerte, Traits, Preise. enum Element { FEUER, WASSER, WIND, BLITZ, ERDE, EIS } enum Role { TANK, DPS, HEILER } const ELEMENT_COUNT := 6 const ELEMENT_NAMES := ["Feuer", "Wasser", "Wind", "Blitz", "Erde", "Eis"] const ELEMENT_COLORS := [ Color(0.92, 0.31, 0.16), Color(0.18, 0.52, 0.90), Color(0.48, 0.85, 0.60), Color(0.96, 0.84, 0.22), Color(0.62, 0.45, 0.24), Color(0.66, 0.92, 0.98), ] const NONE_COLOR := Color(0.62, 0.62, 0.66) const ROLE_NAMES := ["Tank", "DPS", "Heiler"] const ROLE_SHORT := ["T", "D", "H"] # Basiswerte auf 1-Star. const ROLE_STATS := { Role.TANK: {"hp": 700.0, "atk": 45.0, "armor": 0.40, "aspd": 0.60, "range": 1, "mana": 80.0}, Role.DPS: {"hp": 450.0, "atk": 65.0, "armor": 0.18, "aspd": 0.80, "range": 3, "mana": 100.0}, Role.HEILER: {"hp": 520.0, "atk": 38.0, "armor": 0.22, "aspd": 0.70, "range": 3, "mana": 90.0}, } # Das Hauptelement prägt die Werte: Feuer schlägt hart, Erde hält aus, Wind ist # schnell, Blitz trifft kritisch, Wasser zaubert öfter, Eis ist zäh und langsam. const ELEMENT_PROFILE := { Element.FEUER: {"hp": 0.90, "atk": 1.18, "armor": 0.0, "aspd": 1.0, "mana": 1.0, "crit": 0.0}, Element.WASSER: {"hp": 1.0, "atk": 0.95, "armor": 0.0, "aspd": 1.0, "mana": 0.75, "crit": 0.0}, Element.WIND: {"hp": 0.92, "atk": 0.95, "armor": 0.0, "aspd": 1.20, "mana": 1.0, "crit": 0.0}, Element.BLITZ: {"hp": 0.95, "atk": 1.0, "armor": 0.0, "aspd": 1.05, "mana": 1.0, "crit": 0.15}, Element.ERDE: {"hp": 1.22, "atk": 0.92, "armor": 0.08, "aspd": 0.90, "mana": 1.0, "crit": 0.0}, Element.EIS: {"hp": 1.08, "atk": 0.95, "armor": 0.05, "aspd": 0.95, "mana": 0.9, "crit": 0.0}, } const PROFILE_HINT := { Element.FEUER: "hart, aber weich", Element.WASSER: "zaubert oft", Element.WIND: "schnell", Element.BLITZ: "kritisch", Element.ERDE: "zäh und langsam", Element.EIS: "robust", } # Das Hauptelement gibt der Fähigkeit ihre Form. Gilt für jede Rolle: # ein Feuer-Heiler brennt, ein Erde-Tank erschüttert, ein Eis-Schütze friert. const ELEMENT_FORM := { Element.FEUER: {"name": "Flächenbrand", "desc": "trifft auch Nachbarn des Ziels (60%) und entzündet sie"}, Element.WASSER: {"name": "Welle", "desc": "gibt Verbündeten im Umkreis 2 je 20 Mana"}, Element.WIND: {"name": "Sturmschritt", "desc": "Verbündete im Umkreis 2 werden 3 s um 25% schneller"}, Element.BLITZ: {"name": "Kette", "desc": "springt auf 2 weitere Gegner (60%)"}, Element.ERDE: {"name": "Erschütterung","desc": "betäubt Gegner neben dem Ziel 0,6 s"}, Element.EIS: {"name": "Frostfeld", "desc": "verlangsamt Gegner um das Ziel 3 s um 30%"}, } # Bots: wie gierig sie horten und wie gezielt sie jagen. enum Difficulty { LEICHT, NORMAL, SCHWER } const DIFFICULTY_NAMES := ["Leicht", "Normal", "Schwer"] const BOT_GREED := { Difficulty.LEICHT: [0.6, 0.9], Difficulty.NORMAL: [0.3, 0.8], Difficulty.SCHWER: [0.15, 0.4], } # Schere-Stein-Papier. Elemente im Ring: jedes schlägt genau eines und verliert # gegen genau eines. Rollen als Dreieck. Nur das Hauptelement zählt. const ELEMENT_BEATS := { Element.WASSER: Element.FEUER, # Wasser löscht Feuer Element.FEUER: Element.EIS, # Feuer schmilzt Eis Element.EIS: Element.WIND, # Eis lähmt Wind Element.WIND: Element.ERDE, # Wind trägt Erde ab Element.ERDE: Element.BLITZ, # Erde erdet Blitz Element.BLITZ: Element.WASSER, # Blitz leitet durch Wasser } const ROLE_BEATS := { Role.TANK: Role.DPS, # Tank hält Schützen aus Role.DPS: Role.HEILER, # Schütze bricht Heiler Role.HEILER: Role.TANK, # Heiler überdauert Tank } const ELEMENT_COUNTER := 0.20 const ROLE_COUNTER := 0.15 # Multiplikator pro Sternstufe (Index = star - 1). const STAR_MULT := [1.0, 1.8, 3.24] const MAX_STAR := 3 # Jedes zusätzliche Element gibt einen kleinen Grundwert-Bonus. const ELEMENT_STAT_BONUS := 0.06 # --- Oekonomie ------------------------------------------------------------- const START_HP := 100 const BASE_INCOME := 5 const WIN_GOLD := 1 const REROLL_COST := 2 const PRICE_NONE := 1 const PRICE_ELEMENT := 2 ## Zinsen: je 10 Gold ein Gold dazu, gedeckelt bei 50 Gold. const INTEREST_STEP := 10 const INTEREST_MAX := 5 const SHOP_SLOTS := 5 ## Je aufgestelltem Element wird ein Shop-Platz zu 25% seltener neutral. ## Multiplikativ, damit Neutral nie ganz verschwindet. const NEUTRAL_FALLOFF := 0.75 const BENCH_SLOTS := 9 const START_GOLD := 4 # --- Geteilter Pool -------------------------------------------------------- ## Alle 8 Spieler teilen sich diesen Vorrat pro Element. Ein 3-Star bindet ## allein 9 Stück, dazu kommt was auf Bänken und Brettern liegt. ## Bei 100 hält der Vorrat eine ganze Partie, geht im Endspiel aber auf rund ein Fünftel. const POOL_PER_ELEMENT := 100 # --- Board ----------------------------------------------------------------- const BOARD_COLS := 7 const BOARD_ROWS := 4 # --- Level und Erfahrung --------------------------------------------------- ## Das Spielerlevel ist die Anzahl der Kaderplätze. Jede Runde gibt XP, ## weitere XP kauft man. Level gegen Würfeln ist die TFT-Kernentscheidung. const START_LEVEL := 3 const MAX_LEVEL := 9 const XP_PER_ROUND := 2 const XP_BUY_COST := 4 const XP_BUY_AMOUNT := 4 const XP_TABLE := {3: 4, 4: 8, 5: 12, 6: 20, 7: 32, 8: 48} # --- Planung --------------------------------------------------------------- const PLAN_SECONDS := 30.0 ## Die ersten Runden gehen gegen die Wildnis, damit jeder in Ruhe aufbaut. const PVE_ROUNDS := 2 const SAVE_PATH := "user://spielstand.json" # --- Kampf ----------------------------------------------------------------- ## Heiler heilen nur Verbündete in dieser Hex-Reichweite. const HEAL_RANGE := 2 # --- Element-Traits -------------------------------------------------------- # steps = Anzahl aufgestellter Einheiten mit diesem Element. const ELEMENT_TRAITS := { Element.FEUER: { "steps": [2, 4, 6], "desc": ["+12% Angriff", "+26% Angriff", "+48% Angriff"], "values": [0.12, 0.26, 0.48], }, Element.WASSER: { "steps": [2, 4, 6], "desc": ["+20% Mana", "+40% Mana", "+70% Mana"], "values": [0.20, 0.40, 0.70], }, Element.WIND: { "steps": [2, 4, 6], "desc": ["+10% Tempo", "+22% Tempo", "+40% Tempo"], "values": [0.10, 0.22, 0.40], }, Element.BLITZ: { "steps": [2, 4, 6], "desc": ["+15% Krit", "+30% Krit", "+55% Krit"], "values": [0.15, 0.30, 0.55], }, Element.ERDE: { "steps": [2, 4, 6], "desc": ["+12% Leben", "+26% Leben", "+48% Leben"], "values": [0.12, 0.26, 0.48], }, Element.EIS: { "steps": [2, 4, 6], "desc": ["Treffer -12% Tempo", "Treffer -22% Tempo", "Treffer -35% Tempo"], "values": [0.12, 0.22, 0.35], }, } # --- Kombi-Traits ---------------------------------------------------------- # Aktiv, wenn beide Elemente mindestens COMBO_MIN Einheiten haben. const COMBO_MIN := 2 const COMBO_TRAITS := [ {"a": Element.FEUER, "b": Element.WIND, "key": "feuersturm", "name": "Feuersturm", "desc": "Fähigkeiten setzen in Brand"}, {"a": Element.WASSER, "b": Element.EIS, "key": "frost", "name": "Frost", "desc": "Gegner starten 15% langsamer"}, {"a": Element.BLITZ, "b": Element.ERDE, "key": "magnetsturm","name": "Magnetsturm", "desc": "Fähigkeiten betäuben 0.6s"}, {"a": Element.FEUER, "b": Element.ERDE, "key": "magma", "name": "Magma", "desc": "20% Schaden reflektiert"}, {"a": Element.WIND, "b": Element.BLITZ, "key": "sturmfront", "name": "Sturmfront", "desc": "Angriffe springen auf ein 2. Ziel"}, {"a": Element.WASSER, "b": Element.FEUER, "key": "dampf", "name": "Dampf", "desc": "18% Lebensraub"}, ] static func element_name(e: int) -> String: return ELEMENT_NAMES[e] static func element_color(e: int) -> Color: return ELEMENT_COLORS[e] static func role_name(r: int) -> String: return ROLE_NAMES[r] ## Aktive Stufe eines Element-Traits (0 = inaktiv, sonst 1..3). static func trait_tier(element: int, count: int) -> int: var steps: Array = ELEMENT_TRAITS[element]["steps"] var tier := 0 for i in steps.size(): if count >= steps[i]: tier = i + 1 return tier static func beats_element(attacker: int, defender: int) -> bool: return attacker >= 0 and defender >= 0 and ELEMENT_BEATS.get(attacker, -1) == defender static func beats_role(attacker: int, defender: int) -> bool: return ROLE_BEATS.get(attacker, -1) == defender ## Schadensfaktor eines Angreifers gegen einen Verteidiger (Hauptelement und Rolle). static func counter_multiplier(att_el: int, att_role: int, def_el: int, def_role: int) -> float: var m := 1.0 if beats_element(att_el, def_el): m += ELEMENT_COUNTER elif beats_element(def_el, att_el): m -= ELEMENT_COUNTER if beats_role(att_role, def_role): m += ROLE_COUNTER elif beats_role(def_role, att_role): m -= ROLE_COUNTER return m ## Element, das dieses Element schlägt (das Gegenstück zu ELEMENT_BEATS). static func beaten_by(element: int) -> int: for e in ELEMENT_BEATS: if ELEMENT_BEATS[e] == element: return e return -1 static func role_beaten_by(role: int) -> int: for r in ROLE_BEATS: if ROLE_BEATS[r] == role: return r return -1 ## Was die Fähigkeit einer Rolle auf einer Sternstufe tut. static func star_effect(role: int, star: int) -> String: match role: Role.TANK: return "Schild und spottet 3 Gegner" if star == 2 else "Schild, Spott und 25% Reflexion" Role.DPS: return "Ziel plus Splash auf Nachbarn" if star == 2 else "Ziel plus Fläche im Radius 2" _: return "heilt 3 Verbündete" if star == 2 else "heilt alle und schildet sie" ## Zusatzwirkung eines Nebenelements. static func element_effect(element: int) -> String: match element: Element.FEUER: return "Brand" Element.WASSER: return "Mana und stärkere Heilung" Element.WIND: return "Tempo-Schub" Element.BLITZ: return "Kettenblitz" Element.ERDE: return "Betäubung" _: return "Verlangsamung" ## XP bis zum nächsten Level, 0 auf Maximalstufe. static func xp_needed(level: int) -> int: return XP_TABLE.get(level, 0) ## Grundschaden einer verlorenen Runde, steigt alle vier Runden. static func round_base_damage(round_no: int) -> int: return 2 + int(float(round_no - 1) / 4.0) ## Wahrscheinlichkeit, dass ein Shop-Platz einen neutralen Avatar zeigt. ## Ohne eigene Elemente gibt es nur Neutral. static func neutral_chance(unlocked_count: int) -> float: return pow(NEUTRAL_FALLOFF, float(unlocked_count)) ## Zinsen auf das gehaltene Gold. static func interest(gold: int) -> int: return mini(INTEREST_MAX, int(float(gold) / float(INTEREST_STEP))) ## Streak-Bonusgold (Sieg- oder Niederlagenserie). static func streak_gold(streak: int) -> int: var s := absi(streak) if s >= 5: return 3 if s == 4: return 2 if s == 3: return 1 return 0