Elemente, Kombos, Fallen, KI mit Vorausschau, Kampagne, Hotseat und Netzwerk, Werkstatt, Erfolge, Tagesaufgaben, Tests und Exporte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
352 lines
13 KiB
GDScript
352 lines
13 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
|
||
|
||
|
||
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
|
||
mouse_entered.connect(func():
|
||
lit = true
|
||
queue_redraw()
|
||
hover.emit(card, true))
|
||
mouse_exited.connect(func():
|
||
lit = false
|
||
queue_redraw()
|
||
hover.emit(card, 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)
|
||
|
||
func _draw() -> void:
|
||
if lit:
|
||
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
|
||
for k in card.elements.size():
|
||
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
|
||
Style.text(self, "%dx %s" % [count, card.name], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
||
Style.text(self, 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]
|
||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||
search_box = LineEdit.new()
|
||
search_box.position = Vector2(400, 55)
|
||
search_box.size = Vector2(160, 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, 180, 44), save_deck, Color("285a3c"))
|
||
Style.button(self, "Leeren", Rect2(1050, 740, 90, 44), clear_deck)
|
||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
||
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
preview.visible = false
|
||
add_child(preview)
|
||
refresh()
|
||
|
||
|
||
func _input(event: InputEvent) -> void:
|
||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus() and not name_box.has_focus():
|
||
app.show_screen(MenuScreen.new())
|
||
|
||
|
||
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.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 (Werkstatt)" % card.name, 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 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.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
||
var elems := Game.deck_elements(dc)
|
||
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
||
elems_label.text = "Elemente: " + (", ".join(elems) if not elems.is_empty() else "–")
|
||
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()
|