- Die dreizehn Erfolge samt Belohnungen, Meldungen und Spielstandfeld sind weg - Der bisherige Erfolgs-Bildschirm heißt Statistik und zeigt Kämpfe, Tagesaufgabe, Bestenliste, beste Karten und den CSV-Export Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
584 lines
23 KiB
GDScript
584 lines
23 KiB
GDScript
class_name Profile
|
|
extends RefCounted
|
|
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Statistik, Tagesaufgabe, Einstellungen.
|
|
|
|
const SAVE_VERSION := 5
|
|
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
|
|
const SAVE_PATH := "user://spielstand.json"
|
|
const BOOSTER_COST := 100
|
|
const ELEMENT_BOOSTER_COST := 120 # nur Karten eines Elements (auch seine Kombos)
|
|
const CODE_PREFIX := "DINGER1:" # Deck-Code zum Teilen
|
|
const BOOSTER_SIZE := 5
|
|
const RARITY_WEIGHTS := {"gewöhnlich": 70.0, "selten": 20.0, "episch": 7.5, "legendär": 2.0, "mythisch": 0.5}
|
|
const START_COINS := 150
|
|
const REWARD_WIN := {1: 100, 2: 150, 3: 200, 4: 250, 5: 300}
|
|
const REWARD_LOSS := 50
|
|
const DECK_SLOTS := 5
|
|
# Schwierigkeit: Faktor auf die Zielstärke der Gegnerdecks, Belohnung und KI-Tiefe.
|
|
const DIFFICULTIES := {
|
|
"leicht": {"name": "Leicht", "factor": 0.9, "reward": 0.75, "smart": false, "tier": "leicht"},
|
|
"normal": {"name": "Normal", "factor": 1.0, "reward": 1.0, "smart": true, "tier": ""},
|
|
"schwer": {"name": "Schwer", "factor": 1.2, "reward": 1.5, "smart": true, "tier": "schwer"},
|
|
}
|
|
# Werkstatt: Zerlegen bringt Staub, Herstellen kostet Staub.
|
|
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600}
|
|
const DUST_CRAFT := {"gewöhnlich": 40, "selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200}
|
|
|
|
const DAILY_TASKS := [
|
|
{"id": "sieg", "kind": "win", "text": "Gewinne einen Kampf", "goal": 1, "reward": 80},
|
|
{"id": "kaempfe", "kind": "game", "text": "Bestreite zwei Kämpfe", "goal": 2, "reward": 60},
|
|
{"id": "booster", "kind": "booster", "text": "Öffne einen Booster", "goal": 1, "reward": 40},
|
|
{"id": "wesen", "kind": "creature_played", "text": "Spiele fünf Wesen", "goal": 5, "reward": 50},
|
|
{"id": "falle", "kind": "trap_set", "text": "Lege eine Falle", "goal": 1, "reward": 50},
|
|
{"id": "kombo", "kind": "combo", "text": "Löse zwei Feldkombos aus", "goal": 2, "reward": 60},
|
|
{"id": "herstellen", "kind": "craft", "text": "Stelle eine Karte her", "goal": 1, "reward": 50},
|
|
{"id": "tisch", "kind": "win_vs3", "text": "Gewinne gegen 3 oder mehr Gegner", "goal": 1, "reward": 120},
|
|
{"id": "feuer", "kind": "win_element:Feuer", "text": "Gewinne mit einem Feuer-Deck", "goal": 1, "reward": 90},
|
|
{"id": "wasser", "kind": "win_element:Wasser", "text": "Gewinne mit einem Wasser-Deck", "goal": 1, "reward": 90},
|
|
{"id": "erde", "kind": "win_element:Erde", "text": "Gewinne mit einem Erde-Deck", "goal": 1, "reward": 90},
|
|
{"id": "luft", "kind": "win_element:Luft", "text": "Gewinne mit einem Luft-Deck", "goal": 1, "reward": 90},
|
|
]
|
|
|
|
static var today_override := "" # für Tests
|
|
|
|
var coins := 0
|
|
var dust := 0
|
|
var collection := {} # id -> Anzahl
|
|
var decks: Array = [] # DECK_SLOTS Einträge {"name": String, "cards": Array von ids}
|
|
var active_deck := 0
|
|
var stats := {"games": 0, "wins": 0, "boosters": 0, "wins_by_opp": {}}
|
|
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
|
var daily := {"date": "", "task": "", "progress": 0, "done": false}
|
|
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
|
var notices: Array[String] = [] # Meldungen für die Oberfläche (Tagesaufgabe, Fortschritt)
|
|
var card_stats := {} # id -> {"played", "games", "wins"}
|
|
var booster_since_mythic := 0
|
|
var tutorial_done := false
|
|
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
|
var tutorial2_done := false
|
|
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0, "margins": []}
|
|
|
|
## Das aktive Deck (Kartenliste des gewählten Slots).
|
|
var deck: Array:
|
|
get:
|
|
return decks[active_deck].cards
|
|
set(value):
|
|
decks[active_deck].cards = value
|
|
|
|
|
|
func _init() -> void:
|
|
for i in DECK_SLOTS:
|
|
decks.append({"name": "Deck %d" % (i + 1), "cards": []})
|
|
|
|
|
|
static func starter() -> Profile:
|
|
var p := Profile.new()
|
|
p.coins = START_COINS
|
|
for c in Cards.of_rarity("gewöhnlich"):
|
|
if c.id.begins_with("f_") or c.id.begins_with("w_") or c.id.begins_with("e_") or c.id.begins_with("l_"):
|
|
p.collection[c.id] = 2
|
|
# Zwei seltene Karten zum Start, damit es etwas zum Aufbauen gibt
|
|
p.collection["f_drache"] = 1
|
|
p.collection["e_baum"] = 1
|
|
var starter_ids := ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche",
|
|
"e_wall", "e_wurzel", "e_haerten", "e_golem", "e_troll"]
|
|
for cid in starter_ids:
|
|
p.deck.append(cid)
|
|
p.deck.append(cid)
|
|
p.deck[p.deck.find("f_asche")] = "f_drache"
|
|
p.deck[p.deck.find("e_troll")] = "e_baum"
|
|
return p
|
|
|
|
|
|
## 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; fehlende Felder bekommen Standardwerte (Migration).
|
|
static func from_dict(d: Dictionary) -> Profile:
|
|
var p := Profile.new()
|
|
var version := int(d.get("version", 1))
|
|
p.coins = int(d.get("coins", 0))
|
|
p.dust = int(d.get("dust", 0))
|
|
var coll = d.get("collection", {})
|
|
for cid in coll:
|
|
if Cards.has_id(cid):
|
|
p.collection[cid] = int(coll[cid])
|
|
if version >= 3:
|
|
var list = d.get("decks", [])
|
|
for i in mini(list.size(), DECK_SLOTS):
|
|
p.decks[i].name = str(list[i].get("name", "Deck %d" % (i + 1)))
|
|
p.decks[i].cards = []
|
|
for cid in list[i].get("cards", []):
|
|
if Cards.has_id(cid):
|
|
p.decks[i].cards.append(cid)
|
|
p.active_deck = clampi(int(d.get("active_deck", 0)), 0, DECK_SLOTS - 1)
|
|
else:
|
|
for cid in d.get("deck", []):
|
|
if Cards.has_id(cid):
|
|
p.decks[0].cards.append(cid)
|
|
if version >= 2:
|
|
var st = d.get("stats", {})
|
|
p.stats.games = int(st.get("games", 0))
|
|
p.stats.wins = int(st.get("wins", 0))
|
|
p.stats.boosters = int(st.get("boosters", 0))
|
|
for k in st.get("wins_by_opp", {}):
|
|
p.stats.wins_by_opp[str(k)] = int(st.wins_by_opp[k])
|
|
p.campaign_stage = int(d.get("campaign_stage", 0))
|
|
var dl = d.get("daily", {})
|
|
p.daily = {"date": str(dl.get("date", "")), "task": str(dl.get("task", "")),
|
|
"progress": int(dl.get("progress", 0)), "done": bool(dl.get("done", false))}
|
|
var se = d.get("settings", {})
|
|
p.settings = {"volume": float(se.get("volume", 0.8)), "music": float(se.get("music", 0.5)),
|
|
"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"
|
|
if version >= 4:
|
|
for cid in d.get("card_stats", {}):
|
|
if Cards.has_id(cid):
|
|
var st = d.card_stats[cid]
|
|
p.card_stats[cid] = {"played": int(st.get("played", 0)), "games": int(st.get("games", 0)), "wins": int(st.get("wins", 0))}
|
|
p.booster_since_mythic = int(d.get("booster_since_mythic", 0))
|
|
p.tutorial_done = bool(d.get("tutorial_done", false))
|
|
if version >= 5:
|
|
p.perk = str(d.get("perk", ""))
|
|
if not Game.PERKS.has(p.perk):
|
|
p.perk = ""
|
|
var rc = d.get("records", {})
|
|
p.records = {"streak": int(rc.get("streak", 0)), "best_streak": int(rc.get("best_streak", 0)),
|
|
"fastest_win": int(rc.get("fastest_win", 0)), "loss_streak": int(rc.get("loss_streak", 0)),
|
|
"margins": Array(rc.get("margins", [])).map(func(m): return int(m))}
|
|
p.tutorial2_done = bool(d.get("tutorial2_done", false))
|
|
return p
|
|
|
|
|
|
func to_dict() -> Dictionary:
|
|
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
|
"decks": decks, "active_deck": active_deck,
|
|
"stats": stats, "campaign_stage": campaign_stage,
|
|
"daily": daily, "settings": settings, "card_stats": card_stats,
|
|
"booster_since_mythic": booster_since_mythic, "tutorial_done": tutorial_done,
|
|
"perk": perk, "records": records, "tutorial2_done": tutorial2_done}
|
|
|
|
|
|
## 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
|
|
|
|
|
|
# --- Decks und Sammlung -----------------------------------------------------------
|
|
|
|
func deck_cards(slot: int = -1) -> Array:
|
|
var ids: Array = decks[slot if slot >= 0 else active_deck].cards
|
|
var out := []
|
|
for cid in ids:
|
|
out.append(Cards.by_id(cid))
|
|
return out
|
|
|
|
|
|
func deck_valid(slot: int = -1) -> Array:
|
|
return Game.validate_deck(deck_cards(slot), collection)
|
|
|
|
|
|
func deck_name(slot: int = -1) -> String:
|
|
return decks[slot if slot >= 0 else active_deck].name
|
|
|
|
|
|
func rename_deck(slot: int, name: String) -> String:
|
|
var n := name.strip_edges()
|
|
if n.is_empty() or n.length() > 20:
|
|
return "Name muss 1 bis 20 Zeichen haben"
|
|
decks[slot].name = n
|
|
return ""
|
|
|
|
|
|
## Füllt einen Slot mit eigenen Karten auf 20 auf, entlang einer Energiekurve.
|
|
## Ohne Karten im Slot werden die beiden Elemente mit den meisten eigenen Karten gewählt.
|
|
func autofill_deck(slot: int) -> String:
|
|
var cards: Array = decks[slot].cards
|
|
var elems := Game.deck_elements(deck_cards(slot))
|
|
if elems.is_empty():
|
|
var count := {}
|
|
for c in owned_cards():
|
|
for e in c.elements:
|
|
count[e] = count.get(e, 0) + 1
|
|
var ranked: Array = count.keys()
|
|
ranked.sort_custom(func(a, b): return count[a] > count[b])
|
|
elems = ranked.slice(0, Game.MAX_DECK_ELEMENTS)
|
|
var pool := owned_cards().filter(func(c): return c.elements.all(func(e): return e in elems))
|
|
pool.sort_custom(func(a, b): return Cards.RARITIES.find(a.rarity) > Cards.RARITIES.find(b.rarity))
|
|
var counts := {}
|
|
for cid in cards:
|
|
counts[cid] = counts.get(cid, 0) + 1
|
|
var progressed := true
|
|
while cards.size() < Game.DECK_SIZE and progressed:
|
|
progressed = false
|
|
for cost in range(1, 11):
|
|
if cards.size() >= Game.DECK_SIZE:
|
|
break
|
|
for c in pool:
|
|
if c.cost != cost:
|
|
continue
|
|
var have: int = counts.get(c.id, 0)
|
|
if have < mini(collection.get(c.id, 0), Game.MAX_COPIES):
|
|
cards.append(c.id)
|
|
counts[c.id] = have + 1
|
|
progressed = true
|
|
break
|
|
var ok := Game.validate_deck(deck_cards(slot), collection)
|
|
return "" if ok[0] else ok[1]
|
|
|
|
|
|
## Bestenliste: Karten mit den meisten Siegen (mindestens eine Partie).
|
|
func top_cards(n: int) -> Array:
|
|
var rows := []
|
|
for cid in card_stats:
|
|
var st: Dictionary = card_stats[cid]
|
|
if st.games > 0:
|
|
rows.append({"card": Cards.by_id(cid), "games": st.games, "wins": st.wins, "played": st.played,
|
|
"rate": float(st.wins) / st.games})
|
|
rows.sort_custom(func(a, b): return a.wins > b.wins or (a.wins == b.wins and a.rate > b.rate))
|
|
return rows.slice(0, n)
|
|
|
|
|
|
func owned_cards() -> Array:
|
|
return Cards.all().filter(func(c): return collection.get(c.id, 0) > 0)
|
|
|
|
|
|
func distinct_owned() -> int:
|
|
var n := 0
|
|
for cid in collection:
|
|
if collection[cid] > 0:
|
|
n += 1
|
|
return n
|
|
|
|
|
|
# --- Werkstatt --------------------------------------------------------------------
|
|
|
|
## Zerlegt n Kopien in Staub. Gibt "" zurück oder eine Fehlermeldung.
|
|
func dismantle(cid: String, n: int = 1) -> String:
|
|
var owned: int = collection.get(cid, 0)
|
|
if owned < n:
|
|
return "Nicht genug Kopien"
|
|
var needed := 0
|
|
for d in decks:
|
|
needed = maxi(needed, d.cards.count(cid))
|
|
if owned - n < needed:
|
|
return "Wird in einem Deck gebraucht"
|
|
collection[cid] = owned - n
|
|
dust += DUST_DISMANTLE[Cards.by_id(cid).rarity] * n
|
|
return ""
|
|
|
|
|
|
## Stellt eine Karte aus Staub her. Gibt "" zurück oder eine Fehlermeldung.
|
|
func craft(cid: String) -> String:
|
|
var cost: int = DUST_CRAFT[Cards.by_id(cid).rarity]
|
|
if dust < cost:
|
|
return "Nicht genug Staub (%d nötig)" % cost
|
|
dust -= cost
|
|
collection[cid] = collection.get(cid, 0) + 1
|
|
daily_event("craft")
|
|
return ""
|
|
|
|
|
|
## Zerlegt alle Kopien über dem Deck-Limit. Gibt den gewonnenen Staub zurück.
|
|
func dismantle_extras() -> int:
|
|
var before := dust
|
|
for cid in collection.keys():
|
|
var extra: int = collection[cid] - Game.MAX_COPIES
|
|
if extra > 0:
|
|
dismantle(cid, extra)
|
|
return dust - before
|
|
|
|
|
|
# --- Booster ----------------------------------------------------------------------
|
|
|
|
## Kartenpool einer Seltenheit, bei Elementboostern nur Karten mit diesem Element.
|
|
static func booster_pool(rarity: String, element: String) -> Array:
|
|
var pool := Cards.of_rarity(rarity)
|
|
if element != "":
|
|
pool = pool.filter(func(c): return element in c.elements)
|
|
return pool
|
|
|
|
|
|
static func roll_booster(r: RandomNumberGenerator, element: String = "") -> Array:
|
|
var rarities := RARITY_WEIGHTS.keys()
|
|
var weights := PackedFloat32Array(RARITY_WEIGHTS.values())
|
|
var cards := []
|
|
for i in BOOSTER_SIZE:
|
|
var rarity: String = rarities[r.rand_weighted(weights)]
|
|
if i == BOOSTER_SIZE - 1 and rarity == "gewöhnlich" \
|
|
and cards.all(func(c): return c.rarity == "gewöhnlich"):
|
|
rarity = "selten" # mindestens eine seltene Karte pro Booster
|
|
var pool := booster_pool(rarity, element)
|
|
cards.append(pool[r.randi_range(0, pool.size() - 1)])
|
|
return cards
|
|
|
|
|
|
## Deck-Code zum Teilen: "DINGER1:id*anzahl,…" (sortiert, daher vergleichbar).
|
|
static func deck_code(card_ids: Array) -> String:
|
|
var counts := {}
|
|
for cid in card_ids:
|
|
counts[cid] = counts.get(cid, 0) + 1
|
|
var keys := counts.keys()
|
|
keys.sort()
|
|
return CODE_PREFIX + ",".join(keys.map(func(k): return "%s*%d" % [k, counts[k]]))
|
|
|
|
|
|
## Karten-IDs aus einem Deck-Code; leer bei ungültigem Code.
|
|
static func parse_deck_code(code: String) -> Array:
|
|
var s := code.strip_edges()
|
|
if not s.begins_with(CODE_PREFIX):
|
|
return []
|
|
var out := []
|
|
for part in s.substr(CODE_PREFIX.length()).split(",", false):
|
|
var kv: PackedStringArray = part.strip_edges().split("*")
|
|
if kv.size() != 2 or not Cards.has_id(kv[0]) or not kv[1].is_valid_int():
|
|
return []
|
|
for i in clampi(int(kv[1]), 0, Game.MAX_COPIES):
|
|
out.append(kv[0])
|
|
return out
|
|
|
|
|
|
## Öffnet einen Booster (element: Elementbooster). Gibt die Karten zurück, leer wenn die Münzen nicht reichen.
|
|
func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Array:
|
|
var cost := ELEMENT_BOOSTER_COST if element != "" else BOOSTER_COST
|
|
if coins < cost:
|
|
return []
|
|
if r == null:
|
|
r = Game.make_rng()
|
|
coins -= cost
|
|
var cards := roll_booster(r, element)
|
|
booster_since_mythic += 1
|
|
var has_mythic: bool = cards.any(func(c): return c.rarity == "mythisch")
|
|
if booster_since_mythic >= PITY_BOOSTERS and not has_mythic:
|
|
var pool := booster_pool("mythisch", element)
|
|
cards[BOOSTER_SIZE - 1] = pool[r.randi_range(0, pool.size() - 1)]
|
|
has_mythic = true
|
|
if has_mythic:
|
|
booster_since_mythic = 0
|
|
for c in cards:
|
|
collection[c.id] = collection.get(c.id, 0) + 1
|
|
stats.boosters += 1
|
|
daily_event("booster")
|
|
return cards
|
|
|
|
|
|
# --- Kampfergebnis, Tagesaufgabe -------------------------------------------
|
|
|
|
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
|
|
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
|
## turns: Dauer der Partie für die Bestenliste.
|
|
func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, played_ids: Array = [],
|
|
turns: int = 0, reward_factor: float = 1.0, remaining_life: int = -1) -> int:
|
|
stats.games += 1
|
|
records.margins.append(maxi(remaining_life, 0) if won else 0)
|
|
while records.margins.size() > 5:
|
|
records.margins.pop_front()
|
|
if won:
|
|
records.streak += 1
|
|
records.loss_streak = 0
|
|
records.best_streak = maxi(records.best_streak, records.streak)
|
|
if turns > 0 and (records.fastest_win == 0 or turns < records.fastest_win):
|
|
records.fastest_win = turns
|
|
else:
|
|
records.streak = 0
|
|
records.loss_streak += 1
|
|
var tip := suggest_difficulty()
|
|
if tip != "" and stage < 0:
|
|
notices.append("Tipp: %s. Schwierigkeit %s?" % [
|
|
"%d Siege in Folge" % records.streak if won else "%d Niederlagen in Folge" % records.loss_streak, DIFFICULTIES[tip].name])
|
|
for cid in played_ids:
|
|
_stat(cid).played += 1
|
|
var seen := {}
|
|
for c in deck_used:
|
|
if not seen.has(c.id):
|
|
seen[c.id] = true
|
|
_stat(c.id).games += 1
|
|
if won:
|
|
_stat(c.id).wins += 1
|
|
var reward := REWARD_LOSS
|
|
if won:
|
|
stats.wins += 1
|
|
var key := str(n_opp)
|
|
stats.wins_by_opp[key] = int(stats.wins_by_opp.get(key, 0)) + 1
|
|
reward = int(round(REWARD_WIN[n_opp] * 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)
|
|
coins += reward
|
|
daily_event("game")
|
|
if won:
|
|
daily_event("win")
|
|
if n_opp >= 3:
|
|
daily_event("win_vs3")
|
|
for e in Game.deck_elements(deck_used):
|
|
daily_event("win_element:" + e)
|
|
return reward
|
|
|
|
|
|
func _stat(cid: String) -> Dictionary:
|
|
if not card_stats.has(cid):
|
|
card_stats[cid] = {"played": 0, "games": 0, "wins": 0}
|
|
return card_stats[cid]
|
|
|
|
|
|
func difficulty() -> Dictionary:
|
|
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
|
|
|
|
|
## Vorschlag: rauf nach drei deutlichen Siegen in Folge (im Schnitt mindestens 12 Leben übrig) oder fünf
|
|
## Siegen egal wie knapp; runter nach drei Niederlagen in Folge. "" = kein Vorschlag.
|
|
func suggest_difficulty() -> String:
|
|
var order := ["leicht", "normal", "schwer"]
|
|
var i := order.find(str(settings.get("difficulty", "normal")))
|
|
if i < order.size() - 1 and records.streak >= 3:
|
|
var last: Array = records.margins.slice(maxi(0, records.margins.size() - 3))
|
|
var avg := 0.0
|
|
for m in last:
|
|
avg += float(m) / last.size()
|
|
if records.streak >= 5 or (records.margins.size() >= 3 and avg >= 12.0):
|
|
return order[i + 1]
|
|
if records.loss_streak >= 3 and i > 0:
|
|
return order[i - 1]
|
|
return ""
|
|
|
|
|
|
## Eigene Deckstärke, mit Kartenstatistik gewichtet.
|
|
func deck_rating(cards: Array = []) -> float:
|
|
var deck_used: Array = cards if not cards.is_empty() else deck_cards()
|
|
return Game.deck_rating(deck_used, card_stats)
|
|
|
|
|
|
## Zielstärke der Gegnerdecks: eigene Deckstärke mal Schwierigkeitsfaktor.
|
|
func target_rating(cards: Array = []) -> float:
|
|
return deck_rating(cards) * float(difficulty().factor)
|
|
|
|
|
|
## Tauscht die schwächste Karte des aktiven Decks gegen cid, wenn das Deck dadurch stärker und gültig bleibt.
|
|
## dry_run: nur prüfen. Gibt "" zurück oder den Grund, warum nicht.
|
|
func improve_deck_with(cid: String, dry_run: bool = false) -> String:
|
|
if collection.get(cid, 0) <= 0:
|
|
return "Karte nicht in der Sammlung"
|
|
var card := Cards.by_id(cid)
|
|
if deck.count(cid) >= mini(collection[cid], Game.MAX_COPIES):
|
|
return "Schon so oft im Deck wie möglich"
|
|
var elems := Game.deck_elements(deck_cards())
|
|
for e in card.elements:
|
|
if not (e in elems):
|
|
elems.append(e)
|
|
if elems.size() > Game.MAX_DECK_ELEMENTS:
|
|
return "Passt nicht zu den Elementen des Decks"
|
|
var weakest := ""
|
|
var weakest_value := Cards.card_value(card)
|
|
for other in deck:
|
|
if other == cid:
|
|
continue
|
|
var v := Cards.card_value(Cards.by_id(other))
|
|
if v < weakest_value:
|
|
weakest_value = v
|
|
weakest = other
|
|
if weakest == "":
|
|
return "Keine schwächere Karte im Deck"
|
|
if dry_run:
|
|
return ""
|
|
var trial: Array = deck.duplicate()
|
|
trial[trial.find(weakest)] = cid
|
|
var ok := Game.validate_deck(trial.map(func(id): return Cards.by_id(id)), collection)
|
|
if not ok[0]:
|
|
return ok[1]
|
|
deck = trial
|
|
return ""
|
|
|
|
|
|
## Karten aus new_ids, die das aktive Deck verbessern würden.
|
|
func deck_suggestions(new_ids: Array) -> Array:
|
|
var out := []
|
|
for cid in new_ids:
|
|
if not (cid in out) and improve_deck_with(cid, true) == "":
|
|
out.append(cid)
|
|
return out
|
|
|
|
|
|
func perk_unlocked() -> bool:
|
|
return campaign_stage >= Campaign.STAGES.size()
|
|
|
|
|
|
## Schreibt Statistik als CSV (Kartenwerte und Gesamtzahlen), z. B. für Spieltests.
|
|
func export_stats(path: String) -> String:
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
if f == null:
|
|
return "Datei nicht schreibbar"
|
|
f.store_line("karte;seltenheit;gespielt;partien;siege")
|
|
for cid in card_stats:
|
|
var st: Dictionary = card_stats[cid]
|
|
var c := Cards.by_id(cid)
|
|
f.store_line("%s;%s;%d;%d;%d" % [c.name, c.rarity, st.played, st.games, st.wins])
|
|
f.store_line("")
|
|
f.store_line("kaempfe;%d" % stats.games)
|
|
f.store_line("siege;%d" % stats.wins)
|
|
f.store_line("booster;%d" % stats.boosters)
|
|
f.store_line("beste_serie;%d" % records.best_streak)
|
|
f.store_line("schnellster_sieg;%d" % records.fastest_win)
|
|
f.close()
|
|
return ""
|
|
|
|
|
|
static func today() -> String:
|
|
return today_override if today_override != "" else Time.get_date_string_from_system()
|
|
|
|
|
|
## Wählt die Tagesaufgabe neu, wenn ein neuer Tag begonnen hat.
|
|
func refresh_daily() -> void:
|
|
var t := today()
|
|
if daily.date != t:
|
|
var day_number := int(Time.get_unix_time_from_datetime_string(t + "T00:00:00") / 86400)
|
|
var task: Dictionary = DAILY_TASKS[day_number % DAILY_TASKS.size()]
|
|
daily = {"date": t, "task": task.id, "progress": 0, "done": false}
|
|
|
|
|
|
func daily_task() -> Dictionary:
|
|
refresh_daily()
|
|
for t in DAILY_TASKS:
|
|
if t.id == daily.task:
|
|
return t
|
|
return DAILY_TASKS[0]
|
|
|
|
|
|
## Zählt ein Ereignis (siehe DAILY_TASKS) für die Tagesaufgabe. Gibt die Belohnung zurück.
|
|
func daily_event(kind: String, n: int = 1) -> int:
|
|
var task := daily_task()
|
|
if daily.done or task.kind != kind:
|
|
return 0
|
|
daily.progress += n
|
|
if daily.progress >= task.goal:
|
|
daily.done = true
|
|
coins += task.reward
|
|
notices.append("Tagesaufgabe erfüllt: %s (+%d Münzen)" % [task.text, task.reward])
|
|
return task.reward
|
|
return 0
|