Anleitung 2, Gamepad, große Schrift, Profil-Reset, Würfel-Limit, Tipp nach Spielverlauf, KI-Zeitbudget, Deck-Warnung, Gischtläufer, Kampfbildschirm aufgeteilt
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
@@ -34,7 +34,10 @@ Der Spielstand liegt unter `user://spielstand.json`
|
||||
- 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
|
||||
- Gegnervorschau vor dem Kampf (Element und Stärke, neu würfelbar), Vorschlag zur Schwierigkeit nach drei Siegen oder Niederlagen
|
||||
- Deckstärke mit Kurvenabzug und Gewichtung durch die eigene Kartenstatistik; Booster schlagen passende Karten fürs Deck vor
|
||||
- Resonanz gedeckelt (+1, +2, +3, +3), günstige Wasser-Antworten (Ebbe, Tiefenstoß, Strudel), Wasser-Wesen angehoben
|
||||
- Resonanz gedeckelt (+1, +2, +3, +3), günstige Wasser-Antworten (Ebbe, Tiefenstoß, Strudel), Wasser-Wesen angehoben, Gischtläufer für Sturm
|
||||
- Anleitung Teil 2 (Mulligan, Fallen, Feldkombo Dampf), Gamepad-Steuerung, große Schrift, Profil zurücksetzen
|
||||
- Gegnervorschau mit höchstens drei Neuwürfen, Schwierigkeitstipp nach Spielverlauf (deutliche Siege oder drei Niederlagen)
|
||||
- KI mit Zeitbudget je Entscheidung (150 ms), Warnung bei ungespeichertem Deck
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -70,7 +73,7 @@ godot --headless --path . --export-release Windows build/dinger-windows.exe
|
||||
- `scripts/ai.gd` – KI mit Vorausschau (plus gierige Vergleichs-KI)
|
||||
- `scripts/profile.gd` – Spielstand (versioniert), Booster, Erfolge, Tagesaufgabe, Einstellungen
|
||||
- `scripts/campaign.gd` – Kampagnenstufen
|
||||
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte), Erfolge, Einstellungen, Regeln, Ton
|
||||
- `scripts/ui/` – Oberfläche: Menü, Gegnerauswahl, Kampagne, Hotseat, Lobby, Booster, Deckbau, Werkstatt, Kampf (Bildschirm, Layout, Effekte, Anleitung, Mulligan, Gamepad), Erfolge, Einstellungen, Regeln, Ton
|
||||
- `scenes/main.tscn` – Startszene
|
||||
- `scripts/net.gd` – Netzwerkspiel (ENet) mit Wiederverbinden
|
||||
- `scripts/challenge.gd` – wöchentliche Herausforderungen
|
||||
|
||||
@@ -936,6 +936,29 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "wl_gischt",
|
||||
"name": "Gischtläufer",
|
||||
"elements": [
|
||||
"Wasser",
|
||||
"Luft"
|
||||
],
|
||||
"cost": 2,
|
||||
"rarity": "selten",
|
||||
"kind": "Wesen",
|
||||
"attack": 2,
|
||||
"health": 2,
|
||||
"keywords": [
|
||||
"Flink"
|
||||
],
|
||||
"effects": [
|
||||
{
|
||||
"kind": "draw",
|
||||
"target": "none",
|
||||
"amount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "wl_koenigin",
|
||||
"name": "Sturmkönigin",
|
||||
|
||||
@@ -95,6 +95,7 @@ static func map_target(g2: Game, game: Game, t):
|
||||
|
||||
|
||||
const FOLLOW_UPS := 10 # Folgeschritte je Kandidat in der zweiten Suchstufe
|
||||
static var time_budget_ms := 150 # Zeitbudget je Entscheidung; danach der beste bisherige Zug
|
||||
|
||||
|
||||
## Mulligan der KI: teure Karten im ersten Zug zurücklegen.
|
||||
@@ -111,7 +112,12 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
return {"type": "mulligan", "cards": mulligan_cards(p)}
|
||||
var best := {"type": "end"}
|
||||
var best_score := evaluate(game, p) + 0.01
|
||||
var t0 := Time.get_ticks_msec()
|
||||
var examined := 0
|
||||
for action in candidate_actions(game, p):
|
||||
if examined > 0 and Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||
break
|
||||
examined += 1
|
||||
var g2 := game.clone_for(p)
|
||||
apply_on_clone(g2, game, action)
|
||||
var p2: Game.Player = g2.players[game.current_idx]
|
||||
@@ -119,7 +125,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
if g2.winner == "":
|
||||
var count := 0
|
||||
for f in candidate_actions(g2, p2):
|
||||
if count >= FOLLOW_UPS:
|
||||
if count >= FOLLOW_UPS or Time.get_ticks_msec() - t0 > time_budget_ms:
|
||||
break
|
||||
count += 1
|
||||
var g3 := g2.clone_for(p2)
|
||||
|
||||
@@ -7,9 +7,9 @@ const RESONANCE_STEPS := [0, 1, 2, 3, 3]
|
||||
|
||||
# Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, tests/strength_probe.gd).
|
||||
const ARCHETYPE_STRENGTH := {
|
||||
"Feuer": 36, "Wasser": 30, "Erde": 63, "Luft": 50,
|
||||
"Feuer+Wasser": 46, "Feuer+Erde": 70, "Feuer+Luft": 40,
|
||||
"Wasser+Erde": 46, "Wasser+Luft": 20, "Erde+Luft": 46,
|
||||
"Feuer": 40, "Wasser": 56, "Erde": 46, "Luft": 63,
|
||||
"Feuer+Wasser": 40, "Feuer+Erde": 83, "Feuer+Luft": 40,
|
||||
"Wasser+Erde": 53, "Wasser+Luft": 23, "Erde+Luft": 60,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -64,13 +64,14 @@ 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"}
|
||||
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 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 records := {"streak": 0, "best_streak": 0, "fastest_win": 0, "loss_streak": 0}
|
||||
var tutorial2_done := false
|
||||
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).
|
||||
@@ -158,7 +159,8 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
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"))}
|
||||
"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:
|
||||
@@ -174,8 +176,10 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
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))}
|
||||
"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.challenge_week = int(d.get("challenge_week", -1))
|
||||
p.tutorial2_done = bool(d.get("tutorial2_done", false))
|
||||
return p
|
||||
|
||||
|
||||
@@ -185,7 +189,7 @@ func to_dict() -> Dictionary:
|
||||
"stats": stats, "achievements": achievements, "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, "challenge_week": challenge_week}
|
||||
"perk": perk, "records": records, "challenge_week": challenge_week, "tutorial2_done": tutorial2_done}
|
||||
|
||||
|
||||
## Speichert und sichert den vorherigen Stand als .bak.
|
||||
@@ -376,8 +380,11 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
## turns: Dauer der Partie für die Bestenliste. challenge: Herausforderung dieser Woche.
|
||||
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) -> int:
|
||||
turns: int = 0, challenge: Dictionary = {}, 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
|
||||
@@ -483,12 +490,18 @@ func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
## Vorschlag: nach drei Siegen in Folge eine Stufe rauf, nach drei Niederlagen runter. "" = kein Vorschlag.
|
||||
## 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 records.streak >= 3 and i < order.size() - 1:
|
||||
return order[i + 1]
|
||||
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 ""
|
||||
|
||||
139
scripts/ui/battle_gamepad.gd
Normal file
139
scripts/ui/battle_gamepad.gd
Normal file
@@ -0,0 +1,139 @@
|
||||
class_name BattleGamepad
|
||||
extends RefCounted
|
||||
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Hand, eigene Wesen und Ziele,
|
||||
## A bestätigt, B bricht ab, Start beendet den Zug, Y überspringt Gegnerzüge, X macht rückgängig.
|
||||
|
||||
var screen # BattleScreen
|
||||
var active := false
|
||||
var zone := "hand"
|
||||
var index := 0
|
||||
var targets: Array = []
|
||||
|
||||
|
||||
func _init(s) -> void:
|
||||
screen = s
|
||||
|
||||
|
||||
func button(button: int) -> void:
|
||||
active = true
|
||||
if screen.game == null:
|
||||
return
|
||||
match button:
|
||||
JOY_BUTTON_START:
|
||||
if screen.my_turn() and not screen.any_overlay():
|
||||
screen.end_turn()
|
||||
JOY_BUTTON_B:
|
||||
if screen.confirm.visible:
|
||||
screen.confirm.visible = false
|
||||
screen.cancel_all()
|
||||
zone = "hand"
|
||||
JOY_BUTTON_Y:
|
||||
if screen.b_skip.visible:
|
||||
screen.skip_ai = true
|
||||
JOY_BUTTON_X:
|
||||
if screen.b_undo.visible:
|
||||
screen.undo()
|
||||
JOY_BUTTON_A:
|
||||
confirm()
|
||||
JOY_BUTTON_DPAD_LEFT:
|
||||
move(-1)
|
||||
JOY_BUTTON_DPAD_RIGHT:
|
||||
move(1)
|
||||
JOY_BUTTON_DPAD_UP, JOY_BUTTON_DPAD_DOWN:
|
||||
if zone != "target":
|
||||
zone = "own" if zone == "hand" else "hand"
|
||||
index = 0
|
||||
screen.refresh()
|
||||
|
||||
|
||||
func items() -> Array:
|
||||
match zone:
|
||||
"hand":
|
||||
return screen.hand_views
|
||||
"own":
|
||||
return screen.own_views.filter(func(v): return v.creature.can_attack())
|
||||
"target":
|
||||
return targets
|
||||
return []
|
||||
|
||||
|
||||
func move(dir: int) -> void:
|
||||
var n := items().size()
|
||||
if n > 0:
|
||||
index = posmod(index + dir, n)
|
||||
|
||||
|
||||
func confirm() -> void:
|
||||
if screen.handover.visible:
|
||||
screen.take_over()
|
||||
return
|
||||
if screen.mulligan.visible:
|
||||
screen.do_mulligan([])
|
||||
return
|
||||
if screen.overlay.visible:
|
||||
screen.leave()
|
||||
return
|
||||
if screen.confirm.visible:
|
||||
return
|
||||
var items := items()
|
||||
if items.is_empty():
|
||||
return
|
||||
index = clampi(index, 0, items.size() - 1)
|
||||
var item = items[index]
|
||||
match zone:
|
||||
"hand":
|
||||
screen.on_click("hand", item.card)
|
||||
if screen.pending != null:
|
||||
targets = []
|
||||
var valid: Array = screen.game.valid_targets(screen.perspective, screen.pending.target_type())
|
||||
for v in screen.enemy_views:
|
||||
if v.creature in valid:
|
||||
targets.append(v)
|
||||
for v in screen.own_views:
|
||||
if v.creature in valid:
|
||||
targets.append(v)
|
||||
for o in screen.others():
|
||||
if o in valid:
|
||||
targets.append(screen.hero_panels[o])
|
||||
zone = "target"
|
||||
index = 0
|
||||
"own":
|
||||
screen.on_click("own", item.creature)
|
||||
if screen.selected != null:
|
||||
targets = []
|
||||
for v in screen.enemy_views:
|
||||
if screen.game.can_attack(screen.selected, v.creature)[0]:
|
||||
targets.append(v)
|
||||
for o in screen.others():
|
||||
if o.alive() and screen.game.can_attack(screen.selected, o)[0]:
|
||||
targets.append(screen.hero_panels[o])
|
||||
zone = "target"
|
||||
index = 0
|
||||
"target":
|
||||
if item is CreatureView:
|
||||
screen.on_tile_click(item.creature)
|
||||
else:
|
||||
for o in screen.hero_panels:
|
||||
if screen.hero_panels[o] == item:
|
||||
screen.on_click("hero", o)
|
||||
zone = "hand"
|
||||
index = 0
|
||||
|
||||
|
||||
func draw_cursor() -> void:
|
||||
Style.clear(screen.cursor_layer)
|
||||
if not active or screen.game == null or not screen.my_turn():
|
||||
return
|
||||
var items := items()
|
||||
if items.is_empty():
|
||||
return
|
||||
index = clampi(index, 0, items.size() - 1)
|
||||
var item = items[index]
|
||||
var r: Rect2 = Rect2(item.slot, item.size) if item is CreatureView else Rect2(item.position, item.size)
|
||||
var hl := BattleScreen.Highlight.new()
|
||||
hl.position = r.position - Vector2(6, 6)
|
||||
hl.size = r.size + Vector2(12, 12)
|
||||
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
screen.cursor_layer.add_child(hl)
|
||||
|
||||
|
||||
1
scripts/ui/battle_gamepad.gd.uid
Normal file
1
scripts/ui/battle_gamepad.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8wth8k7f7uvk
|
||||
63
scripts/ui/battle_mulligan.gd
Normal file
63
scripts/ui/battle_mulligan.gd
Normal file
@@ -0,0 +1,63 @@
|
||||
class_name BattleMulligan
|
||||
extends Control
|
||||
## Mulligan-Maske: Karten anklicken, behalten oder tauschen.
|
||||
|
||||
signal decided(indexes: Array)
|
||||
|
||||
var layer: Control
|
||||
var b_keep: Button
|
||||
var b_swap: Button
|
||||
var selected: Array = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
visible = false
|
||||
var dark := ColorRect.new()
|
||||
dark.color = Color(0, 0, 0, 0.67)
|
||||
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dark.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(dark)
|
||||
Style.panel(self, Rect2(140, 250, 1000, 320), Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(self, "Hand tauschen?", 26, Style.TEXT, Rect2(140, 262, 1000, 40), "center", true)
|
||||
Style.label(self, "Klicke Karten an die du zurücklegen willst. Einmal pro Partie.", 15, Style.MUTED, Rect2(140, 300, 1000, 24), "center")
|
||||
layer = Control.new()
|
||||
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(layer)
|
||||
b_keep = Style.button(self, "Behalten", Rect2(430, 515, 200, 44), func(): finish(false), Color("285a3c"))
|
||||
b_swap = Style.button(self, "Tauschen", Rect2(650, 515, 200, 44), func(): finish(true))
|
||||
|
||||
|
||||
func open(hand: Array) -> void:
|
||||
selected = []
|
||||
visible = true
|
||||
Style.clear(layer)
|
||||
for i in hand.size():
|
||||
var r := BattleLayout.hand_rect(hand.size(), i)
|
||||
var v := CardView.new(hand[i], r.size)
|
||||
v.position = Vector2(r.position.x, 322)
|
||||
var idx := i
|
||||
v.clicked.connect(func(_view): toggle(idx))
|
||||
layer.add_child(v)
|
||||
_borders()
|
||||
|
||||
|
||||
func toggle(idx: int) -> void:
|
||||
if idx in selected:
|
||||
selected.erase(idx)
|
||||
else:
|
||||
selected.append(idx)
|
||||
_borders()
|
||||
|
||||
|
||||
func _borders() -> void:
|
||||
var views := layer.get_children()
|
||||
for i in views.size():
|
||||
views[i].border = Style.YELLOW if i in selected else null
|
||||
views[i].queue_redraw()
|
||||
b_swap.text = ("Tauschen (%d)" % selected.size()) if not selected.is_empty() else "Tauschen"
|
||||
|
||||
|
||||
func finish(swap: bool) -> void:
|
||||
visible = false
|
||||
decided.emit(selected.duplicate() if swap else [])
|
||||
1
scripts/ui/battle_mulligan.gd.uid
Normal file
1
scripts/ui/battle_mulligan.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://gwdm38e83cbj
|
||||
@@ -24,8 +24,18 @@ var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||
var mulligan_selected: Array = []
|
||||
var tutorial_step := 0
|
||||
var guide: BattleTutorial
|
||||
var tutorial_part := 1
|
||||
var tutorial_step: int:
|
||||
get:
|
||||
return guide.step if guide != null else 0
|
||||
var pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
get:
|
||||
return pad.zone
|
||||
var gp_index: int:
|
||||
get:
|
||||
return pad.index
|
||||
var challenge := {} # wöchentliche Herausforderung oder leer
|
||||
var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
@@ -69,10 +79,23 @@ var notice_label: Label
|
||||
var confirm: Control
|
||||
var handover: Control
|
||||
var handover_label: Label
|
||||
var mulligan_box: Control
|
||||
var mulligan_layer: Control
|
||||
var b_keep: Button
|
||||
var b_swap: Button
|
||||
var mulligan: BattleMulligan
|
||||
var mulligan_box: Control:
|
||||
get:
|
||||
return mulligan
|
||||
var mulligan_layer: Control:
|
||||
get:
|
||||
return mulligan.layer
|
||||
var b_keep: Button:
|
||||
get:
|
||||
return mulligan.b_keep
|
||||
var b_swap: Button:
|
||||
get:
|
||||
return mulligan.b_swap
|
||||
var mulligan_selected: Array:
|
||||
get:
|
||||
return mulligan.selected
|
||||
var cursor_layer: Control
|
||||
var tutorial_panel: Panel
|
||||
var tutorial_text: RichTextLabel
|
||||
var b_tut_next: Button
|
||||
@@ -94,17 +117,6 @@ class Highlight extends Control:
|
||||
var a := 0.55 + 0.45 * sin(t * 6.0)
|
||||
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
|
||||
|
||||
const TUTORIAL_STEPS := [
|
||||
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
||||
"played_creature"],
|
||||
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
|
||||
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
|
||||
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig +1 Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
|
||||
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an alle gegnerischen Wesen.", "combo"],
|
||||
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
|
||||
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
|
||||
]
|
||||
|
||||
|
||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||
n_opp = n
|
||||
@@ -124,9 +136,10 @@ static func online(seat: int) -> BattleScreen:
|
||||
return b
|
||||
|
||||
|
||||
static func guided() -> BattleScreen:
|
||||
static func guided(part: int = 1) -> BattleScreen:
|
||||
var b := BattleScreen.new(1, -1)
|
||||
b.tutorial = true
|
||||
b.tutorial_part = part
|
||||
return b
|
||||
|
||||
|
||||
@@ -162,6 +175,7 @@ func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
fx = BattleFx.new(self)
|
||||
fx.enabled = s.animations
|
||||
pad = BattleGamepad.new(self)
|
||||
ai_delay = 0.75 / float(s.ai_speed)
|
||||
_build_static()
|
||||
if is_online():
|
||||
@@ -177,10 +191,12 @@ func _ready() -> void:
|
||||
elif not hotseat_decks.is_empty():
|
||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
||||
elif tutorial:
|
||||
guide = BattleTutorial.new(self, tutorial_part)
|
||||
var r := Game.make_rng()
|
||||
var opps := Campaign.opponents(0, r, Game.deck_rating(app.profile.deck_cards()))
|
||||
var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards()))
|
||||
opps[0].perks = []
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
||||
guide.inject_cards(game.human.hand)
|
||||
elif not challenge.is_empty():
|
||||
var rules: Dictionary = challenge.rules.duplicate()
|
||||
rules["target_rating"] = Game.deck_rating(app.profile.deck_cards())
|
||||
@@ -273,16 +289,11 @@ func _build_static() -> void:
|
||||
highlight_layer = _layer()
|
||||
zoom_layer = _layer()
|
||||
fx_layer = _layer()
|
||||
cursor_layer = _layer()
|
||||
# Mulligan
|
||||
mulligan_box = _overlay()
|
||||
Style.panel(mulligan_box, Rect2(140, 250, 1000, 320), Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(mulligan_box, "Hand tauschen?", 26, Style.TEXT, Rect2(140, 262, 1000, 40), "center", true)
|
||||
Style.label(mulligan_box, "Klicke Karten an die du zurücklegen willst. Einmal pro Partie.", 15, Style.MUTED, Rect2(140, 300, 1000, 24), "center")
|
||||
mulligan_layer = Control.new()
|
||||
mulligan_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mulligan_box.add_child(mulligan_layer)
|
||||
b_keep = Style.button(mulligan_box, "Behalten", Rect2(430, 515, 200, 44), func(): do_mulligan(false), Color("285a3c"))
|
||||
b_swap = Style.button(mulligan_box, "Tauschen", Rect2(650, 515, 200, 44), func(): do_mulligan(true))
|
||||
mulligan = BattleMulligan.new()
|
||||
mulligan.decided.connect(do_mulligan)
|
||||
add_child(mulligan)
|
||||
# Spielende
|
||||
overlay = _overlay()
|
||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||
@@ -327,8 +338,11 @@ func _overlay() -> Control:
|
||||
func leave() -> void:
|
||||
if is_online():
|
||||
net.leave()
|
||||
if tutorial and not app.profile.tutorial_done:
|
||||
app.profile.tutorial_done = true
|
||||
if tutorial:
|
||||
if tutorial_part == 1:
|
||||
app.profile.tutorial_done = true
|
||||
else:
|
||||
app.profile.tutorial2_done = true
|
||||
app.save()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
@@ -415,7 +429,7 @@ func my_turn() -> bool:
|
||||
|
||||
|
||||
func any_overlay() -> bool:
|
||||
return confirm.visible or handover.visible or mulligan_box.visible or overlay.visible
|
||||
return confirm.visible or handover.visible or mulligan.visible or overlay.visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -424,6 +438,9 @@ func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
cancel_all()
|
||||
return
|
||||
if event is InputEventJoypadButton and event.pressed:
|
||||
pad.button(event.button_index)
|
||||
return
|
||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||
return
|
||||
match event.keycode:
|
||||
@@ -445,8 +462,8 @@ func _input(event: InputEvent) -> void:
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
if handover.visible:
|
||||
take_over()
|
||||
elif mulligan_box.visible:
|
||||
do_mulligan(false)
|
||||
elif mulligan.visible:
|
||||
do_mulligan([])
|
||||
elif overlay.visible:
|
||||
leave()
|
||||
elif tutorial and b_tut_next.visible:
|
||||
@@ -494,7 +511,7 @@ func _process(delta: float) -> void:
|
||||
handover.visible = true
|
||||
hand_layer.visible = false
|
||||
trap_layer.visible = false
|
||||
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan_box.visible and not handover.visible:
|
||||
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible and not handover.visible:
|
||||
show_mulligan()
|
||||
return
|
||||
if is_client():
|
||||
@@ -517,9 +534,12 @@ func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
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
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor)
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor, game.human.life)
|
||||
if tutorial:
|
||||
app.profile.tutorial_done = true
|
||||
if tutorial_part == 1:
|
||||
app.profile.tutorial_done = true
|
||||
else:
|
||||
app.profile.tutorial2_done = true
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
else:
|
||||
@@ -610,39 +630,11 @@ func end_turn() -> void:
|
||||
|
||||
|
||||
func show_mulligan() -> void:
|
||||
mulligan_selected = []
|
||||
mulligan_box.visible = true
|
||||
Style.clear(mulligan_layer)
|
||||
var hand: Array = perspective.hand
|
||||
for i in hand.size():
|
||||
var r := BattleLayout.hand_rect(hand.size(), i)
|
||||
var v := CardView.new(hand[i], r.size)
|
||||
v.position = Vector2(r.position.x, 322)
|
||||
var idx := i
|
||||
v.clicked.connect(func(_view): toggle_mulligan(idx))
|
||||
mulligan_layer.add_child(v)
|
||||
_mulligan_borders()
|
||||
mulligan.open(perspective.hand)
|
||||
|
||||
|
||||
func toggle_mulligan(idx: int) -> void:
|
||||
if idx in mulligan_selected:
|
||||
mulligan_selected.erase(idx)
|
||||
else:
|
||||
mulligan_selected.append(idx)
|
||||
_mulligan_borders()
|
||||
|
||||
|
||||
func _mulligan_borders() -> void:
|
||||
var views := mulligan_layer.get_children()
|
||||
for i in views.size():
|
||||
views[i].border = Style.YELLOW if i in mulligan_selected else null
|
||||
views[i].queue_redraw()
|
||||
b_swap.text = "Tauschen (%d)" % mulligan_selected.size() if not mulligan_selected.is_empty() else "Tauschen"
|
||||
|
||||
|
||||
func do_mulligan(swap: bool) -> void:
|
||||
mulligan_box.visible = false
|
||||
var idxs: Array = mulligan_selected.duplicate() if swap else []
|
||||
func do_mulligan(idxs: Array) -> void:
|
||||
mulligan.visible = false
|
||||
if is_client():
|
||||
net.send_action({"type": "mulligan", "hand": idxs})
|
||||
return
|
||||
@@ -826,69 +818,21 @@ func _note_events(evs: Array) -> void:
|
||||
|
||||
# --- Anleitung ------------------------------------------------------------------------------
|
||||
|
||||
func tutorial_condition(kind: String) -> bool:
|
||||
var h := game.human
|
||||
match kind:
|
||||
"played_creature":
|
||||
return game.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
||||
"turn_passed":
|
||||
return h.turns >= 2
|
||||
"attacked":
|
||||
return game.log.any(func(l): return l.ends_with("greift Fenn an.") or (" greift " in l and l.begins_with(h.board[0].name) if not h.board.is_empty() else false))
|
||||
"resonance":
|
||||
return h.board.any(func(c): return c.resonance > 0)
|
||||
"combo":
|
||||
return game.log.any(func(l): return l.begins_with("Feldkombo"))
|
||||
"spell":
|
||||
return game.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
||||
return false
|
||||
|
||||
|
||||
func tutorial_next() -> void:
|
||||
if tutorial_step < TUTORIAL_STEPS.size() - 1:
|
||||
tutorial_step += 1
|
||||
if guide != null and guide.next():
|
||||
app.sfx.play("click")
|
||||
refresh()
|
||||
|
||||
|
||||
## Hebt hervor, was der aktuelle Schritt verlangt.
|
||||
func _tutorial_refresh() -> void:
|
||||
guide.advance()
|
||||
tutorial_text.text = guide.text()
|
||||
b_tut_next.visible = not guide.finished()
|
||||
|
||||
|
||||
func _tutorial_highlights(mine: bool) -> void:
|
||||
Style.clear(highlight_layer)
|
||||
if not mine or any_overlay():
|
||||
return
|
||||
var g := game
|
||||
var h := perspective
|
||||
var rects: Array[Rect2] = []
|
||||
match TUTORIAL_STEPS[tutorial_step][1]:
|
||||
"played_creature":
|
||||
for v in hand_views:
|
||||
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"turn_passed":
|
||||
rects.append(b_end.get_rect())
|
||||
"attacked":
|
||||
for v in own_views:
|
||||
if v.creature.can_attack():
|
||||
rects.append(Rect2(v.slot, v.size))
|
||||
if not own_views.is_empty() and own_views.any(func(v): return v.creature.can_attack()):
|
||||
for o in others():
|
||||
if o.alive() and hero_panels.has(o):
|
||||
rects.append(hero_panels[o].get_rect())
|
||||
"resonance":
|
||||
for v in hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"combo":
|
||||
for v in hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"spell":
|
||||
for v in hand_views:
|
||||
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
for r in rects:
|
||||
for r in guide.highlight_rects(mine):
|
||||
var hl := Highlight.new()
|
||||
hl.position = r.position - Vector2(4, 4)
|
||||
hl.size = r.size + Vector2(8, 8)
|
||||
@@ -896,13 +840,6 @@ func _tutorial_highlights(mine: bool) -> void:
|
||||
highlight_layer.add_child(hl)
|
||||
|
||||
|
||||
func _tutorial_refresh() -> void:
|
||||
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
|
||||
tutorial_step += 1
|
||||
tutorial_text.text = "%d/%d %s" % [tutorial_step + 1, TUTORIAL_STEPS.size(), TUTORIAL_STEPS[tutorial_step][0]]
|
||||
b_tut_next.visible = tutorial_step < TUTORIAL_STEPS.size() - 1
|
||||
|
||||
|
||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||
|
||||
func refresh() -> void:
|
||||
@@ -1053,6 +990,7 @@ func refresh() -> void:
|
||||
if tutorial:
|
||||
_tutorial_refresh()
|
||||
_tutorial_highlights(mine)
|
||||
pad.draw_cursor()
|
||||
|
||||
Style.clear(zoom_layer)
|
||||
if zoom_player != null and zoom_player.alive():
|
||||
@@ -1076,7 +1014,7 @@ func refresh() -> void:
|
||||
overlay.visible = true
|
||||
confirm.visible = false
|
||||
handover.visible = false
|
||||
mulligan_box.visible = false
|
||||
mulligan.visible = false
|
||||
|
||||
|
||||
func build_zoom(valid: Array) -> void:
|
||||
|
||||
144
scripts/ui/battle_tutorial.gd
Normal file
144
scripts/ui/battle_tutorial.gd
Normal file
@@ -0,0 +1,144 @@
|
||||
class_name BattleTutorial
|
||||
extends RefCounted
|
||||
## Geführte Partien: Schritte, Bedingungen und Hervorhebungen (ausgelagert aus dem Kampfbildschirm).
|
||||
|
||||
const STEPS1 := [
|
||||
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
||||
"played_creature"],
|
||||
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
|
||||
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
|
||||
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
|
||||
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an alle gegnerischen Wesen.", "combo"],
|
||||
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
|
||||
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
|
||||
]
|
||||
const STEPS2 := [
|
||||
["Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.", "mulligan"],
|
||||
["Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.", "trap_set"],
|
||||
["Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.", "trap_fired"],
|
||||
["Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.", "combo_dampf"],
|
||||
["Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!", ""],
|
||||
]
|
||||
|
||||
var screen # BattleScreen
|
||||
var part := 1
|
||||
var step := 0
|
||||
|
||||
|
||||
func _init(s, p: int = 1) -> void:
|
||||
screen = s
|
||||
part = p
|
||||
|
||||
|
||||
func steps() -> Array:
|
||||
return STEPS1 if part == 1 else STEPS2
|
||||
|
||||
|
||||
func stage_index() -> int:
|
||||
return 0 if part == 1 else 1
|
||||
|
||||
|
||||
## Teil 2 bekommt die Lehrkarten auf die Hand.
|
||||
func inject_cards(hand: Array) -> void:
|
||||
if part == 2:
|
||||
hand.append(Cards.by_id("w_falle"))
|
||||
hand.append(Cards.by_id("w_reiter"))
|
||||
|
||||
|
||||
func condition(kind: String) -> bool:
|
||||
var g: Game = screen.game
|
||||
var h: Game.Player = g.human
|
||||
match kind:
|
||||
"played_creature":
|
||||
return g.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
||||
"turn_passed":
|
||||
return h.turns >= 2
|
||||
"attacked":
|
||||
return g.log.any(func(l): return l.begins_with(h.board[0].name + " greift") if not h.board.is_empty() else false) \
|
||||
or g.log.any(func(l): return l.ends_with("greift Fenn an."))
|
||||
"resonance":
|
||||
return h.board.any(func(c): return c.resonance > 0)
|
||||
"combo":
|
||||
return g.log.any(func(l): return l.begins_with("Feldkombo"))
|
||||
"spell":
|
||||
return g.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
||||
"mulligan":
|
||||
return h.mulligan_done
|
||||
"trap_set":
|
||||
return g.log.any(func(l): return l == "Du legt eine Falle.")
|
||||
"trap_fired":
|
||||
return g.log.any(func(l): return l.begins_with("Falle von Du"))
|
||||
"combo_dampf":
|
||||
return g.log.any(func(l): return l.begins_with("Feldkombo Dampf"))
|
||||
return false
|
||||
|
||||
|
||||
func advance() -> void:
|
||||
while step < steps().size() - 1 and condition(steps()[step][1]):
|
||||
step += 1
|
||||
|
||||
|
||||
func next() -> bool:
|
||||
if step < steps().size() - 1:
|
||||
step += 1
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func text() -> String:
|
||||
return "%d/%d %s" % [step + 1, steps().size(), steps()[step][0]]
|
||||
|
||||
|
||||
func finished() -> bool:
|
||||
return step >= steps().size() - 1
|
||||
|
||||
|
||||
## Rechtecke, die der aktuelle Schritt hervorhebt.
|
||||
func highlight_rects(mine: bool) -> Array[Rect2]:
|
||||
var rects: Array[Rect2] = []
|
||||
if not mine or screen.any_overlay():
|
||||
return rects
|
||||
var g: Game = screen.game
|
||||
var h: Game.Player = screen.perspective
|
||||
match steps()[step][1]:
|
||||
"played_creature":
|
||||
for v in screen.hand_views:
|
||||
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"turn_passed", "trap_fired":
|
||||
rects.append(screen.b_end.get_rect())
|
||||
"attacked":
|
||||
for v in screen.own_views:
|
||||
if v.creature.can_attack():
|
||||
rects.append(Rect2(v.slot, v.size))
|
||||
if screen.own_views.any(func(v): return v.creature.can_attack()):
|
||||
for o in screen.others():
|
||||
if o.alive() and screen.hero_panels.has(o):
|
||||
rects.append(screen.hero_panels[o].get_rect())
|
||||
"resonance":
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"combo":
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"spell":
|
||||
for v in screen.hand_views:
|
||||
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"trap_set":
|
||||
for v in screen.hand_views:
|
||||
if v.card.is_trap() and g.can_play(h, v.card)[0]:
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
"combo_dampf":
|
||||
var has_fire: bool = h.board.any(func(b): return "Feuer" in b.card.elements)
|
||||
for v in screen.hand_views:
|
||||
var c: Cards.Card = v.card
|
||||
if not c.is_creature() or not g.can_play(h, c)[0]:
|
||||
continue
|
||||
if (has_fire and "Wasser" in c.elements) or (not has_fire and "Feuer" in c.elements):
|
||||
rects.append(Rect2(v.position, v.size))
|
||||
return rects
|
||||
1
scripts/ui/battle_tutorial.gd.uid
Normal file
1
scripts/ui/battle_tutorial.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cg78afyod2873
|
||||
@@ -27,6 +27,7 @@ var slot_buttons: Array = []
|
||||
var b_activate: Button
|
||||
var b_autofill: Button
|
||||
var name_box: LineEdit
|
||||
var confirm: Control
|
||||
|
||||
|
||||
class DeckRow extends Control:
|
||||
@@ -151,7 +152,21 @@ func _ready() -> void:
|
||||
msg_label = Style.label(self, "", 16, Style.GREEN, Rect2(860, 736, 400, 24))
|
||||
Style.button(self, "Speichern", Rect2(860, 740, 180, 44), save_deck, Color("285a3c"))
|
||||
Style.button(self, "Leeren", Rect2(1050, 740, 90, 44), clear_deck)
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), leave)
|
||||
confirm = Control.new()
|
||||
confirm.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
confirm.visible = false
|
||||
var dark := ColorRect.new()
|
||||
dark.color = Color(0, 0, 0, 0.67)
|
||||
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dark.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
confirm.add_child(dark)
|
||||
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(confirm, "Ungespeicherte Änderungen", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
||||
Style.label(confirm, "Das Deck wurde geändert, aber nicht gespeichert.", 16, Style.MUTED, Rect2(340, 360, 600, 24), "center")
|
||||
Style.button(confirm, "Verwerfen", Rect2(380, 420, 240, 46), func(): app.show_screen(MenuScreen.new()), Color("5a2828"))
|
||||
Style.button(confirm, "Weiter bearbeiten", Rect2(660, 420, 240, 46), func(): confirm.visible = false)
|
||||
add_child(confirm)
|
||||
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
||||
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview.visible = false
|
||||
@@ -159,9 +174,23 @@ func _ready() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func dirty() -> bool:
|
||||
return deck != app.profile.decks[slot].cards
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if dirty():
|
||||
confirm.visible = true
|
||||
else:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus() and not name_box.has_focus():
|
||||
app.show_screen(MenuScreen.new())
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
else:
|
||||
leave()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
|
||||
@@ -23,10 +23,24 @@ func _ready() -> void:
|
||||
get_tree().root.add_child.call_deferred(net)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
profile.refresh_daily()
|
||||
apply_settings()
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func apply_settings() -> void:
|
||||
sfx.set_volume(profile.settings.volume)
|
||||
sfx.set_music_volume(profile.settings.music)
|
||||
set_language(profile.settings.language)
|
||||
show_screen(MenuScreen.new())
|
||||
Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0
|
||||
theme = Style.make_theme()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung, Decks und Fortschritt werden verworfen.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
profile.refresh_daily()
|
||||
save()
|
||||
apply_settings()
|
||||
|
||||
|
||||
func set_language(code: String) -> void:
|
||||
|
||||
@@ -35,8 +35,10 @@ func _ready() -> void:
|
||||
col.add_child(center)
|
||||
var sz := Vector2(270, 44)
|
||||
var p0: Profile = app.profile
|
||||
b_tutorial = Style.cbutton(grid, "Anleitung", sz, func(): app.show_screen(BattleScreen.guided()),
|
||||
Color("5a4628") if not p0.tutorial_done else null)
|
||||
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
|
||||
b_tutorial = Style.cbutton(grid, "Anleitung 2: Fallen" if part2 else "Anleitung", sz,
|
||||
func(): app.show_screen(BattleScreen.guided(2 if part2 else 1)),
|
||||
Color("5a4628") if not (p0.tutorial_done and p0.tutorial2_done) else null)
|
||||
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_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
|
||||
@@ -82,6 +84,9 @@ func refresh() -> void:
|
||||
if ok[0] and not p.tutorial_done:
|
||||
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
|
||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
||||
elif ok[0] and not p.tutorial2_done:
|
||||
warn_label.text = "Anleitung 2 zeigt Mulligan, Fallen und die Feldkombo Dampf."
|
||||
warn_label.add_theme_color_override("font_color", Style.GOLD)
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -9,6 +9,8 @@ var diff_buttons := {}
|
||||
var previews := {} # n -> Array von Game.Player
|
||||
var preview_labels := {}
|
||||
var b_tip: Button
|
||||
var b_reroll: Button
|
||||
var rerolls_left := 3
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -38,7 +40,7 @@ func _ready() -> void:
|
||||
Style.spacer(col, 4)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Neu würfeln", Vector2(150, 40), func(): refresh(true))
|
||||
b_reroll = Style.cbutton(br, "Neu würfeln", Vector2(190, 40), reroll)
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 40), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh(true)
|
||||
|
||||
@@ -55,6 +57,14 @@ func take_tip() -> void:
|
||||
set_difficulty(tip)
|
||||
|
||||
|
||||
## Höchstens dreimal neu würfeln, sonst ließen sich schwache Gegner erwürfeln.
|
||||
func reroll() -> void:
|
||||
if rerolls_left <= 0:
|
||||
return
|
||||
rerolls_left -= 1
|
||||
refresh(true)
|
||||
|
||||
|
||||
func start(n: int) -> void:
|
||||
app.show_screen(BattleScreen.against(previews[n]))
|
||||
|
||||
@@ -82,6 +92,8 @@ func refresh(reroll: bool = false) -> void:
|
||||
for i in n:
|
||||
opps.append(Game.random_opponent(r, i + 1, Game.START_LIFE, p.target_rating(), str(d.tier)))
|
||||
previews[n] = opps
|
||||
b_reroll.text = tr("Neu würfeln (%d)") % rerolls_left
|
||||
b_reroll.disabled = rerolls_left <= 0
|
||||
for n in previews:
|
||||
var parts: Array[String] = []
|
||||
for o in previews[n]:
|
||||
|
||||
@@ -7,6 +7,8 @@ var music: HSlider
|
||||
var speed: HSlider
|
||||
var language: OptionButton
|
||||
var music_label: Label
|
||||
var big_font: CheckButton
|
||||
var confirm: Control
|
||||
var anim: CheckButton
|
||||
var volume_label: Label
|
||||
var speed_label: Label
|
||||
@@ -58,6 +60,12 @@ func _ready() -> void:
|
||||
anim.toggled.connect(_on_anim)
|
||||
grid.add_child(anim)
|
||||
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "Große Schrift", 20, Style.TEXT, "left", false, 200)
|
||||
big_font = CheckButton.new()
|
||||
big_font.button_pressed = bool(s.get("big_font", false))
|
||||
big_font.toggled.connect(_on_big_font)
|
||||
grid.add_child(big_font)
|
||||
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "Sprache", 20, Style.TEXT, "left", false, 200)
|
||||
language = OptionButton.new()
|
||||
language.custom_minimum_size = Vector2(280, 36)
|
||||
@@ -69,13 +77,40 @@ func _ready() -> void:
|
||||
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.spacer(col, 20)
|
||||
Style.clabel(col, "Der Spielstand liegt unter %s" % ProjectSettings.globalize_path(Profile.SAVE_PATH), 13, Style.MUTED)
|
||||
Style.spacer(col, 60)
|
||||
var r := Style.row(col)
|
||||
Style.spacer(col, 30)
|
||||
var r := Style.row(col, 16)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
|
||||
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
confirm = Control.new()
|
||||
confirm.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
confirm.visible = false
|
||||
var dark := ColorRect.new()
|
||||
dark.color = Color(0, 0, 0, 0.67)
|
||||
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dark.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
confirm.add_child(dark)
|
||||
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.RED, 2)
|
||||
Style.label(confirm, "Profil wirklich zurücksetzen?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
||||
Style.label(confirm, "Sammlung, Decks, Münzen und Fortschritt gehen verloren.", 16, Style.MUTED, Rect2(340, 360, 600, 24), "center")
|
||||
Style.button(confirm, "Ja, zurücksetzen", Rect2(380, 420, 240, 46), _reset, Color("5a2828"))
|
||||
Style.button(confirm, "Abbrechen", Rect2(660, 420, 240, 46), func(): confirm.visible = false)
|
||||
add_child(confirm)
|
||||
_labels()
|
||||
|
||||
|
||||
func _reset() -> void:
|
||||
app.reset_profile()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func _on_big_font(on: bool) -> void:
|
||||
app.profile.settings.big_font = on
|
||||
app.save()
|
||||
app.apply_settings()
|
||||
app.show_screen(SettingsScreen.new())
|
||||
|
||||
|
||||
func _labels() -> void:
|
||||
volume_label.text = "%d %%" % int(round(volume.value * 100))
|
||||
music_label.text = "%d %%" % int(round(music.value * 100))
|
||||
|
||||
@@ -2,6 +2,11 @@ class_name Style
|
||||
## Farben, Schriften, Theme und Zeichenhilfen.
|
||||
|
||||
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
||||
static var font_scale := 1.0 # Einstellung „Große Schrift“
|
||||
|
||||
|
||||
static func fs(size: int) -> int:
|
||||
return int(round(size * font_scale))
|
||||
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
||||
|
||||
const BG := Color("1a1c24")
|
||||
@@ -38,7 +43,7 @@ static func box(bg: Color, radius: int = 8, border_col = null, border_w: int = 0
|
||||
static func make_theme() -> Theme:
|
||||
var t := Theme.new()
|
||||
t.default_font = FONT
|
||||
t.default_font_size = 16
|
||||
t.default_font_size = fs(16)
|
||||
t.set_stylebox("normal", "Button", box(PANEL_LIGHT, 8, MUTED, 2))
|
||||
t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2))
|
||||
t.set_stylebox("pressed", "Button", box(darker(PANEL_LIGHT), 8, MUTED, 2))
|
||||
@@ -47,7 +52,7 @@ static func make_theme() -> Theme:
|
||||
for name in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
|
||||
t.set_color(name, "Button", TEXT)
|
||||
t.set_color("font_disabled_color", "Button", MUTED)
|
||||
t.set_font_size("font_size", "Button", 18)
|
||||
t.set_font_size("font_size", "Button", fs(18))
|
||||
t.set_color("font_color", "Label", TEXT)
|
||||
t.set_stylebox("panel", "Panel", box(PANEL, 8))
|
||||
t.set_stylebox("normal", "RichTextLabel", StyleBoxEmpty.new())
|
||||
@@ -84,7 +89,7 @@ static func label(parent: Node, text: String, size: int, color: Color, rect: Rec
|
||||
l.text = text
|
||||
l.position = rect.position
|
||||
l.size = rect.size
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_font_size_override("font_size", fs(size))
|
||||
l.add_theme_color_override("font_color", color)
|
||||
if bold:
|
||||
l.add_theme_font_override("font", FONT_BOLD)
|
||||
@@ -168,7 +173,8 @@ static func clear(node: Node) -> void:
|
||||
|
||||
# --- Zeichenhilfen für _draw() ---------------------------------------------------
|
||||
|
||||
static func text(ci: CanvasItem, s: String, size: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
|
||||
static func text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
|
||||
var size := fs(size_in)
|
||||
var font: Font = FONT_BOLD if bold else FONT
|
||||
var dim := font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||
var x := pos.x
|
||||
|
||||
@@ -110,12 +110,13 @@ func _initialize() -> void:
|
||||
test_starter_gegen_passende_gegner()
|
||||
test_kurve_statistik_vorschlag_verbesserung()
|
||||
test_wasser_antworten()
|
||||
test_ki_zeitbudget()
|
||||
print("%d Prüfungen bestanden, %d fehlgeschlagen" % [passed, failed])
|
||||
quit(1 if failed > 0 else 0)
|
||||
|
||||
|
||||
func test_kartenpool() -> void:
|
||||
check(Cards.all().size() == 109, "109 Karten")
|
||||
check(Cards.all().size() == 110, "110 Karten")
|
||||
for key in Cards.COMBOS:
|
||||
var found: bool = Cards.all().any(func(c): return c.is_combo() and Cards.combo_key(c.elements) == key)
|
||||
check(found, "Kombo vorhanden: " + key)
|
||||
@@ -1282,8 +1283,18 @@ func test_kurve_statistik_vorschlag_verbesserung() -> void:
|
||||
var mono := starter
|
||||
check(p.suggest_difficulty() == "", "kein Vorschlag am Anfang")
|
||||
for i in 3:
|
||||
p.record_battle(true, 1, mono)
|
||||
check(p.suggest_difficulty() == "schwer" and p.notices.any(func(n): return n.begins_with("Tipp: 3 Siege")), "drei Siege: Schwer vorschlagen")
|
||||
p.record_battle(true, 1, mono, -1, [], 6, {}, 1.0, 4)
|
||||
check(p.suggest_difficulty() == "", "drei knappe Siege: noch kein Vorschlag")
|
||||
p.records.streak = 0
|
||||
p.records.margins = []
|
||||
for i in 3:
|
||||
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")
|
||||
p.records.streak = 0
|
||||
p.records.margins = []
|
||||
for i in 5:
|
||||
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")
|
||||
p.settings.difficulty = "schwer"
|
||||
p.take_notices()
|
||||
for i in 3:
|
||||
@@ -1319,3 +1330,27 @@ func test_wasser_antworten() -> void:
|
||||
g.play_card(h, C("w_strudel"), t)
|
||||
check(o.board.is_empty() and C("e_troll") in o.hand and h.hand.size() == hand, "Strudel: zurück auf die Hand, eine Karte gezogen")
|
||||
check(C("w_ebbe").rarity == "gewöhnlich" and Profile.starter().collection.has("w_ebbe"), "Ebbe in der Startsammlung")
|
||||
|
||||
|
||||
func test_ki_zeitbudget() -> void:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 12
|
||||
var g := Game.new_battle(Profile.starter().deck_cards(), 5, r)
|
||||
var h: Game.Player = g.players[0]
|
||||
h.mulligan_done = true
|
||||
h.energy = 10
|
||||
h.max_energy = 10
|
||||
for i in 8:
|
||||
h.hand.append(Cards.by_id("f_funke"))
|
||||
for p in g.players:
|
||||
for i in 3:
|
||||
put(p, "e_troll")
|
||||
AI.time_budget_ms = 1
|
||||
var t0 := Time.get_ticks_msec()
|
||||
var act := AI.choose_action(g, h)
|
||||
var ms := Time.get_ticks_msec() - t0
|
||||
AI.time_budget_ms = 150
|
||||
check(act.type in ["play", "attack", "end"] and ms < 80, "Zeitbudget greift (%d ms)" % ms)
|
||||
check(Cards.by_id("wl_gischt").keywords == ["Flink"] and Cards.by_id("wl_gischt").combo() == "Sturm", "Gischtläufer für Sturm")
|
||||
var v := Profile.from_dict(JSON.parse_string(JSON.stringify(Profile.starter().to_dict())))
|
||||
check(v.settings.has("big_font") and not v.tutorial2_done and v.records.margins is Array, "Version 5 mit neuen Feldern")
|
||||
|
||||
@@ -74,6 +74,14 @@ func key(code: int) -> void:
|
||||
await frames(2)
|
||||
|
||||
|
||||
func pad(button: int) -> void:
|
||||
var ev := InputEventJoypadButton.new()
|
||||
ev.button_index = button
|
||||
ev.pressed = true
|
||||
get_viewport().push_input(ev)
|
||||
await frames(2)
|
||||
|
||||
|
||||
## Mulligan-Maske wegklicken (Hand behalten), falls sie offen ist.
|
||||
func clear_mulligan(scr) -> void:
|
||||
if scr.mulligan_box.visible:
|
||||
@@ -155,9 +163,10 @@ func _ready() -> void:
|
||||
await frames(3)
|
||||
var fast := OS.get_environment("DINGER_FAST") != ""
|
||||
var suite := [test_menu_und_regeln, test_booster, test_deckbau, test_gegnerauswahl, test_einstellungen,
|
||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_herausforderung]
|
||||
test_erfolge, test_kampagne, test_werkstatt, test_sprache, test_anleitung, test_anleitung2, test_herausforderung]
|
||||
if not fast:
|
||||
suite += [test_hotseat, test_netzwerk, test_kampf.bind(2, 3), test_kampf_abbrechen]
|
||||
suite.append(test_profil_zuruecksetzen)
|
||||
for t in suite:
|
||||
await ensure_menu()
|
||||
await t.call()
|
||||
@@ -290,7 +299,14 @@ func test_deckbau() -> void:
|
||||
await click(find_button("Auto-Ausfüllen"))
|
||||
check(scr.deck.size() == 20 and main.profile.deck_valid(2)[0], "Slot 3 automatisch gefüllt und spielbar")
|
||||
await shot("25_deck_slots")
|
||||
await click(scr.deck_list.get_children()[0]) # ungespeicherte Änderung: eine Karte entfernt
|
||||
check(scr.dirty(), "Deck geändert")
|
||||
await click(find_button("Zurück"))
|
||||
check(scr.confirm.visible and screen() == scr, "Warnung bei ungespeicherten Änderungen")
|
||||
await click(find_button("Weiter bearbeiten"))
|
||||
check(not scr.confirm.visible, "weiter bearbeiten")
|
||||
await click(find_button("Zurück"))
|
||||
await click(find_button("Verwerfen"))
|
||||
check(screen() is MenuScreen and main.profile.deck.size() == 20, "Änderungen verworfen")
|
||||
|
||||
|
||||
@@ -360,6 +376,9 @@ func test_gegnerauswahl() -> void:
|
||||
check(sel is OpponentSelectScreen, "Gegnerauswahl geöffnet")
|
||||
check("Deckstärke" in sel.rating_label.text, "Deckstärke angezeigt")
|
||||
check(sel.preview_labels[3].text.count("(") == 3 and sel.previews[3].size() == 3, "Vorschau zeigt drei Gegner mit Stärke")
|
||||
for i in 3:
|
||||
await click(sel.b_reroll)
|
||||
check(sel.rerolls_left == 0 and sel.b_reroll.disabled, "höchstens dreimal neu würfeln")
|
||||
await click(find_button("Schwer"))
|
||||
check(main.profile.settings.difficulty == "schwer" and Profile.load_or_new(SAVE).settings.difficulty == "schwer", "Schwer gewählt und gespeichert")
|
||||
var previewed: Array = sel.previews[1].map(func(o): return o.title)
|
||||
@@ -382,11 +401,13 @@ func test_gegnerauswahl() -> void:
|
||||
await click(find_button("Einzelspieler"))
|
||||
await click(find_button("Normal"))
|
||||
main.profile.records.streak = 3
|
||||
main.profile.records.margins = [20, 22, 18]
|
||||
screen().refresh()
|
||||
check(screen().b_tip.visible and "Schwer" in screen().tip_label.text, "Vorschlag nach drei Siegen")
|
||||
check(screen().b_tip.visible and "Schwer" in screen().tip_label.text, "Vorschlag nach drei deutlichen Siegen")
|
||||
await click(find_button("Übernehmen"))
|
||||
check(main.profile.settings.difficulty == "schwer", "Vorschlag übernommen")
|
||||
main.profile.records.streak = 0
|
||||
main.profile.records.margins = []
|
||||
await click(find_button("Normal"))
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
@@ -434,7 +455,15 @@ func test_einstellungen() -> void:
|
||||
check(main.profile.settings.animations == false, "Animationen abschaltbar")
|
||||
check(is_equal_approx(Profile.load_or_new(SAVE).settings.volume, 0.3), "Einstellungen auf Platte")
|
||||
scr.anim.button_pressed = true
|
||||
scr.big_font.button_pressed = true
|
||||
await frames(2)
|
||||
check(main.profile.settings.big_font and Style.font_scale > 1.0 and screen() is SettingsScreen, "große Schrift aktiv")
|
||||
screen().big_font.button_pressed = false
|
||||
await frames(2)
|
||||
check(is_equal_approx(Style.font_scale, 1.0), "normale Schrift")
|
||||
var scr_b = screen()
|
||||
await shot("13_einstellungen")
|
||||
scr = scr_b
|
||||
await click(find_button("Zurück"))
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
@@ -484,6 +513,45 @@ func test_werkstatt() -> void:
|
||||
check(screen() is MenuScreen, "zurück im Menü")
|
||||
|
||||
|
||||
func test_anleitung2() -> void:
|
||||
check(main.profile.tutorial_done and not main.profile.tutorial2_done, "Teil 1 erledigt, Teil 2 offen")
|
||||
check(find_button("Anleitung 2: Fallen") != null, "Menü bietet Teil 2 an")
|
||||
await click(find_button("Anleitung 2: Fallen"))
|
||||
var scr = screen()
|
||||
check(scr is BattleScreen and scr.tutorial and scr.tutorial_part == 2, "Anleitung 2 gestartet")
|
||||
scr.ai_delay = 0.0
|
||||
var g: Game = scr.game
|
||||
check(g.opponents[0].name == "Mara" and g.human.hand.any(func(c): return c.id == "w_falle") and g.human.hand.any(func(c): return c.id == "w_reiter"), "Mara, Frostfalle und Wellenreiter auf der Hand")
|
||||
await frames(2)
|
||||
check(scr.mulligan_box.visible and scr.tutorial_step == 0, "Schritt 1: Mulligan-Maske")
|
||||
await click(scr.b_keep)
|
||||
check(scr.tutorial_step == 1 and "Frostfalle" in scr.tutorial_text.text, "Schritt 2: Falle legen")
|
||||
await shot("29_anleitung2")
|
||||
# Zug 1 hat nur 1 Energie: Zug beenden, dann Falle legen
|
||||
await key(KEY_SPACE)
|
||||
await wait_for_human(scr)
|
||||
check(scr.highlight_layer.get_child_count() >= 1, "Falle hervorgehoben")
|
||||
var idx: int = g.human.hand.map(func(c): return c.id).find("w_falle")
|
||||
check(idx >= 0 and idx < 9, "Falle auf der Hand")
|
||||
await key(KEY_1 + idx)
|
||||
check(g.human.traps.size() == 1 and scr.tutorial_step == 2, "Falle gelegt, Schritt 3")
|
||||
await give_up()
|
||||
check(main.profile.tutorial2_done and find_button("Anleitung") != null, "Teil 2 erledigt")
|
||||
|
||||
|
||||
func test_profil_zuruecksetzen() -> void:
|
||||
await click(find_button("Einstellungen"))
|
||||
await click(find_button("Profil zurücksetzen"))
|
||||
var scr = screen()
|
||||
check(scr.confirm.visible, "Rückfrage")
|
||||
await click(find_button("Abbrechen"))
|
||||
check(not scr.confirm.visible, "abgebrochen")
|
||||
await click(find_button("Profil zurücksetzen"))
|
||||
await click(find_button("Ja, zurücksetzen"))
|
||||
check(screen() is MenuScreen and main.profile.coins == Profile.START_COINS and not main.profile.tutorial_done, "Profil zurückgesetzt")
|
||||
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()
|
||||
@@ -602,6 +670,20 @@ func test_anleitung() -> void:
|
||||
await key(KEY_1 + idx)
|
||||
check(g.human.board.size() == 1, "erneut gespielt")
|
||||
await shot("22_anleitung")
|
||||
# Gamepad: Zeiger bewegen, Karte spielen
|
||||
await pad(JOY_BUTTON_DPAD_RIGHT)
|
||||
check(scr.gp_zone == "hand" and scr.gp_index == 1 and scr.cursor_layer.get_child_count() == 1, "Gamepad-Zeiger auf zweiter Handkarte")
|
||||
var pad_idx := -1
|
||||
for i in g.human.hand.size():
|
||||
if g.human.hand[i].is_creature() and g.can_play(g.human, g.human.hand[i])[0]:
|
||||
pad_idx = i
|
||||
break
|
||||
if pad_idx >= 0:
|
||||
while scr.gp_index != pad_idx:
|
||||
await pad(JOY_BUTTON_DPAD_RIGHT)
|
||||
var board_before: int = g.human.board.size()
|
||||
await pad(JOY_BUTTON_A)
|
||||
check(g.human.board.size() == board_before + 1, "Gamepad A spielt das Wesen")
|
||||
scr.ai_delay = 0.75
|
||||
await key(KEY_SPACE)
|
||||
check(g.current() != g.human or g.turn >= 2, "Zug per Leertaste beendet")
|
||||
@@ -639,9 +721,16 @@ func test_netzwerk() -> void:
|
||||
var pid := OS.create_process(OS.get_executable_path(), args)
|
||||
check(pid > 0, "Bot-Prozess gestartet")
|
||||
var wait := 0
|
||||
while main.net.names.size() < 2 and wait < 900:
|
||||
while main.net.names.size() < 2 and wait < 600:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
if main.net.names.size() < 2:
|
||||
print("HINWEIS: Bot kam nicht, zweiter Versuch")
|
||||
OS.kill(pid)
|
||||
pid = OS.create_process(OS.get_executable_path(), args)
|
||||
while main.net.names.size() < 2 and wait < 1500:
|
||||
await frames(1)
|
||||
wait += 1
|
||||
check(main.net.names.size() == 2, "Bot ist beigetreten (Pfad %s)" % main.net.get_path())
|
||||
await shot("23_lobby")
|
||||
await click(find_button("Start"))
|
||||
|
||||
@@ -107,3 +107,10 @@ Tipp: %s,Tip: %s
|
||||
Übernehmen,Apply
|
||||
Neu würfeln,Reroll
|
||||
Deck verbessern: %s ins aktive Deck (%s),Improve deck: add %s to the active deck (%s)
|
||||
Anleitung 2: Fallen,Tutorial 2: Traps
|
||||
Neu würfeln (%d),Reroll (%d)
|
||||
Profil zurücksetzen,Reset profile
|
||||
Große Schrift,Large text
|
||||
Verwerfen,Discard
|
||||
Weiter bearbeiten,Keep editing
|
||||
Ungespeicherte Änderungen,Unsaved changes
|
||||
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user