Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer

- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert)
- Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich
- Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt
- Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster
- Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab
- Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen
- Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten
- Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext
- Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests
- Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-16 18:50:09 +02:00
parent b6d5916a8d
commit 4c6bf96eeb
32 changed files with 1355 additions and 174 deletions

View File

@@ -6,6 +6,8 @@ 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
@@ -34,6 +36,7 @@ const ACHIEVEMENTS := [
{"id": "kombokoenig", "name": "Kombokönig", "text": "Gewinne mit mindestens 6 Kombokarten im Deck.", "reward": 100},
{"id": "kampagnenheld", "name": "Kampagnenheld", "text": "Schließe Kapitel 1 der Kampagne ab.", "reward": 300},
{"id": "elementmeister", "name": "Meister der Elemente", "text": "Schließe Kapitel 2 der Kampagne ab.", "reward": 500},
{"id": "archimagier", "name": "Archimagier", "text": "Schließe Kapitel 3 der Kampagne ab.", "reward": 800},
{"id": "serie", "name": "Siegesserie", "text": "Gewinne fünf Kämpfe in Folge.", "reward": 150},
{"id": "wochenheld", "name": "Wochenheld", "text": "Bestehe eine wöchentliche Herausforderung.", "reward": 100},
]
@@ -336,7 +339,15 @@ func dismantle_extras() -> int:
# --- Booster ----------------------------------------------------------------------
static func roll_booster(r: RandomNumberGenerator) -> Array:
## 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 := []
@@ -345,23 +356,49 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
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 := Cards.of_rarity(rarity)
var pool := booster_pool(rarity, element)
cards.append(pool[r.randi_range(0, pool.size() - 1)])
return cards
## Öffnet einen Booster. Gibt die Karten zurück, leer wenn die Münzen nicht reichen.
func open_booster(r: RandomNumberGenerator = null) -> Array:
if coins < BOOSTER_COST:
## 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 -= BOOSTER_COST
var cards := roll_booster(r)
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 := Cards.of_rarity("mythisch")
var pool := booster_pool("mythisch", element)
cards[BOOSTER_SIZE - 1] = pool[r.randi_range(0, pool.size() - 1)]
has_mythic = true
if has_mythic:
@@ -478,6 +515,8 @@ func _achieved(id: String, ctx: Dictionary) -> bool:
"kampagnenheld":
return campaign_stage >= Campaign.STAGES.size()
"elementmeister":
return campaign_stage >= 2 * Campaign.STAGES.size()
"archimagier":
return campaign_stage >= Campaign.total()
"serie":
return records.best_streak >= 5