Erfolge entfernt, der Bildschirm zeigt nur noch Statistik

- 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
This commit is contained in:
Team3
2026-09-17 21:37:58 +02:00
parent 43073351b0
commit 89db101137
11 changed files with 29 additions and 120 deletions

View File

@@ -28,7 +28,7 @@ Der Spielstand liegt unter `user://spielstand.json`
- 100 Karten in `data/karten.json`: 4 Elemente, 10 Zweier-Kombos (auch gleiche Elemente), 4 mythische Dreifach-Kombos, 4 Fallen
- Resonanz: gleiche Elemente auf dem Feld verstärken sich. Feldkombos: verschiedene Elemente lösen beim Ausspielen Effekte aus
- Fallen liegen verdeckt und lösen im gegnerischen Zug aus (die KI kennt fremde Fallen nicht)
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern, Erfolge, Tagesaufgabe
- Jeder gegen jeden mit ausgeloster Zugreihenfolge, 1 bis 5 KI-Gegner (KI mit Vorausschau), Kampagne mit 10 Magiern je Kapitel, Tagesaufgabe
- Booster, fünf Deck-Slots, Werkstatt (Zerlegen und Herstellen aus Staub), Einstellungen (Ton, KI-Tempo, Animationen)
- Spielstand versioniert mit Sicherungskopie (`spielstand.json.bak`)
- Anleitung (geführte erste Partie), Mulligan im ersten Zug, Tastatur (Leertaste, 1 bis 9, Esc, Enter)
@@ -66,7 +66,7 @@ Der Spielstand liegt unter `user://spielstand.json`
`tests/run_headless.sh` führt Import, statische Prüfung (`tests/lint.gd`) und Headless-Prüfungen aus. `.github/workflows/tests.yml`:
Headless und schneller Fenster-Test bei jedem Push, voller Fenster-Test und Archetypen-Messung nachts um 3 Uhr oder von Hand (Xvfb, lokal nicht geprüft).
`DINGER_FAST=1` lässt den Fenster-Test ohne die langen Kämpfe laufen (etwa ein Drittel der Zeit).
Statistiken aus Spieltests lassen sich unter Erfolge als CSV exportieren.
Statistiken aus Spieltests lassen sich unter Statistik als CSV exportieren.
```
godot --headless --path . --import # einmalig nach dem Klonen: Ressourcen importieren (macht start.sh auch)
@@ -98,9 +98,9 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
- `scripts/deckgen.gd` – Deckstärke, Gegnerdecks entlang der Energiekurve, Archetypen
- `scripts/netstate.gd` – Zustandsbilder fürs Netzwerk
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Erfolge, Tagesaufgabe, Einstellungen
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Tagesaufgabe, Einstellungen
- `scripts/campaign.gd` – Kampagnenstufen
- `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), Statistik, Einstellungen, Regeln, Ton
- `scenes/main.tscn` – Startszene
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
- `translations/ui.csv` – englische Oberflächentexte

View File

@@ -5,7 +5,7 @@
- PC-Spiel
- Jeder gegen jeden: Spieler und 1 bis 5 KI-Gegner (oder Spieler), alle mit gleichen Regeln
- Einzelspieler-Knopf führt zur Auswahl der Gegnerzahl
- Kampagne gegen benannte Magier, Erfolge und Tagesaufgaben als Fortschritt
- Kampagne gegen benannte Magier und Tagesaufgaben als Fortschritt
- Im lokalen Netz zu mehreren spielbar, auch als Zuschauer
- Ein unterbrochener Kampf lässt sich später fortsetzen
- Geführte erste Partie als Einstieg

View File

@@ -1,6 +1,6 @@
class_name Profile
extends RefCounted
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Statistik, Erfolge, Tagesaufgabe, Einstellungen.
## Spielstand: Sammlung, Münzen, Decks, Staub, Booster, Statistik, Tagesaufgabe, Einstellungen.
const SAVE_VERSION := 5
const PITY_BOOSTERS := 20 # danach ist eine mythische Karte garantiert
@@ -24,22 +24,6 @@ const DIFFICULTIES := {
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 ACHIEVEMENTS := [
{"id": "erster_sieg", "name": "Erster Sieg", "text": "Gewinne einen Kampf.", "reward": 50},
{"id": "duellant", "name": "Duellant", "text": "Gewinne ein Duell.", "reward": 50},
{"id": "tischherr", "name": "Herr des Tisches", "text": "Gewinne gegen 3 oder mehr Gegner.", "reward": 100},
{"id": "chaosmeister", "name": "Chaosmeister", "text": "Gewinne gegen 5 Gegner.", "reward": 200},
{"id": "sammler", "name": "Sammler", "text": "Besitze 50 verschiedene Karten.", "reward": 100},
{"id": "vollstaendig", "name": "Vollständig", "text": "Besitze jede Karte.", "reward": 500},
{"id": "boosterfreund", "name": "Boosterfreund", "text": "Öffne 10 Booster.", "reward": 100},
{"id": "elementarist", "name": "Elementarist", "text": "Gewinne mit einem Deck aus nur einem Element.", "reward": 100},
{"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},
]
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},
@@ -63,11 +47,10 @@ 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 achievements: Array = []
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 (Erfolge, Tagesaufgabe)
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
@@ -151,8 +134,6 @@ static func from_dict(d: Dictionary) -> Profile:
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])
for a in d.get("achievements", []):
p.achievements.append(str(a))
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", "")),
@@ -186,7 +167,7 @@ static func from_dict(d: Dictionary) -> Profile:
func to_dict() -> Dictionary:
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
"decks": decks, "active_deck": active_deck,
"stats": stats, "achievements": achievements, "campaign_stage": campaign_stage,
"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}
@@ -320,7 +301,6 @@ func craft(cid: String) -> String:
dust -= cost
collection[cid] = collection.get(cid, 0) + 1
daily_event("craft")
check_achievements()
return ""
@@ -404,11 +384,10 @@ func open_booster(r: RandomNumberGenerator = null, element: String = "") -> Arra
collection[c.id] = collection.get(c.id, 0) + 1
stats.boosters += 1
daily_event("booster")
check_achievements()
return cards
# --- Kampfergebnis, Erfolge, Tagesaufgabe -------------------------------------------
# --- Kampfergebnis, Tagesaufgabe -------------------------------------------
## Verbucht ein Kampfergebnis und gibt die Münzbelohnung zurück.
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
@@ -459,7 +438,6 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
daily_event("win_vs3")
for e in Game.deck_elements(deck_used):
daily_event("win_element:" + e)
check_achievements({"won": won, "n_opp": n_opp, "deck": deck_used})
return reward
@@ -469,53 +447,6 @@ func _stat(cid: String) -> Dictionary:
return card_stats[cid]
func check_achievements(ctx: Dictionary = {}) -> Array:
var unlocked := []
for a in ACHIEVEMENTS:
if a.id in achievements:
continue
if _achieved(a.id, ctx):
achievements.append(a.id)
coins += a.reward
unlocked.append(a)
notices.append("Erfolg: %s (+%d Münzen)" % [a.name, a.reward])
return unlocked
func _achieved(id: String, ctx: Dictionary) -> bool:
var won: bool = ctx.get("won", false)
var n: int = ctx.get("n_opp", 0)
var deck_used: Array = ctx.get("deck", [])
match id:
"erster_sieg":
return stats.wins >= 1
"duellant":
return won and n == 1
"tischherr":
return won and n >= 3
"chaosmeister":
return won and n == 5
"sammler":
return distinct_owned() >= 50
"vollstaendig":
return distinct_owned() >= Cards.all().size()
"boosterfreund":
return stats.boosters >= 10
"elementarist":
return won and Game.deck_elements(deck_used).size() == 1
"kombokoenig":
return won and deck_used.filter(func(c): return c.is_combo()).size() >= 6
"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
return false
func difficulty() -> Dictionary:
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)

View File

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

View File

@@ -46,7 +46,7 @@ func _ready() -> void:
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, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
Style.cbutton(grid, "Erfolge", sz, func(): app.show_screen(AchievementsScreen.new()))
Style.cbutton(grid, "Statistik", sz, func(): app.show_screen(StatsScreen.new()))
Style.cbutton(grid, "Regeln", sz, func(): app.show_screen(RulesScreen.new()))
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())

View File

@@ -1,5 +1,6 @@
class_name AchievementsScreen
class_name StatsScreen
extends Control
## Statistik: Kämpfe, Tagesaufgabe, Bestenliste, beste Karten und CSV-Export.
var app
var export_label: Label
@@ -8,7 +9,7 @@ var export_label: Label
func _ready() -> void:
var p: Profile = app.profile
var col := Style.column(self, 900, 12, 4)
Style.clabel(col, "Erfolge", 32, Style.GOLD, "center", true)
Style.clabel(col, "Statistik", 32, Style.GOLD, "center", true)
var st: Dictionary = p.stats
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.total()], 16, Style.MUTED)
@@ -19,17 +20,6 @@ func _ready() -> void:
Style.clabel(col, "Serie: %d (beste %d) · Schnellster Sieg: %s" % [p.records.streak, p.records.best_streak,
("%d Züge" % p.records.fastest_win) if p.records.fastest_win > 0 else "–"], 15, Style.MUTED)
Style.spacer(col, 4)
for a in Profile.ACHIEVEMENTS:
var done: bool = a.id in p.achievements
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
col.add_child(panel)
var r := Style.row(panel, 12)
r.custom_minimum_size = Vector2(0, 36)
Style.clabel(r, "✓" if done else "·", 22, Style.GREEN if done else Style.MUTED, "center", true, 40)
Style.clabel(r, a.name, 18, Style.TEXT if done else Style.MUTED, "left", true, 250)
Style.clabel(r, a.text, 15, Style.MUTED, "left", false, 380)
Style.clabel(r, "+%d" % a.reward, 16, Style.GOLD, "right", true, 90)
var top := p.top_cards(5)
if not top.is_empty():
Style.spacer(col, 4)
@@ -52,5 +42,5 @@ func export_stats() -> void:
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())

View File

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

View File

@@ -93,7 +93,7 @@ func _initialize() -> void:
test_ki_besser_als_gierig()
test_ki_spiele_enden()
test_spielstand_migration()
test_erfolge_und_tagesaufgabe()
test_tagesaufgabe()
test_kampagne()
test_fallen()
test_ki_sieht_fallen_nicht()
@@ -648,29 +648,25 @@ func test_spielstand_migration() -> void:
var alt := {"coins": 300, "collection": {"f_kobold": 2, "unbekannt": 1}, "deck": ["f_kobold", "f_kobold"]}
var p := Profile.from_dict(alt)
check(p.coins == 300 and p.collection == {"f_kobold": 2} and p.deck.size() == 2, "Version 1 gelesen")
check(p.stats.games == 0 and p.achievements.is_empty() and p.campaign_stage == 0, "Standardwerte ergänzt")
check(p.stats.games == 0 and p.campaign_stage == 0, "Standardwerte ergänzt")
check(p.settings.volume == 0.8 and p.settings.animations, "Einstellungen Standard")
p.stats.wins = 3
p.achievements.append("erster_sieg")
p.campaign_stage = 2
p.settings.volume = 0.3
var d := p.to_dict()
check(d.version == Profile.SAVE_VERSION, "Version gespeichert")
var p2 := Profile.from_dict(JSON.parse_string(JSON.stringify(d)))
check(p2.stats.wins == 3 and p2.achievements == ["erster_sieg"] and p2.campaign_stage == 2, "Version 2 gelesen")
check(p2.stats.wins == 3 and p2.campaign_stage == 2, "Version 2 gelesen")
check(is_equal_approx(p2.settings.volume, 0.3), "Einstellungen gelesen")
func test_erfolge_und_tagesaufgabe() -> void:
func test_tagesaufgabe() -> void:
Profile.today_override = "2026-09-16"
var p := Profile.starter()
var coins := p.coins
var reward := p.record_battle(true, 1, p.deck_cards())
check(reward == Profile.REWARD_WIN[1], "Duell-Belohnung")
check("erster_sieg" in p.achievements and "duellant" in p.achievements, "Erfolge Erster Sieg und Duellant")
check(not ("tischherr" in p.achievements), "Herr des Tisches noch offen")
check(p.coins == coins + reward + 100 + (p.daily_task().reward if p.daily.done else 0), "Münzen inklusive Erfolge")
check(p.notices.size() >= 2, "Meldungen vorhanden")
check(p.coins == coins + reward + (p.daily_task().reward if p.daily.done else 0), "Münzen verbucht")
p.take_notices()
check(p.notices.is_empty(), "Meldungen abgeholt")
check(p.daily.date == "2026-09-16" and p.daily.task != "", "Tagesaufgabe gesetzt")
@@ -690,14 +686,12 @@ func test_erfolge_und_tagesaufgabe() -> void:
p.refresh_daily()
check(p.daily.date == "2026-09-17" and not p.daily.done and p.daily.progress == 0, "neuer Tag, neue Aufgabe")
Profile.today_override = ""
# Mono-Deck-Erfolg
var mono := []
for cid in ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche", "f_glutpfeil", "f_elementar"]:
for i in 3:
mono.append(Cards.by_id(cid))
mono = mono.slice(0, 20)
p.record_battle(true, 3, mono)
check("elementarist" in p.achievements and "tischherr" in p.achievements, "Elementarist und Herr des Tisches")
check(p.stats.wins == 3 and p.stats.wins_by_opp["1"] == 2, "Statistik")
@@ -719,7 +713,7 @@ func test_kampagne() -> void:
check(p.campaign_stage == 1 and reward == Profile.REWARD_LOSS, "Niederlage schaltet nichts frei")
p.campaign_stage = 9
p.record_battle(true, 5, p.deck_cards(), 9)
check(p.campaign_stage == 10 and "kampagnenheld" in p.achievements, "Kampagne abgeschlossen")
check(p.campaign_stage == 10, "Kampagne abgeschlossen")
var sim := Game.new_battle_with(Profile.starter().deck_cards(), Campaign.opponents(0, r), r)
var guard := 0
while sim.winner == "" and guard < 300:
@@ -1140,7 +1134,7 @@ func test_kapitel_zwei_und_merkmalwahl() -> void:
check(g.human.perks == ["feuer_angriff"] and g.log.any(func(l): return l.begins_with("Du: Feuerwesen")), "eigenes Merkmal wirkt und steht im Verlauf")
p.campaign_stage = 19
p.record_battle(true, 5, p.deck_cards(), 19)
check(p.campaign_stage == 20 and "elementmeister" in p.achievements, "Kapitel 2 abgeschlossen")
check(p.campaign_stage == 20, "Kapitel 2 abgeschlossen")
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(p.to_dict())))
check(v.perk == "feuer_angriff" and v.campaign_stage == 20, "Version 5 gelesen")
@@ -1163,7 +1157,6 @@ func test_bestenliste() -> void:
check(p.records.streak == 0 and p.records.best_streak == 2, "Niederlage bricht die Serie")
for i in 5:
p.record_battle(true, 1, mono, -1, [], 10)
check("serie" in p.achievements, "Siegesserie-Erfolg")
var path := "user://test_statistik.csv"
check(p.export_stats(path) == "", "Statistik exportiert")
var text := FileAccess.get_file_as_string(path)

View File

@@ -165,7 +165,7 @@ func _ready() -> void:
await frames(3)
var fast := OS.get_environment("DINGER_FAST") != ""
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_statistik, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2,
test_kampf_fortsetzen, test_gamepad_deckbau, test_deckcode, test_elementbooster]
if not fast:
suite += [test_kampf.bind(2, 3), test_kampf_abbrechen]
@@ -202,13 +202,8 @@ func test_booster() -> void:
var scr = screen()
check(scr is BoosterScreen, "Boosterbildschirm")
var coins: int = main.profile.coins
var ach_before: int = main.profile.achievements.size()
await click(scr.b_buy)
var bonus := 0
for a in Profile.ACHIEVEMENTS:
if a.id in main.profile.achievements.slice(ach_before):
bonus += a.reward
check(main.profile.coins == coins - Profile.BOOSTER_COST + bonus, "Booster bezahlt (plus Erfolgsmünzen %d)" % bonus)
check(main.profile.coins == coins - Profile.BOOSTER_COST, "Booster bezahlt")
check(scr.cards.size() == 5 and scr.revealed.is_empty(), "5 verdeckte Karten")
await shot("03_booster_zu")
var backs: Array = scr.slots.get_children().filter(func(c): return c is BoosterScreen.CardBack)
@@ -573,15 +568,15 @@ func test_einstellungen() -> void:
check(screen() is MenuScreen, "zurück im Menü")
func test_erfolge() -> void:
await click(find_button("Erfolge"))
check(screen() is AchievementsScreen, "Erfolge geöffnet")
func test_statistik() -> void:
await click(find_button("Statistik"))
check(screen() is StatsScreen, "Statistik geöffnet")
var csv := "user://statistik.csv"
if FileAccess.file_exists(csv):
DirAccess.remove_absolute(ProjectSettings.globalize_path(csv))
await click(find_button("Statistik exportieren"))
check(FileAccess.file_exists(csv) and "Gespeichert" in screen().export_label.text, "Statistik exportiert")
await shot("14_erfolge")
await shot("14_statistik")
await click(find_button("Zurück"))
check(screen() is MenuScreen, "zurück im Menü")

View File

@@ -7,7 +7,6 @@ Netzwerkspiel,Network game
Booster öffnen,Open boosters
Deck bauen,Build deck
Werkstatt,Workshop
Erfolge,Achievements
Regeln,Rules
Einstellungen,Settings
Anleitung,Tutorial
@@ -209,3 +208,4 @@ Starten,Start
%d Gegner,%d opponents
Übernehmen,Apply
KI-Gegner,AI opponents
Statistik,Statistics
1 keys en
7 Booster öffnen Open boosters
8 Deck bauen Build deck
9 Werkstatt Workshop
Erfolge Achievements
10 Regeln Rules
11 Einstellungen Settings
12 Anleitung Tutorial
208 %d Gegner %d opponents
209 Übernehmen Apply
210 KI-Gegner AI opponents
211 Statistik Statistics

Binary file not shown.