Files
cards/scripts/ui/booster_screen.gd
T
team3 1d563b9eec
Tests / headless (push) Waiting to run
Tests / fenster-schnell (push) Waiting to run
Tests / fenster-voll (push) Skipped
Tests / balance (push) Skipped
update
2026-10-14 17:35:09 +02:00

287 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name BoosterScreen
extends Control
## Booster kaufen und öffnen: unten die Pack-Kacheln der offenen Kapitel (Neutral und die Elemente in
## Kapitelreihenfolge, gesperrte sind unsichtbar) mit ×1, ×5, ×10; die Karten fliegen aus dem Pack in ein Raster
## (ein Pack: fünf große, mehrere: zehn je Reihe, beste Stufe zuerst) und liegen verdeckt. Klick
## oder „Alle aufdecken“ dreht sie gestaffelt um, seltene blitzen auf; danach steht das Fazit.
const CARD_W := 200
const CARD_H := 280
const SMALL_W := 108
const SMALL_H := 151
const AREA := Rect2(40, 100, 1200, 460)
const QUANTITIES := [1, 5, 10]
var app
var cards: Array = [] # {"card", "rarity", "new", "upgraded", "dust"} je Karte
var revealed := {}
var coins_label: Label
var summary_label: Label
var b_buy: Button # Neutral ×1 (Tests)
var b_all: Button
var scroll: ScrollContainer
var slots: Control
var pack_tiles := {} # Element -> PackTile (Kapitelreihenfolge)
var pack_buttons := {} # Element -> der ×1-Knopf
var fresh := {} # gerade aufgedeckt: Index -> Verzögerung fürs Umdrehen
var fly_from := Vector2.ZERO # woher die Karten einfliegen (Mitte der gekauften Kachel)
var fly := false # beim nächsten refresh einfliegen
var last_pack := "" # zuletzt gekauftes Pack (pulsiert)
class CardBack extends Control:
signal clicked
func _init(sz: Vector2) -> void:
size = sz
custom_minimum_size = sz
mouse_filter = Control.MOUSE_FILTER_STOP
focus_mode = Control.FOCUS_ALL
focus_entered.connect(queue_redraw)
focus_exited.connect(queue_redraw)
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event()
clicked.emit()
elif has_focus() and event.is_action_pressed("ui_accept"):
accept_event()
clicked.emit()
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size)
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 5 if has_focus() else 3), rect)
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
Style.text(self, "?", 48 if size.x >= 180 else 30, Style.GOLD, size / 2, "center", true)
## Pack-Kachel eines Elements: Symbol, Name, Preis; die Kaufknöpfe hängt der Bildschirm an.
class PackTile extends Control:
var element: String
var locked := false
var hint := ""
var pulse := false
var buy_buttons: Array = []
func _init(e: String, rect: Rect2) -> void:
element = e
position = rect.position
size = rect.size
pivot_offset = rect.size / 2
func _draw() -> void:
var col: Color = Cards.ELEMENT_COLORS[element]
draw_style_box(Style.box(Style.darker(col, 0.5), 10, Style.GOLD if pulse else Color(col, 0.7), 2), Rect2(Vector2.ZERO, size))
Style.element_icon(self, element, Vector2(32, 40), 34, Style.lighter(col, 0.4))
Style.text(self, tr(element), 16, Style.TEXT, Vector2(60, 30), "midleft", true)
Style.text(self, hint, 13, Style.MUTED, Vector2(60, 52), "midleft")
func _ready() -> void:
Style.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
scroll = ScrollContainer.new()
scroll.position = AREA.position
scroll.size = AREA.size
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
slots = Control.new()
slots.mouse_filter = Control.MOUSE_FILTER_PASS
slots.custom_minimum_size = AREA.size
scroll.add_child(slots)
summary_label = Style.label(self, "", 16, Style.GOLD, Rect2(0, 566, 1280, 24), "center", true)
b_all = Style.button(self, "Alle aufdecken", Rect2(540, 594, 200, 40), reveal_all)
for i in Campaign.CHAPTER_ELEMENTS.size():
var e: String = Campaign.CHAPTER_ELEMENTS[i]
var tile := PackTile.new(e, Rect2(70 + i * 230, 644, 220, 136))
add_child(tile)
for k in QUANTITIES.size():
var b := Style.button(tile, "×%d" % QUANTITIES[k], Rect2(10 + k * 68, 88, 64, 36), buy.bind(e, QUANTITIES[k]))
b.add_theme_font_size_override("font_size", 14)
tile.buy_buttons.append(b)
pack_tiles[e] = tile
pack_buttons[e] = tile.buy_buttons[0]
b_buy = pack_buttons[Cards.NEUTRAL]
refresh()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())
## Kauft n Packs eines Elements; die Karten liegen verdeckt, mehrere Packs nach Stufe sortiert.
func buy(element: String, n: int) -> void:
var p: Profile = app.profile
var before: int = p.coins
var got: Array = p.open_boosters(element, n)
if got.is_empty():
return
app.save()
app.sfx.play("play")
if n > 1:
got.sort_custom(func(a, b): return Cards.RARITIES.find(a.rarity) > Cards.RARITIES.find(b.rarity))
cards = got
revealed = {}
fresh = {}
fly = true
var tile: PackTile = pack_tiles[element]
fly_from = tile.position + tile.size / 2
last_pack = element
scroll.scroll_vertical = 0
refresh()
_count_coins(before, p.coins)
## Die Münzen zählen sichtbar herunter.
func _count_coins(from: int, to: int) -> void:
if not Style.animations or from == to:
return
create_tween().tween_method(func(v: float): coins_label.text = "Münzen: %d" % int(round(v)), float(from), float(to), 0.5)
func reveal(i: int) -> void:
revealed[i] = true
fresh = {i: 0.0}
app.sfx.play("combo" if cards[i].rarity != "Common" else "click")
refresh()
## Deckt alles auf, von vorn nach hinten gestaffelt; ab Ultra Rare bleibt die Karte kurz allein.
func reveal_all() -> void:
fresh = {}
var delay := 0.0
var step := 0.12 if cards.size() <= Profile.BOOSTER_SIZE else 0.06
for i in cards.size():
if not revealed.has(i):
fresh[i] = delay
delay += step
if Cards.RARITIES.find(cards[i].rarity) >= 3:
delay += 0.35
revealed[i] = true
app.sfx.play("play")
refresh()
## Umdrehen: die Karte wächst aus der Mitte; seltene Stufen blitzen in ihrer Farbe auf.
func flip(v: CardView, delay: float, rarity: String) -> void:
if not Style.animations:
return
v.scale.x = 0.0
var tw := create_tween()
tw.tween_interval(delay)
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
if rarity == "Common":
return
var burst := Style.panel(slots, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
burst.pivot_offset = burst.size / 2
burst.modulate.a = 0.0
var bt := create_tween()
bt.tween_interval(delay + 0.1)
bt.tween_property(burst, "modulate:a", 1.0, 0.05)
bt.parallel().tween_property(burst, "scale", Vector2(1.25, 1.25), 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
bt.tween_property(burst, "modulate:a", 0.0, 0.3)
bt.tween_callback(burst.queue_free)
## Einflug einer verdeckten Karte aus dem Pack auf ihren Platz, reihenweise gestaffelt.
func fly_in(back: Control, target: Vector2, i: int, per_row: int) -> void:
if not Style.animations:
return
back.position = fly_from - AREA.position - back.size / 2
back.scale = Vector2(0.4, 0.4)
var tw := create_tween().set_parallel()
var delay := (i / per_row) * 0.1 + (i % per_row) * 0.03
tw.tween_property(back, "position", target, 0.3).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.tween_property(back, "scale", Vector2.ONE, 0.3).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
## Platz der i-ten Karte: ein Pack fünf große in einer Reihe, sonst zehn kleine je Reihe.
func slot_rect(i: int) -> Rect2:
var single := cards.size() <= Profile.BOOSTER_SIZE
var w := CARD_W if single else SMALL_W
var h := CARD_H if single else SMALL_H
var per_row := 5 if single else 10
var gap := 20 if single else 12
var x0 := (AREA.size.x - per_row * w - (per_row - 1) * gap) / 2
var row_h := h + (60 if single else 44)
return Rect2(x0 + (i % per_row) * (w + gap), 10 + (i / per_row) * row_h, w, h)
## Fazit nach dem Aufdecken: neue Karten, Aufwertungen, Staub.
func summary() -> String:
if cards.is_empty() or revealed.size() < cards.size():
return ""
var fresh_n := 0
var up := 0
var dust := 0
for res in cards:
if res.get("new", false):
fresh_n += 1
elif res.upgraded:
up += 1
dust += int(res.dust)
return tr("%d neu · %d aufgewertet · +%d Staub") % [fresh_n, up, dust]
func refresh() -> void:
var p: Profile = app.profile
coins_label.text = "Münzen: %d" % p.coins
var open: Array = p.unlocked_elements()
var x0 := (1280 - open.size() * 220 - (open.size() - 1) * 10) / 2
for e in pack_tiles:
var tile: PackTile = pack_tiles[e]
tile.locked = not p.booster_unlocked(e)
tile.visible = not tile.locked
if not tile.locked:
tile.position = Vector2(x0 + open.find(e) * 230, 644)
tile.hint = tr("%d Münzen") % p.pack_price(e)
for k in QUANTITIES.size():
tile.buy_buttons[k].disabled = tile.locked or p.coins < p.pack_price(e) * QUANTITIES[k]
tile.pulse = e == last_pack and not cards.is_empty() and revealed.size() >= cards.size()
tile.queue_redraw()
b_all.disabled = cards.is_empty() or revealed.size() >= cards.size()
summary_label.text = summary()
Style.clear(slots)
var single := cards.size() <= Profile.BOOSTER_SIZE
var per_row := 5 if single else 10
var n := maxi(cards.size(), Profile.BOOSTER_SIZE)
var rows := (n + per_row - 1) / per_row
slots.custom_minimum_size = Vector2(AREA.size.x, maxf(AREA.size.y, slot_rect(n - 1).end.y + 60))
for i in n:
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 res: Dictionary = cards[i]
var c: Cards.Card = res.card
var v := CardView.new(c.leveled(Cards.RARITIES.find(res.rarity)), r.size, false, res.rarity)
v.position = r.position
slots.add_child(v)
if fresh.has(i):
flip(v, fresh[i], res.rarity)
var fs := 16 if single else 12
Style.label(slots, tr(res.rarity), fs, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x - 10, r.end.y + 6, r.size.x + 20, 20), "center", true)
var y2 := r.end.y + (30 if single else 24)
if res.get("new", false):
Style.label(slots, "Neu!", fs, Style.GOLD, Rect2(r.position.x, y2, r.size.x, 20), "center", true)
elif res.upgraded:
Style.label(slots, "Aufgewertet", fs, Style.GREEN, Rect2(r.position.x - 10, y2, r.size.x + 20, 20), "center", true)
else:
Style.label(slots, "+%d Staub" % res.dust, fs - 2, Style.MUTED, Rect2(r.position.x, y2, r.size.x, 20), "center")
else:
var back := CardBack.new(r.size)
back.position = r.position
back.pivot_offset = r.size / 2
back.clicked.connect(reveal.bind(i))
slots.add_child(back)
if fly:
fly_in(back, r.position, i, per_row)
fly = false
fresh = {}
if rows > 0 and last_pack != "" and pack_tiles[last_pack].pulse and Style.animations:
var tile: PackTile = pack_tiles[last_pack]
var tw := tile.create_tween().set_loops(3)
tw.tween_property(tile, "scale", Vector2(1.04, 1.04), 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
tw.tween_property(tile, "scale", Vector2.ONE, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)