Kampf fortsetzen, Gamepad im Deckbau, Deck-Codes, Kapitel 3, Elementbooster, englische Karten, Host-Wechsel und Zuschauer
- KI-Bewertung mit Bedrohung und Führenden-Bremse (62:38 gegen die alte im Duell, gleichauf zu viert) - Balance: Sand +1/+1 statt Schild, Erde+Luft und Erde geschwächt, Wasser angehoben, Messlauf mit N=200 möglich - Einzelspieler-Kämpfe werden laufend gespeichert (voller Spielstand, viewer -1) und im Menü fortgesetzt - Karten, Deckzeilen und Kartenrücken fokussierbar: Gamepad in Deckbau, Werkstatt und Booster - Kartennamen (name_en) und Kartentexte übersetzbar, Übersetzungstest deckt cards.gd mit ab - Deck-Code DINGER1:id*anzahl mit Zwischenablage; acht neue Karten; Kampagne Kapitel 3; Elementbooster 120 Münzen - Netzwerk: Nachfolge-Host mit vollem Spielstand, Neuverbindung der anderen, Zuschauer (Sicht -2); Bot kann hosten - Große Schrift: Kartennamen, Kartentext, Knopfleisten, Gegnervorschau, Dialogtext - Anleitung 1 garantiert ein 1-Energie-Wesen; Tests: DINGER_ONLY für einzelne Oberflächentests - Oberflächentest auch auf Xephyr mit llvmpipe (wie Xvfb in der CI) grün Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
@@ -63,7 +63,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
screen.preview_timer = 1.8
|
||||
"mulligan":
|
||||
if ev.count > 0:
|
||||
popup("tauscht %d Karten" % ev.count, Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
popup(("tauscht 1 Karte" if int(ev.count) == 1 else "tauscht %d Karten" % ev.count), Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
|
||||
|
||||
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
||||
|
||||
@@ -9,7 +9,8 @@ var stage := -1 # Kampagnenstufe oder -1
|
||||
var tutorial := false
|
||||
var hotseat_decks: Array = []
|
||||
var hotseat_ai := 0
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, sonst -1
|
||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||
var net: Net
|
||||
var game: Game
|
||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||
@@ -44,6 +45,7 @@ var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||
var saved_game: Game = null # fortgesetzter Kampf (siehe resume)
|
||||
var undo_log_len := 0
|
||||
var undo_t := 0.0
|
||||
|
||||
@@ -80,6 +82,7 @@ var result_label: Label
|
||||
var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var b_save_leave: Button
|
||||
var handover: Control
|
||||
var handover_label: Label
|
||||
var mulligan: BattleMulligan
|
||||
@@ -135,6 +138,7 @@ static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
|
||||
|
||||
static func online(seat: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
b.net_play = true
|
||||
b.online_seat = seat
|
||||
return b
|
||||
|
||||
@@ -152,6 +156,15 @@ static func against(opps: Array) -> BattleScreen:
|
||||
return b
|
||||
|
||||
|
||||
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
||||
static func resume(data: Dictionary) -> BattleScreen:
|
||||
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.saved_game = Game.from_snapshot(data.game)
|
||||
return s
|
||||
|
||||
|
||||
static func weekly(ch: Dictionary) -> BattleScreen:
|
||||
var b := BattleScreen.new(int(ch.n_opp), -1)
|
||||
b.challenge = ch
|
||||
@@ -159,11 +172,16 @@ static func weekly(ch: Dictionary) -> BattleScreen:
|
||||
|
||||
|
||||
func is_hotseat() -> bool:
|
||||
return game != null and game.humans.size() > 1 and online_seat < 0
|
||||
return game != null and game.humans.size() > 1 and not net_play
|
||||
|
||||
|
||||
func is_online() -> bool:
|
||||
return online_seat >= 0
|
||||
return net_play
|
||||
|
||||
|
||||
## Zuschauer im Netzwerk: kein eigener Platz, keine Hand.
|
||||
func spectating() -> bool:
|
||||
return is_online() and online_seat < 0
|
||||
|
||||
|
||||
func is_client() -> bool:
|
||||
@@ -184,6 +202,8 @@ func _ready() -> void:
|
||||
if is_online():
|
||||
net = app.net
|
||||
net.ended.connect(_on_net_ended)
|
||||
net.migrated.connect(_on_migrated)
|
||||
net.status_changed.connect(_on_net_status)
|
||||
if net.is_host:
|
||||
game = net.game
|
||||
net.action_applied.connect(after_action)
|
||||
@@ -191,6 +211,8 @@ func _ready() -> void:
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
wait_label.visible = true
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif saved_game != null:
|
||||
game = saved_game
|
||||
elif not hotseat_decks.is_empty():
|
||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
||||
elif tutorial:
|
||||
@@ -227,7 +249,7 @@ func own_rules() -> Dictionary:
|
||||
func _begin() -> void:
|
||||
n_opp = game.players.size() - 1
|
||||
if is_online():
|
||||
perspective = game.players[online_seat]
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
else:
|
||||
game.events.clear()
|
||||
perspective = game.current() if game.current().is_human else game.humans[0]
|
||||
@@ -235,6 +257,8 @@ func _begin() -> void:
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
game.events.clear()
|
||||
b_save_leave.visible = resumable()
|
||||
save_battle()
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -304,7 +328,14 @@ func _build_static() -> void:
|
||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
||||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||||
# Aufgeben bestätigen
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "Der Kampf endet ohne Münzen.", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
confirm = Style.overlay(self)
|
||||
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
|
||||
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
||||
Style.label(confirm, tr("Aufgeben beendet den Kampf ohne Münzen.") + "\n" + tr("Speichern hebt ihn für später auf."), 15, Style.MUTED, Rect2(340, 356, 600, 48), "center")
|
||||
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
|
||||
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
|
||||
b_save_leave.add_theme_font_size_override("font_size", 14)
|
||||
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
||||
# Übergabe zwischen Menschen (Hotseat)
|
||||
handover = _overlay()
|
||||
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
||||
@@ -324,9 +355,31 @@ func _overlay() -> Control:
|
||||
return Style.overlay(self)
|
||||
|
||||
|
||||
## Einzelspieler-Kämpfe (auch Kampagne und Herausforderung) lassen sich unterbrechen und fortsetzen.
|
||||
func resumable() -> bool:
|
||||
return counts_for_profile() and not tutorial
|
||||
|
||||
|
||||
func save_battle() -> void:
|
||||
if not resumable() or game == null or game.winner != "":
|
||||
return
|
||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "challenge": challenge, "played_ids": played_ids, "game": game.snapshot(-1)}
|
||||
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(d))
|
||||
f.close()
|
||||
|
||||
|
||||
func save_and_leave() -> void:
|
||||
save_battle()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if is_online():
|
||||
net.leave()
|
||||
if resumable() and game != null and game.winner == "":
|
||||
app.clear_battle_save() # aufgegeben
|
||||
if tutorial:
|
||||
if tutorial_part == 1:
|
||||
app.profile.tutorial_done = true
|
||||
@@ -398,7 +451,9 @@ func build_hero_boxes() -> void:
|
||||
if not o.perks.is_empty():
|
||||
title += " ★"
|
||||
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (44 if c else 20), 26), "left", true)
|
||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (58 if c else 20), 26), "left", true)
|
||||
labels.name.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
||||
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
||||
@@ -414,6 +469,8 @@ func build_hero_boxes() -> void:
|
||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||
|
||||
func my_turn() -> bool:
|
||||
if spectating():
|
||||
return false
|
||||
return game != null and game.winner == "" and game.current() == perspective
|
||||
|
||||
|
||||
@@ -500,7 +557,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.visible and not handover.visible:
|
||||
elif cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible and not handover.visible:
|
||||
show_mulligan()
|
||||
return
|
||||
if is_client():
|
||||
@@ -558,6 +615,7 @@ func _finish() -> void:
|
||||
app.profile.tutorial2_done = true
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
else:
|
||||
var wp := game.winner_player
|
||||
var mine: bool = wp != null and wp == perspective
|
||||
@@ -704,7 +762,7 @@ func on_click(kind: String, obj) -> void:
|
||||
selected = obj
|
||||
say(tr("Wähle ein Angriffsziel"))
|
||||
else:
|
||||
say(tr("%s kann nicht angreifen") % obj.name)
|
||||
say(tr("%s kann nicht angreifen") % obj.card.title())
|
||||
after_action()
|
||||
|
||||
|
||||
@@ -761,13 +819,13 @@ func combo_hints(card: Cards.Card) -> Array[String]:
|
||||
var combos := {}
|
||||
for other in perspective.board:
|
||||
if other.card.elements.any(func(e): return e in card.elements):
|
||||
partners.append(other.name)
|
||||
partners.append(other.card.title())
|
||||
for e1 in card.elements:
|
||||
for e2 in other.card.elements:
|
||||
if e1 != e2:
|
||||
var name := Cards.combo_name([e1, e2])
|
||||
if not combos.has(name):
|
||||
combos[name] = other.name
|
||||
combos[name] = other.card.title()
|
||||
if not partners.is_empty():
|
||||
out.append("Resonanz +%d mit %s" % [mini(partners.size(), Game.RESONANCE_MAX), ", ".join(partners)])
|
||||
for name in combos:
|
||||
@@ -784,11 +842,28 @@ func _on_snapshot(snap: Dictionary) -> void:
|
||||
if first:
|
||||
_begin()
|
||||
return
|
||||
perspective = game.players[online_seat]
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
build_hero_boxes()
|
||||
after_action()
|
||||
|
||||
|
||||
## Host-Wechsel: das Spiel läuft jetzt auf diesem Rechner weiter.
|
||||
func _on_migrated() -> void:
|
||||
game = net.game
|
||||
if not net.action_applied.is_connected(after_action):
|
||||
net.action_applied.connect(after_action)
|
||||
wait_label.visible = false
|
||||
perspective = game.players[online_seat]
|
||||
build_hero_boxes()
|
||||
say("Du bist jetzt Host.")
|
||||
after_action()
|
||||
|
||||
|
||||
func _on_net_status() -> void:
|
||||
wait_label.text = net.status
|
||||
wait_label.visible = true
|
||||
|
||||
|
||||
func _on_net_ended(reason: String) -> void:
|
||||
if game == null or game.winner == "":
|
||||
say(reason)
|
||||
@@ -814,6 +889,7 @@ func after_action() -> void:
|
||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||
refresh()
|
||||
fx.play_events(evs, centers)
|
||||
save_battle()
|
||||
|
||||
|
||||
func _note_events(evs: Array) -> void:
|
||||
@@ -962,21 +1038,24 @@ func refresh() -> void:
|
||||
trap_views.append(tv)
|
||||
|
||||
Style.clear(hand_layer)
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
var playable: bool = mine and g.can_play(h, card)[0]
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size)
|
||||
v.position = r.position
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
v.dim = not playable
|
||||
v.hover.connect(show_hand_preview)
|
||||
v.clicked.connect(func(_view): on_click("hand", card))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
if spectating():
|
||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center")
|
||||
else:
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
var playable: bool = mine and g.can_play(h, card)[0]
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size)
|
||||
v.position = r.position
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
v.dim = not playable
|
||||
v.hover.connect(show_hand_preview)
|
||||
v.clicked.connect(func(_view): on_click("hand", card))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
|
||||
var bar_border = Style.BLUE if mine else null
|
||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||
@@ -987,10 +1066,12 @@ func refresh() -> void:
|
||||
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
||||
deck_label.text = "Deck %d" % h.deck.size()
|
||||
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
||||
if spectating():
|
||||
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
||||
b_end.disabled = not mine
|
||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||
b_skip.visible = ai_running
|
||||
b_end.visible = not ai_running
|
||||
b_end.visible = not ai_running and not spectating()
|
||||
b_undo.visible = undo_state != null and mine
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
|
||||
@@ -38,8 +38,11 @@ func stage_index() -> int:
|
||||
return 0 if part == 1 else 1
|
||||
|
||||
|
||||
## Teil 1: ein Wesen für 1 Energie auf der Hand, damit der erste Schritt immer möglich ist.
|
||||
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
|
||||
func inject_cards(g: Game) -> void:
|
||||
if part == 1 and not g.human.hand.any(func(c): return c.is_creature() and c.cost == 1):
|
||||
g.human.hand.append(Cards.by_id("f_kobold"))
|
||||
if part == 2:
|
||||
g.human.hand.append(Cards.by_id("w_falle"))
|
||||
g.human.hand.append(Cards.by_id("w_reiter"))
|
||||
|
||||
@@ -15,6 +15,7 @@ var b_all: Button
|
||||
var b_improve: Button
|
||||
var suggestion := ""
|
||||
var slots: Control
|
||||
var element_buttons: Array = []
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
@@ -24,15 +25,21 @@ class CardBack extends Control:
|
||||
size = sz
|
||||
custom_minimum_size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
focus_entered.connect(queue_redraw)
|
||||
focus_exited.connect(queue_redraw)
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit()
|
||||
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||
accept_event()
|
||||
clicked.emit()
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 3), rect)
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect)
|
||||
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
|
||||
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
|
||||
|
||||
@@ -48,15 +55,20 @@ func _ready() -> void:
|
||||
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
||||
Style.label(self, "5 Karten pro Booster, mindestens eine seltene. Klicke auf eine Karte zum Aufdecken.",
|
||||
16, Style.MUTED, Rect2(0, 115, 1280, 24), "center")
|
||||
Style.label(self, "Elementbooster (%d Münzen): nur Karten des Elements und seiner Kombos" % Profile.ELEMENT_BOOSTER_COST,
|
||||
14, Style.MUTED, Rect2(0, 586, 1280, 22), "center")
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
element_buttons.append(Style.button(self, e, Rect2(322 + i * 162, 612, 150, 38), buy.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8)))
|
||||
slots = Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 18, Style.RED, Rect2(0, 610, 1280, 30), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(360, 720, 260, 48), buy, Color("5a4628"))
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 140, 48), reveal_all)
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 1280, 22), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(290, 720, 330, 48), buy, Color("5a4628"))
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all)
|
||||
b_improve = Style.button(self, "", Rect2(360, 650, 560, 44), improve, Color("285a3c"))
|
||||
b_improve.visible = false
|
||||
Style.button(self, "Zurück", Rect2(800, 720, 120, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -65,10 +77,10 @@ func _input(event: InputEvent) -> void:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func buy() -> void:
|
||||
func buy(element: String = "") -> void:
|
||||
var p: Profile = app.profile
|
||||
var before: Dictionary = p.collection.duplicate()
|
||||
var got: Array = p.open_booster()
|
||||
var got: Array = p.open_booster(null, element)
|
||||
if got.is_empty():
|
||||
msg_label.text = "Nicht genug Münzen"
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
@@ -106,7 +118,7 @@ func improve() -> void:
|
||||
var err: String = app.profile.improve_deck_with(suggestion)
|
||||
if err == "":
|
||||
app.save()
|
||||
msg_label.text = "%s ist jetzt im Deck %s" % [Cards.by_id(suggestion).name, app.profile.deck_name()]
|
||||
msg_label.text = tr("%s ist jetzt im Deck %s") % [Cards.by_id(suggestion).title(), app.profile.deck_name()]
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
||||
suggestion = ""
|
||||
else:
|
||||
@@ -119,6 +131,8 @@ func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||
for b in element_buttons:
|
||||
b.disabled = p.coins < Profile.ELEMENT_BOOSTER_COST
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= 5
|
||||
# Deck-Vorschlag, sobald alle Karten offen liegen
|
||||
suggestion = ""
|
||||
@@ -129,7 +143,7 @@ func refresh() -> void:
|
||||
suggestion = better[0]
|
||||
b_improve.visible = suggestion != ""
|
||||
if suggestion != "":
|
||||
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).name, p.deck_name()]
|
||||
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).title(), p.deck_name()]
|
||||
Style.clear(slots)
|
||||
for i in 5:
|
||||
var r := slot_rect(i)
|
||||
@@ -140,7 +154,7 @@ func refresh() -> void:
|
||||
var v := CardView.new(c, r.size)
|
||||
v.position = r.position
|
||||
slots.add_child(v)
|
||||
Style.label(slots, c.rarity, 16, Cards.RARITY_COLORS[c.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
Style.label(slots, tr(c.rarity), 16, Cards.RARITY_COLORS[c.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
if new_ids.has(c.id):
|
||||
Style.label(slots, "Neu!", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
else:
|
||||
|
||||
@@ -8,7 +8,7 @@ var perk_box: OptionButton
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
chapter = 2 if p.campaign_stage >= Campaign.STAGES.size() else 1
|
||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||
build()
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ func build() -> void:
|
||||
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
||||
var tabs := Style.row(col, 10)
|
||||
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
var b1 := Style.cbutton(tabs, "Kapitel 1", Vector2(160, 36), func(): set_chapter(1), Color("285a3c") if chapter == 1 else null)
|
||||
var b2 := Style.cbutton(tabs, "Kapitel 2", Vector2(160, 36), func(): set_chapter(2), Color("285a3c") if chapter == 2 else null)
|
||||
b2.disabled = p.campaign_stage < Campaign.STAGES.size()
|
||||
for c in Campaign.CHAPTERS.size():
|
||||
var b := Style.cbutton(tabs, "Kapitel %d" % (c + 1), Vector2(140, 36), set_chapter.bind(c + 1), Color("285a3c") if chapter == c + 1 else null)
|
||||
b.disabled = p.campaign_stage < c * Campaign.STAGES.size()
|
||||
if p.perk_unlocked():
|
||||
var pr := Style.row(tabs, 8)
|
||||
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
||||
@@ -36,9 +36,11 @@ func build() -> void:
|
||||
p.perk = "" if i == 0 else keys[i - 1]
|
||||
app.save())
|
||||
pr.add_child(perk_box)
|
||||
Style.clabel(col, "Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★)." if chapter == 1
|
||||
else "Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.", 14, Style.MUTED)
|
||||
var offset := 0 if chapter == 1 else Campaign.STAGES.size()
|
||||
var intro := ["Zehn Magier je Kapitel. Jede Stufe bringt mehr Gegner an den Tisch. Bosse haben Merkmale (★).",
|
||||
"Die Magier kehren mit zwei Merkmalen und größerem Gefolge zurück.",
|
||||
"Kapitel 3: drei Merkmale, volle Tische und die stärksten Decks."]
|
||||
Style.clabel(col, intro[chapter - 1], 14, Style.MUTED)
|
||||
var offset := (chapter - 1) * Campaign.STAGES.size()
|
||||
for k in Campaign.STAGES.size():
|
||||
var i := offset + k
|
||||
var st := Campaign.stage(i)
|
||||
@@ -76,7 +78,7 @@ func build() -> void:
|
||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||
b.disabled = true
|
||||
if p.campaign_stage >= Campaign.total():
|
||||
Style.clabel(col, "Beide Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
||||
Style.clabel(col, "Alle drei Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
||||
|
||||
@@ -17,6 +17,7 @@ func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
follow_focus = true # Gamepad: der fokussierten Karte hinterherscrollen
|
||||
grid = GridContainer.new()
|
||||
grid.columns = COLS
|
||||
grid.add_theme_constant_override("h_separation", GAP)
|
||||
@@ -31,7 +32,7 @@ func fill(cards: Array, label_fn: Callable, dim_fn: Callable) -> void:
|
||||
var cell := Control.new()
|
||||
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
||||
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var v := CardView.new(c, Vector2(CW, CH))
|
||||
var v := CardView.new(c, Vector2(CW, CH), true)
|
||||
v.dim = dim_fn.call(c)
|
||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||
v.hover.connect(func(card, entered): card_hover.emit(card, entered))
|
||||
|
||||
@@ -10,19 +10,31 @@ var border = null # Color oder null (dann Seltenheitsfarbe)
|
||||
var dim := false
|
||||
|
||||
|
||||
func _init(c: Cards.Card, sz: Vector2) -> void:
|
||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false) -> void:
|
||||
card = c
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(card, true))
|
||||
mouse_exited.connect(func(): hover.emit(card, false))
|
||||
if focusable:
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
focus_entered.connect(func():
|
||||
hover.emit(card, true)
|
||||
queue_redraw())
|
||||
focus_exited.connect(func():
|
||||
hover.emit(card, false)
|
||||
queue_redraw())
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
@@ -41,10 +53,11 @@ func _draw() -> void:
|
||||
draw_circle(cc, r, Color("14141c"))
|
||||
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
|
||||
# Name und Kombo
|
||||
var nm := Style.fit(card.name, true, f_name, size.x - 2 * r - 12)
|
||||
Style.text(self, nm, f_name, Style.TEXT, Vector2(size.x / 2 + r / 2.0, 4), "midtop", true)
|
||||
var name_left := 2 * r + 10
|
||||
var nm := Style.fit(card.title(), true, f_name, size.x - name_left - 6)
|
||||
Style.text(self, nm, f_name, Style.TEXT, Vector2((name_left + size.x - 6) / 2.0, 4), "midtop", true)
|
||||
if mid:
|
||||
var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind
|
||||
var sub: String = (tr(card.combo()) + " · " + tr(card.kind)) if card.is_combo() else tr(card.kind)
|
||||
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
||||
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
|
||||
var tsize := Style.fs(f_text)
|
||||
@@ -60,11 +73,14 @@ func _draw() -> void:
|
||||
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
||||
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
|
||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
||||
var max_lines := maxi(1, int((box.size.y - 6) / Style.FONT.get_height(tsize))) # große Schrift: lieber kürzen als überlaufen
|
||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
|
||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, -1, Style.TEXT)
|
||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||
# Werte
|
||||
if card.is_creature():
|
||||
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
|
||||
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
|
||||
if dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||
|
||||
@@ -44,7 +44,7 @@ func _draw() -> void:
|
||||
_draw_mini()
|
||||
return
|
||||
var fs := 11 if small else 13
|
||||
var nm := Style.fit(creature.name, true, fs, size.x - 6)
|
||||
var nm := Style.fit(creature.card.title(), true, fs, size.x - 6)
|
||||
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
||||
var badges: Array[String] = []
|
||||
if creature.taunt():
|
||||
|
||||
@@ -28,6 +28,8 @@ var b_activate: Button
|
||||
var b_autofill: Button
|
||||
var name_box: LineEdit
|
||||
var confirm: Control
|
||||
var code_dialog: Control
|
||||
var code_box: LineEdit
|
||||
|
||||
|
||||
class DeckRow extends Control:
|
||||
@@ -43,26 +45,30 @@ class DeckRow extends Control:
|
||||
size = sz
|
||||
custom_minimum_size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func():
|
||||
lit = true
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
var light := func(on: bool):
|
||||
lit = on
|
||||
queue_redraw()
|
||||
hover.emit(card, true))
|
||||
mouse_exited.connect(func():
|
||||
lit = false
|
||||
queue_redraw()
|
||||
hover.emit(card, false))
|
||||
hover.emit(card, on)
|
||||
mouse_entered.connect(light.bind(true))
|
||||
mouse_exited.connect(light.bind(false))
|
||||
focus_entered.connect(light.bind(true))
|
||||
focus_exited.connect(light.bind(false))
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(card)
|
||||
elif has_focus() and event.is_action_pressed("ui_accept"):
|
||||
accept_event()
|
||||
clicked.emit(card)
|
||||
|
||||
func _draw() -> void:
|
||||
if lit:
|
||||
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
|
||||
for k in card.elements.size():
|
||||
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
|
||||
Style.text(self, "%dx %s" % [count, card.name], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
||||
Style.text(self, "%dx %s" % [count, card.title()], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
||||
Style.text(self, str(card.cost), 15, Style.GOLD, Vector2(size.x - 10, size.y / 2), "midright", true)
|
||||
|
||||
|
||||
@@ -93,10 +99,11 @@ func _ready() -> void:
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(400, 55)
|
||||
search_box.size = Vector2(160, 34)
|
||||
search_box.position = Vector2(424, 55)
|
||||
search_box.size = Vector2(136, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
@@ -150,9 +157,23 @@ func _ready() -> void:
|
||||
curve.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(curve)
|
||||
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), leave)
|
||||
Style.button(self, "Speichern", Rect2(860, 740, 140, 44), save_deck, Color("285a3c"))
|
||||
Style.button(self, "Leeren", Rect2(1008, 740, 70, 44), clear_deck)
|
||||
Style.button(self, "Code", Rect2(1086, 740, 84, 44), open_code)
|
||||
Style.button(self, "Zurück", Rect2(1178, 740, 82, 44), leave)
|
||||
# Deck-Code zum Teilen: kopieren, einfügen, übernehmen
|
||||
code_dialog = Style.overlay(self)
|
||||
Style.panel(code_dialog, Rect2(240, 270, 800, 240), Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(code_dialog, "Deck-Code", 26, Style.TEXT, Rect2(240, 290, 800, 40), "center", true)
|
||||
Style.label(code_dialog, "Zum Teilen kopieren oder einen Code einfügen. Nicht vorhandene Karten werden weggelassen.", 15, Style.MUTED, Rect2(240, 334, 800, 24), "center")
|
||||
code_box = LineEdit.new()
|
||||
code_box.position = Vector2(270, 368)
|
||||
code_box.size = Vector2(740, 36)
|
||||
code_dialog.add_child(code_box)
|
||||
Style.button(code_dialog, "In Zwischenablage", Rect2(270, 430, 180, 44), func(): DisplayServer.clipboard_set(code_box.text))
|
||||
Style.button(code_dialog, "Aus Zwischenablage", Rect2(460, 430, 180, 44), func(): code_box.text = DisplayServer.clipboard_get())
|
||||
Style.button(code_dialog, "Übernehmen", Rect2(650, 430, 170, 44), func(): apply_code(code_box.text), Color("285a3c"))
|
||||
Style.button(code_dialog, "Schließen", Rect2(830, 430, 180, 44), func(): code_dialog.visible = false)
|
||||
confirm = Style.confirm(self, "Ungespeicherte Änderungen", "Das Deck wurde geändert, aber nicht gespeichert.",
|
||||
"Verwerfen", func(): app.show_screen(MenuScreen.new()), "Weiter bearbeiten", Style.GOLD)
|
||||
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
||||
@@ -174,9 +195,11 @@ func leave() -> void:
|
||||
|
||||
|
||||
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():
|
||||
if event.is_action_pressed("ui_cancel") and not search_box.has_focus() and not name_box.has_focus() and not code_box.has_focus():
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
elif code_dialog.visible:
|
||||
code_dialog.visible = false
|
||||
else:
|
||||
leave()
|
||||
|
||||
@@ -244,7 +267,7 @@ func cards() -> Array:
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
var q := search.to_lower()
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
match sort_mode:
|
||||
0:
|
||||
out.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
||||
@@ -308,6 +331,34 @@ func save_deck() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func open_code() -> void:
|
||||
code_box.text = Profile.deck_code(deck)
|
||||
code_dialog.visible = true
|
||||
|
||||
|
||||
## Übernimmt einen Deck-Code; Karten, die in der Sammlung fehlen, werden gemeldet und weggelassen.
|
||||
func apply_code(code: String) -> void:
|
||||
var ids: Array = Profile.parse_deck_code(code)
|
||||
if ids.is_empty():
|
||||
say("Ungültiger Code", false)
|
||||
return
|
||||
var p: Profile = app.profile
|
||||
var missing := {}
|
||||
deck = []
|
||||
for cid in ids:
|
||||
var owned: int = p.collection.get(cid, 0)
|
||||
if deck.count(cid) < owned and deck.size() < Game.DECK_SIZE:
|
||||
deck.append(cid)
|
||||
else:
|
||||
missing[cid] = true
|
||||
code_dialog.visible = false
|
||||
if missing.is_empty():
|
||||
say("Code übernommen", true)
|
||||
else:
|
||||
say(tr("Code übernommen, es fehlen: ") + ", ".join(missing.keys().map(func(cid): return Cards.by_id(cid).title())), false)
|
||||
refresh()
|
||||
|
||||
|
||||
func deck_cards() -> Array:
|
||||
var out := []
|
||||
for cid in deck:
|
||||
@@ -361,7 +412,7 @@ func refresh() -> void:
|
||||
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
||||
var elems := Game.deck_elements(dc)
|
||||
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
||||
elems_label.text = "Elemente: " + (", ".join(elems) if not elems.is_empty() else "–")
|
||||
elems_label.text = tr("Elemente: ") + (", ".join(elems.map(func(e): return tr(e))) if not elems.is_empty() else "–")
|
||||
if not dc.is_empty():
|
||||
elems_label.text += " · " + tr("Stärke %.1f") % Game.deck_rating(dc)
|
||||
var ok: Array = Game.validate_deck(dc, p.collection)
|
||||
|
||||
@@ -9,6 +9,7 @@ var list_label: Label
|
||||
var ip_box: LineEdit
|
||||
var b_host: Button
|
||||
var b_join: Button
|
||||
var b_watch: Button
|
||||
var b_start: Button
|
||||
var ai_buttons: Array = []
|
||||
var n_ai := 0
|
||||
@@ -32,6 +33,7 @@ func _ready() -> void:
|
||||
ip_box.text = "127.0.0.1"
|
||||
hr.add_child(ip_box)
|
||||
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
||||
b_watch = Style.cbutton(hr, "Zuschauen", Vector2(150, 46), _watch)
|
||||
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
||||
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||
Style.spacer(col, 6)
|
||||
@@ -71,6 +73,13 @@ func _join() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Als Zuschauer beitreten: sieht die Partie ohne Hände, spielt nicht mit.
|
||||
func _watch() -> void:
|
||||
net.spectator = true
|
||||
net.join(ip_box.text.strip_edges())
|
||||
refresh()
|
||||
|
||||
|
||||
func _set_ai(n: int) -> void:
|
||||
n_ai = n
|
||||
refresh()
|
||||
@@ -96,9 +105,12 @@ func refresh() -> void:
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
||||
for id in net.spectators:
|
||||
lines.append("%s (%s)" % [net.spectators[id], tr("Zuschauer")])
|
||||
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
||||
b_host.disabled = net.connected
|
||||
b_join.disabled = net.connected
|
||||
b_watch.disabled = net.connected
|
||||
ip_box.editable = not net.connected
|
||||
var humans := ids.size()
|
||||
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
||||
|
||||
@@ -40,6 +40,7 @@ func apply_settings() -> void:
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
profile.refresh_daily()
|
||||
clear_battle_save()
|
||||
save()
|
||||
apply_settings()
|
||||
|
||||
@@ -108,6 +109,16 @@ func save() -> void:
|
||||
profile.save(save_path)
|
||||
|
||||
|
||||
## Datei für einen unterbrochenen Kampf (neben dem Spielstand).
|
||||
func battle_save_path() -> String:
|
||||
return save_path.get_basename() + "_kampf.json"
|
||||
|
||||
|
||||
func clear_battle_save() -> void:
|
||||
if FileAccess.file_exists(battle_save_path()):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(battle_save_path()))
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net != null and net.is_inside_tree():
|
||||
net.leave()
|
||||
|
||||
@@ -8,6 +8,7 @@ var b_hotseat: Button
|
||||
var b_online: Button
|
||||
var b_tutorial: Button
|
||||
var b_challenge: Button
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
var deck_label: Label
|
||||
@@ -36,6 +37,8 @@ func _ready() -> void:
|
||||
var sz := Vector2(270, 44)
|
||||
var p0: Profile = app.profile
|
||||
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
|
||||
if FileAccess.file_exists(app.battle_save_path()):
|
||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||
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)
|
||||
@@ -58,6 +61,17 @@ func _ready() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen.
|
||||
func _resume() -> void:
|
||||
var json := JSON.new()
|
||||
var text := FileAccess.get_file_as_string(app.battle_save_path())
|
||||
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"):
|
||||
app.clear_battle_save()
|
||||
app.show_screen(MenuScreen.new())
|
||||
return
|
||||
app.show_screen(BattleScreen.resume(json.data))
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
var ok: Array = p.deck_valid()
|
||||
@@ -74,7 +88,7 @@ func refresh() -> void:
|
||||
coll_label.text = tr("Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d") % [
|
||||
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.total()]
|
||||
var elems := Game.deck_elements(p.deck_cards())
|
||||
deck_label.text = tr("Aktives Deck: %s (%s)") % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else tr("leer")]
|
||||
deck_label.text = tr("Aktives Deck: %s (%s)") % [p.deck_name(), ", ".join(elems.map(func(e): return tr(e))) if not elems.is_empty() else tr("leer")]
|
||||
var task := p.daily_task()
|
||||
if p.daily.done:
|
||||
daily_label.text = tr("Tagesaufgabe erledigt: %s ✓") % task.text
|
||||
|
||||
@@ -37,6 +37,9 @@ func _ready() -> void:
|
||||
var info := (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 13, Style.MUTED, "left", false, 150)
|
||||
preview_labels[n] = Style.clabel(r, "", 13, Style.TEXT, "left", false, 600)
|
||||
preview_labels[n].autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
preview_labels[n].custom_minimum_size = Vector2(600, 0)
|
||||
preview_labels[n].size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.spacer(col, 4)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
|
||||
@@ -70,7 +70,7 @@ func _ready() -> void:
|
||||
language = OptionButton.new()
|
||||
language.custom_minimum_size = Vector2(280, 36)
|
||||
language.add_item("Deutsch", 0)
|
||||
language.add_item("English (Oberfläche)", 1)
|
||||
language.add_item("English", 1)
|
||||
language.select(1 if s.language == "en" else 0)
|
||||
language.item_selected.connect(_on_language)
|
||||
grid.add_child(language)
|
||||
|
||||
@@ -285,7 +285,7 @@ static func rules_text() -> String:
|
||||
Cards.RESONANCE_TEXT,
|
||||
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
|
||||
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an das stärkste gegnerische Wesen, Blitz 2 Schaden",
|
||||
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
|
||||
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen +1/+1.",
|
||||
"",
|
||||
"Schlüsselwörter:",
|
||||
]
|
||||
@@ -295,9 +295,13 @@ static func rules_text() -> String:
|
||||
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
|
||||
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
|
||||
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
|
||||
lines.append("Elementbooster: 5 Karten eines Elements (und seiner Kombos) für %d Münzen." % Profile.ELEMENT_BOOSTER_COST)
|
||||
lines.append("Deck-Code: im Deckbau unter „Code“ kopieren und mit anderen teilen.")
|
||||
lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.")
|
||||
lines.append("")
|
||||
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
||||
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
||||
lines.append("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.")
|
||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
||||
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
||||
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
||||
|
||||
@@ -24,9 +24,10 @@ func _ready() -> void:
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(400, 55)
|
||||
search_box.position = Vector2(424, 55)
|
||||
search_box.size = Vector2(200, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
@@ -51,7 +52,7 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus():
|
||||
if event.is_action_pressed("ui_cancel") and not search_box.has_focus():
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ func cards() -> Array:
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
var q := search.to_lower()
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
return out
|
||||
|
||||
|
||||
@@ -88,7 +89,7 @@ func dismantle() -> void:
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
say("%s zerlegt (+%d Staub)" % [selected.name, Profile.DUST_DISMANTLE[selected.rarity]], true)
|
||||
say(tr("%s zerlegt (+%d Staub)") % [selected.title(), Profile.DUST_DISMANTLE[selected.rarity]], true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
@@ -102,7 +103,7 @@ func craft() -> void:
|
||||
app.sfx.play("play")
|
||||
app.save()
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
say("%s hergestellt" % selected.name + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
||||
say(tr("%s hergestellt") % selected.title() + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
Reference in New Issue
Block a user