Dinger: Kartenspiel in Godot 4.7

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
This commit is contained in:
Team3
2026-09-16 13:22:07 +02:00
commit 536a523f50
84 changed files with 9859 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
class_name AchievementsScreen
extends Control
var app
func _ready() -> void:
var p: Profile = app.profile
var col := Style.column(self, 900, 20, 5)
Style.clabel(col, "Erfolge", 36, Style.GOLD, "center", true)
var st: Dictionary = p.stats
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.STAGES.size()], 16, Style.MUTED)
var task := p.daily_task()
var daily_text: String = ("Tagesaufgabe erledigt: %s ✓" % task.text) if p.daily.done else \
"Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
Style.clabel(col, daily_text, 16, Style.BLUE)
Style.spacer(col, 8)
for a in Profile.ACHIEVEMENTS:
var done: bool = a.id in p.achievements
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
col.add_child(panel)
var r := Style.row(panel, 12)
r.custom_minimum_size = Vector2(0, 42)
Style.clabel(r, "✓" if done else "·", 22, Style.GREEN if done else Style.MUTED, "center", true, 40)
Style.clabel(r, a.name, 18, Style.TEXT if done else Style.MUTED, "left", true, 250)
Style.clabel(r, a.text, 15, Style.MUTED, "left", false, 380)
Style.clabel(r, "+%d" % a.reward, 16, Style.GOLD, "right", true, 90)
var top := p.top_cards(5)
if not top.is_empty():
Style.spacer(col, 4)
var parts: Array[String] = []
for row in top:
parts.append("%s %d/%d" % [row.card.name, row.wins, row.games])
Style.clabel(col, "Beste Karten (Siege/Partien): " + " · ".join(parts), 14, Style.MUTED)
Style.spacer(col, 6)
var br := Style.row(col)
br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -0,0 +1 @@
uid://cw4rtcukefdl8

159
scripts/ui/battle_fx.gd Normal file
View File

@@ -0,0 +1,159 @@
class_name BattleFx
extends RefCounted
## Animationen und Töne des Kampfes aus den Spielereignissen.
var screen # BattleScreen
var enabled := true
func _init(s) -> void:
screen = s
func sfx(kind: String) -> void:
screen.app.sfx.play(kind)
## Spielt die Ereignisse einer Aktion ab. centers: vorher gemerkte Positionen (für gestorbene Wesen).
func play_events(evs: Array, centers: Dictionary) -> void:
var g: Game = screen.game
var delay := 0.0
for ev in evs:
match ev.type:
"attack":
sfx("attack")
var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
lunge(atk, center_of(ev.target, centers))
flash_target(ev.target)
delay = 0.12
"damage":
sfx("damage")
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
"heal":
sfx("heal")
popup("+%d" % ev.amount, Style.GREEN, center_of(ev.target, centers), delay)
"buff":
popup(ev.text, Style.GREEN, center_of(ev.target, centers), delay)
"blocked":
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay)
"freeze":
popup("Eis", Color("aaddff"), center_of(ev.target, centers), delay)
"combo":
sfx("combo")
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
if where.x < 0:
where = screen.target_center({"kind": "player", "index": ev.player})
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":
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 %d Karten" % ev.count, Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
var c: Vector2 = centers.get(screen.ref_key(ref), Vector2(-1, -1))
if c.x < 0:
c = screen.target_center(ref)
return c
func popup(text: String, color: Color, at: Vector2, delay: float) -> void:
if not enabled or at.x < 0:
return
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 22)
l.add_theme_font_override("font", Style.FONT_BOLD)
l.add_theme_color_override("font_color", color)
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
screen.fx_layer.add_child(l)
l.reset_size()
l.position = at - l.size / 2
l.modulate.a = 0.0
var tw: Tween = screen.create_tween()
tw.tween_interval(delay)
tw.tween_property(l, "modulate:a", 1.0, 0.08)
tw.tween_property(l, "position:y", l.position.y - 44, 0.8).set_ease(Tween.EASE_OUT)
tw.parallel().tween_property(l, "modulate:a", 0.0, 0.5).set_delay(0.35)
tw.tween_callback(l.queue_free)
func lunge(tile: CreatureView, target: Vector2) -> void:
if tile == null or not enabled or target.x < 0:
return
tile.busy = true
var start := tile.slot
var dest := start + (target - (start + tile.size / 2)) * 0.35
var tw := tile.create_tween()
tw.tween_property(tile, "position", dest, 0.12).set_ease(Tween.EASE_OUT)
tw.tween_property(tile, "position", start, 0.15).set_ease(Tween.EASE_IN)
tw.tween_callback(func():
tile.busy = false
tile.position = tile.slot)
func flash_target(ref: Dictionary) -> void:
if not enabled:
return
if ref.kind == "creature":
var tile: CreatureView = screen.tiles.get(ref.uid)
if tile == null:
return
tile.flash = true
tile.queue_redraw()
var tw := tile.create_tween()
tw.tween_interval(0.35)
tw.tween_callback(func():
tile.flash = false
tile.queue_redraw())
else:
screen.flash_until[int(ref.index)] = Time.get_ticks_msec() + 350
var tw: Tween = screen.create_tween()
tw.tween_interval(0.37)
tw.tween_callback(screen.refresh)
func pop_in(tile: CreatureView) -> void:
if not enabled:
return
tile.scale = Vector2(0.2, 0.2)
tile.modulate.a = 0.0
var tw := tile.create_tween().set_parallel(true)
tw.tween_property(tile, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tw.tween_property(tile, "modulate:a", 1.0, 0.15)
func fade_out(tile: CreatureView) -> void:
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
if not enabled:
tile.queue_free()
return
var tw := tile.create_tween().set_parallel(true)
tw.tween_property(tile, "modulate:a", 0.0, 0.3)
tw.tween_property(tile, "scale", Vector2(0.6, 0.6), 0.3)
tw.chain().tween_callback(tile.queue_free)
func move_to(tile: CreatureView, pos: Vector2) -> void:
if enabled:
screen.create_tween().tween_property(tile, "position", pos, 0.2)
else:
tile.position = pos

View File

@@ -0,0 +1 @@
uid://defclm4nv6rkn

View File

@@ -0,0 +1,40 @@
class_name BattleLayout
## Feste Bildschirmaufteilung des Kampfes (1280x800).
const SIZES := {1: [110, 110, 10], 2: [96, 110, 8], 3: [78, 100, 4], 4: [54, 72, 4], 5: [44, 62, 3]}
const HAND_W := 130
const HAND_H := 185
const HAND_GAP := 8
const BAR := Rect2(335, 415, 610, 68)
const OWN_X := (1280 - 5 * 110 - 4 * 10) / 2
static func compact(n_others: int) -> bool:
return n_others >= 4
static func hero_box_rect(n_others: int, i: int) -> Rect2:
var col_w := 1280 / n_others
var margin := 6 if compact(n_others) else 10
return Rect2(i * col_w + margin, 10, col_w - 2 * margin, 64)
static func own_slot(j: int) -> Rect2:
return Rect2(OWN_X + j * 120, 250, 110, 140)
static func other_slot(n_others: int, i: int, j: int) -> Rect2:
var col_w := 1280 / n_others
var sz: Array = SIZES[n_others]
var bx: int = i * col_w + (col_w - 5 * sz[0] - 4 * sz[2]) / 2
return Rect2(bx + j * (sz[0] + sz[2]), 84, sz[0], sz[1])
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)

View File

@@ -0,0 +1 @@
uid://5sjeymvcgfl7

945
scripts/ui/battle_screen.gd Normal file
View File

@@ -0,0 +1,945 @@
class_name BattleScreen
extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Modi: Einzelspieler, Kampagne, Anleitung, Hotseat (Perspektivwechsel), Netzwerk (Host oder Mitspieler).
var app
var n_opp: int
var stage := -1 # Kampagnenstufe oder -1
var tutorial := false
var hotseat_decks: Array = []
var hotseat_ai := 0
var online_seat := -1 # Netzwerk: eigener Platz, sonst -1
var net: Net
var game: Game
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 msg_t := 0.0
var ai_delay := 0.75
var ai_timer := 0.0
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 mulligan_selected: Array = []
var tutorial_step := 0
# Ansichten (auch für Tests)
var tiles := {}
var hand_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)
var hero_layer: Control
var tile_layer: Control
var hand_layer: Control
var trap_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 status_label: Label
var msg_label: Label
var b_end: Button
var b_giveup: Button
var overlay: Control
var result_label: Label
var reward_label: Label
var notice_label: Label
var confirm: Control
var handover: Control
var handover_label: Label
var mulligan_box: Control
var mulligan_layer: Control
var b_keep: Button
var b_swap: Button
var tutorial_panel: Panel
var tutorial_text: RichTextLabel
var b_tut_next: Button
var wait_label: Label
const TUTORIAL_STEPS := [
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
"played_creature"],
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig +1 Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an alle gegnerischen Wesen.", "combo"],
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
]
func _init(n: int = 2, campaign_stage: int = -1) -> void:
n_opp = n
stage = campaign_stage
static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
var b := BattleScreen.new(0, -1)
b.hotseat_decks = decks
b.hotseat_ai = n_ai
return b
static func online(seat: int) -> BattleScreen:
var b := BattleScreen.new(0, -1)
b.online_seat = seat
return b
static func guided() -> BattleScreen:
var b := BattleScreen.new(1, -1)
b.tutorial = true
return b
func is_hotseat() -> bool:
return game != null and game.humans.size() > 1 and online_seat < 0
func is_online() -> bool:
return online_seat >= 0
func is_client() -> bool:
return is_online() and not net.is_host
func counts_for_profile() -> bool:
return not is_hotseat() and not is_online()
func _ready() -> void:
var s: Dictionary = app.profile.settings
fx = BattleFx.new(self)
fx.enabled = s.animations
ai_delay = 0.75 / float(s.ai_speed)
_build_static()
if is_online():
net = app.net
net.ended.connect(_on_net_ended)
if net.is_host:
game = net.game
net.action_applied.connect(after_action)
else:
net.snapshot_received.connect(_on_snapshot)
wait_label.visible = true
return # der erste Zustand kommt vom Host
elif not hotseat_decks.is_empty():
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
elif tutorial:
var r := Game.make_rng()
var opps := Campaign.opponents(0, r)
opps[0].perks = []
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
elif stage >= 0:
var r := Game.make_rng()
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r)
else:
game = Game.new_battle(app.profile.deck_cards(), n_opp)
_begin()
func _begin() -> void:
n_opp = game.players.size() - 1
if is_online():
perspective = game.players[online_seat]
else:
game.events.clear()
perspective = game.current() if game.current().is_human else game.humans[0]
build_hero_boxes()
if is_online() and net.is_host:
net.broadcast()
game.events.clear()
refresh()
func _build_static() -> void:
Style.panel(self, Rect2(10, 205, 320, 340))
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.bbcode_enabled = true
log_box.scroll_following = true
log_box.add_theme_font_size_override("normal_font_size", 13)
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(log_box)
if tutorial:
tutorial_panel = Style.panel(self, Rect2(10, 205, 320, 150), Style.PANEL, 8, Style.GOLD, 2)
tutorial_panel.mouse_filter = Control.MOUSE_FILTER_STOP
Style.label(self, "Anleitung", 14, Style.GOLD, Rect2(20, 209, 200, 20), "left", true)
tutorial_text = RichTextLabel.new()
tutorial_text.position = Vector2(20, 230)
tutorial_text.size = Vector2(300, 90)
tutorial_text.bbcode_enabled = false
tutorial_text.scroll_active = false
tutorial_text.add_theme_font_size_override("normal_font_size", 13)
tutorial_text.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tutorial_text)
b_tut_next = Style.button(self, "Weiter", Rect2(230, 318, 90, 30), tutorial_next)
b_tut_next.add_theme_font_size_override("font_size", 14)
log_box.position = Vector2(18, 365)
log_box.size = Vector2(304, 173)
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)
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")
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"))
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()
preview_holder = _layer()
zoom_layer = _layer()
fx_layer = _layer()
# Mulligan
mulligan_box = _overlay()
Style.panel(mulligan_box, Rect2(140, 250, 1000, 320), Style.PANEL, 10, Style.GOLD, 2)
Style.label(mulligan_box, "Hand tauschen?", 26, Style.TEXT, Rect2(140, 262, 1000, 40), "center", true)
Style.label(mulligan_box, "Klicke Karten an die du zurücklegen willst. Einmal pro Partie.", 15, Style.MUTED, Rect2(140, 300, 1000, 24), "center")
mulligan_layer = Control.new()
mulligan_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
mulligan_box.add_child(mulligan_layer)
b_keep = Style.button(mulligan_box, "Behalten", Rect2(430, 515, 200, 44), func(): do_mulligan(false), Color("285a3c"))
b_swap = Style.button(mulligan_box, "Tauschen", Rect2(650, 515, 200, 44), func(): do_mulligan(true))
# Spielende
overlay = _overlay()
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
# Aufgeben bestätigen
confirm = _overlay()
Style.panel(confirm, Rect2(390, 300, 500, 180), Style.PANEL, 10, Style.MUTED, 2)
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(390, 320, 500, 40), "center", true)
Style.label(confirm, "Der Kampf endet ohne Münzen.", 16, Style.MUTED, Rect2(390, 365, 500, 24), "center")
Style.button(confirm, "Ja, aufgeben", Rect2(420, 410, 200, 46), leave, Color("5a2828"))
Style.button(confirm, "Weiterspielen", Rect2(660, 410, 200, 46), func(): confirm.visible = false)
# Übergabe zwischen Menschen (Hotseat)
handover = _overlay()
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
handover_label = Style.label(handover, "", 30, Style.TEXT, Rect2(340, 310, 600, 44), "center", true)
Style.label(handover, "Maus weitergeben, dann weiter. Die Hand des Vorgängers ist verdeckt.", 16, Style.MUTED, Rect2(340, 362, 600, 24), "center")
Style.button(handover, "Weiter", Rect2(540, 420, 200, 50), take_over, Color("285a3c"))
func _layer() -> Control:
var c := Control.new()
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(c)
return c
func _overlay() -> Control:
var o := Control.new()
o.set_anchors_preset(Control.PRESET_FULL_RECT)
o.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
o.add_child(dark)
add_child(o)
return o
func leave() -> void:
if is_online():
net.leave()
if tutorial and not app.profile.tutorial_done:
app.profile.tutorial_done = true
app.save()
app.show_screen(MenuScreen.new())
# --- Layout-Helfer ------------------------------------------------------------------
func others() -> Array:
var out := []
var n := game.players.size()
var start := game.players.find(perspective)
for i in range(1, n):
out.append(game.players[(start + i) % n])
return out
func compact() -> bool:
return BattleLayout.compact(others().size())
func slot_rect(p: Game.Player, j: int) -> Rect2:
if p == perspective:
return BattleLayout.own_slot(j)
var os := others()
return BattleLayout.other_slot(os.size(), os.find(p), j)
func ref_key(ref: Dictionary) -> String:
return "c%d" % ref.uid if ref.kind == "creature" else "p%d" % ref.index
func target_center(ref: Dictionary) -> Vector2:
if ref.kind == "creature":
if tiles.has(ref.uid):
var t: CreatureView = tiles[ref.uid]
return t.slot + t.size / 2
return Vector2(-1, -1)
var p: Game.Player = game.players[ref.index]
if p == perspective:
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
if hero_panels.has(p):
var panel: Panel = hero_panels[p]
return panel.position + panel.size / 2
return Vector2(-1, -1)
func build_hero_boxes() -> void:
Style.clear(hero_layer)
hero_panels = {}
hero_labels = {}
var os := others()
var c := compact()
for i in os.size():
var o: Game.Player = os[i]
var box := BattleLayout.hero_box_rect(os.size(), i)
var panel := Style.panel(hero_layer, box)
panel.gui_input.connect(func(ev):
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
on_click("hero", o))
if not o.perks.is_empty():
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
hero_panels[o] = panel
var labels := {}
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
if not o.perks.is_empty():
title += " ★"
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (44 if c else 20), 26), "left", true)
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
Rect2(box.position.x + 90, box.position.y + 38, box.size.x - 100, 20), "right")
hero_labels[o] = labels
if c:
var b := Style.button(hero_layer, "Lupe", Rect2(box.end.x - 44, box.position.y + 5, 38, 22), func():
zoom_player = o
refresh())
b.add_theme_font_size_override("font_size", 11)
# --- Ablauf und Eingabe ------------------------------------------------------------------
func my_turn() -> bool:
return game != null and game.winner == "" and game.current() == perspective
func any_overlay() -> bool:
return confirm.visible or handover.visible or mulligan_box.visible or overlay.visible
func _input(event: InputEvent) -> void:
if game == null:
return
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
cancel_all()
return
if not (event is InputEventKey and event.pressed and not event.echo):
return
match event.keycode:
KEY_ESCAPE:
if confirm.visible:
confirm.visible = false
elif selected != null or pending != null or zoom_player != null:
cancel_all()
elif not any_overlay():
confirm.visible = true
KEY_SPACE:
if my_turn() and not any_overlay():
end_turn()
KEY_ENTER, KEY_KP_ENTER:
if handover.visible:
take_over()
elif mulligan_box.visible:
do_mulligan(false)
elif overlay.visible:
leave()
elif tutorial and b_tut_next.visible:
tutorial_next()
_:
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
var idx: int = event.keycode - KEY_1
if idx < perspective.hand.size():
on_click("hand", perspective.hand[idx])
func cancel_all() -> void:
if confirm.visible:
confirm.visible = false
cancel()
zoom_player = null
refresh()
func _process(delta: float) -> void:
if game == null:
return
msg_t = maxf(0.0, msg_t - delta)
if msg_t <= 0.0 and msg_label.text != "":
msg_label.text = ""
if preview_timer > 0.0:
preview_timer -= delta
if preview_timer <= 0.0:
show_preview(null, false)
if game.winner != "":
if not ended:
ended = true
_finish()
return
var cur := game.current()
if cur.is_human:
if cur != perspective and is_hotseat() and not handover.visible:
handover_label.text = "%s ist dran" % cur.name
handover.visible = true
hand_layer.visible = false
trap_layer.visible = false
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan_box.visible and not handover.visible:
show_mulligan()
return
if is_client():
return
ai_timer -= delta
if ai_timer > 0:
return
var action := AI.choose_action(game, cur)
AI.apply_action(game, cur, action)
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
after_action()
func _finish() -> void:
var won := game.winner == "human"
if counts_for_profile():
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids)
if tutorial:
app.profile.tutorial_done = true
app.save()
app.sfx.play("win" if won else "lose")
else:
var wp := game.winner_player
var mine: bool = wp != null and wp == perspective
app.sfx.play("win" if mine else "lose")
refresh()
func take_over() -> void:
handover.visible = false
hand_layer.visible = true
trap_layer.visible = true
perspective = game.current()
cancel()
zoom_player = null
build_hero_boxes()
refresh()
func say(m: String) -> void:
msg_t = 2.5
msg_label.text = m
func cancel() -> void:
selected = null
pending = 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 err := game.play_card(perspective, card, target)
if err != "":
say(err)
func end_turn() -> void:
if not my_turn():
return
cancel()
zoom_player = null
if is_client():
net.send_action({"type": "end"})
return
game.end_turn()
ai_timer = ai_delay
after_action()
func show_mulligan() -> void:
mulligan_selected = []
mulligan_box.visible = true
Style.clear(mulligan_layer)
var hand: Array = perspective.hand
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_mulligan(idx))
mulligan_layer.add_child(v)
_mulligan_borders()
func toggle_mulligan(idx: int) -> void:
if idx in mulligan_selected:
mulligan_selected.erase(idx)
else:
mulligan_selected.append(idx)
_mulligan_borders()
func _mulligan_borders() -> void:
var views := mulligan_layer.get_children()
for i in views.size():
views[i].border = Style.YELLOW if i in mulligan_selected else null
views[i].queue_redraw()
b_swap.text = "Tauschen (%d)" % mulligan_selected.size() if not mulligan_selected.is_empty() else "Tauschen"
func do_mulligan(swap: bool) -> void:
mulligan_box.visible = false
var idxs: Array = mulligan_selected.duplicate() if swap else []
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 pending != null:
var card = pending
pending = null
if obj in g.valid_targets(perspective, card.target_type()):
play(card, obj)
else:
say("Abgebrochen")
zoom_player = null
after_action()
return
if selected != null:
var cr = selected
selected = null
if kind == "enemy" or kind == "hero":
if is_client():
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
else:
var err := g.attack(cr, obj)
if err != "":
say(err)
zoom_player = null
after_action()
return
match kind:
"hand":
var ok := g.can_play(perspective, obj)
if not ok[0]:
say(ok[1])
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
pending = obj
say("Wähle ein Ziel")
else:
play(obj, null)
"own":
if obj.can_attack():
selected = obj
say("Wähle ein Angriffsziel")
else:
say("%s kann nicht angreifen" % obj.name)
after_action()
func on_tile_click(cr: Game.Creature) -> void:
var o = cr.owner()
if o == null:
return
if o == perspective:
on_click("own", cr)
elif o.alive():
on_click("enemy", cr)
func show_preview(card, entered: bool) -> void:
Style.clear(preview_holder)
if entered and card != null:
var v := CardView.new(card, Vector2(210, 294))
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.name)
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.name
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
# --- Netzwerk -------------------------------------------------------------------------------
func _on_snapshot(snap: Dictionary) -> void:
var first := game == null
game = Game.from_snapshot(snap)
wait_label.visible = false
if first:
_begin()
return
perspective = game.players[online_seat]
build_hero_boxes()
after_action()
func _on_net_ended(reason: String) -> void:
if game == null or game.winner == "":
say(reason)
await get_tree().create_timer(1.5).timeout
if is_inside_tree():
app.show_screen(MenuScreen.new())
# --- Ereignisse ----------------------------------------------------------------------------
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
func after_action() -> void:
if is_online() and net.is_host:
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"]:
if ev.has(key):
centers[ref_key(ev[key])] = target_center(ev[key])
refresh()
fx.play_events(evs, centers)
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")
# --- Anleitung ------------------------------------------------------------------------------
func tutorial_condition(kind: String) -> bool:
var h := game.human
match kind:
"played_creature":
return game.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
"turn_passed":
return h.turns >= 2
"attacked":
return game.log.any(func(l): return l.ends_with("greift Fenn an.") or (" greift " in l and l.begins_with(h.board[0].name) if not h.board.is_empty() else false))
"resonance":
return h.board.any(func(c): return c.resonance > 0)
"combo":
return game.log.any(func(l): return l.begins_with("Feldkombo"))
"spell":
return game.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
return false
func tutorial_next() -> void:
if tutorial_step < TUTORIAL_STEPS.size() - 1:
tutorial_step += 1
app.sfx.play("click")
refresh()
func _tutorial_refresh() -> void:
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
tutorial_step += 1
tutorial_text.text = "%d/%d %s" % [tutorial_step + 1, TUTORIAL_STEPS.size(), TUTORIAL_STEPS[tutorial_step][0]]
b_tut_next.visible = tutorial_step < TUTORIAL_STEPS.size() - 1
# --- Ansicht angleichen --------------------------------------------------------------------
func refresh() -> void:
if game == null:
return
var g := game
var h := perspective
var now := Time.get_ticks_msec()
var mine := my_turn()
var valid: Array = g.valid_targets(h, pending.target_type()) if pending != null else []
hand_views = []
own_views = []
enemy_views = []
trap_views = []
for o in others():
var panel: Panel = hero_panels[o]
var bcol = null
var bw := 0
if g.current() == o:
bcol = Style.BLUE
bw = 3
if o in valid or (selected != null and o.alive() and g.can_attack(selected, o)[0]):
bcol = Style.ORANGE
bw = 3
if int(flash_until.get(g.players.find(o), 0)) > now:
bcol = Style.RED
bw = 4
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if o.alive() else Style.darker(Style.PANEL, 0.6), 8, bcol, bw))
panel.mouse_filter = Control.MOUSE_FILTER_STOP if o.alive() else Control.MOUSE_FILTER_IGNORE
var labels: Dictionary = hero_labels[o]
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]
else:
labels.life.text = "besiegt"
labels.life.add_theme_color_override("font_color", Style.MUTED)
labels.stats.text = ""
var seen := {}
for p in g.players:
for j in p.board.size():
var cr: Game.Creature = p.board[j]
var rect := slot_rect(p, j)
seen[cr.uid] = true
var tile: CreatureView
if tiles.has(cr.uid):
tile = tiles[cr.uid]
tile.creature = cr
if tile.size != rect.size:
tile.size = rect.size
tile.custom_minimum_size = rect.size
tile.pivot_offset = rect.size / 2
else:
tile = CreatureView.new(cr, rect.size)
tile.position = rect.position
tile.slot = rect.position
tiles[cr.uid] = tile
tile_layer.add_child(tile)
tile.hover.connect(show_preview)
var uid := cr.uid
tile.clicked.connect(func(_v):
var c := game.find_creature(uid)
if c != null:
on_tile_click(c))
fx.pop_in(tile)
tile.slot = rect.position
if not tile.busy and tile.position != rect.position:
fx.move_to(tile, rect.position)
var b = null
var w := 2
if cr == selected:
b = Style.YELLOW
w = 4
elif cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
b = Style.ORANGE
w = 3
elif p == h and mine and cr.can_attack():
b = Style.GREEN
w = 3
tile.border = b
tile.width = w
tile.queue_redraw()
if p == h:
own_views.append(tile)
else:
enemy_views.append(tile)
for uid in tiles.keys():
if not seen.has(uid):
var t: CreatureView = tiles[uid]
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(hand_layer)
for i in h.hand.size():
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)
v.position = r.position
if card == pending:
v.border = Style.YELLOW
elif playable:
v.border = Style.GREEN
v.dim = not playable
v.hover.connect(show_hand_preview)
v.clicked.connect(func(_view): on_click("hand", card))
hand_layer.add_child(v)
hand_views.append(v)
var bar_border = Style.BLUE if mine else null
if int(flash_until.get(g.players.find(h), 0)) > now:
bar_border = Style.RED
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()
status_label.text = "Zug %d · %s" % [g.turn, "Du bist dran" if mine else g.current().name + " ist dran …"]
b_end.disabled = not mine
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"):
lines.append("[color=#f5c850]%s[/color]" % line)
else:
lines.append(line)
log_box.text = "\n".join(lines)
if tutorial:
_tutorial_refresh()
Style.clear(zoom_layer)
if zoom_player != null and zoom_player.alive():
build_zoom(valid)
if g.winner != "":
var wp := g.winner_player
if counts_for_profile():
var won := g.winner == "human"
result_label.text = "Sieg!" if won else "Niederlage"
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
reward_label.text = "+%d Münzen" % maxi(reward, 0)
var notes: Array[String] = app.profile.take_notices()
if not notes.is_empty():
notice_label.text = "\n".join(notes)
else:
var mine_win: bool = wp != null and wp == perspective
result_label.text = ("%s gewinnt!" % wp.name) if wp != null and wp.is_human else "Die KI gewinnt"
result_label.add_theme_color_override("font_color", Style.GREEN if mine_win else Style.RED)
reward_label.text = "Gemeinsame Partie, keine Münzen"
overlay.visible = true
confirm.visible = false
handover.visible = false
mulligan_box.visible = false
func build_zoom(valid: Array) -> void:
var o: Game.Player = zoom_player
var g := game
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],
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]
var b = null
if cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
b = Style.ORANGE
var v := CreatureView.new(cr, Vector2(110, 140), b, 3 if b != null else 2)
v.position = Vector2(box.position.x + 20 + j * 122, box.position.y + 56)
v.hover.connect(show_preview)
v.clicked.connect(func(_x): on_tile_click(cr))
zoom_layer.add_child(v)
if o.board.is_empty():
Style.label(zoom_layer, "Keine Wesen auf dem Feld", 16, Style.MUTED, Rect2(box.position.x + 20, box.position.y + 110, 600, 30))
var hero_ok: bool = (o in valid) or (selected != null and g.can_attack(selected, o)[0])
var bh := Style.button(zoom_layer, "Held als Ziel", Rect2(box.position.x + 20, box.end.y - 60, 180, 44), func(): on_click("hero", o), Color("5a3c28"))
bh.disabled = not hero_ok
Style.button(zoom_layer, "Schließen", Rect2(box.end.x - 180, box.end.y - 60, 160, 44), func():
zoom_player = null
refresh())

View File

@@ -0,0 +1 @@
uid://bmf82cuhs7h1a

View File

@@ -0,0 +1,122 @@
class_name BoosterScreen
extends Control
const CARD_W := 200
const CARD_H := 280
var app
var cards: Array = []
var revealed := {}
var new_ids := {}
var coins_label: Label
var msg_label: Label
var b_buy: Button
var b_all: Button
var slots: Control
class CardBack extends Control:
signal clicked
func _init(sz: Vector2) -> void:
size = sz
custom_minimum_size = sz
mouse_filter = Control.MOUSE_FILTER_STOP
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
accept_event()
clicked.emit()
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size)
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 3), rect)
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
func slot_rect(i: int) -> Rect2:
var gap := 20
var x0 := (1280 - 5 * CARD_W - 4 * 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")
slots = Control.new()
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(slots)
msg_label = Style.label(self, "", 18, Style.RED, Rect2(0, 610, 1280, 30), "center")
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(360, 720, 260, 48), buy, Color("5a4628"))
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 140, 48), reveal_all)
Style.button(self, "Zurück", Rect2(800, 720, 120, 48), func(): app.show_screen(MenuScreen.new()))
refresh()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())
func buy() -> void:
var p: Profile = app.profile
var before: Dictionary = p.collection.duplicate()
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)
return
app.save()
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)
refresh()
func reveal(i: int) -> void:
revealed[i] = true
app.sfx.play("click")
refresh()
func reveal_all() -> void:
for i in 5:
revealed[i] = true
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
b_all.disabled = cards.is_empty() or revealed.size() >= 5
Style.clear(slots)
for i in 5:
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)
v.position = r.position
slots.add_child(v)
Style.label(slots, 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)
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")
else:
var back := CardBack.new(r.size)
back.position = r.position
back.clicked.connect(reveal.bind(i))
slots.add_child(back)

View File

@@ -0,0 +1 @@
uid://d3irimwei0s4d

View File

@@ -0,0 +1,59 @@
class_name CampaignScreen
extends Control
var app
func _ready() -> void:
var p: Profile = app.profile
var col := Style.column(self, 1000, 24, 6)
Style.clabel(col, "Kampagne", 36, Style.GOLD, "center", true)
Style.clabel(col, "Zehn Magier warten. Jede Stufe bringt mehr Gegner an den Tisch, alle nach denselben Regeln.", 16, Style.MUTED)
Style.spacer(col, 6)
for i in Campaign.STAGES.size():
var st: Dictionary = Campaign.STAGES[i]
var done := i < p.campaign_stage
var next := i == p.campaign_stage
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
Style.GOLD if next else null, 2 if next else 0))
col.add_child(panel)
var r := Style.row(panel, 12)
r.custom_minimum_size = Vector2(0, 50)
Style.clabel(r, str(i + 1), 20, Style.GOLD if next else Style.MUTED, "center", true, 40)
Style.clabel(r, "%s · %s" % [st.name, st.title], 17, Style.TEXT if not done else Style.MUTED, "left", true, 300)
var dots := Control.new()
dots.custom_minimum_size = Vector2(60, 0)
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
var elems: Array = st.elements
dots.draw.connect(func():
for k in elems.size():
dots.draw_circle(Vector2(10 + k * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[k]]))
r.add_child(dots)
var table: String = "Duell" if st.extra == 0 else "%d am Tisch" % (st.extra + 2)
Style.clabel(r, "%s · %d Münzen\n★ %s" % [table, st.reward, Game.PERKS[st.perk]], 13, Style.MUTED, "left", false, 300)
if done:
Style.clabel(r, "abgeschlossen", 14, Style.GREEN, "right", false, 110)
Style.cbutton(r, "Wiederholen", Vector2(140, 38), _start.bind(i))
elif next:
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
Style.cbutton(r, "Kämpfen", Vector2(140, 38), _start.bind(i), Color("285a3c"))
else:
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
var b := Style.cbutton(r, "Gesperrt", Vector2(140, 38), func(): pass)
b.disabled = true
if p.campaign_stage >= Campaign.STAGES.size():
Style.clabel(col, "Kampagne abgeschlossen. Der Archimagier ist besiegt!", 18, Style.GREEN, "center", true)
Style.spacer(col, 4)
var br := Style.row(col)
br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
func _start(stage: int) -> void:
app.show_screen(BattleScreen.new(0, stage))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -0,0 +1 @@
uid://bk50mk881o2hr

50
scripts/ui/card_grid.gd Normal file
View File

@@ -0,0 +1,50 @@
class_name CardGrid
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
const CH := 140
const GAP := 12
var grid: GridContainer
func _init(rect: Rect2) -> void:
position = rect.position
size = rect.size
horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
grid = GridContainer.new()
grid.columns = COLS
grid.add_theme_constant_override("h_separation", GAP)
grid.add_theme_constant_override("v_separation", GAP)
add_child(grid)
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool
func fill(cards: Array, label_fn: Callable, dim_fn: 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))
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")
grid.add_child(cell)
func views() -> Array:
var out := []
for cell in grid.get_children():
for v in cell.get_children():
if v is CardView:
out.append(v)
return out

View File

@@ -0,0 +1 @@
uid://bil3i64hn7rfd

65
scripts/ui/card_view.gd Normal file
View File

@@ -0,0 +1,65 @@
class_name CardView
extends Control
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
signal clicked(view: CardView)
signal hover(card: Cards.Card, entered: bool)
var card: Cards.Card
var border = null # Color oder null (dann Seltenheitsfarbe)
var dim := false
func _init(c: Cards.Card, sz: Vector2) -> void:
card = c
custom_minimum_size = sz
size = sz
mouse_filter = Control.MOUSE_FILTER_STOP
mouse_entered.connect(func(): hover.emit(card, true))
mouse_exited.connect(func(): 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(self)
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size)
var big := size.x >= 180
var mid := size.x >= 115
var f_name := 18 if big else (14 if mid else 12)
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]
Style.border(self, rect, bcol, 3 if big else 2, 8)
# Kosten
var r := 16 if big else 12
var cc := Vector2(r + 4, r + 4)
draw_circle(cc, r, Color("14141c"))
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
# Name und Kombo
var nm := Style.fit(card.name, true, f_name, size.x - 2 * r - 12)
Style.text(self, nm, f_name, Style.TEXT, Vector2(size.x / 2 + r / 2.0, 4), "midtop", true)
if mid:
var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
# Bild
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0)
var band_bottom: float = size.y * 0.42 - 4
if band_bottom - band_top > 14:
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
# Textfeld
var box := Rect2(6, size.y * 0.42, size.x - 12, size.y * 0.42)
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(f_text)),
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, f_text, 6 if big else 4, 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)
if dim:
draw_rect(rect, Color(0, 0, 0, 0.47))

View File

@@ -0,0 +1 @@
uid://oom648upeavd

View File

@@ -0,0 +1,92 @@
class_name CreatureView
extends Control
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
signal clicked(view: CreatureView)
signal hover(card: Cards.Card, entered: bool)
var creature: Game.Creature
var border = null
var width := 2
var flash := false # kurz rot: wurde gerade angegriffen
var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren)
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
creature = cr
border = b
width = w
custom_minimum_size = sz
size = sz
pivot_offset = sz / 2
mouse_filter = Control.MOUSE_FILTER_STOP
mouse_entered.connect(func(): hover.emit(creature.card, true))
mouse_exited.connect(func(): hover.emit(creature.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(self)
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, size)
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]
if flash:
bcol = Style.RED
Style.border(self, rect, bcol, 4 if flash else width, 6)
if mini:
_draw_mini()
return
var fs := 11 if small else 13
var nm := Style.fit(creature.name, true, fs, size.x - 6)
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
var badges: Array[String] = []
if creature.taunt():
badges.append("Wächter")
if creature.shield:
badges.append("Schild")
if creature.attacks_per_turn() == 2:
badges.append("x2")
if creature.frozen:
badges.append("Eis")
if creature.resonance > 0:
badges.append("Resonanz")
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
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)
if creature.frozen:
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
func _draw_mini() -> void:
var letters: Array[String] = []
if creature.taunt():
letters.append("W")
if creature.shield:
letters.append("S")
if creature.attacks_per_turn() == 2:
letters.append("2")
if creature.frozen:
letters.append("E")
if creature.resonance > 0:
letters.append("R")
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
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)
if creature.frozen:
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))

View File

@@ -0,0 +1 @@
uid://bcnj0sdt2jbjb

351
scripts/ui/deck_screen.gd Normal file
View File

@@ -0,0 +1,351 @@
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()

View File

@@ -0,0 +1 @@
uid://yg8pbj56mib5

View File

@@ -0,0 +1,111 @@
class_name HotseatScreen
extends Control
## Lokal zu mehreren am selben Rechner: Spielerzahl, KI-Zahl und Decks wählen.
var app
var n_humans := 2
var n_ai := 0
var deck_choice: Array = [] # je Mensch: Slot-Index oder -1 für Zufallsdeck
var human_buttons: Array = []
var ai_buttons: Array = []
var deck_rows: VBoxContainer
var b_start: Button
var hint: Label
func _ready() -> void:
var col := Style.column(self, 760, 40, 10)
Style.clabel(col, "Lokal zu mehreren", 36, Style.GOLD, "center", true)
Style.clabel(col, "Alle spielen am selben Rechner und reichen die Maus weiter. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
Style.spacer(col, 10)
Style.clabel(col, "Spieler", 20, Style.TEXT, "center", true)
var hr := Style.row(col, 10)
hr.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(2, 5):
human_buttons.append(Style.cbutton(hr, "%d Spieler" % n, Vector2(150, 44), func(): set_humans(n)))
Style.clabel(col, "KI-Gegner", 20, Style.TEXT, "center", true)
var ar := Style.row(col, 10)
ar.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(0, 5):
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 44), func(): set_ai(n)))
Style.spacer(col, 6)
Style.clabel(col, "Decks", 20, Style.TEXT, "center", true)
deck_rows = VBoxContainer.new()
deck_rows.add_theme_constant_override("separation", 6)
col.add_child(deck_rows)
Style.spacer(col, 6)
hint = Style.clabel(col, "", 15, Style.ORANGE)
var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER
b_start = Style.cbutton(br, "Start", Vector2(200, 48), start, Color("285a3c"))
Style.cbutton(br, "Zurück", Vector2(160, 48), func(): app.show_screen(MenuScreen.new()))
for i in 4:
deck_choice.append(-1)
var p: Profile = app.profile
if p.deck_valid()[0]:
deck_choice[0] = p.active_deck
refresh()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())
func set_humans(n: int) -> void:
n_humans = n
n_ai = mini(n_ai, Game.MAX_OPPONENTS + 1 - n_humans)
refresh()
func set_ai(n: int) -> void:
n_ai = n
refresh()
func valid_slots() -> Array:
var out := []
for i in Profile.DECK_SLOTS:
if app.profile.deck_valid(i)[0]:
out.append(i)
return out
func refresh() -> void:
var p: Profile = app.profile
for i in human_buttons.size():
human_buttons[i].disabled = (i + 2) == n_humans
for i in ai_buttons.size():
ai_buttons[i].disabled = i == n_ai or n_humans + i > Game.MAX_OPPONENTS + 1
Style.clear(deck_rows)
var slots := valid_slots()
for h in n_humans:
var r := Style.row(deck_rows, 12)
r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.clabel(r, "Spieler %d" % (h + 1), 16, Style.TEXT, "right", true, 140)
var ob := OptionButton.new()
ob.custom_minimum_size = Vector2(340, 36)
ob.add_item("Zufallsdeck", -1)
for slot in slots:
var elems := Game.deck_elements(p.deck_cards(slot))
ob.add_item("%s (%s)" % [p.deck_name(slot), ", ".join(elems)], slot)
ob.select(ob.get_item_index(deck_choice[h]) if deck_choice[h] >= 0 and slots.has(deck_choice[h]) else 0)
var idx := h
ob.item_selected.connect(func(i: int): deck_choice[idx] = ob.get_item_id(i))
r.add_child(ob)
hint.text = "%d am Tisch" % (n_humans + n_ai)
func start() -> void:
var p: Profile = app.profile
var r := Game.make_rng()
var decks := []
for h in n_humans:
var slot: int = deck_choice[h]
if slot >= 0 and p.deck_valid(slot)[0]:
decks.append(p.deck_cards(slot))
else:
var pool: Array = Cards.ELEMENTS.duplicate()
Game.shuffle(pool, r)
decks.append(Game.build_opponent_deck(r, [pool[0], pool[1]]))
app.show_screen(BattleScreen.hotseat(decks, n_ai))

View File

@@ -0,0 +1 @@
uid://crlne4y0uiygl

112
scripts/ui/lobby_screen.gd Normal file
View File

@@ -0,0 +1,112 @@
class_name LobbyScreen
extends Control
## Netzwerkspiel im lokalen Netz: Spiel eröffnen oder beitreten.
var app
var net: Net
var status_label: Label
var list_label: Label
var ip_box: LineEdit
var b_host: Button
var b_join: Button
var b_start: Button
var ai_buttons: Array = []
var n_ai := 0
var addr_label: Label
func _ready() -> void:
net = app.net
net.my_name = "Spieler"
net.my_deck = app.profile.deck.duplicate()
var col := Style.column(self, 760, 40, 10)
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
Style.clabel(col, "Im selben Netz: einer eröffnet, die anderen treten mit seiner Adresse bei. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
Style.spacer(col, 10)
var hr := Style.row(col, 12)
hr.alignment = BoxContainer.ALIGNMENT_CENTER
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
ip_box = LineEdit.new()
ip_box.custom_minimum_size = Vector2(220, 46)
ip_box.placeholder_text = "Adresse des Hosts"
ip_box.text = "127.0.0.1"
hr.add_child(ip_box)
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
addr_label = Style.clabel(col, "", 14, Style.MUTED)
status_label = Style.clabel(col, "", 16, Style.BLUE)
Style.spacer(col, 6)
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
list_label = Style.clabel(col, "", 16, Style.TEXT)
Style.spacer(col, 6)
Style.clabel(col, "KI-Gegner (nur Host)", 16, Style.MUTED)
var ar := Style.row(col, 10)
ar.alignment = BoxContainer.ALIGNMENT_CENTER
for n in range(0, 5):
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 40), func(): _set_ai(n)))
Style.spacer(col, 10)
var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
Style.cbutton(br, "Zurück", Vector2(160, 48), _back)
net.lobby_changed.connect(refresh)
net.started.connect(_started)
net.ended.connect(func(_reason): refresh())
refresh()
func _exit_tree() -> void:
if net.lobby_changed.is_connected(refresh):
net.lobby_changed.disconnect(refresh)
if net.started.is_connected(_started):
net.started.disconnect(_started)
func _host() -> void:
net.host()
refresh()
func _join() -> void:
net.join(ip_box.text.strip_edges())
refresh()
func _set_ai(n: int) -> void:
n_ai = n
refresh()
func _start() -> void:
net.start_battle(n_ai)
func _started(seat: int) -> void:
app.show_screen(BattleScreen.online(seat))
func _back() -> void:
net.leave()
app.show_screen(MenuScreen.new())
func refresh() -> void:
status_label.text = net.status
var lines: Array[String] = []
var ids: Array = net.names.keys()
ids.sort()
for id in ids:
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
b_host.disabled = net.connected
b_join.disabled = net.connected
ip_box.editable = not net.connected
var humans := ids.size()
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
for i in ai_buttons.size():
ai_buttons[i].disabled = not net.is_host or i == n_ai or humans + i > Game.MAX_OPPONENTS + 1
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not ip_box.has_focus():
_back()

View File

@@ -0,0 +1 @@
uid://bhnnxjh6rsxnf

52
scripts/ui/main.gd Normal file
View File

@@ -0,0 +1,52 @@
extends Control
## Einstieg: hält den Spielstand und wechselt zwischen den Bildschirmen.
var profile: Profile
var screen: Control
var save_path: String = Profile.SAVE_PATH
var sfx: Sfx
var net: Net
func _ready() -> void:
theme = Style.make_theme()
var bg := Style.Backdrop.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(bg)
sfx = Sfx.new()
add_child(sfx)
# Fester Pfad /root/Net: Netzwerkaufrufe werden über den Knotenpfad zugestellt,
# der bei Host und Mitspielern gleich sein muss.
net = Net.new()
net.name = "Net"
get_tree().root.add_child.call_deferred(net)
profile = Profile.load_or_new(save_path)
profile.refresh_daily()
sfx.set_volume(profile.settings.volume)
sfx.set_music_volume(profile.settings.music)
set_language(profile.settings.language)
show_screen(MenuScreen.new())
func set_language(code: String) -> void:
TranslationServer.set_locale(code)
func show_screen(s: Control) -> void:
if screen != null:
screen.queue_free()
screen = s
s.app = self
s.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(s)
func save() -> void:
profile.save(save_path)
func _exit_tree() -> void:
if net != null and net.is_inside_tree():
net.leave()
net.queue_free()

1
scripts/ui/main.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bkltqeiif4e0j

84
scripts/ui/menu_screen.gd Normal file
View File

@@ -0,0 +1,84 @@
class_name MenuScreen
extends Control
var app
var b_single: Button
var b_campaign: Button
var b_hotseat: Button
var b_online: Button
var b_tutorial: 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)
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
grid.add_theme_constant_override("h_separation", 12)
grid.add_theme_constant_override("v_separation", 8)
var center := CenterContainer.new()
center.add_child(grid)
col.add_child(center)
var sz := Vector2(270, 44)
var p0: Profile = app.profile
b_tutorial = Style.cbutton(grid, "Anleitung", sz, func(): app.show_screen(BattleScreen.guided()),
Color("5a4628") if not p0.tutorial_done else null)
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_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.new()), Color("285a3c"))
b_online = 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, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
Style.cbutton(grid, "Erfolge", sz, func(): app.show_screen(AchievementsScreen.new()))
Style.cbutton(grid, "Regeln", sz, func(): app.show_screen(RulesScreen.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()
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_tutorial.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 = "Münzen: %d · Staub: %d" % [p.coins, p.dust]
coll_label.text = "Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d" % [
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.STAGES.size()]
var elems := Game.deck_elements(p.deck_cards())
deck_label.text = "Aktives Deck: %s (%s)" % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else "leer"]
var task := p.daily_task()
if p.daily.done:
daily_label.text = "Tagesaufgabe erledigt: %s ✓" % task.text
else:
daily_label.text = "Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
warn_label.text = "" if ok[0] else "Aktives Deck nicht spielbar: " + ok[1]
if ok[0] and not p.tutorial_done:
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
warn_label.add_theme_color_override("font_color", Style.GOLD)
var notes := p.take_notices()
notice_label.text = " · ".join(notes)
if not notes.is_empty():
app.save()

View File

@@ -0,0 +1 @@
uid://3ib6kcyl21f

View File

@@ -0,0 +1,29 @@
class_name OpponentSelectScreen
extends Control
## Einzelspieler: Auswahl, gegen wie viele Gegner gekämpft wird.
var app
func _ready() -> void:
var col := Style.column(self, 900, 50, 14)
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
Style.clabel(col, "Wie viele Gegner? Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben." % Game.START_LIFE, 18, Style.MUTED)
Style.clabel(col, "Du gewinnst, wenn alle anderen besiegt sind. Mehr Gegner, mehr Chaos, mehr Münzen.", 16, Style.MUTED)
Style.spacer(col, 10)
for i in range(1, Game.MAX_OPPONENTS + 1):
var n := i
var r := Style.row(col, 20)
r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(r, "%d Gegner" % n, Vector2(200, 56), func(): app.show_screen(BattleScreen.new(n)), Color("285a3c"))
var info := ("Duell" if n == 1 else "%d Spieler am Tisch" % (n + 1)) + " · Sieg: %d Münzen" % Profile.REWARD_WIN[n]
Style.clabel(r, info, 16, Style.MUTED, "left", false, 420)
Style.spacer(col, 20)
var br := Style.row(col)
br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -0,0 +1 @@
uid://cjpjji3gxmxvf

View File

@@ -0,0 +1,25 @@
class_name RulesScreen
extends Control
var app
func _ready() -> void:
Style.label(self, "Regeln", 36, Style.GOLD, Rect2(0, 18, 1280, 50), "center", true)
var scroll := ScrollContainer.new()
scroll.position = Vector2(60, 78)
scroll.size = Vector2(1160, 640)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
var text := Label.new()
text.text = Style.rules_text()
text.autowrap_mode = TextServer.AUTOWRAP_WORD
text.custom_minimum_size = Vector2(1140, 0)
text.add_theme_font_size_override("font_size", 15)
scroll.add_child(text)
Style.button(self, "Zurück", Rect2(560, 735, 160, 44), func(): app.show_screen(MenuScreen.new()))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -0,0 +1 @@
uid://bkemrytc2pjem

View File

@@ -0,0 +1,121 @@
class_name SettingsScreen
extends Control
var app
var volume: HSlider
var music: HSlider
var speed: HSlider
var language: OptionButton
var music_label: Label
var anim: CheckButton
var volume_label: Label
var speed_label: Label
func _ready() -> void:
var s: Dictionary = app.profile.settings
var col := Style.column(self, 700, 50, 24)
Style.clabel(col, "Einstellungen", 36, Style.GOLD, "center", true)
Style.spacer(col, 30)
var grid := GridContainer.new()
grid.columns = 3
grid.add_theme_constant_override("h_separation", 24)
grid.add_theme_constant_override("v_separation", 30)
col.add_child(grid)
Style.clabel(grid, "Lautstärke", 20, Style.TEXT, "left", false, 200)
volume = HSlider.new()
volume.custom_minimum_size = Vector2(280, 24)
volume.min_value = 0
volume.max_value = 1
volume.step = 0.05
volume.value = s.volume
volume.value_changed.connect(_on_volume)
grid.add_child(volume)
volume_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
Style.clabel(grid, "Musik", 20, Style.TEXT, "left", false, 200)
music = HSlider.new()
music.custom_minimum_size = Vector2(280, 24)
music.min_value = 0
music.max_value = 1
music.step = 0.05
music.value = s.music
music.value_changed.connect(_on_music)
grid.add_child(music)
music_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
Style.clabel(grid, "KI-Tempo", 20, Style.TEXT, "left", false, 200)
speed = HSlider.new()
speed.custom_minimum_size = Vector2(280, 24)
speed.min_value = 0.5
speed.max_value = 3.0
speed.step = 0.25
speed.value = s.ai_speed
speed.value_changed.connect(_on_speed)
grid.add_child(speed)
speed_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
Style.clabel(grid, "Animationen", 20, Style.TEXT, "left", false, 200)
anim = CheckButton.new()
anim.button_pressed = s.animations
anim.toggled.connect(_on_anim)
grid.add_child(anim)
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
Style.clabel(grid, "Sprache", 20, Style.TEXT, "left", false, 200)
language = OptionButton.new()
language.custom_minimum_size = Vector2(280, 36)
language.add_item("Deutsch", 0)
language.add_item("English (Oberfläche)", 1)
language.select(1 if s.language == "en" else 0)
language.item_selected.connect(_on_language)
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, 60)
var r := Style.row(col)
r.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
_labels()
func _labels() -> void:
volume_label.text = "%d %%" % int(round(volume.value * 100))
music_label.text = "%d %%" % int(round(music.value * 100))
speed_label.text = "%.2fx" % speed.value
func _on_music(v: float) -> void:
app.profile.settings.music = v
app.sfx.set_music_volume(v)
app.save()
_labels()
func _on_language(i: int) -> void:
var code := "en" if i == 1 else "de"
app.profile.settings.language = code
app.save()
app.set_language(code)
app.show_screen(SettingsScreen.new())
func _on_volume(v: float) -> void:
app.profile.settings.volume = v
app.sfx.set_volume(v)
app.sfx.play("click")
app.save()
_labels()
func _on_speed(v: float) -> void:
app.profile.settings.ai_speed = v
app.save()
_labels()
func _on_anim(on: bool) -> void:
app.profile.settings.animations = on
app.save()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -0,0 +1 @@
uid://i4f1frwa2cmm

107
scripts/ui/sfx.gd Normal file
View File

@@ -0,0 +1,107 @@
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
var music_player: AudioStreamPlayer
var music_volume := 0.5
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)
streams["damage"] = _tone(140, 0.12, "square")
streams["heal"] = _tone(523, 0.22, "sine", 784)
streams["combo"] = _tone(660, 0.2, "sine", 1100)
streams["win"] = _tone(400, 0.6, "sine", 1200)
streams["lose"] = _tone(320, 0.7, "square", 90)
for i in 4:
var p := AudioStreamPlayer.new()
add_child(p)
players.append(p)
music_player = AudioStreamPlayer.new()
music_player.stream = _music()
add_child(music_player)
set_music_volume(music_volume)
## Leise Hintergrundmusik: Arpeggio über vier Akkorde, als Schleife erzeugt.
func _music() -> AudioStreamWAV:
var rate := 22050
var chords := [[220.0, 261.63, 329.63], [174.61, 220.0, 261.63], [130.81, 164.81, 196.0], [196.0, 246.94, 293.66]]
var note_len := 0.25
var pattern := [0, 1, 2, 1, 0, 2, 1, 2]
var total := chords.size() * pattern.size() * note_len
var n := int(total * rate)
var data := PackedByteArray()
data.resize(n * 2)
for i in n:
var t := float(i) / rate
var step := int(t / note_len)
var chord: Array = chords[(step / pattern.size()) % chords.size()]
var f: float = chord[pattern[step % pattern.size()]]
var lt := t - step * note_len
var env := minf(1.0, lt * 30.0) * maxf(0.0, 1.0 - lt / note_len)
var v := sin(TAU * f * t) * 0.6 + sin(TAU * f * 2.0 * t) * 0.15 + sin(TAU * chord[0] * 0.5 * t) * 0.25
data.encode_s16(i * 2, int(clampf(v * env * 0.25, -1.0, 1.0) * 32767))
var wav := AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_16_BITS
wav.mix_rate = rate
wav.stereo = false
wav.data = data
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
wav.loop_begin = 0
wav.loop_end = n
return wav
func set_music_volume(v: float) -> void:
music_volume = clampf(v, 0.0, 1.0)
if music_volume <= 0.0:
music_player.stop()
else:
music_player.volume_db = linear_to_db(music_volume * 0.6)
if not music_player.playing:
music_player.play()
func set_volume(v: float) -> void:
volume = clampf(v, 0.0, 1.0)
func play(kind: String) -> void:
if volume <= 0.0 or not streams.has(kind):
return
for p in players:
if not p.playing:
p.stream = streams[kind]
p.volume_db = linear_to_db(volume)
p.play()
return
func _tone(freq: float, dur: float, shape: String, freq_end: float = -1.0) -> AudioStreamWAV:
var rate := 22050
var n := int(dur * rate)
var data := PackedByteArray()
data.resize(n * 2)
for i in n:
var t := float(i) / rate
var f := freq if freq_end < 0 else lerpf(freq, freq_end, t / dur)
var env := 1.0 - t / dur
var v := sin(TAU * f * t)
if shape == "square":
v = 1.0 if v >= 0 else -1.0
data.encode_s16(i * 2, int(clampf(v * env * 0.4, -1.0, 1.0) * 32767))
var wav := AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_16_BITS
wav.mix_rate = rate
wav.stereo = false
wav.data = data
return wav

1
scripts/ui/sfx.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bprwiyidhn7ek

365
scripts/ui/style.gd Normal file
View File

@@ -0,0 +1,365 @@
class_name Style
## Farben, Schriften, Theme und Zeichenhilfen.
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
const BG := Color("1a1c24")
const PANEL := Color("282b36")
const PANEL_LIGHT := Color("3a3e4c")
const TEXT := Color("ececf0")
const MUTED := Color("9698a5")
const GOLD := Color("f5c850")
const GREEN := Color("5ad282")
const RED := Color("e65555")
const BLUE := Color("6eaaf0")
const YELLOW := Color("ffe65a")
const ORANGE := Color("ff963c")
static func darker(c: Color, f: float = 0.65) -> Color:
return Color(c.r * f, c.g * f, c.b * f, c.a)
static func lighter(c: Color, d: float = 0.1) -> Color:
return Color(minf(1, c.r + d), minf(1, c.g + d), minf(1, c.b + d), c.a)
static func box(bg: Color, radius: int = 8, border_col = null, border_w: int = 0) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(radius)
if border_col != null:
sb.border_color = border_col
sb.set_border_width_all(border_w)
return sb
static func make_theme() -> Theme:
var t := Theme.new()
t.default_font = FONT
t.default_font_size = 16
t.set_stylebox("normal", "Button", box(PANEL_LIGHT, 8, MUTED, 2))
t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2))
t.set_stylebox("pressed", "Button", box(darker(PANEL_LIGHT), 8, MUTED, 2))
t.set_stylebox("disabled", "Button", box(darker(PANEL, 0.9), 8, PANEL, 2))
t.set_stylebox("focus", "Button", StyleBoxEmpty.new())
for name in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
t.set_color(name, "Button", TEXT)
t.set_color("font_disabled_color", "Button", MUTED)
t.set_font_size("font_size", "Button", 18)
t.set_color("font_color", "Label", TEXT)
t.set_stylebox("panel", "Panel", box(PANEL, 8))
t.set_stylebox("normal", "RichTextLabel", StyleBoxEmpty.new())
t.set_stylebox("focus", "RichTextLabel", StyleBoxEmpty.new())
t.set_stylebox("panel", "ScrollContainer", StyleBoxEmpty.new())
t.set_stylebox("normal", "LineEdit", box(PANEL_LIGHT, 6, MUTED, 1))
t.set_stylebox("focus", "LineEdit", box(PANEL_LIGHT, 6, BLUE, 2))
t.set_color("font_color", "LineEdit", TEXT)
t.set_color("font_placeholder_color", "LineEdit", MUTED)
t.set_stylebox("normal", "OptionButton", box(PANEL_LIGHT, 6, MUTED, 1))
t.set_stylebox("hover", "OptionButton", box(lighter(PANEL_LIGHT), 6, MUTED, 1))
t.set_stylebox("pressed", "OptionButton", box(darker(PANEL_LIGHT), 6, MUTED, 1))
t.set_stylebox("focus", "OptionButton", StyleBoxEmpty.new())
t.set_color("font_color", "OptionButton", TEXT)
t.set_color("font_color", "CheckButton", TEXT)
t.set_stylebox("focus", "CheckButton", StyleBoxEmpty.new())
return t
# --- Knoten-Hilfen -------------------------------------------------------------
static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8, border_col = null, border_w: int = 0) -> Panel:
var p := Panel.new()
p.position = rect.position
p.size = rect.size
p.add_theme_stylebox_override("panel", box(bg, radius, border_col, border_w))
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(p)
return p
static func label(parent: Node, text: String, size: int, color: Color, rect: Rect2, align: String = "left", bold: bool = false) -> Label:
var l := Label.new()
l.text = text
l.position = rect.position
l.size = rect.size
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", color)
if bold:
l.add_theme_font_override("font", FONT_BOLD)
match align:
"center":
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
"right":
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(l)
return l
static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable, color = null) -> Button:
var b := Button.new()
b.text = text
b.position = rect.position
b.size = rect.size
b.pressed.connect(on_pressed)
if color != null:
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
parent.add_child(b)
return b
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
var v := VBoxContainer.new()
v.anchor_left = 0.5
v.anchor_right = 0.5
v.anchor_top = 0
v.anchor_bottom = 0
v.offset_left = -width / 2
v.offset_right = width / 2
v.offset_top = top
v.add_theme_constant_override("separation", sep)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(v)
return v
static func row(parent: Node, sep: int = 12) -> HBoxContainer:
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", sep)
h.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(h)
return h
## Label in einem Container.
static func clabel(parent: Node, text: String, size: int, color: Color, align: String = "center", bold: bool = false, min_w: float = 0) -> Label:
var l := label(parent, text, size, color, Rect2(0, 0, 0, 0), align, bold)
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL if min_w == 0 else Control.SIZE_FILL
if min_w > 0:
l.custom_minimum_size = Vector2(min_w, 0)
return l
## Knopf in einem Container.
static func cbutton(parent: Node, text: String, size: Vector2, on_pressed: Callable, color = null) -> Button:
var b := button(parent, text, Rect2(Vector2.ZERO, size), on_pressed, color)
b.custom_minimum_size = size
return b
static func spacer(parent: Node, h: float) -> Control:
var c := Control.new()
c.custom_minimum_size = Vector2(0, h)
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(c)
return c
static func clear(node: Node) -> void:
for c in node.get_children():
node.remove_child(c)
c.queue_free()
# --- Zeichenhilfen für _draw() ---------------------------------------------------
static func text(ci: CanvasItem, s: String, size: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
var font: Font = FONT_BOLD if bold else FONT
var dim := font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
var x := pos.x
var y := pos.y
match anchor:
"center":
x -= dim.x / 2
y -= dim.y / 2
"midtop":
x -= dim.x / 2
"topright":
x -= dim.x
"midleft":
y -= dim.y / 2
"midright":
x -= dim.x
y -= dim.y / 2
"bottomleft":
y -= dim.y
"bottomright":
x -= dim.x
y -= dim.y
"midbottom":
x -= dim.x / 2
y -= dim.y
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
var font: Font = FONT_BOLD if bold else FONT
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
s = s.substr(0, s.length() - 2) + "…"
return s
static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius: int) -> void:
var sb := StyleBoxFlat.new()
sb.draw_center = false
sb.border_color = color
sb.set_border_width_all(width)
sb.set_corner_radius_all(radius)
ci.draw_style_box(sb, rect)
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)))
static func rules_text() -> String:
var lines := [
"Ziel: Besiege alle anderen. Du verlierst, wenn dein Leben auf 0 fällt.",
"Du spielst gegen 1 bis 5 Magier, jeder gegen jeden, alle nach denselben Regeln:",
"%d Leben, pro Zug 1 Energie mehr (höchstens 10) und 1 Karte. Die Zugreihenfolge wird ausgelost." % Game.START_LIFE,
"Wer nicht beginnt, startet mit einer Karte mehr und hat im ersten Zug 1 Energie extra.",
"Fallen liegen verdeckt (höchstens 2) und lösen aus, wenn dich jemand angreift oder ein Wesen ausspielt.",
"",
"Karten kosten Energie. Wesen kommen aufs Feld (max. 5) und greifen ab dem nächsten Zug an.",
"Zauber wirken sofort. Ein Wesen greift ein gegnerisches Wesen oder direkt den Gegner an;",
"beide Seiten verursachen ihren Angriffswert als Schaden.",
"",
"Elemente: Feuer (Schaden), Wasser (Kontrolle), Erde (Zähigkeit), Luft (Tempo).",
"Kombos verbinden zwei Elemente, auch das gleiche doppelt (Feuer+Feuer = Inferno).",
"Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.",
Cards.RESONANCE_TEXT,
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an alle gegnerischen Wesen, Blitz 2 Schaden",
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
"",
"Schlüsselwörter:",
]
for k in Cards.KEYWORDS:
lines.append(" %s: %s" % [k, Cards.KEYWORD_HELP[k]])
lines.append("")
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
lines.append("")
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
return "\n".join(lines)
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
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:
_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)
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
var h := size / 2
match element:
"Feuer":
var n := 1 + variant
for i in n:
var off := Vector2((i - (n - 1) / 2.0) * size * 0.32, 0)
var s := 1.0 if i == n / 2 else 0.7
_flame(ci, c + off, h * s, col)
"Wasser":
for i in 2 + variant % 2:
var y := c.y + (i - 0.5) * size * 0.3
for k in 3:
var x := c.x - size * 0.4 + k * size * 0.4
ci.draw_arc(Vector2(x, y), size * 0.2, PI, TAU, 12, col, maxf(2, size * 0.09))
"Erde":
var n := 1 + variant
for i in n:
var off := (i - (n - 1) / 2.0) * size * 0.4
var s := 1.0 if i == n / 2 else 0.65
var pts := PackedVector2Array([c + Vector2(off - h * s, h * 0.7), c + Vector2(off, -h * s), c + Vector2(off + h * s, h * 0.7)])
ci.draw_colored_polygon(pts, col)
ci.draw_line(c + Vector2(-h, h * 0.7), c + Vector2(h, h * 0.7), col, maxf(2, size * 0.08))
"Luft":
for i in 2 + variant:
var r := h * (1.0 - i * 0.28)
ci.draw_arc(c + Vector2(i * size * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, size * 0.09))
static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
var w := h * 0.75
var pts := PackedVector2Array([
c + Vector2(0, -h), c + Vector2(w * 0.55, -h * 0.25), c + Vector2(w * 0.4, h * 0.45),
c + Vector2(0, h), c + Vector2(-w * 0.4, h * 0.45), c + Vector2(-w * 0.55, -h * 0.25)])
ci.draw_colored_polygon(pts, col)
var inner := PackedVector2Array([
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75))
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
class Backdrop extends Control:
func _draw() -> void:
var sz := size
draw_rect(Rect2(Vector2.ZERO, sz), Style.BG)
var c := Vector2(sz.x * 0.5, sz.y * 0.35)
for i in range(8, 0, -1):
var r := sz.x * 0.09 * i
draw_circle(c, r, Color(0.16, 0.18, 0.26, 0.05))
for k in range(-20, 40):
var x := k * 80.0
draw_line(Vector2(x, 0), Vector2(x + sz.y * 0.6, sz.y), Color(1, 1, 1, 0.012), 1.0)

1
scripts/ui/style.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://diaenhpjc2b3i

View File

@@ -0,0 +1,151 @@
class_name WorkshopScreen
extends Control
## Werkstatt: Karten zerlegen (Staub) und herstellen.
var app
var filter := ""
var search := ""
var selected: Cards.Card = null
var grid: CardGrid
var search_box: LineEdit
var dust_label: Label
var msg_label: Label
var preview_holder: Control
var info_label: Label
var b_dismantle: Button
var b_craft: Button
var b_extras: Button
func _ready() -> void:
Style.label(self, "Werkstatt", 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]
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(200, 34)
search_box.placeholder_text = "Suche"
search_box.text_changed.connect(func(t: String):
search = t
refresh())
add_child(search_box)
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
grid.card_clicked.connect(select)
add_child(grid)
Style.panel(self, Rect2(845, 50, 420, 680))
dust_label = Style.label(self, "", 22, Style.BLUE, Rect2(860, 58, 390, 30), "left", true)
preview_holder = Control.new()
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")
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
refresh()
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus():
app.show_screen(MenuScreen.new())
func set_filter(e: String) -> void:
filter = e
refresh()
func cards() -> Array:
var out: Array = Cards.all().duplicate()
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())
return out
func select(card: Cards.Card) -> void:
selected = card
msg_label.text = ""
refresh()
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 dismantle() -> void:
if selected == null:
return
var err: String = app.profile.dismantle(selected.id)
if err == "":
app.sfx.play("click")
app.save()
say("%s zerlegt (+%d Staub)" % [selected.name, Profile.DUST_DISMANTLE[selected.rarity]], 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("%s hergestellt" % selected.name + (" · " + " · ".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)
Style.clear(preview_holder)
if selected == null:
info_label.text = "Keine Karte gewählt"
b_dismantle.text = "Zerlegen"
b_craft.text = "Herstellen"
b_dismantle.disabled = true
b_craft.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\nIm Besitz: %dx · Gespielt %dx · %d Partien, %d Siege" % [
selected.combo(), selected.rarity, 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

View File

@@ -0,0 +1 @@
uid://cf42h6bsh8lvg