423 lines
16 KiB
GDScript
423 lines
16 KiB
GDScript
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()
|