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()