update
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped

This commit is contained in:
team3 committed 2026-10-14 17:35:09 +02:00
1 parent f2c609a08b
commit 1d563b9eec
34 files changed
+5486 -643

No files matched your search

+185 -46
View File
@@ -1,24 +1,30 @@
class_name Profile
extends RefCounted
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
## 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 := 7
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
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
const REWARD_WIN := 100
const REWARD_LOSS := 50
# Schwierigkeit: Leben der Gegner, KI und Münzen.
# 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", "life": 25, "reward": 0.75, "smart": false},
"normal": {"name": "Normal", "life": 30, "reward": 1.0, "smart": true},
"schwer": {"name": "Schwer", "life": 35, "reward": 1.5, "smart": true},
"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}
@@ -26,6 +32,8 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
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
@@ -34,24 +42,20 @@ var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfort
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
## 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
## Hat der Spieler schon ein Startdeck gewählt?
func started() -> bool:
return not collection.is_empty()
## Das Startdeck eines Elements wird Sammlung und Stapel.
func choose_start(element: String) -> void:
## Alle neutralen Karten in Common werden die Sammlung, ein Kurvenstapel daraus der Stapel.
func give_start() -> void:
collection = {}
deck = DeckGen.start_deck(element)
for id in deck:
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.
@@ -74,6 +78,11 @@ 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", {})
@@ -85,6 +94,8 @@ static func from_dict(d: Dictionary) -> Profile:
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)),
@@ -99,8 +110,8 @@ static func from_dict(d: Dictionary) -> Profile:
func to_dict() -> Dictionary:
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
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.
@@ -182,7 +193,7 @@ func dismantle(cid: String) -> String:
if not owns(cid):
return "Karte nicht in der Sammlung"
var lvl := level(cid)
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
dust += dust_gain(DUST_DISMANTLE[Cards.RARITIES[lvl]])
if lvl == 0:
collection.erase(cid)
deck.erase(cid)
@@ -204,11 +215,21 @@ func craft(cid: String) -> String:
# --- Booster ----------------------------------------------------------------------
## Fünf Karten mit gewürfelter Seltenheit.
static func roll_booster(r: RandomNumberGenerator) -> Array:
## 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 := PackedFloat32Array(RARITY_WEIGHTS.values())
var pool := Cards.all()
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)]
@@ -218,16 +239,29 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
return out
## Öffnet einen Booster. 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 die Münzen nicht reichen.
func open_booster(r: RandomNumberGenerator = null) -> Array:
if coins < BOOSTER_COST:
## Ö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()
coins -= BOOSTER_COST
var out := roll_booster(r)
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)
@@ -239,33 +273,138 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
deck.append(res.card.id)
elif lvl > level(res.card.id):
res["upgraded"] = true
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
res["dust"] = dust_gain(DUST_DISMANTLE[rarity(res.card.id)])
collection[res.card.id] = lvl
else:
res["dust"] = DUST_DISMANTLE[res.rarity]
res["dust"] = dust_gain(DUST_DISMANTLE[res.rarity])
dust += res.dust
return out
# --- Kampf und Kampagne -----------------------------------------------------------
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
func record_battle(won: bool, stage: int = -1, reward_factor: float = 1.0) -> int:
var reward := REWARD_LOSS
## 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:
reward = int(round(REWARD_WIN * reward_factor))
if stage >= 0 and stage == campaign_stage:
campaign_stage += 1
reward = Campaign.stage(stage).reward
notices.append("Kampagne: %s besiegt" % Campaign.stage(stage).name)
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.size()
return campaign_stage >= Campaign.STAGES_PER_CHAPTER