class_name Profile extends RefCounted ## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen. ## Man startet mit allen neutralen Karten (Stapel: 30 nach Manakurve); die Elemente schaltet die Kampagne frei (je Kapitel einen ## Booster), die Werkstatt bringt den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe ## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird. const SAVE_VERSION := 8 const CRAFT_COST := 200 # Staub für eine neue Karte (Common) const SAVE_PATH := "user://spielstand.json" const BOOSTER_COST := 100 # neutraler Booster const ELEMENT_BOOSTER_COST := 200 # Booster eines Elements (ab seinem Kapitel) const BOOSTER_SIZE := 5 const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0} const START_COINS := 150 # Münzen nach dem Kampf: je eigener Zug, je ausgespielte Karte und je Zug unter FAST_TURN_SEC # Sekunden; ein Sieg verdoppelt alles, die Schwierigkeit multipliziert (siehe battle_coins). const COINS_TURN := 5 const COINS_CARD := 5 const COINS_FAST := 5 const FAST_TURN_SEC := 15.0 # Schwierigkeit im Einzelspieler: Stufe der Gegnerkarten relativ zu deiner Deckstärke (level), # KI (smart: Vorausschau, sonst gierig), ein zufälliges Merkmal (perk) und der Münzfaktor. const DIFFICULTIES := { "leicht": {"name": "Leicht", "level": -1, "smart": false, "perk": false, "reward": 1.0}, "normal": {"name": "Normal", "level": 0, "smart": true, "perk": false, "reward": 1.5}, "schwer": {"name": "Schwer", "level": 1, "smart": true, "perk": true, "reward": 2.0}, } # Sammlung: eine Stufe abgeben bringt Staub, eine Stufe aufwerten kostet Staub. const DUST_DISMANTLE := {"Common": 50, "Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000} const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultimate Rare": 1000} var coins := 0 var dust := 0 var xp := 0 # Erfahrung: alle je verdienten Münzen (Skills.level_for) var skills := {} # Skill-ID -> gelernte Stufe (Skills.SKILLS) var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES) var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX) var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false} var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt) var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1) ## Neues Profil: alle neutralen Karten in der Sammlung, 30 davon nach Manakurve im Stapel. static func starter() -> Profile: var p := Profile.new() p.coins = START_COINS p.give_start() return p ## Alle neutralen Karten in Common werden die Sammlung, ein Kurvenstapel daraus der Stapel. func give_start() -> void: collection = {} for id in DeckGen.element_deck(Cards.NEUTRAL): collection[id] = 0 deck = DeckGen.curve_deck(Cards.NEUTRAL, Game.make_rng()) ## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil. static func load_or_new(path: String = SAVE_PATH) -> Profile: for candidate in [path, path + ".bak"]: if FileAccess.file_exists(candidate): var json := JSON.new() if json.parse(FileAccess.get_file_as_string(candidate)) == OK and json.data is Dictionary: return from_dict(json.data) if FileAccess.file_exists(path): DirAccess.rename_absolute(ProjectSettings.globalize_path(path), ProjectSettings.globalize_path(path) + ".corrupt") var p := starter() p.save(path) return p ## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und ## Einstellungen und bekommen die Startsammlung. static func from_dict(d: Dictionary) -> Profile: var p := Profile.new() p.coins = int(d.get("coins", 0)) p.dust = int(d.get("dust", 0)) p.xp = int(d.get("xp", 0)) var sk = d.get("skills", {}) for id in sk: if Skills.SKILLS.has(id): p.skills[id] = clampi(int(sk[id]), 0, int(Skills.SKILLS[id].max)) p.campaign_stage = int(d.get("campaign_stage", 0)) if int(d.get("version", 1)) >= SAVE_VERSION: var coll = d.get("collection", {}) for cid in coll: if Cards.has_id(cid): p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1) var ids := [] for cid in d.get("deck", []): if p.collection.has(cid) and not (cid in ids): ids.append(cid) p.deck = ids.slice(0, Game.DECK_MAX) if p.collection.is_empty(): p.give_start() var se = d.get("settings", {}) p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)), "ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)), "language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")), "big_font": bool(se.get("big_font", false))} if not DIFFICULTIES.has(p.settings.difficulty): p.settings.difficulty = "normal" p.perk = str(d.get("perk", "")) if not Game.PERKS.has(p.perk): p.perk = "" return p func to_dict() -> Dictionary: return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "xp": xp, "skills": skills, "collection": collection, "deck": deck, "campaign_stage": campaign_stage, "settings": settings, "perk": perk} ## Speichert und sichert den vorherigen Stand als .bak. func save(path: String = SAVE_PATH) -> void: var abs := ProjectSettings.globalize_path(path) if FileAccess.file_exists(path): DirAccess.copy_absolute(abs, abs + ".bak") var f := FileAccess.open(path, FileAccess.WRITE) f.store_string(JSON.stringify(to_dict(), "\t")) f.close() func take_notices() -> Array[String]: var out := notices.duplicate() notices.clear() return out # --- Sammlung und Stapel ----------------------------------------------------------- func owns(cid: String) -> bool: return collection.has(cid) func level(cid: String) -> int: return int(collection.get(cid, 0)) func rarity(cid: String) -> String: return Cards.RARITIES[level(cid)] ## Karten-ID -> Stufe für den Kampf (nur aufgewertete Karten, siehe Game.Player.looks). func looks() -> Dictionary: var out := {} for cid in collection: if int(collection[cid]) > 0: out[cid] = int(collection[cid]) return out ## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt, ## falls er zu klein ist (Karten können durch Zerlegen wegfallen). func deck_cards() -> Array: return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN, collection) ## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung. func toggle_deck(cid: String) -> String: if cid in deck: deck.erase(cid) return "" if not owns(cid): return "Karte nicht in der Sammlung" if deck.size() >= Game.DECK_MAX: return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX deck.append(cid) return "" ## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung. func upgrade(cid: String) -> String: if not owns(cid): return "Karte nicht in der Sammlung" var lvl := level(cid) if lvl >= Cards.RARITIES.size() - 1: return "Schon auf der höchsten Stufe" var cost: int = DUST_UPGRADE[Cards.RARITIES[lvl + 1]] if dust < cost: return "Nicht genug Staub (%d nötig)" % cost dust -= cost collection[cid] = lvl + 1 return "" ## Entzaubern: eine Stufe abgeben bringt Staub; eine Common-Karte verlässt die Sammlung. ## Gibt "" zurück oder eine Fehlermeldung. func dismantle(cid: String) -> String: if not owns(cid): return "Karte nicht in der Sammlung" var lvl := level(cid) dust += dust_gain(DUST_DISMANTLE[Cards.RARITIES[lvl]]) if lvl == 0: collection.erase(cid) deck.erase(cid) else: collection[cid] = lvl - 1 return "" ## Herstellen: eine fehlende Karte für Staub als Common. Gibt "" zurück oder eine Fehlermeldung. func craft(cid: String) -> String: if owns(cid): return "Karte schon in der Sammlung" if dust < CRAFT_COST: return "Nicht genug Staub (%d nötig)" % CRAFT_COST dust -= CRAFT_COST collection[cid] = 0 return "" # --- Booster ---------------------------------------------------------------------- ## Preis eines Boosters: neutral BOOSTER_COST, je Element ELEMENT_BOOSTER_COST. static func booster_cost(element: String) -> int: return BOOSTER_COST if element == Cards.NEUTRAL else ELEMENT_BOOSTER_COST ## Der Booster eines Elements ist zu haben, sobald sein Kampagnenkapitel offen ist (neutral immer). func booster_unlocked(element: String) -> bool: return campaign_stage >= Campaign.STAGES_PER_CHAPTER * Campaign.CHAPTER_ELEMENTS.find(element) ## Fünf Karten des Elements mit gewürfelter Seltenheit; luck (Packglück) verschiebt die Chancen. static func roll_booster(r: RandomNumberGenerator, element: String, luck: int = 0) -> Array: var rarities := RARITY_WEIGHTS.keys() var weights := Skills.pack_weights(luck) var pool := Cards.all().filter(func(c): return c.elements[0] == element) var out := [] for i in BOOSTER_SIZE: var rar: String = rarities[r.rand_weighted(weights)] if i == BOOSTER_SIZE - 1 and out.all(func(x): return x.rarity == "Common"): rar = "Rare" # mindestens eine Rare pro Booster out.append({"card": pool[r.randi_range(0, pool.size() - 1)], "rarity": rar}) return out ## Öffnet einen Booster des Elements. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). ## Von jeder Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine ## gleiche oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn der Booster ## noch gesperrt ist oder die Münzen nicht reichen. func open_booster(element: String, r: RandomNumberGenerator = null) -> Array: return open_boosters(element, 1, r) ## Öffnet n Booster des Elements auf einmal; leer, wenn gesperrt oder die Münzen für alle nicht reichen. func open_boosters(element: String, n: int, r: RandomNumberGenerator = null) -> Array: if not booster_unlocked(element) or coins < pack_price(element) * n: return [] if r == null: r = Game.make_rng() var out := [] for i in n: out += _open_one(element, r) return out func _open_one(element: String, r: RandomNumberGenerator) -> Array: coins -= pack_price(element) var out := roll_booster(r, element, skill("packglueck")) for res in out: var lvl: int = Cards.RARITIES.find(res.rarity) res["new"] = not owns(res.card.id) res["upgraded"] = false res["dust"] = 0 if res.new: collection[res.card.id] = lvl if deck.size() < Game.DECK_MAX: deck.append(res.card.id) elif lvl > level(res.card.id): res["upgraded"] = true res["dust"] = dust_gain(DUST_DISMANTLE[rarity(res.card.id)]) collection[res.card.id] = lvl else: res["dust"] = dust_gain(DUST_DISMANTLE[res.rarity]) dust += res.dust return out # --- Kampf und Kampagne ----------------------------------------------------------- ## Münzen eines Kampfes: COINS_TURN je eigenem Zug, COINS_CARD je ausgespielter Karte, COINS_FAST ## je schnellem Zug; ein Sieg verdoppelt, factor ist der Münzfaktor der Schwierigkeit. static func battle_coins(won: bool, turns: int, cards: int, fast: int, factor: float = 1.0) -> int: var pts := COINS_TURN * turns + COINS_CARD * cards + COINS_FAST * fast if won: pts *= 2 return int(round(pts * factor)) ## Verbucht ein Kampfergebnis mit den verdienten Münzen (battle_coins, mal Münzsegen) und gibt die ## Gutschrift zurück; sie zählt auch als Erfahrung, ein neues Level meldet sich. stage >= 0: ## Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei und bringt ihre Belohnung obendrauf. func record_battle(won: bool, earned: int, stage: int = -1) -> int: var reward := int(round(earned * coin_factor())) if won and stage >= 0 and stage == campaign_stage: campaign_stage += 1 reward += Campaign.stage(stage).reward notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name) coins += reward var before := player_level() xp += reward if player_level() > before: var gained := player_level() - before notices.append(("Level %d erreicht: +%d Skillpunkt" if gained == 1 else "Level %d erreicht: +%d Skillpunkte") % [player_level(), gained]) return reward # --- Skilltree ---------------------------------------------------------------------- ## Spielerlevel aus der Erfahrung (nicht die Kartenstufe level(cid)). func player_level() -> int: return Skills.level_for(xp) ## Gelernte Stufe eines Skills. func skill(id: String) -> int: return int(skills.get(id, 0)) ## Freie Skillpunkte: ein Punkt je Level, abzüglich der gelernten Stufen. func skill_points() -> int: var spent := 0 for id in skills: spent += int(skills[id]) return player_level() - spent ## Lernt die nächste Stufe eines Skills für einen Punkt. Gibt "" zurück oder eine Fehlermeldung. func learn(id: String) -> String: if skill(id) >= int(Skills.SKILLS[id].max): return "Schon auf der höchsten Stufe" if skill_points() <= 0: return "Kein Skillpunkt frei" skills[id] = skill(id) + 1 return "" ## Preis eines Boosters mit Händler-Rabatt. func pack_price(element: String) -> int: return int(round(booster_cost(element) * (1.0 - 0.1 * skill("packpreis")))) func coin_factor() -> float: return 1.0 + 0.1 * skill("muenzen") ## Staub mit Staubsammler-Bonus. func dust_gain(base: int) -> int: return int(round(base * (1.0 + 0.25 * skill("staub")))) ## Merkmale im Kampf: das gewählte (nach Kapitel 1) und die der Kampfskills. func battle_perks() -> Array: var out := [] if perk != "" and perk_unlocked(): out.append(perk) for id in Skills.PERK_OF: if skill(id) > 0 and not (Skills.PERK_OF[id] in out): out.append(Skills.PERK_OF[id]) return out func difficulty() -> Dictionary: return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal) ## Deine Stufe: die Deckstärke des Kampfstapels (100 Common … 500 Ultimate Rare) als Seltenheitsstufe. func deck_level() -> int: return clampi(roundi((DeckGen.strength(deck_cards(), collection) - 100) / 100.0), 0, Cards.RARITIES.size() - 1) ## Stufe der Gegnerkarten im Einzelspieler: deine Stufe plus den Versatz der Schwierigkeit. func opponent_level() -> int: return clampi(deck_level() + int(difficulty().level), 0, Cards.RARITIES.size() - 1) ## Elemente der offenen Kampagnenkapitel (Neutral immer). func unlocked_elements() -> Array: return Campaign.CHAPTER_ELEMENTS.slice(0, campaign_stage / Campaign.STAGES_PER_CHAPTER + 1) ## Alle Karten der offenen Elemente: was Sammlung, Deck und Booster zeigen. func unlocked_cards() -> Array: var open := unlocked_elements() return Cards.all().filter(func(c): return c.elements[0] in open) ## Einzelspieler-Gegner: ein Element aus den offenen Kapiteln, 30 Karten nach Manakurve in opponent_level; ## auf Schwer ein zufälliges Merkmal. Leben wie deins. func single_opponent(r: RandomNumberGenerator) -> Game.Player: var elements := unlocked_elements() var element: String = elements[r.randi_range(0, elements.size() - 1)] var opp := Game.Player.new("Gegner", false, Game.START_LIFE, element) opp.deck = DeckGen.curve_deck(element, r) var level := opponent_level() for id in opp.deck: opp.looks[id] = level if bool(difficulty().perk): var keys: Array = Game.PERKS.keys() opp.perks = [keys[r.randi_range(0, keys.size() - 1)]] return opp func perk_unlocked() -> bool: return campaign_stage >= Campaign.STAGES_PER_CHAPTER