update
This commit is contained in:
@@ -46,24 +46,16 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
popup("Feldkombo %s!" % ev.name, Style.GOLD, where + Vector2(0, -30), 0.1)
|
||||
"play":
|
||||
sfx("play")
|
||||
"spell":
|
||||
sfx("play")
|
||||
var caster: Game.Player = g.players[ev.player]
|
||||
popup(ev.card.name, Style.GOLD, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
if caster != screen.perspective:
|
||||
screen.show_preview(ev.card, true)
|
||||
screen.preview_timer = 1.6
|
||||
"trap_set":
|
||||
sfx("click")
|
||||
popup("Falle gelegt", Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
"trap":
|
||||
"fuse":
|
||||
sfx("combo")
|
||||
popup("Falle: %s!" % ev.card.name, Style.ORANGE, screen.target_center({"kind": "player", "index": ev.player}) + Vector2(0, -20), 0.0)
|
||||
screen.show_preview(ev.card, true)
|
||||
screen.preview_timer = 1.8
|
||||
"mulligan":
|
||||
if ev.count > 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)
|
||||
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
"field":
|
||||
sfx("combo")
|
||||
popup("Feld: %s" % Cards.FIELD_NAMES[ev.element], Cards.ELEMENT_COLORS[ev.element], Vector2(640, 196), 0.0)
|
||||
"buy":
|
||||
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
||||
"reroll":
|
||||
sfx("click")
|
||||
|
||||
|
||||
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
||||
@@ -73,9 +65,24 @@ func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
||||
return c
|
||||
|
||||
|
||||
## Mehrere Meldungen an derselben Stelle rücken nach unten, sonst liegen sie übereinander.
|
||||
func _free_spot(at: Vector2) -> Vector2:
|
||||
for step in 6:
|
||||
var spot := at + Vector2(0, step * 26)
|
||||
var taken := false
|
||||
for l in screen.fx_layer.get_children():
|
||||
if l is Label and l.get_meta("spot", Vector2(-999, -999)).distance_to(spot) < 20:
|
||||
taken = true
|
||||
break
|
||||
if not taken:
|
||||
return spot
|
||||
return at
|
||||
|
||||
|
||||
func popup(text: String, color: Color, at: Vector2, delay: float) -> void:
|
||||
if not enabled or at.x < 0:
|
||||
return
|
||||
at = _free_spot(at)
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", 22)
|
||||
@@ -84,6 +91,7 @@ func popup(text: String, color: Color, at: Vector2, delay: float) -> void:
|
||||
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
l.add_theme_constant_override("outline_size", 6)
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
l.set_meta("spot", at)
|
||||
screen.fx_layer.add_child(l)
|
||||
l.reset_size()
|
||||
l.position = at - l.size / 2
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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.
|
||||
## Gamepad-Steuerung des Kampfes: Steuerkreuz bewegt einen Zeiger über Laden, 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 zone := "shop"
|
||||
var index := 0
|
||||
var targets: Array = []
|
||||
|
||||
@@ -26,7 +27,7 @@ func button(button: int) -> void:
|
||||
if screen.confirm.visible:
|
||||
screen.confirm.visible = false
|
||||
screen.cancel_all()
|
||||
zone = "hand"
|
||||
zone = "shop"
|
||||
JOY_BUTTON_Y:
|
||||
if screen.b_skip.visible:
|
||||
screen.skip_ai = true
|
||||
@@ -41,16 +42,35 @@ func button(button: int) -> void:
|
||||
move(1)
|
||||
JOY_BUTTON_DPAD_UP, JOY_BUTTON_DPAD_DOWN:
|
||||
if zone != "target":
|
||||
zone = "own" if zone == "hand" else "hand"
|
||||
index = 0
|
||||
_next_zone(1 if button == JOY_BUTTON_DPAD_DOWN else -1)
|
||||
screen.refresh()
|
||||
|
||||
|
||||
## Wechselt den Bereich und überspringt leere (eine leere Hand ist keine Station).
|
||||
## step 0: beim aktuellen Bereich bleiben, falls er etwas enthält.
|
||||
func _next_zone(step: int) -> void:
|
||||
var order := ["shop", "hand", "own"]
|
||||
var at := order.find(zone)
|
||||
index = 0
|
||||
if step == 0 and not items().is_empty():
|
||||
return
|
||||
var dir := -1 if step < 0 else 1
|
||||
for i in order.size():
|
||||
at = posmod(at + dir, order.size())
|
||||
zone = order[at]
|
||||
if not items().is_empty():
|
||||
return
|
||||
|
||||
|
||||
func items() -> Array:
|
||||
match zone:
|
||||
"shop":
|
||||
return screen.shop_views
|
||||
"hand":
|
||||
return screen.hand_views
|
||||
"own":
|
||||
if screen.fusing: # zum Verschmelzen zählt jedes eigene Wesen, nicht nur angriffsbereite
|
||||
return screen.own_views
|
||||
return screen.own_views.filter(func(v): return v.creature.can_attack())
|
||||
"target":
|
||||
return targets
|
||||
@@ -64,9 +84,6 @@ func move(dir: int) -> void:
|
||||
|
||||
|
||||
func confirm() -> void:
|
||||
if screen.mulligan.visible:
|
||||
screen.do_mulligan([])
|
||||
return
|
||||
if screen.overlay.visible:
|
||||
screen.leave()
|
||||
return
|
||||
@@ -78,6 +95,8 @@ func confirm() -> void:
|
||||
index = clampi(index, 0, items.size() - 1)
|
||||
var item = items[index]
|
||||
match zone:
|
||||
"shop":
|
||||
screen.on_click("shop", int(item.get_meta("slot", 0)))
|
||||
"hand":
|
||||
screen.on_click("hand", item.card)
|
||||
if screen.pending != null:
|
||||
@@ -113,8 +132,8 @@ func confirm() -> void:
|
||||
for o in screen.hero_panels:
|
||||
if screen.hero_panels[o] == item:
|
||||
screen.on_click("hero", o)
|
||||
zone = "hand"
|
||||
index = 0
|
||||
zone = "own"
|
||||
_next_zone(0)
|
||||
|
||||
|
||||
## Pulsierender Rahmen als Zeiger.
|
||||
|
||||
@@ -30,11 +30,13 @@ static func other_slot(n_others: int, i: int, j: int) -> Rect2:
|
||||
return Rect2(bx + j * (sz[0] + sz[2]), 84, sz[0], sz[1])
|
||||
|
||||
|
||||
## Laden: fünf Angebote über der Hand.
|
||||
static func shop_rect(i: int) -> Rect2:
|
||||
return Rect2(340 + i * 121, 487, 115, 112)
|
||||
|
||||
|
||||
static func hand_rect(count: int, i: int) -> Rect2:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
var x := (1280 - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), 800 - HAND_H - 8, HAND_W, HAND_H)
|
||||
|
||||
|
||||
static func trap_rect(i: int) -> Rect2:
|
||||
return Rect2(400 + i * 78, 490, 70, 98)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://gwdm38e83cbj
|
||||
@@ -14,6 +14,8 @@ var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||
var fx: BattleFx
|
||||
var selected = null # Game.Creature
|
||||
var pending = null # Cards.Card, wartet auf Ziel
|
||||
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken
|
||||
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung
|
||||
var msg_t := 0.0
|
||||
var ai_delay := 0.75
|
||||
var ai_timer := 0.0
|
||||
@@ -24,7 +26,6 @@ var reward := -1
|
||||
var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||
var pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
get:
|
||||
@@ -42,9 +43,9 @@ var undo_t := 0.0
|
||||
# Ansichten (auch für Tests)
|
||||
var tiles := {}
|
||||
var hand_views: Array = []
|
||||
var shop_views: Array = []
|
||||
var own_views: Array = []
|
||||
var enemy_views: Array = []
|
||||
var trap_views: Array = []
|
||||
var hero_panels := {} # Player -> Panel
|
||||
var hero_labels := {}
|
||||
var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
@@ -52,43 +53,29 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_layer: Control
|
||||
var trap_layer: Control
|
||||
var shop_layer: Control
|
||||
var zoom_layer: Control
|
||||
var fx_layer: Control
|
||||
var log_box: RichTextLabel
|
||||
var preview_holder: Control
|
||||
var preview_hint: Label
|
||||
var bar_panel: Panel
|
||||
var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
var deck_label: Label
|
||||
var income_label: Label
|
||||
var status_label: Label
|
||||
var msg_label: Label
|
||||
var b_end: Button
|
||||
var b_fuse: Button
|
||||
var b_reroll: Button
|
||||
var b_giveup: Button
|
||||
var field_label: Label
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var b_save_leave: 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 wait_label: Label
|
||||
var b_skip: Button
|
||||
@@ -110,7 +97,6 @@ static func online(seat: int) -> BattleScreen:
|
||||
## 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.played_ids = Array(data.get("played_ids", []))
|
||||
s.saved_game = Game.from_snapshot(data.game)
|
||||
return s
|
||||
|
||||
@@ -155,17 +141,19 @@ func _ready() -> void:
|
||||
game = saved_game
|
||||
elif stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
|
||||
game = Game.new_battle_with(Campaign.opponents(stage), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
||||
game = Game.new_battle(n_opp, Game.make_rng(), own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||
_begin()
|
||||
|
||||
|
||||
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar) und Zielstärke der Gegner.
|
||||
## Kartenoptik, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
||||
func own_rules() -> Dictionary:
|
||||
var p: Profile = app.profile
|
||||
var rules := {"target_rating": p.target_rating(), "tier": str(p.difficulty().tier)}
|
||||
var rules := {"looks": p.looks()}
|
||||
if stage < 0:
|
||||
rules["opponent_life"] = int(p.difficulty().life)
|
||||
if p.perk != "" and p.perk_unlocked():
|
||||
rules["human_perks"] = [p.perk]
|
||||
return rules
|
||||
@@ -188,11 +176,11 @@ func _begin() -> void:
|
||||
|
||||
|
||||
func _build_static() -> void:
|
||||
Style.panel(self, Rect2(10, 205, 320, 340))
|
||||
Style.panel(self, Rect2(10, 205, 320, 290))
|
||||
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20))
|
||||
log_box = RichTextLabel.new()
|
||||
log_box.position = Vector2(18, 230)
|
||||
log_box.size = Vector2(304, 308)
|
||||
log_box.size = Vector2(304, 258)
|
||||
log_box.bbcode_enabled = true
|
||||
log_box.scroll_following = true
|
||||
log_box.add_theme_font_size_override("normal_font_size", 13)
|
||||
@@ -200,13 +188,12 @@ func _build_static() -> void:
|
||||
add_child(log_box)
|
||||
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
||||
Style.panel(self, Rect2(950, 205, 320, 380))
|
||||
preview_hint = Style.label(self, "Karte überfahren für Details", 14, Style.MUTED, Rect2(950, 345, 320, 24), "center")
|
||||
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10)
|
||||
bar_panel = Style.panel(self, BattleLayout.BAR)
|
||||
name_label = Style.label(self, "Du", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
||||
name_label = Style.label(self, "", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
||||
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true)
|
||||
deck_label = Style.label(self, "", 16, Style.MUTED, Rect2(780, 424, 150, 24), "right")
|
||||
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 24), "right")
|
||||
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
||||
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
||||
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
||||
@@ -215,20 +202,21 @@ func _build_static() -> void:
|
||||
b_skip.visible = false
|
||||
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
|
||||
b_undo.visible = false
|
||||
field_label = Style.label(self, "", 15, Style.MUTED, Rect2(340, 211, 600, 24), "center")
|
||||
b_fuse = Style.button(self, "Verschmelzen", Rect2(10, 503, 150, 40), toggle_fuse, Color("3c2858"))
|
||||
b_fuse.visible = false
|
||||
b_reroll = Style.button(self, "", Rect2(170, 503, 150, 40), do_reroll, Color("285a5a"))
|
||||
b_reroll.add_theme_font_size_override("font_size", 14)
|
||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||
wait_label.visible = false
|
||||
hero_layer = _layer()
|
||||
tile_layer = _layer()
|
||||
trap_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
shop_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
zoom_layer = _layer()
|
||||
fx_layer = _layer()
|
||||
cursor_layer = _layer()
|
||||
# Mulligan
|
||||
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)
|
||||
@@ -239,7 +227,6 @@ func _build_static() -> void:
|
||||
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)
|
||||
@@ -265,7 +252,7 @@ func resumable() -> bool:
|
||||
func save_battle() -> void:
|
||||
if not resumable() or game == null or game.winner != "":
|
||||
return
|
||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "played_ids": played_ids, "game": game.snapshot(-1)}
|
||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "game": game.snapshot(-1)}
|
||||
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(d))
|
||||
@@ -371,7 +358,7 @@ func my_turn() -> bool:
|
||||
|
||||
|
||||
func any_overlay() -> bool:
|
||||
return confirm.visible or mulligan.visible or overlay.visible
|
||||
return confirm.visible or overlay.visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -389,7 +376,7 @@ func _input(event: InputEvent) -> void:
|
||||
KEY_ESCAPE:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
elif selected != null or pending != null or zoom_player != null:
|
||||
elif selected != null or pending != null or fusing or zoom_player != null:
|
||||
cancel_all()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
@@ -402,9 +389,7 @@ func _input(event: InputEvent) -> void:
|
||||
if b_undo.visible:
|
||||
undo()
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
if mulligan.visible:
|
||||
do_mulligan([])
|
||||
elif overlay.visible:
|
||||
if overlay.visible:
|
||||
leave()
|
||||
_:
|
||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
||||
@@ -444,8 +429,6 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
var cur := game.current()
|
||||
if cur.is_human:
|
||||
if cur == perspective and not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible:
|
||||
show_mulligan()
|
||||
return
|
||||
if is_client():
|
||||
return
|
||||
@@ -477,7 +460,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func _start_ai(cur: Game.Player) -> void:
|
||||
ai_copy = game.clone_for(cur)
|
||||
ai_copy = game.clone()
|
||||
var copy := ai_copy
|
||||
var idx := game.current_idx
|
||||
var box := ai_box
|
||||
@@ -494,7 +477,7 @@ func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if counts_for_profile():
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life)
|
||||
reward = app.profile.record_battle(won, n_opp, stage, factor)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
@@ -513,19 +496,21 @@ func say(m: String) -> void:
|
||||
func cancel() -> void:
|
||||
selected = null
|
||||
pending = null
|
||||
fusing = false
|
||||
fuse_first = null
|
||||
|
||||
|
||||
func play(card: Cards.Card, target) -> void:
|
||||
if is_client():
|
||||
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
|
||||
return
|
||||
var before: Game = game.clone() if card.is_creature() and not is_online() else null
|
||||
var before: Game = game.clone() if not is_online() else null
|
||||
var log_len := game.log.size()
|
||||
var err := game.play_card(perspective, card, target)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
return
|
||||
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte, Kombos, Fallen)
|
||||
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
|
||||
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||
undo_state = before
|
||||
undo_log_len = log_len
|
||||
@@ -534,6 +519,52 @@ func play(card: Cards.Card, target) -> void:
|
||||
clear_undo()
|
||||
|
||||
|
||||
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse).
|
||||
func toggle_fuse() -> void:
|
||||
var on := not fusing
|
||||
cancel()
|
||||
fusing = on
|
||||
say(tr("Zwei eigene Wesen wählen") if on else tr("Abgebrochen"))
|
||||
after_action()
|
||||
|
||||
|
||||
## Kauft ein Angebot aus dem Laden auf die Hand.
|
||||
func do_buy(slot: int) -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "buy", "slot": slot})
|
||||
return
|
||||
var err := game.buy(perspective, slot)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
|
||||
|
||||
## Würfelt den Laden neu (kostet Mana).
|
||||
func do_reroll() -> void:
|
||||
if not my_turn() or any_overlay():
|
||||
return
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "reroll"})
|
||||
after_action()
|
||||
return
|
||||
var err := game.reroll(perspective)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
after_action()
|
||||
|
||||
|
||||
func do_fuse(a, b) -> void:
|
||||
cancel()
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
|
||||
return
|
||||
var err := game.fuse(perspective, a, b)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
|
||||
|
||||
func clear_undo() -> void:
|
||||
undo_state = null
|
||||
undo_t = 0.0
|
||||
@@ -574,26 +605,23 @@ func end_turn() -> void:
|
||||
after_action()
|
||||
|
||||
|
||||
func show_mulligan() -> void:
|
||||
mulligan.open(perspective.hand)
|
||||
|
||||
|
||||
func do_mulligan(idxs: Array) -> void:
|
||||
mulligan.visible = false
|
||||
if is_client():
|
||||
net.send_action({"type": "mulligan", "hand": idxs})
|
||||
return
|
||||
var cards := []
|
||||
for i in idxs:
|
||||
cards.append(perspective.hand[i])
|
||||
game.mulligan(perspective, cards)
|
||||
after_action()
|
||||
|
||||
|
||||
func on_click(kind: String, obj) -> void:
|
||||
var g := game
|
||||
if not my_turn() or any_overlay():
|
||||
return
|
||||
if fusing:
|
||||
if kind != "own":
|
||||
cancel()
|
||||
say(tr("Abgebrochen"))
|
||||
elif fuse_first == null:
|
||||
fuse_first = obj
|
||||
say(tr("Zweites Wesen wählen"))
|
||||
elif obj == fuse_first:
|
||||
fuse_first = null
|
||||
else:
|
||||
do_fuse(fuse_first, obj)
|
||||
after_action()
|
||||
return
|
||||
if pending != null:
|
||||
var card = pending
|
||||
pending = null
|
||||
@@ -619,6 +647,8 @@ func on_click(kind: String, obj) -> void:
|
||||
after_action()
|
||||
return
|
||||
match kind:
|
||||
"shop":
|
||||
do_buy(int(obj))
|
||||
"hand":
|
||||
var ok := g.can_play(perspective, obj)
|
||||
if not ok[0]:
|
||||
@@ -647,61 +677,17 @@ func on_tile_click(cr: Game.Creature) -> void:
|
||||
on_click("enemy", cr)
|
||||
|
||||
|
||||
func show_preview(card, entered: bool) -> void:
|
||||
func show_preview(card, entered: bool, owner_looks: Dictionary = {}) -> void:
|
||||
Style.clear(preview_holder)
|
||||
if entered and card != null:
|
||||
var v := CardView.new(card, Vector2(210, 294))
|
||||
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||
v.position = Vector2(1005, 215)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
preview_hint.visible = not (entered and card != null)
|
||||
|
||||
|
||||
## Vorschau einer Handkarte mit Hinweisen auf Resonanz und Feldkombos mit dem eigenen Feld.
|
||||
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
|
||||
Style.clear(preview_holder)
|
||||
preview_hint.visible = not entered
|
||||
if not entered:
|
||||
return
|
||||
var hints: Array[String] = combo_hints(card)
|
||||
var v := CardView.new(card, Vector2(190, 266) if not hints.is_empty() else Vector2(210, 294))
|
||||
v.position = Vector2(1015, 212) if not hints.is_empty() else Vector2(1005, 215)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
if not hints.is_empty():
|
||||
var box := RichTextLabel.new()
|
||||
box.position = Vector2(958, 480)
|
||||
box.size = Vector2(304, 46)
|
||||
box.bbcode_enabled = true
|
||||
box.scroll_active = false
|
||||
box.fit_content = false
|
||||
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
box.add_theme_font_size_override("normal_font_size", 11)
|
||||
box.add_theme_color_override("default_color", Style.GOLD)
|
||||
box.text = "[center]" + "\n".join(hints.slice(0, 2)) + "[/center]"
|
||||
preview_holder.add_child(box)
|
||||
|
||||
|
||||
func combo_hints(card: Cards.Card) -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
if not card.is_creature():
|
||||
return out
|
||||
var partners: Array[String] = []
|
||||
var combos := {}
|
||||
for other in perspective.board:
|
||||
if other.card.elements.any(func(e): return e in card.elements):
|
||||
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.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:
|
||||
out.append("Feldkombo %s mit %s: %s" % [name, combos[name], Cards.FIELD_COMBO_TEXT[name]])
|
||||
return out
|
||||
show_preview(card, entered, perspective.looks)
|
||||
|
||||
|
||||
# --- Netzwerk -------------------------------------------------------------------------------
|
||||
@@ -751,8 +737,6 @@ func after_action() -> void:
|
||||
net.broadcast()
|
||||
var evs := game.events.duplicate()
|
||||
game.events.clear()
|
||||
if counts_for_profile():
|
||||
_note_events(evs)
|
||||
var centers := {}
|
||||
for ev in evs:
|
||||
for key in ["target", "attacker", "source"]:
|
||||
@@ -763,22 +747,6 @@ func after_action() -> void:
|
||||
save_battle()
|
||||
|
||||
|
||||
func _note_events(evs: Array) -> void:
|
||||
var hi := game.players.find(game.human)
|
||||
for ev in evs:
|
||||
if ev.get("player", -1) != hi:
|
||||
continue
|
||||
match ev.type:
|
||||
"play", "spell":
|
||||
played_ids.append(ev.card.id)
|
||||
if ev.type == "play":
|
||||
app.profile.daily_event("creature_played")
|
||||
"trap_set":
|
||||
app.profile.daily_event("trap_set")
|
||||
"combo":
|
||||
app.profile.daily_event("combo")
|
||||
|
||||
|
||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||
|
||||
func refresh() -> void:
|
||||
@@ -790,9 +758,9 @@ func refresh() -> void:
|
||||
var mine := my_turn()
|
||||
var valid: Array = g.valid_targets(h, pending.target_type()) if pending != null else []
|
||||
hand_views = []
|
||||
shop_views = []
|
||||
own_views = []
|
||||
enemy_views = []
|
||||
trap_views = []
|
||||
|
||||
for o in others():
|
||||
var panel: Panel = hero_panels[o]
|
||||
@@ -813,8 +781,8 @@ func refresh() -> void:
|
||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||
if o.alive():
|
||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||
var traps_txt := (" F %d" if compact() else " Fallen %d") % o.traps.size() if not o.traps.is_empty() else ""
|
||||
labels.stats.text = ("H %d D %d%s ◆ %d" if compact() else "Hand %d Deck %d%s ◆ %d") % [o.hand.size(), o.deck.size(), traps_txt, o.max_energy]
|
||||
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \
|
||||
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy])
|
||||
else:
|
||||
labels.life.text = "besiegt"
|
||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||
@@ -840,7 +808,8 @@ func refresh() -> void:
|
||||
tile.slot = rect.position
|
||||
tiles[cr.uid] = tile
|
||||
tile_layer.add_child(tile)
|
||||
tile.hover.connect(show_preview)
|
||||
var looks: Dictionary = p.looks
|
||||
tile.hover.connect(func(card, entered): show_preview(card, entered, looks))
|
||||
var uid := cr.uid
|
||||
tile.clicked.connect(func(_v):
|
||||
var c := game.find_creature(uid)
|
||||
@@ -852,9 +821,12 @@ func refresh() -> void:
|
||||
fx.move_to(tile, rect.position)
|
||||
var b = null
|
||||
var w := 2
|
||||
if cr == selected:
|
||||
if cr == selected or cr == fuse_first:
|
||||
b = Style.YELLOW
|
||||
w = 4
|
||||
elif fusing and p == h and mine:
|
||||
b = Style.PURPLE
|
||||
w = 3
|
||||
elif cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
||||
b = Style.ORANGE
|
||||
w = 3
|
||||
@@ -874,15 +846,23 @@ func refresh() -> void:
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
|
||||
Style.clear(trap_layer)
|
||||
if not h.traps.is_empty():
|
||||
Style.label(trap_layer, "Fallen", 13, Style.MUTED, Rect2(335, 492, 60, 20))
|
||||
for i in h.traps.size():
|
||||
var tv := CardView.new(h.traps[i], BattleLayout.trap_rect(i).size)
|
||||
tv.position = BattleLayout.trap_rect(i).position
|
||||
tv.hover.connect(show_preview)
|
||||
trap_layer.add_child(tv)
|
||||
trap_views.append(tv)
|
||||
Style.clear(shop_layer)
|
||||
if not spectating():
|
||||
for i in h.shop.size():
|
||||
var offer = h.shop[i]
|
||||
var sr := BattleLayout.shop_rect(i)
|
||||
if offer == null:
|
||||
Style.panel(shop_layer, sr, Style.darker(Style.PANEL, 0.8), 8, Style.PANEL_LIGHT, 2)
|
||||
continue
|
||||
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
||||
sv.position = sr.position
|
||||
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
||||
sv.hover.connect(show_hand_preview)
|
||||
var slot := i
|
||||
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
||||
sv.clicked.connect(func(_view): on_click("shop", slot))
|
||||
shop_layer.add_child(sv)
|
||||
shop_views.append(sv)
|
||||
|
||||
Style.clear(hand_layer)
|
||||
if spectating():
|
||||
@@ -892,7 +872,7 @@ func refresh() -> void:
|
||||
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)
|
||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
||||
v.position = r.position
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
@@ -910,8 +890,8 @@ func refresh() -> void:
|
||||
bar_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
||||
name_label.text = h.name
|
||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
||||
deck_label.text = "Deck %d" % h.deck.size()
|
||||
energy_label.text = "◆ %d" % h.energy
|
||||
income_label.text = tr("+%d je Zug") % h.max_energy
|
||||
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
|
||||
@@ -920,13 +900,29 @@ func refresh() -> void:
|
||||
b_skip.visible = ai_running
|
||||
b_end.visible = not ai_running and not spectating()
|
||||
b_undo.visible = undo_state != null and mine
|
||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||
b_reroll.visible = mine and not spectating()
|
||||
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST
|
||||
b_reroll.disabled = h.energy < Game.REROLL_COST
|
||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||
if g.field_element == "":
|
||||
var parts: Array[String] = []
|
||||
for e in Cards.ELEMENTS:
|
||||
parts.append("%s %d" % [tr(e), int(g.field_charge.get(e, 0))])
|
||||
field_label.text = tr("Feld: neutral") + " · " + " ".join(parts)
|
||||
field_label.add_theme_color_override("font_color", Style.MUTED)
|
||||
else:
|
||||
field_label.text = "%s: %s" % [tr(Cards.FIELD_NAMES[g.field_element]), tr(Cards.FIELD_TEXT[g.field_element])]
|
||||
field_label.add_theme_color_override("font_color", Cards.ELEMENT_COLORS[g.field_element])
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
var line := g.log[i]
|
||||
if line.begins_with("---"):
|
||||
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
||||
elif line.begins_with("Feldkombo") or line.begins_with("Falle von"):
|
||||
elif line.begins_with("Das Feld wird"):
|
||||
lines.append("[color=#8c78dc]%s[/color]" % line)
|
||||
elif line.begins_with("Feldkombo"):
|
||||
lines.append("[color=#f5c850]%s[/color]" % line)
|
||||
else:
|
||||
lines.append(line)
|
||||
@@ -954,7 +950,6 @@ func refresh() -> void:
|
||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
||||
overlay.visible = true
|
||||
confirm.visible = false
|
||||
mulligan.visible = false
|
||||
|
||||
|
||||
func build_zoom(valid: Array) -> void:
|
||||
@@ -963,7 +958,7 @@ func build_zoom(valid: Array) -> void:
|
||||
var box := Rect2(190, 190, 900, 300)
|
||||
var back := Style.panel(zoom_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||||
back.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d Deck %d Fallen %d ◆ %d" % [o.name, o.title, o.life, o.hand.size(), o.deck.size(), o.traps.size(), o.max_energy],
|
||||
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d ◆ %d (+%d)" % [o.name, o.title, o.life, o.hand.size(), o.energy, o.max_energy],
|
||||
18, Style.TEXT, Rect2(box.position.x + 20, box.position.y + 12, 800, 30), "left", true)
|
||||
for j in o.board.size():
|
||||
var cr: Game.Creature = o.board[j]
|
||||
|
||||
@@ -5,17 +5,13 @@ const CARD_W := 200
|
||||
const CARD_H := 280
|
||||
|
||||
var app
|
||||
var cards: Array = []
|
||||
var cards: Array = [] # {"card", "rarity", "upgraded", "dust"} je Karte
|
||||
var revealed := {}
|
||||
var new_ids := {}
|
||||
var coins_label: Label
|
||||
var msg_label: Label
|
||||
var b_buy: Button
|
||||
var b_all: Button
|
||||
var b_improve: Button
|
||||
var suggestion := ""
|
||||
var slots: Control
|
||||
var element_buttons: Array = []
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
@@ -46,28 +42,19 @@ class CardBack extends Control:
|
||||
|
||||
func slot_rect(i: int) -> Rect2:
|
||||
var gap := 20
|
||||
var x0 := (1280 - 5 * CARD_W - 4 * gap) / 2
|
||||
var x0 := (1280 - Profile.BOOSTER_SIZE * CARD_W - (Profile.BOOSTER_SIZE - 1) * gap) / 2
|
||||
return Rect2(x0 + i * (CARD_W + gap), 230, CARD_W, CARD_H)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Booster", 36, Style.GOLD, Rect2(0, 20, 1280, 50), "center", true)
|
||||
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, "", 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(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
@@ -77,10 +64,9 @@ func _input(event: InputEvent) -> void:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func buy(element: String = "") -> void:
|
||||
func buy() -> void:
|
||||
var p: Profile = app.profile
|
||||
var before: Dictionary = p.collection.duplicate()
|
||||
var got: Array = p.open_booster(null, element)
|
||||
var got: Array = p.open_booster()
|
||||
if got.is_empty():
|
||||
msg_label.text = "Nicht genug Münzen"
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
@@ -89,13 +75,7 @@ func buy(element: String = "") -> void:
|
||||
app.sfx.play("play")
|
||||
cards = got
|
||||
revealed = {}
|
||||
new_ids = {}
|
||||
for c in cards:
|
||||
if before.get(c.id, 0) == 0:
|
||||
new_ids[c.id] = true
|
||||
var notes: Array[String] = p.take_notices()
|
||||
msg_label.text = " · ".join(notes)
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
||||
msg_label.text = ""
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -106,59 +86,32 @@ func reveal(i: int) -> void:
|
||||
|
||||
|
||||
func reveal_all() -> void:
|
||||
for i in 5:
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
revealed[i] = true
|
||||
refresh()
|
||||
|
||||
|
||||
## Nimmt die vorgeschlagene Karte ins aktive Deck (ersetzt die schwächste).
|
||||
func improve() -> void:
|
||||
if suggestion == "":
|
||||
return
|
||||
var err: String = app.profile.improve_deck_with(suggestion)
|
||||
if err == "":
|
||||
app.save()
|
||||
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:
|
||||
msg_label.text = err
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
refresh()
|
||||
|
||||
|
||||
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 = ""
|
||||
if not cards.is_empty() and revealed.size() >= 5:
|
||||
var ids: Array = cards.map(func(c): return c.id)
|
||||
var better: Array = p.deck_suggestions(ids)
|
||||
if not better.is_empty():
|
||||
suggestion = better[0]
|
||||
b_improve.visible = suggestion != ""
|
||||
if suggestion != "":
|
||||
b_improve.text = tr("Deck verbessern: %s ins aktive Deck (%s)") % [Cards.by_id(suggestion).title(), p.deck_name()]
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= Profile.BOOSTER_SIZE
|
||||
Style.clear(slots)
|
||||
for i in 5:
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
var r := slot_rect(i)
|
||||
if cards.is_empty():
|
||||
Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2)
|
||||
elif revealed.has(i):
|
||||
var c: Cards.Card = cards[i]
|
||||
var v := CardView.new(c, r.size)
|
||||
var res: Dictionary = cards[i]
|
||||
var c: Cards.Card = res.card
|
||||
var v := CardView.new(c, r.size, false, res.rarity)
|
||||
v.position = r.position
|
||||
slots.add_child(v)
|
||||
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)
|
||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
if res.upgraded:
|
||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
else:
|
||||
Style.label(slots, "jetzt %dx" % p.collection[c.id], 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||
else:
|
||||
var back := CardBack.new(r.size)
|
||||
back.position = r.position
|
||||
|
||||
@@ -70,7 +70,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, "Alle drei Kapitel abgeschlossen. Der Archimagier ist endgültig besiegt!", 16, Style.GREEN, "center", true)
|
||||
Style.clabel(col, "Alle Kapitel abgeschlossen", 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()))
|
||||
|
||||
@@ -3,7 +3,6 @@ extends ScrollContainer
|
||||
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte.
|
||||
|
||||
signal card_clicked(card: Cards.Card)
|
||||
signal card_hover(card: Cards.Card, entered: bool)
|
||||
|
||||
const COLS := 7
|
||||
const CW := 100
|
||||
@@ -25,17 +24,16 @@ func _init(rect: Rect2) -> void:
|
||||
add_child(grid)
|
||||
|
||||
|
||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool
|
||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable) -> void:
|
||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool; rarity_fn(card) -> String (Optik)
|
||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable, rarity_fn: Callable = Callable()) -> void:
|
||||
Style.clear(grid)
|
||||
for c in cards:
|
||||
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), true)
|
||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c) if rarity_fn.is_valid() else "gewöhnlich")
|
||||
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))
|
||||
cell.add_child(v)
|
||||
var text: String = label_fn.call(c)
|
||||
Style.label(cell, text, 12, Style.MUTED if v.dim else Style.TEXT, Rect2(0, CH + 1, CW, 16), "center")
|
||||
|
||||
@@ -7,12 +7,14 @@ signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var card: Cards.Card
|
||||
var border = null # Color oder null (dann Seltenheitsfarbe)
|
||||
var rarity := "gewöhnlich" # nur Optik: Rahmenfarbe der eigenen Stufe
|
||||
var dim := false
|
||||
|
||||
|
||||
## 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:
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
||||
card = c
|
||||
rarity = rar
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
@@ -45,7 +47,7 @@ func _draw() -> void:
|
||||
var f_text := 14 if big else (11 if mid else 10)
|
||||
var f_stat := 24 if big else (18 if mid else 15)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[card.rarity]
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[rarity]
|
||||
Style.border(self, rect, bcol, 3 if big else 2, 8)
|
||||
# Kosten
|
||||
var r := 16 if big else 12
|
||||
@@ -57,17 +59,17 @@ func _draw() -> void:
|
||||
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 = (tr(card.combo()) + " · " + tr(card.kind)) if card.is_combo() else tr(card.kind)
|
||||
var sub: String = tr(card.combo()) if card.is_combo() else ""
|
||||
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)
|
||||
var text_w: float = size.x - 20
|
||||
var needed: float = Style.FONT.get_multiline_string_size(card.text(), HORIZONTAL_ALIGNMENT_LEFT, text_w, tsize).y + 8 if card.text() != "" else size.y * 0.42
|
||||
var box_bottom: float = size.y - (28 if big else 22)
|
||||
var box_top: float = maxf(size.y * 0.30, box_bottom - maxf(needed, size.y * 0.42))
|
||||
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) # unter Name und Kombo
|
||||
var box_top: float = maxf(maxf(size.y * 0.30, band_top), box_bottom - maxf(needed, size.y * 0.42))
|
||||
var box := Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||
# Bild
|
||||
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0)
|
||||
var band_bottom: float = box_top - 4
|
||||
if band_bottom - band_top > 14:
|
||||
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
||||
@@ -77,9 +79,8 @@ func _draw() -> void:
|
||||
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, 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)
|
||||
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():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name CollectionScreen
|
||||
extends Control
|
||||
## Sammlung: alle Karten mit Besitzstand; ausgewählte Karten lassen sich
|
||||
## zerlegen (Staub) oder herstellen.
|
||||
## Sammlung: alle Karten gehören dir. Gesammelt wird die Seltenheitsstufe — reine Optik.
|
||||
## Aufwerten kostet Staub, eine Stufe abgeben bringt Staub.
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
@@ -14,14 +14,11 @@ var msg_label: Label
|
||||
var preview_holder: Control
|
||||
var info_label: Label
|
||||
var b_dismantle: Button
|
||||
var b_craft: Button
|
||||
var b_extras: Button
|
||||
var b_upgrade: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.label(self, "Karte anklicken, dann zerlegen oder herstellen. Überschuss: alles über %d Kopien." % Game.MAX_COPIES,
|
||||
14, Style.MUTED, Rect2(240, 18, 600, 24))
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
@@ -44,10 +41,9 @@ func _ready() -> void:
|
||||
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(preview_holder)
|
||||
info_label = Style.label(self, "", 14, Style.MUTED, Rect2(860, 396, 390, 66), "center")
|
||||
b_dismantle = Style.button(self, "", Rect2(860, 470, 390, 44), dismantle, Color("5a3c28"))
|
||||
b_craft = Style.button(self, "", Rect2(860, 524, 390, 44), craft, Color("285a3c"))
|
||||
b_extras = Style.button(self, "", Rect2(860, 600, 390, 44), dismantle_extras)
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 660, 390, 48), "center")
|
||||
b_upgrade = Style.button(self, "", Rect2(860, 480, 390, 44), upgrade, Color("285a3c"))
|
||||
b_dismantle = Style.button(self, "", Rect2(860, 534, 390, 44), dismantle, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 600, 390, 48), "center")
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
@@ -83,71 +79,57 @@ func say(m: String, ok: bool) -> void:
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
|
||||
|
||||
|
||||
func upgrade() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var err: String = app.profile.upgrade(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("play")
|
||||
app.save()
|
||||
say(tr("%s ist jetzt %s") % [selected.title(), tr(app.profile.rarity(selected.id))], true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func dismantle() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var lvl: int = app.profile.level(selected.id)
|
||||
var err: String = app.profile.dismantle(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
say(tr("%s zerlegt (+%d Staub)") % [selected.title(), Profile.DUST_DISMANTLE[selected.rarity]], true)
|
||||
say(tr("%s zerlegt (+%d Staub)") % [selected.title(), Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]], true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func craft() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var err: String = app.profile.craft(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("play")
|
||||
app.save()
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
say(tr("%s hergestellt") % selected.title() + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func dismantle_extras() -> void:
|
||||
var got: int = app.profile.dismantle_extras()
|
||||
if got > 0:
|
||||
app.save()
|
||||
say("Überschuss zerlegt: +%d Staub" % got, true)
|
||||
else:
|
||||
say("Kein Überschuss vorhanden", false)
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
dust_label.text = "Staub: %d" % p.dust
|
||||
grid.fill(cards(),
|
||||
func(c): return "%dx" % p.collection.get(c.id, 0) if p.collection.get(c.id, 0) > 0 else "fehlt",
|
||||
func(c): return p.collection.get(c.id, 0) == 0)
|
||||
grid.fill(cards(), func(c): return tr(p.rarity(c.id)), func(_c): return false, func(c): return p.rarity(c.id))
|
||||
Style.clear(preview_holder)
|
||||
if selected == null:
|
||||
info_label.text = "Keine Karte gewählt"
|
||||
b_upgrade.text = "Aufwerten"
|
||||
b_dismantle.text = "Zerlegen"
|
||||
b_craft.text = "Herstellen"
|
||||
b_upgrade.disabled = true
|
||||
b_dismantle.disabled = true
|
||||
b_craft.disabled = true
|
||||
return
|
||||
var v := CardView.new(selected, Vector2(200, 280), false, p.rarity(selected.id))
|
||||
v.position = Vector2(955, 100)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var lvl := p.level(selected.id)
|
||||
info_label.text = "%s · %s" % [tr(selected.combo()), tr(p.rarity(selected.id))]
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
b_upgrade.text = "Höchste Stufe"
|
||||
b_upgrade.disabled = true
|
||||
else:
|
||||
var v := CardView.new(selected, Vector2(200, 280))
|
||||
v.position = Vector2(955, 100)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var owned: int = p.collection.get(selected.id, 0)
|
||||
var st: Dictionary = p.card_stats.get(selected.id, {"played": 0, "games": 0, "wins": 0})
|
||||
info_label.text = "%s · %s · Wert %.1f\nIm Besitz: %dx · Gespielt %dx · %d Partien, %d Siege" % [
|
||||
selected.combo(), selected.rarity, Cards.card_value(selected), owned, st.played, st.games, st.wins]
|
||||
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[selected.rarity]
|
||||
b_craft.text = "Herstellen (%d Staub)" % Profile.DUST_CRAFT[selected.rarity]
|
||||
b_dismantle.disabled = owned == 0
|
||||
b_craft.disabled = p.dust < Profile.DUST_CRAFT[selected.rarity]
|
||||
var extras := 0
|
||||
for cid in p.collection:
|
||||
extras += maxi(0, p.collection[cid] - Game.MAX_COPIES)
|
||||
b_extras.text = "Überschuss zerlegen (%d Karten)" % extras
|
||||
b_extras.disabled = extras == 0
|
||||
var next: String = Cards.RARITIES[lvl + 1]
|
||||
b_upgrade.text = "Aufwerten zu %s (%d Staub)" % [tr(next), Profile.DUST_UPGRADE[next]]
|
||||
b_upgrade.disabled = p.dust < Profile.DUST_UPGRADE[next]
|
||||
b_dismantle.text = "Zerlegen" if lvl == 0 else "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
b_dismantle.disabled = lvl == 0
|
||||
|
||||
@@ -36,7 +36,9 @@ func _draw() -> void:
|
||||
var small := size.x < 100
|
||||
var mini := size.x < 60
|
||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[creature.card.rarity]
|
||||
var owner = creature.owner()
|
||||
var lvl: int = int(owner.looks.get(creature.card.id, 0)) if owner != null else 0
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[Cards.RARITIES[lvl]]
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
@@ -57,12 +59,14 @@ func _draw() -> void:
|
||||
badges.append("Eis")
|
||||
if creature.resonance > 0:
|
||||
badges.append("Resonanz")
|
||||
if creature.field_bonus > 0:
|
||||
badges.append("Feld")
|
||||
var y := 18.0 if small else 22.0
|
||||
for b in badges:
|
||||
Style.text(self, b, 10 if small else 12, Color("ebebf5"), Vector2(size.x / 2, y), "midtop")
|
||||
y += 12 if small else 15
|
||||
var f := 16 if small else 20
|
||||
var atk_col := Style.GREEN if creature.resonance > 0 else Style.YELLOW
|
||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), f, atk_col, Vector2(6, size.y - 4), "bottomleft", true)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(self, str(creature.health), f, hp_col, Vector2(size.x - 6, size.y - 4), "bottomright", true)
|
||||
@@ -83,8 +87,10 @@ func _draw_mini() -> void:
|
||||
letters.append("E")
|
||||
if creature.resonance > 0:
|
||||
letters.append("R")
|
||||
if creature.field_bonus > 0:
|
||||
letters.append("F")
|
||||
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
||||
var atk_col := Style.GREEN if creature.resonance > 0 else Style.YELLOW
|
||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(self, str(creature.health), 13, hp_col, Vector2(size.x - 4, size.y - 2), "bottomright", true)
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
class_name DeckScreen
|
||||
extends Control
|
||||
## Deckbau mit fünf Slots, Suche, Sortierung und fehlenden Karten.
|
||||
|
||||
const SORTS := ["Kosten", "Name", "Element", "Seltenheit"]
|
||||
|
||||
var app
|
||||
var slot := 0
|
||||
var deck: Array = []
|
||||
var filter := ""
|
||||
var search := ""
|
||||
var sort_mode := 0
|
||||
var show_missing := false
|
||||
var msg_label: Label
|
||||
var grid: CardGrid
|
||||
var deck_list: Control
|
||||
var deck_label: Label
|
||||
var elems_label: Label
|
||||
var valid_label: Label
|
||||
var active_label: Label
|
||||
var curve: Control
|
||||
var preview: CardView
|
||||
var search_box: LineEdit
|
||||
var sort_box: OptionButton
|
||||
var missing_box: CheckButton
|
||||
var slot_buttons: Array = []
|
||||
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:
|
||||
signal clicked(card)
|
||||
signal hover(card, entered: bool)
|
||||
var card: Cards.Card
|
||||
var count: int
|
||||
var lit := false
|
||||
|
||||
func _init(c: Cards.Card, n: int, sz: Vector2) -> void:
|
||||
card = c
|
||||
count = n
|
||||
size = sz
|
||||
custom_minimum_size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
var light := func(on: bool):
|
||||
lit = on
|
||||
queue_redraw()
|
||||
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.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)
|
||||
|
||||
|
||||
class EnergyCurve extends Control:
|
||||
var cards: Array = []
|
||||
|
||||
func _draw() -> void:
|
||||
Style.text(self, "Energiekurve", 12, Style.MUTED, Vector2(0, 0))
|
||||
var counts := {}
|
||||
for c in cards:
|
||||
var k: int = mini(c.cost, 8)
|
||||
counts[k] = counts.get(k, 0) + 1
|
||||
for i in range(1, 9):
|
||||
var n: int = counts.get(i, 0)
|
||||
var x := (i - 1) * 48
|
||||
draw_rect(Rect2(x, 104 - n * 5, 36, n * 5), Style.BLUE)
|
||||
if n > 0:
|
||||
Style.text(self, str(n), 11, Style.TEXT, Vector2(x + 18, 102 - n * 5), "midbottom")
|
||||
Style.text(self, str(i) + ("+" if i == 8 else ""), 12, Style.MUTED, Vector2(x + 18, 108), "midtop")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
slot = app.profile.active_deck
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
Style.label(self, "Deck bauen", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.label(self, "Klick: Karte ins Deck. Klick in der Liste: Karte entfernen. Mausrad: scrollen.",
|
||||
14, Style.MUTED, Rect2(300, 18, 700, 24))
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
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(424, 55)
|
||||
search_box.size = Vector2(136, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
refresh())
|
||||
add_child(search_box)
|
||||
sort_box = OptionButton.new()
|
||||
sort_box.position = Vector2(570, 55)
|
||||
sort_box.size = Vector2(130, 34)
|
||||
for s in SORTS:
|
||||
sort_box.add_item(s)
|
||||
sort_box.item_selected.connect(func(i: int):
|
||||
sort_mode = i
|
||||
refresh())
|
||||
add_child(sort_box)
|
||||
missing_box = CheckButton.new()
|
||||
missing_box.position = Vector2(710, 58)
|
||||
missing_box.text = "Fehlende"
|
||||
missing_box.toggled.connect(func(on: bool):
|
||||
show_missing = on
|
||||
refresh())
|
||||
add_child(missing_box)
|
||||
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
|
||||
grid.card_clicked.connect(add)
|
||||
grid.card_hover.connect(show_preview)
|
||||
add_child(grid)
|
||||
# Rechte Seite: Slots, Deckliste, Kurve
|
||||
Style.panel(self, Rect2(845, 50, 420, 680))
|
||||
for i in Profile.DECK_SLOTS:
|
||||
slot_buttons.append(Style.button(self, str(i + 1), Rect2(860 + i * 46, 58, 40, 32), select_slot.bind(i)))
|
||||
b_activate = Style.button(self, "Aktiv setzen", Rect2(1100, 58, 150, 32), activate)
|
||||
name_box = LineEdit.new()
|
||||
name_box.position = Vector2(860, 98)
|
||||
name_box.size = Vector2(200, 30)
|
||||
name_box.max_length = 20
|
||||
name_box.text_submitted.connect(func(t: String): rename(t))
|
||||
name_box.focus_exited.connect(func(): rename(name_box.text))
|
||||
add_child(name_box)
|
||||
deck_label = Style.label(self, "", 22, Style.TEXT, Rect2(1070, 98, 120, 30), "left", true)
|
||||
active_label = Style.label(self, "", 14, Style.GREEN, Rect2(1180, 104, 75, 24), "right")
|
||||
b_autofill = Style.button(self, "Auto-Ausfüllen", Rect2(1100, 154, 150, 26), autofill)
|
||||
b_autofill.add_theme_font_size_override("font_size", 13)
|
||||
elems_label = Style.label(self, "", 16, Style.MUTED, Rect2(860, 130, 390, 24))
|
||||
valid_label = Style.label(self, "", 14, Style.GREEN, Rect2(860, 156, 235, 22))
|
||||
deck_list = Control.new()
|
||||
deck_list.position = Vector2(860, 182)
|
||||
deck_list.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(deck_list)
|
||||
curve = EnergyCurve.new()
|
||||
curve.position = Vector2(860, 604)
|
||||
curve.size = Vector2(400, 125)
|
||||
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, 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))
|
||||
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview.visible = false
|
||||
add_child(preview)
|
||||
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_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()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if preview.visible:
|
||||
var pos := get_local_mouse_position() + Vector2(16, 16)
|
||||
pos.x = clampf(pos.x, 0, 1280 - preview.size.x)
|
||||
pos.y = clampf(pos.y, 0, 800 - preview.size.y)
|
||||
preview.position = pos
|
||||
|
||||
|
||||
func set_filter(e: String) -> void:
|
||||
filter = e
|
||||
refresh()
|
||||
|
||||
|
||||
func select_slot(i: int) -> void:
|
||||
slot = i
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func rename(t: String) -> void:
|
||||
if t.strip_edges() == app.profile.deck_name(slot):
|
||||
return
|
||||
var err: String = app.profile.rename_deck(slot, t)
|
||||
if err == "":
|
||||
app.save()
|
||||
say("Umbenannt in %s" % app.profile.deck_name(slot), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func autofill() -> void:
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
var err: String = app.profile.autofill_deck(slot)
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
if err == "":
|
||||
app.save()
|
||||
say("Aufgefüllt und gespeichert", true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func activate() -> void:
|
||||
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
|
||||
if not ok[0]:
|
||||
say("Erst ein spielbares Deck speichern", false)
|
||||
return
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
app.profile.active_deck = slot
|
||||
app.save()
|
||||
say("%s ist jetzt das aktive Deck" % app.profile.deck_name(slot), true)
|
||||
refresh()
|
||||
|
||||
|
||||
## Karten der Sammlung (oder alle, wenn Fehlende gezeigt werden), gefiltert und sortiert.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate() if show_missing else app.profile.owned_cards()
|
||||
if filter != "":
|
||||
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.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))
|
||||
1:
|
||||
out.sort_custom(func(a, b): return a.name < b.name)
|
||||
3:
|
||||
out.sort_custom(func(a, b):
|
||||
var ra := Cards.RARITIES.find(a.rarity)
|
||||
var rb := Cards.RARITIES.find(b.rarity)
|
||||
return ra > rb or (ra == rb and a.cost < b.cost))
|
||||
return out
|
||||
|
||||
|
||||
func say(m: String, ok: bool) -> void:
|
||||
msg_label.text = m
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
|
||||
|
||||
|
||||
func add(card: Cards.Card) -> void:
|
||||
var owned: int = app.profile.collection.get(card.id, 0)
|
||||
var have := deck.count(card.id)
|
||||
if owned == 0:
|
||||
say("%s fehlt in deiner Sammlung" % card.title(), false)
|
||||
elif deck.size() >= Game.DECK_SIZE:
|
||||
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
||||
elif have >= mini(owned, Game.MAX_COPIES):
|
||||
say("Nicht mehr Kopien möglich (max. %d)" % mini(owned, Game.MAX_COPIES), false)
|
||||
else:
|
||||
var elems := Game.deck_elements(deck_cards())
|
||||
for e in card.elements:
|
||||
if not (e in elems):
|
||||
elems.append(e)
|
||||
if elems.size() > Game.MAX_DECK_ELEMENTS:
|
||||
say("Höchstens %d Elemente im Deck" % Game.MAX_DECK_ELEMENTS, false)
|
||||
else:
|
||||
deck.append(card.id)
|
||||
app.sfx.play("click")
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func remove(card: Cards.Card) -> void:
|
||||
deck.erase(card.id)
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func clear_deck() -> void:
|
||||
deck = []
|
||||
refresh()
|
||||
|
||||
|
||||
func save_deck() -> void:
|
||||
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
|
||||
if ok[0]:
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
app.save()
|
||||
say("%s gespeichert" % app.profile.deck_name(slot), true)
|
||||
else:
|
||||
say(ok[1], false)
|
||||
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:
|
||||
out.append(Cards.by_id(cid))
|
||||
return out
|
||||
|
||||
|
||||
func show_preview(card: Cards.Card, entered: bool) -> void:
|
||||
if entered:
|
||||
preview.card = card
|
||||
preview.visible = true
|
||||
preview.queue_redraw()
|
||||
elif preview.card == card:
|
||||
preview.visible = false
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
grid.fill(cards(),
|
||||
func(c):
|
||||
var owned: int = p.collection.get(c.id, 0)
|
||||
return "fehlt" if owned == 0 else "%d/%d im Deck" % [deck.count(c.id), owned],
|
||||
func(c):
|
||||
var owned: int = p.collection.get(c.id, 0)
|
||||
return owned == 0 or deck.count(c.id) >= mini(owned, Game.MAX_COPIES))
|
||||
for i in slot_buttons.size():
|
||||
var b: Button = slot_buttons[i]
|
||||
var col: Color = Style.GOLD if i == slot else (Style.GREEN if p.deck_valid(i)[0] else Style.MUTED)
|
||||
b.add_theme_stylebox_override("normal", Style.box(Style.PANEL_LIGHT if i != slot else Style.darker(Style.GOLD, 0.45), 6, col, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Style.PANEL_LIGHT), 6, col, 2))
|
||||
active_label.text = "aktiv" if slot == p.active_deck else ""
|
||||
# Deckliste
|
||||
Style.clear(deck_list)
|
||||
var counts := {}
|
||||
for cid in deck:
|
||||
counts[cid] = counts.get(cid, 0) + 1
|
||||
var listed: Array = counts.keys().map(func(cid): return Cards.by_id(cid))
|
||||
listed.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
||||
for i in listed.size():
|
||||
var c: Cards.Card = listed[i]
|
||||
var row := DeckRow.new(c, counts[c.id], Vector2(400, 20))
|
||||
row.position = Vector2(0, i * 21)
|
||||
row.clicked.connect(remove)
|
||||
row.hover.connect(show_preview)
|
||||
deck_list.add_child(row)
|
||||
var dc := deck_cards()
|
||||
if not name_box.has_focus():
|
||||
name_box.text = p.deck_name(slot)
|
||||
deck_label.text = "%d/%d" % [deck.size(), Game.DECK_SIZE]
|
||||
deck_label.tooltip_text = tr("Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten")
|
||||
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 = 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)
|
||||
valid_label.text = "spielbar" if ok[0] else ok[1]
|
||||
valid_label.add_theme_color_override("font_color", Style.GREEN if ok[0] else Style.ORANGE)
|
||||
curve.cards = dc
|
||||
curve.queue_redraw()
|
||||
@@ -1 +0,0 @@
|
||||
uid://yg8pbj56mib5
|
||||
@@ -19,7 +19,7 @@ var addr_label: Label
|
||||
func _ready() -> void:
|
||||
net = app.net
|
||||
net.my_name = "Spieler"
|
||||
net.my_deck = app.profile.deck.duplicate()
|
||||
net.my_pick = {"looks": app.profile.looks()}
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 10)
|
||||
|
||||
@@ -23,7 +23,6 @@ func _ready() -> void:
|
||||
net.name = "Net"
|
||||
get_tree().root.add_child.call_deferred(net)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
profile.refresh_daily()
|
||||
apply_settings()
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
@@ -36,10 +35,9 @@ func apply_settings() -> void:
|
||||
theme = Style.make_theme()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung, Decks und Fortschritt werden verworfen.
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
profile.refresh_daily()
|
||||
clear_battle_save()
|
||||
save()
|
||||
apply_settings()
|
||||
|
||||
@@ -2,27 +2,19 @@ class_name MenuScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
var b_single: Button
|
||||
var b_campaign: Button
|
||||
var b_online: Button
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
var deck_label: Label
|
||||
var daily_label: Label
|
||||
var warn_label: Label
|
||||
var notice_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 640, 24, 5)
|
||||
var col := Style.column(self, 640, 40, 5)
|
||||
Style.clabel(col, "Dinger", 64, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Ein Kartenspiel: jeder gegen jeden", 22, Style.MUTED)
|
||||
Style.spacer(col, 8)
|
||||
coins_label = Style.clabel(col, "", 20, Style.GOLD)
|
||||
coll_label = Style.clabel(col, "", 16, Style.MUTED)
|
||||
deck_label = Style.clabel(col, "", 16, Style.MUTED)
|
||||
daily_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||
Style.spacer(col, 8)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 2
|
||||
@@ -34,16 +26,14 @@ func _ready() -> void:
|
||||
var sz := Vector2(270, 44)
|
||||
if FileAccess.file_exists(app.battle_save_path()):
|
||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||
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_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
|
||||
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
|
||||
Style.cbutton(grid, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
|
||||
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
||||
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
||||
Style.spacer(col, 10)
|
||||
warn_label = Style.clabel(col, "", 16, Style.RED)
|
||||
notice_label = Style.clabel(col, "", 16, Style.GREEN)
|
||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
refresh()
|
||||
@@ -62,24 +52,9 @@ func _resume() -> void:
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
var ok: Array = p.deck_valid()
|
||||
b_single.disabled = not ok[0]
|
||||
b_campaign.disabled = not ok[0]
|
||||
b_online.disabled = not ok[0]
|
||||
var total := 0
|
||||
for cid in p.collection:
|
||||
total += p.collection[cid]
|
||||
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
||||
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.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
|
||||
else:
|
||||
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
|
||||
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
|
||||
coll_label.text = tr("Aufgewertet: %d von %d Karten · Kampagne: %d/%d") % [
|
||||
p.upgraded_count(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit wählen und die Gegnerzahl schieben. Die Gegnerdecks
|
||||
## werden erst beim Start zufällig gezogen (siehe BattleScreen und DeckGen).
|
||||
## Einzelspieler: Schwierigkeit und Gegnerzahl wählen. Die Karten kommen im Kampf
|
||||
## aus dem Laden (siehe DeckGen.shop_cards).
|
||||
|
||||
var app
|
||||
var rating_label: Label
|
||||
var diff_buttons := {}
|
||||
var count_slider: HSlider
|
||||
var count_label: Label
|
||||
@@ -12,17 +11,16 @@ var info_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 1000, 60, 14)
|
||||
var col := Style.column(self, 1000, 50, 12)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 10)
|
||||
Style.spacer(col, 8)
|
||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in ["leicht", "normal", "schwer"]:
|
||||
for key in Profile.DIFFICULTIES:
|
||||
var k: String = key
|
||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
||||
rating_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 24)
|
||||
Style.spacer(col, 20)
|
||||
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
|
||||
var sr := Style.row(col, 12)
|
||||
sr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
@@ -39,7 +37,7 @@ func _ready() -> void:
|
||||
sr.add_child(count_slider)
|
||||
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
|
||||
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 30)
|
||||
Style.spacer(col, 24)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
||||
@@ -64,16 +62,18 @@ func start() -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
for key in diff_buttons:
|
||||
var b: Button = diff_buttons[key]
|
||||
var active: bool = key == p.settings.difficulty
|
||||
b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f") % [p.deck_rating(), p.target_rating()]
|
||||
_mark(diff_buttons[key], key == p.settings.difficulty)
|
||||
var n := opponents()
|
||||
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
||||
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
|
||||
|
||||
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
||||
var base: Color = col if active else Style.PANEL_LIGHT
|
||||
b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
@@ -76,13 +76,12 @@ func _ready() -> void:
|
||||
grid.add_child(language)
|
||||
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, 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 = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Decks, Münzen und Fortschritt gehen verloren.",
|
||||
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.",
|
||||
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
|
||||
_labels()
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ class_name Sfx
|
||||
extends Node
|
||||
## Kurze, zur Laufzeit erzeugte Töne (keine Audiodateien nötig).
|
||||
|
||||
static var inst: Sfx
|
||||
|
||||
var streams := {}
|
||||
var players: Array[AudioStreamPlayer] = []
|
||||
var volume := 0.8
|
||||
@@ -14,7 +12,6 @@ var music_box := {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
inst = self
|
||||
streams["click"] = _tone(880, 0.05, "sine")
|
||||
streams["play"] = _tone(440, 0.14, "sine", 660)
|
||||
streams["attack"] = _tone(220, 0.16, "square", 90)
|
||||
|
||||
@@ -2,12 +2,8 @@ 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")
|
||||
static var font_scale := 1.0 # Einstellung „Große Schrift“
|
||||
|
||||
const BG := Color("1a1c24")
|
||||
const PANEL := Color("282b36")
|
||||
@@ -20,6 +16,12 @@ const RED := Color("e65555")
|
||||
const BLUE := Color("6eaaf0")
|
||||
const YELLOW := Color("ffe65a")
|
||||
const ORANGE := Color("ff963c")
|
||||
const PURPLE := Color("8c78dc")
|
||||
|
||||
|
||||
## Schriftgröße mit der Einstellung „Große Schrift“.
|
||||
static func fs(size: int) -> int:
|
||||
return int(round(size * font_scale))
|
||||
|
||||
|
||||
static func darker(c: Color, f: float = 0.65) -> Color:
|
||||
@@ -244,65 +246,42 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
var a: Color = Cards.ELEMENT_COLORS[elements[0]]
|
||||
if elements.size() == 1:
|
||||
ci.draw_style_box(box(a, radius), rect)
|
||||
elif elements[0] == elements[1]:
|
||||
# Doppeltes Element: gleiche Farbe mit zweitem, hellem Innenrahmen
|
||||
ci.draw_style_box(box(a, radius), rect)
|
||||
border(ci, rect.grow(-5), lighter(a, 0.27), 2, maxi(2, radius - 3))
|
||||
else:
|
||||
var b: Color = Cards.ELEMENT_COLORS[elements[1]]
|
||||
var half := floorf(rect.size.x / 2)
|
||||
var left := StyleBoxFlat.new()
|
||||
left.bg_color = a
|
||||
left.corner_radius_top_left = radius
|
||||
left.corner_radius_bottom_left = radius
|
||||
var right := StyleBoxFlat.new()
|
||||
right.bg_color = b
|
||||
right.corner_radius_top_right = radius
|
||||
right.corner_radius_bottom_right = radius
|
||||
ci.draw_style_box(left, Rect2(rect.position, Vector2(half, rect.size.y)))
|
||||
ci.draw_style_box(right, Rect2(rect.position + Vector2(half, 0), Vector2(rect.size.x - half, rect.size.y)))
|
||||
ci.draw_style_box(box(Cards.ELEMENT_COLORS[elements[0]], radius), rect)
|
||||
return
|
||||
var n := elements.size()
|
||||
var w := rect.size.x / n
|
||||
for i in n:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Cards.ELEMENT_COLORS[elements[i]]
|
||||
if i == 0:
|
||||
sb.corner_radius_top_left = radius
|
||||
sb.corner_radius_bottom_left = radius
|
||||
if i == n - 1:
|
||||
sb.corner_radius_top_right = radius
|
||||
sb.corner_radius_bottom_right = radius
|
||||
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
|
||||
|
||||
|
||||
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
|
||||
# --- Kartensymbole (prozedural, je Element) --------------------------
|
||||
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
||||
var variant := absi(hash(card.id)) % 3
|
||||
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||
col.a = 0.95
|
||||
if card.is_trap():
|
||||
# Falle: Kreuz im Kreis
|
||||
ci.draw_arc(center, size * 0.5, 0, TAU, 32, col, maxf(2, size * 0.08))
|
||||
var d := size * 0.28
|
||||
ci.draw_line(center + Vector2(-d, -d), center + Vector2(d, d), col, maxf(2, size * 0.1))
|
||||
ci.draw_line(center + Vector2(-d, d), center + Vector2(d, -d), col, maxf(2, size * 0.1))
|
||||
return
|
||||
if card.kind == "Zauber":
|
||||
_star(ci, center, size * 0.55, Color(col, 0.35))
|
||||
var elems: Array = card.elements
|
||||
if elems.size() >= 2 and elems[0] != elems[1]:
|
||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||
col2.a = 0.95
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
else:
|
||||
if elems.size() == 1:
|
||||
_element_glyph(ci, elems[0], center, size, variant, col)
|
||||
if elems.size() >= 2:
|
||||
ci.draw_arc(center, size * 0.6, 0, TAU, 32, Color(col, 0.7), 2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
|
||||
|
||||
static func _star(ci: CanvasItem, c: Vector2, r: float, col: Color) -> void:
|
||||
var pts := PackedVector2Array()
|
||||
for i in 8:
|
||||
var a := TAU * i / 8 - PI / 2
|
||||
var rr := r if i % 2 == 0 else r * 0.45
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * rr)
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
return
|
||||
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
|
||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||
col2.a = 0.95
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
|
||||
|
||||
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
|
||||
|
||||
Reference in New Issue
Block a user