Wöchentliche Herausforderung und die Netzwerk-Hinweise entfernt

- Challenge samt Bildschirm, Menüpunkt, Erfolg "Wochenheld" und Spielstandfeld entfernt;
  record_battle verliert den challenge-Parameter, die Bestenliste bleibt
- Lobby: Erklärzeile unter dem Titel weg, "KI-Gegner (nur Host)" heißt jetzt "KI-Gegner"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-17 21:13:08 +02:00
parent 0f2dc22ef6
commit 43073351b0
14 changed files with 24 additions and 161 deletions

View File

@@ -37,7 +37,7 @@ Der Spielstand liegt unter `user://spielstand.json`
- Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch - Deck-Namen, Auto-Ausfüllen, Hintergrundmusik und Oberfläche wahlweise auf Englisch
- Geführte Anleitung mit Hervorhebungen, Rückgängig für Fehlklicks, Überspringen von Gegnerzügen - Geführte Anleitung mit Hervorhebungen, Rückgängig für Fehlklicks, Überspringen von Gegnerzügen
- Zwei Kampagnenkapitel (Bosse mit ein bis drei Merkmalen), wählbares eigenes Merkmal nach Kapitel 1 - Zwei Kampagnenkapitel (Bosse mit ein bis drei Merkmalen), wählbares eigenes Merkmal nach Kapitel 1
- Wöchentliche Herausforderung mit Regelvarianten, Bestenliste (Serie, schnellster Sieg), Statistik-Export als CSV - Bestenliste (Serie, schnellster Sieg) und Statistik-Export als CSV
- Zehn Fallenkarten, Netzwerk mit Wiederverbinden (Platz bleibt 60 s frei) - Zehn Fallenkarten, Netzwerk mit Wiederverbinden (Platz bleibt 60 s frei)
- Deckstärke aus Seltenheit, Schlüsselwörtern und Effekten; Gegnerdecks werden auf die eigene Stärke gebaut - Deckstärke aus Seltenheit, Schlüsselwörtern und Effekten; Gegnerdecks werden auf die eigene Stärke gebaut
- Schwierigkeit Leicht/Normal/Schwer: Zielstärke 0,9/1,0/1,2-fach, schwache oder starke Archetypen, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach - Schwierigkeit Leicht/Normal/Schwer: Zielstärke 0,9/1,0/1,2-fach, schwache oder starke Archetypen, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
@@ -51,7 +51,7 @@ Der Spielstand liegt unter `user://spielstand.json`
- Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle - Kartentext passt sich der Schriftgröße an, Gamepad auch in Menüs (Fokusrahmen), Anleitung 2 erzwingt Maras Angriff in die Falle
- KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso - KI-Vorausschau in zwei Stufen (erst alle Züge mit den besten Zielen, dann die acht besten vertieft): bei 5 Gegnern 0,1 s statt 2 s, und sie rechnet im Hintergrund-Thread, Musik ebenso
- KI-Bewertung mit Bedrohung (gegnerische Angriffskraft ohne eigenen Wächter, tödliche besonders) und Führenden-Bremse; im Duell 62:38 gegen die alte Bewertung - KI-Bewertung mit Bedrohung (gegnerische Angriffskraft ohne eigenen Wächter, tödliche besonders) und Führenden-Bremse; im Duell 62:38 gegen die alte Bewertung
- Unterbrochene Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) werden laufend gespeichert und lassen sich im Menü fortsetzen - Unterbrochene Einzelspieler-Kämpfe (auch Kampagne) werden laufend gespeichert und lassen sich im Menü fortsetzen
- Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück - Gamepad im Deckbau, in der Werkstatt und beim Booster: Karten, Deckzeilen und Kartenrücken sind fokussierbar, A wählt, B geht zurück
- Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`) - Englisch auch für Kartennamen und Kartentexte (`name_en` in `data/karten.json`, Textbausteine über `tr()`)
- Deck-Codes zum Teilen (`DINGER1:id*anzahl,…`) mit Zwischenablage im Deckbau - Deck-Codes zum Teilen (`DINGER1:id*anzahl,…`) mit Zwischenablage im Deckbau
@@ -103,5 +103,4 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton - `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton
- `scenes/main.tscn` – Startszene - `scenes/main.tscn` – Startszene
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden - `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
- `scripts/challenge.gd` – wöchentliche Herausforderungen
- `translations/ui.csv` – englische Oberflächentexte - `translations/ui.csv` – englische Oberflächentexte

View File

@@ -9,7 +9,7 @@
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer - Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
- Ein unterbrochener Kampf lässt sich später fortsetzen - Ein unterbrochener Kampf lässt sich später fortsetzen
- Geführte erste Partie als Einstieg - Geführte erste Partie als Einstieg
- Wöchentliche Herausforderungen und Bestenliste als Langzeitziel - Bestenliste als Langzeitziel
## Sammeln ## Sammeln
- Booster öffnen - Booster öffnen

View File

@@ -1,33 +0,0 @@
class_name Challenge
## Wöchentliche Herausforderung: feste Regelvariante, einmal pro Woche belohnt.
const LIST := [
{"id": "eile", "name": "Eile", "text": "Alle beginnen mit 3 Energie und 20 Leben, drei Gegner.", "n_opp": 3,
"rules": {"everyone_perk": "start_energie", "start_life": 20}, "reward": 200},
{"id": "resonanz", "name": "Resonanzsturm", "text": "Resonanz zählt für alle doppelt, zwei Gegner.", "n_opp": 2,
"rules": {"everyone_perk": "resonanz_doppelt"}, "reward": 180},
{"id": "flink", "name": "Blitzkrieg", "text": "Alle Wesen sind flink, vier Gegner.", "n_opp": 4,
"rules": {"everyone_perk": "alle_flink"}, "reward": 250},
{"id": "schild", "name": "Panzerschlacht", "text": "Alle Wesen kommen mit Schild, Duell mit 40 Leben.", "n_opp": 1,
"rules": {"everyone_perk": "schild_wesen", "start_life": 40}, "reward": 150},
{"id": "karten", "name": "Kartenflut", "text": "Jeder zieht 2 Karten pro Zug, fünf Gegner am Tisch.", "n_opp": 5,
"rules": {"everyone_perk": "extra_karten"}, "reward": 300},
{"id": "mono", "name": "Reine Lehre", "text": "Nur Decks mit einem Element sind erlaubt, drei Gegner.", "n_opp": 3,
"rules": {"mono_only": true}, "reward": 220},
]
static func week() -> int:
var t := Profile.today()
return int(Time.get_unix_time_from_datetime_string(t + "T00:00:00") / 604800)
static func current() -> Dictionary:
return LIST[week() % LIST.size()]
## Prüft, ob das Deck zur Herausforderung passt. Gibt "" oder eine Fehlermeldung zurück.
static func deck_allowed(ch: Dictionary, deck: Array) -> String:
if ch.rules.get("mono_only", false) and Game.deck_elements(deck).size() != 1:
return "Diese Woche sind nur Decks mit einem Element erlaubt"
return ""

View File

@@ -1 +0,0 @@
uid://n5h6q6kfoo34

View File

@@ -38,7 +38,6 @@ const ACHIEVEMENTS := [
{"id": "elementmeister", "name": "Meister der Elemente", "text": "Schließe Kapitel 2 der Kampagne ab.", "reward": 500}, {"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": "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": "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},
] ]
const DAILY_TASKS := [ const DAILY_TASKS := [
@@ -75,7 +74,6 @@ var tutorial_done := false
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1) var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
var tutorial2_done := false var tutorial2_done := false
var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0, "margins": []} var records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0, "margins": []}
var challenge_week := -1 # Woche der zuletzt bestandenen Herausforderung
## Das aktive Deck (Kartenliste des gewählten Slots). ## Das aktive Deck (Kartenliste des gewählten Slots).
var deck: Array: var deck: Array:
@@ -181,7 +179,6 @@ static func from_dict(d: Dictionary) -> Profile:
p.records = {"streak": int(rc.get("streak", 0)), "best_streak": int(rc.get("best_streak", 0)), 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)), "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))} "margins": Array(rc.get("margins", [])).map(func(m): return int(m))}
p.challenge_week = int(d.get("challenge_week", -1))
p.tutorial2_done = bool(d.get("tutorial2_done", false)) p.tutorial2_done = bool(d.get("tutorial2_done", false))
return p return p
@@ -192,7 +189,7 @@ func to_dict() -> Dictionary:
"stats": stats, "achievements": achievements, "campaign_stage": campaign_stage, "stats": stats, "achievements": achievements, "campaign_stage": campaign_stage,
"daily": daily, "settings": settings, "card_stats": card_stats, "daily": daily, "settings": settings, "card_stats": card_stats,
"booster_since_mythic": booster_since_mythic, "tutorial_done": tutorial_done, "booster_since_mythic": booster_since_mythic, "tutorial_done": tutorial_done,
"perk": perk, "records": records, "challenge_week": challenge_week, "tutorial2_done": tutorial2_done} "perk": perk, "records": records, "tutorial2_done": tutorial2_done}
## Speichert und sichert den vorherigen Stand als .bak. ## Speichert und sichert den vorherigen Stand als .bak.
@@ -415,9 +412,9 @@ func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Arra
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück. ## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei. ## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
## turns: Dauer der Partie für die Bestenliste. challenge: Herausforderung dieser Woche. ## 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 = [], func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, played_ids: Array = [],
turns: int = 0, challenge: Dictionary = {}, reward_factor: float = 1.0, remaining_life: int = -1) -> int: turns: int = 0, reward_factor: float = 1.0, remaining_life: int = -1) -> int:
stats.games += 1 stats.games += 1
records.margins.append(maxi(remaining_life, 0) if won else 0) records.margins.append(maxi(remaining_life, 0) if won else 0)
while records.margins.size() > 5: while records.margins.size() > 5:
@@ -432,7 +429,7 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
records.streak = 0 records.streak = 0
records.loss_streak += 1 records.loss_streak += 1
var tip := suggest_difficulty() var tip := suggest_difficulty()
if tip != "" and stage < 0 and challenge.is_empty(): if tip != "" and stage < 0:
notices.append("Tipp: %s. Schwierigkeit %s?" % [ 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]) "%d Siege in Folge" % records.streak if won else "%d Niederlagen in Folge" % records.loss_streak, DIFFICULTIES[tip].name])
for cid in played_ids: for cid in played_ids:
@@ -454,10 +451,6 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
campaign_stage += 1 campaign_stage += 1
reward = Campaign.stage(stage).reward reward = Campaign.stage(stage).reward
notices.append("Kampagne: %s besiegt!" % Campaign.stage(stage).name) notices.append("Kampagne: %s besiegt!" % Campaign.stage(stage).name)
if not challenge.is_empty() and challenge_week != Challenge.week():
challenge_week = Challenge.week()
reward = challenge.reward
notices.append("Herausforderung bestanden: %s (+%d Münzen)" % [challenge.name, challenge.reward])
coins += reward coins += reward
daily_event("game") daily_event("game")
if won: if won:
@@ -520,8 +513,6 @@ func _achieved(id: String, ctx: Dictionary) -> bool:
return campaign_stage >= Campaign.total() return campaign_stage >= Campaign.total()
"serie": "serie":
return records.best_streak >= 5 return records.best_streak >= 5
"wochenheld":
return challenge_week >= 0
return false return false

View File

@@ -38,7 +38,6 @@ var gp_zone: String:
var gp_index: int: var gp_index: int:
get: get:
return pad.index return pad.index
var challenge := {} # wöchentliche Herausforderung oder leer
var skip_ai := false # Gegnerzüge im Schnelldurchlauf var skip_ai := false # Gegnerzüge im Schnelldurchlauf
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI) var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
@@ -141,18 +140,11 @@ static func guided(part: int = 1) -> BattleScreen:
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle). ## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
static func resume(data: Dictionary) -> BattleScreen: static func resume(data: Dictionary) -> BattleScreen:
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1))) var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
s.challenge = data.get("challenge", {})
s.played_ids = Array(data.get("played_ids", [])) s.played_ids = Array(data.get("played_ids", []))
s.saved_game = Game.from_snapshot(data.game) s.saved_game = Game.from_snapshot(data.game)
return s return s
static func weekly(ch: Dictionary) -> BattleScreen:
var b := BattleScreen.new(int(ch.n_opp), -1)
b.challenge = ch
return b
func is_online() -> bool: func is_online() -> bool:
return net_play return net_play
@@ -198,10 +190,6 @@ func _ready() -> void:
opps[0].perks = [] opps[0].perks = []
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true) game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
guide.inject_cards(game) guide.inject_cards(game)
elif not challenge.is_empty():
var rules: Dictionary = challenge.rules.duplicate()
rules["target_rating"] = Game.deck_rating(app.profile.deck_cards())
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, rules)
elif stage >= 0: elif stage >= 0:
var r := Game.make_rng() var r := Game.make_rng()
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules()) game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
@@ -323,7 +311,7 @@ func _overlay() -> Control:
return Style.overlay(self) return Style.overlay(self)
## Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) lassen sich unterbrechen und fortsetzen. ## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
func resumable() -> bool: func resumable() -> bool:
return counts_for_profile() and not tutorial return counts_for_profile() and not tutorial
@@ -331,7 +319,7 @@ func resumable() -> bool:
func save_battle() -> void: func save_battle() -> void:
if not resumable() or game == null or game.winner != "": if not resumable() or game == null or game.winner != "":
return return
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "challenge": challenge, "played_ids": played_ids, "game": game.snapshot(-1)} var d := {"version": 1, "n_opp": n_opp, "stage": stage, "played_ids": played_ids, "game": game.snapshot(-1)}
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE) var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
if f != null: if f != null:
f.store_string(JSON.stringify(d)) f.store_string(JSON.stringify(d))
@@ -567,8 +555,8 @@ func _exit_tree() -> void:
func _finish() -> void: func _finish() -> void:
var won := game.winner == "human" var won := game.winner == "human"
if counts_for_profile(): if counts_for_profile():
var factor: float = float(app.profile.difficulty().reward) if stage < 0 and challenge.is_empty() and not tutorial else 1.0 var factor: float = float(app.profile.difficulty().reward) if stage < 0 and not tutorial else 1.0
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor, game.human.life) reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life)
if tutorial: if tutorial:
if tutorial_part == 1: if tutorial_part == 1:
app.profile.tutorial_done = true app.profile.tutorial_done = true

View File

@@ -1,42 +0,0 @@
class_name ChallengeScreen
extends Control
## Wöchentliche Herausforderung: feste Regelvariante mit einmaliger Belohnung.
var app
var msg: Label
func _ready() -> void:
var p: Profile = app.profile
var ch := Challenge.current()
var done: bool = p.challenge_week == Challenge.week()
var col := Style.column(self, 760, 60, 14)
Style.clabel(col, "Herausforderung der Woche", 36, Style.GOLD, "center", true)
Style.clabel(col, "Jede Woche eine andere Regel. Die Belohnung gibt es einmal pro Woche, danach die normale.", 16, Style.MUTED)
Style.spacer(col, 20)
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 10, Style.GREEN if done else Style.GOLD, 2))
col.add_child(panel)
var inner := VBoxContainer.new()
inner.add_theme_constant_override("separation", 10)
panel.add_child(inner)
Style.spacer(inner, 6)
Style.clabel(inner, ch.name, 30, Style.TEXT, "center", true)
Style.clabel(inner, ch.text, 18, Style.MUTED)
Style.clabel(inner, "Belohnung: %d Münzen%s" % [ch.reward, " · diese Woche schon bestanden ✓" if done else ""], 16, Style.GOLD)
Style.spacer(inner, 6)
Style.spacer(col, 10)
msg = Style.clabel(col, "", 16, Style.ORANGE)
var deck_err := Challenge.deck_allowed(ch, p.deck_cards())
if deck_err != "":
msg.text = deck_err + " (aktives Deck: %s)" % p.deck_name()
var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER
var b := Style.cbutton(br, "Antreten", Vector2(200, 50), func(): app.show_screen(BattleScreen.weekly(ch)), Color("285a3c"))
b.disabled = deck_err != "" or not p.deck_valid()[0]
Style.cbutton(br, "Zurück", Vector2(160, 50), func(): app.show_screen(MenuScreen.new()))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -1 +0,0 @@
uid://cxheynenqemse

View File

@@ -22,7 +22,6 @@ func _ready() -> void:
net.my_deck = app.profile.deck.duplicate() net.my_deck = app.profile.deck.duplicate()
var col := Style.column(self, 760, 40, 10) var col := Style.column(self, 760, 40, 10)
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true) Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
Style.clabel(col, "Im selben Netz: einer eröffnet, die anderen treten mit seiner Adresse bei. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
Style.spacer(col, 10) Style.spacer(col, 10)
var hr := Style.row(col, 12) var hr := Style.row(col, 12)
hr.alignment = BoxContainer.ALIGNMENT_CENTER hr.alignment = BoxContainer.ALIGNMENT_CENTER
@@ -40,7 +39,7 @@ func _ready() -> void:
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true) Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
list_label = Style.clabel(col, "", 16, Style.TEXT) list_label = Style.clabel(col, "", 16, Style.TEXT)
Style.spacer(col, 6) Style.spacer(col, 6)
Style.clabel(col, "KI-Gegner (nur Host)", 16, Style.MUTED) Style.clabel(col, "KI-Gegner", 16, Style.MUTED)
var ar := Style.row(col, 10) var ar := Style.row(col, 10)
ar.alignment = BoxContainer.ALIGNMENT_CENTER ar.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(0, 5): for n in range(0, 5):

View File

@@ -6,7 +6,6 @@ var b_single: Button
var b_campaign: Button var b_campaign: Button
var b_online: Button var b_online: Button
var b_tutorial: Button var b_tutorial: Button
var b_challenge: Button
var b_resume: Button var b_resume: Button
var coins_label: Label var coins_label: Label
var coll_label: Label var coll_label: Label
@@ -44,7 +43,6 @@ func _ready() -> void:
b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c")) b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c")) b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c")) b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
b_challenge = Style.cbutton(grid, "Herausforderung", sz, func(): app.show_screen(ChallengeScreen.new()), Color("285a3c"))
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new())) Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new())) Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new())) Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
@@ -77,8 +75,6 @@ func refresh() -> void:
b_campaign.disabled = not ok[0] b_campaign.disabled = not ok[0]
b_tutorial.disabled = not ok[0] b_tutorial.disabled = not ok[0]
b_online.disabled = not ok[0] b_online.disabled = not ok[0]
b_challenge.disabled = not ok[0]
b_challenge.text = tr("Herausforderung") + (" ✓" if p.challenge_week == Challenge.week() else "")
var total := 0 var total := 0
for cid in p.collection: for cid in p.collection:
total += p.collection[cid] total += p.collection[cid]

View File

@@ -108,7 +108,7 @@ func _initialize() -> void:
test_deck_umbenennen_und_auffuellen() test_deck_umbenennen_und_auffuellen()
test_neue_fallen() test_neue_fallen()
test_kapitel_zwei_und_merkmalwahl() test_kapitel_zwei_und_merkmalwahl()
test_herausforderung_und_bestenliste() test_bestenliste()
test_deckstaerke_und_schwierigkeit() test_deckstaerke_und_schwierigkeit()
test_starter_gegen_passende_gegner() test_starter_gegen_passende_gegner()
test_kurve_statistik_vorschlag_verbesserung() test_kurve_statistik_vorschlag_verbesserung()
@@ -1145,32 +1145,19 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen") check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
func test_herausforderung_und_bestenliste() -> void: func test_bestenliste() -> void:
Profile.today_override = "2026-09-16"
var ch := Challenge.current()
check(ch.has("rules") and ch.n_opp >= 1, "Herausforderung der Woche")
var weeks := {}
for d in range(1, 43, 7):
Profile.today_override = "2026-%02d-%02d" % [1 + d / 28, 1 + d % 28]
weeks[Challenge.current().id] = true
check(weeks.size() >= 4, "Herausforderungen wechseln wöchentlich")
Profile.today_override = "2026-09-16" Profile.today_override = "2026-09-16"
var mono := [] var mono := []
for i in 20: for i in 20:
mono.append(Cards.by_id("w_reiter")) mono.append(Cards.by_id("w_reiter"))
check(Challenge.deck_allowed(Challenge.LIST[5], Profile.starter().deck_cards()) != "", "Reine Lehre lehnt Zwei-Element-Deck ab")
check(Challenge.deck_allowed(Challenge.LIST[5], mono) == "", "Mono-Deck erlaubt")
var r := RandomNumberGenerator.new() var r := RandomNumberGenerator.new()
r.seed = 4 r.seed = 4
var g := Game.new_battle(mono, 3, r, Challenge.LIST[0].rules) var g := Game.new_battle(mono, 3, r, {"everyone_perk": "start_energie", "start_life": 20})
check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Eile: 20 Leben und Startenergie für alle") check(g.players.all(func(p): return p.life == 20 and "start_energie" in p.perks), "Regelvariante: 20 Leben und Startenergie für alle")
check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie") check(g.players[0].max_energy == 3, "Startspieler hat 3 Energie")
var p := Profile.starter() var p := Profile.starter()
var coins := p.coins p.record_battle(true, 3, mono, -1, [], 7)
var reward := p.record_battle(true, 3, mono, -1, [], 7, Challenge.LIST[0]) p.record_battle(true, 3, mono, -1, [], 9)
check(reward == Challenge.LIST[0].reward and p.challenge_week == Challenge.week() and "wochenheld" in p.achievements, "Herausforderung belohnt")
reward = p.record_battle(true, 3, mono, -1, [], 9, Challenge.LIST[0])
check(reward == Profile.REWARD_WIN[3], "nur einmal pro Woche")
check(p.records.streak == 2 and p.records.best_streak == 2 and p.records.fastest_win == 7, "Serie und schnellster Sieg") check(p.records.streak == 2 and p.records.best_streak == 2 and p.records.fastest_win == 7, "Serie und schnellster Sieg")
p.record_battle(false, 1, mono, -1, [], 12) p.record_battle(false, 1, mono, -1, [], 12)
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie") check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
@@ -1211,7 +1198,7 @@ func test_deckstaerke_und_schwierigkeit() -> void:
check(is_equal_approx(p.target_rating(), rs * 1.2) and p.difficulty().smart, "Schwer: 1,2-fach mit Vorausschau-KI") check(is_equal_approx(p.target_rating(), rs * 1.2) and p.difficulty().smart, "Schwer: 1,2-fach mit Vorausschau-KI")
p.settings.difficulty = "leicht" p.settings.difficulty = "leicht"
check(not p.difficulty().smart and is_equal_approx(p.target_rating(), rs * 0.9), "Leicht: einfache KI, 0,9-fach") check(not p.difficulty().smart and is_equal_approx(p.target_rating(), rs * 0.9), "Leicht: einfache KI, 0,9-fach")
var reward := p.record_battle(true, 1, starter, -1, [], 5, {}, p.difficulty().reward) var reward := p.record_battle(true, 1, starter, -1, [], 5, p.difficulty().reward)
check(reward == int(round(Profile.REWARD_WIN[1] * 0.75)), "Leicht: weniger Münzen") check(reward == int(round(Profile.REWARD_WIN[1] * 0.75)), "Leicht: weniger Münzen")
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict()))) var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert") check(v.settings.difficulty == "leicht", "Schwierigkeit gespeichert")
@@ -1268,17 +1255,17 @@ func test_kurve_statistik_vorschlag_verbesserung() -> void:
var mono := starter var mono := starter
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang") check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
for i in 3: for i in 3:
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 4) p.record_battle(true, 1, mono, -1, [], 6, 1.0, 4)
check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag") check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag")
p.records.streak = 0 p.records.streak = 0
p.records.margins = [] p.records.margins = []
for i in 3: for i in 3:
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 20) p.record_battle(true, 1, mono, -1, [], 6, 1.0, 20)
check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei deutliche Siege: Schwer vorschlagen") check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei deutliche Siege: Schwer vorschlagen")
p.records.streak = 0 p.records.streak = 0
p.records.margins = [] p.records.margins = []
for i in 5: for i in 5:
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 2) p.record_battle(true, 1, mono, -1, [], 6, 1.0, 2)
check(p.suggest_difficulty() == "schwer" and p.records.margins.size() == 5, "fünf Siege egal wie knapp") check(p.suggest_difficulty() == "schwer" and p.records.margins.size() == 5, "fünf Siege egal wie knapp")
p.settings.difficulty = "schwer" p.settings.difficulty = "schwer"
p.take_notices() p.take_notices()

View File

@@ -165,7 +165,7 @@ func _ready() -> void:
await frames(3) await frames(3)
var fast := OS.get_environment("DINGER_FAST") != "" var fast := OS.get_environment("DINGER_FAST") != ""
var suite := [test_menu_und_regeln, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen, var suite := [test_menu_und_regeln, test_gamepad_menue, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung, test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2,
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster] test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
if not fast: if not fast:
suite += [test_kampf.bind(2, 3), test_kampf_abbrechen] suite += [test_kampf.bind(2, 3), test_kampf_abbrechen]
@@ -695,26 +695,6 @@ func test_profil_zuruecksetzen() -> void:
check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert") check(Profile.load_or_new(SAVE).coins == Profile.START_COINS, "zurückgesetzt gespeichert")
func test_herausforderung() -> void:
await click(find_button("Herausforderung"))
var scr = screen()
check(scr is ChallengeScreen, "Herausforderung geöffnet")
var ch := Challenge.current()
var allowed := Challenge.deck_allowed(ch, main.profile.deck_cards()) == ""
check(find_button("Antreten").disabled == not allowed, "Antreten je nach Deck")
await shot("28_herausforderung")
if allowed:
await click(find_button("Antreten"))
var b = screen()
check(b is BattleScreen and b.challenge == ch and b.game.opponents.size() == ch.n_opp, "Herausforderungskampf")
var everyone: String = ch.rules.get("everyone_perk", "")
check(everyone == "" or b.game.players.all(func(p): return everyone in p.perks), "Regel gilt für alle")
await give_up()
else:
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")
func test_sprache() -> void: func test_sprache() -> void:
await click(find_button("Einstellungen")) await click(find_button("Einstellungen"))
var scr = screen() var scr = screen()

View File

@@ -56,7 +56,6 @@ Hand tauschen?,Swap cards?
Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game. Klicke Karten an die du zurücklegen willst. Einmal pro Partie.,Click the cards you want to put back. Once per game.
Fallen,Traps Fallen,Traps
Du bist dran,Your turn Du bist dran,Your turn
Herausforderung,Challenge
Antreten,Enter Antreten,Enter
Statistik exportieren,Export statistics Statistik exportieren,Export statistics
Gegnerzüge überspringen,Skip AI turns Gegnerzüge überspringen,Skip AI turns
@@ -209,3 +208,4 @@ Starten,Start
1 Gegner,1 opponent 1 Gegner,1 opponent
%d Gegner,%d opponents %d Gegner,%d opponents
Übernehmen,Apply Übernehmen,Apply
KI-Gegner,AI opponents
1 keys en
56 Klicke Karten an die du zurücklegen willst. Einmal pro Partie. Click the cards you want to put back. Once per game.
57 Fallen Traps
58 Du bist dran Your turn
Herausforderung Challenge
59 Antreten Enter
60 Statistik exportieren Export statistics
61 Gegnerzüge überspringen Skip AI turns
208 1 Gegner 1 opponent
209 %d Gegner %d opponents
210 Übernehmen Apply
211 KI-Gegner AI opponents

Binary file not shown.