update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
team3
2026-10-10 14:30:29 +02:00
parent c245c7af87
commit a11cee2e0a
37 changed files with 2190 additions and 2414 deletions

View File

@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
func items() -> Array:
match zone:
"deck":
return screen.offer_views
return [screen.deck] if screen.deck.visible else []
"hand":
return screen.hand_views
"own":
@@ -105,7 +105,7 @@ func confirm() -> void:
var item = items[index]
match zone:
"deck":
screen.on_click("deck", index)
screen.on_click("deck", null)
"hand":
screen.on_click("hand", item.card)
"own":

View File

@@ -1,31 +1,29 @@
class_name BattleLayout
## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
## (Rückseiten), sein Brett, seine Elementleiste, die eigene Elementleiste (die Naht liegt auf halber
## Höhe), eigenes Brett, eigene Hand. Links: Kasten des Gegners, die drei offenen Karten des
## Stapels (mittig), eigener Kasten. Rechts oben die Knöpfe, rechts mittig die Vorschau, darunter Meldungen.
## (Rückseiten), sein Brett, eigenes Brett, eigene Hand. Links: Kasten des Gegners, daneben die
## Elementzähler beider Seiten je neben ihrem Brett, Zusatzzeile und Meldungen. Rechts von oben:
## Stapel und Kasten des Gegners, die Vorschau, eigener Kasten und Stapel. Optionen per Esc.
const B_GIVEUP := Rect2(1070, 10, 96, 32)
const B_LOG := Rect2(1172, 10, 96, 32)
const B_END := Rect2(1070, 48, 198, 32)
const OPP_BOX := Rect2(20, 11, 210, 96)
const B_UNDO := Rect2(20, 10, 210, 32) # links oben: Rückgängig, darunter Überspringen (Gegnerzug)
const B_SKIP := Rect2(20, 48, 210, 32)
const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben
const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter
const OPP_HAND_Y := 11
const FIELD_OPP := Rect2(240, 364, 800, 32)
const FIELD := Rect2(240, 404, 800, 32)
const MSG := Rect2(1040, 630, 220, 60)
const STATUS := Rect2(20, 694, 210, 96)
const B_UNDO := Rect2(1070, 86, 198, 32)
const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett
const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau
const MSG := Rect2(20, 650, 210, 80) # Meldungen
const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel
const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
const OWN_X := 345
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
const HAND_H := 140
const HAND_GAP := 10
const HAND_Y := 610
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
const OFFER_X := 70 # die drei offenen Karten des Stapels, links übereinander um die Mitte
const OFFER_Y := 180
const OFFER_GAP := 10
const DECK_LABEL := Rect2(20, 632, 210, 22)
const LOG_PANEL := Rect2(240, 60, 800, 560)
const PREVIEW_SIZE := Vector2(180, 252)
const PREVIEW_SIZE := Vector2(170, 238)
const CENTER_X := 240
const CENTER_W := 800
@@ -61,11 +59,11 @@ static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
## Vorschau der gezeigten Karte: fest rechts zwischen den beiden Kästen.
static func preview_rect(_anchor: Rect2) -> Rect2:
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
return Rect2(Vector2(1070, 278), PREVIEW_SIZE)
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
static func info_rect(preview: Rect2) -> Rect2:
return Rect2(1030, preview.end.y + 8, 230, 80)
## Zusatzzeile (Elementbonus, Eigenschaften) links unter den Elementzählern.
static func info_rect(_preview: Rect2) -> Rect2:
return INFO

View File

@@ -1,17 +1,16 @@
class_name BattleScreen
extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden); den Zug beendet
## die Leertaste oder der Rechtsklick ins Leere.
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
## Modi: Einzelspieler und Kampagne.
var app
var stage := -1 # Kampagnenstufe oder -1
var net_play := false # Netzwerkspiel (auch als Zuschauer)
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
var net: Net
var game: Game
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
var fx: BattleFx
@@ -60,12 +59,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
var hero_layer: Control
var tile_layer: Control
var hand_layer: Control
var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
var offer_layer: Control
var deck_label: Label
var draw_enabled := false
var deck: Deck
var opp_deck: Deck # Stapel des Gegners, nur zum Ansehen
var log_overlay: Control
var b_log: Button
var options: Control # Optionsmenü (Esc): Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden
var status_panel: Panel
var fx_layer: Control
var log_box: RichTextLabel
@@ -74,8 +71,6 @@ var name_label: Label
var life_label: Label
var energy_label: Label
var msg_label: Label
var b_end: Button
var b_giveup: Button
var element_view: ElementView # eigene Elementleiste
var opp_element_view: ElementView # die des Gegners darüber
var info_label: Label # Vorschau: Elementbonus, Merkmale
@@ -89,7 +84,6 @@ var reward_label: Label
var notice_label: Label
var confirm: Control
var cursor_layer: Control
var wait_label: Label
var b_skip: Button
var b_undo: Button
@@ -98,30 +92,6 @@ func _init(campaign_stage: int = -1) -> void:
stage = campaign_stage
static func online(seat: int) -> BattleScreen:
var b := BattleScreen.new(-1)
b.net_play = true
b.online_seat = seat
return b
func is_online() -> bool:
return net_play
## Zuschauer im Netzwerk: kein eigener Platz, keine Hand.
func spectating() -> bool:
return is_online() and online_seat < 0
func is_client() -> bool:
return is_online() and not net.is_host
func counts_for_profile() -> bool:
return not is_online()
func _ready() -> void:
var s: Dictionary = app.profile.settings
fx = BattleFx.new(self)
@@ -129,31 +99,19 @@ func _ready() -> void:
pad = BattleGamepad.new(self)
ai_delay = 0.75 / float(s.ai_speed)
_build_static()
if is_online():
net = app.net
net.ended.connect(_on_net_ended)
net.migrated.connect(_on_migrated)
net.status_changed.connect(_on_net_status)
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 stage >= 0:
if stage >= 0:
var r := Game.make_rng()
game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules())
else:
game = Game.new_battle(Game.make_rng(), own_rules())
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
smart_ai = bool(app.profile.difficulty().smart)
_begin()
## Kartenoptik, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
## Eigener Stapel und Kartenstufen, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
func own_rules() -> Dictionary:
var p: Profile = app.profile
var rules := {"looks": p.looks()}
var rules := {"looks": p.looks(), "deck": p.deck_cards()}
if stage < 0:
rules["opponent_life"] = int(p.difficulty().life)
if p.perk != "" and p.perk_unlocked():
@@ -162,37 +120,22 @@ func own_rules() -> Dictionary:
func _begin() -> void:
if is_online():
perspective = game.players[maxi(online_seat, 0)]
else:
game.events.clear()
perspective = game.current() if game.current().is_human else game.humans[0]
game.events.clear()
perspective = game.human
build_hero_boxes()
if is_online() and net.is_host:
net.broadcast()
game.events.clear()
refresh()
announce_turn()
func _build_static() -> void:
var L := BattleLayout
# oben links: Aufgeben und Verlauf, klein
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
b_giveup.add_theme_font_size_override("font_size", 13)
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
b_log.add_theme_font_size_override("font_size", 13)
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
b_end.add_theme_font_size_override("font_size", 15)
b_skip = Style.button(self, "Überspringen", L.B_END, func(): skip_ai = true)
b_skip.add_theme_font_size_override("font_size", 15)
b_skip = Style.button(self, "Überspringen", L.B_SKIP, func(): skip_ai = true)
b_skip.add_theme_font_size_override("font_size", 13)
b_skip.visible = false
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
Style.panel(self, L.FIELD_OPP, Style.darker(Style.PANEL, 0.9), 8)
opp_element_view = ElementView.new(L.FIELD_OPP)
# Elementzähler beider Seiten links neben ihrem Brett
opp_element_view = ElementView.new(L.OPP_ELEMENTS)
add_child(opp_element_view)
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
element_view = ElementView.new(L.FIELD)
element_view = ElementView.new(L.OWN_ELEMENTS)
add_child(element_view)
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
@@ -207,18 +150,21 @@ func _build_static() -> void:
var sx: float = L.STATUS.position.x
var sy: float = L.STATUS.position.y
var sw: float = L.STATUS.size.x
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 8, sy + 6, sw - 16, 26), "center", true)
name_label = Style.label(self, "", 16, Style.TEXT, Rect2(sx + 8, sy + 4, sw - 16, 22), "center", true)
name_label.clip_text = true
life_label = Style.label(self, "", 30, Style.RED, Rect2(sx + 8, sy + 42, sw / 2 - 8, 40), "left", true)
energy_label = Style.label(self, "", 30, Style.BLUE, Rect2(sx + sw / 2, sy + 42, sw / 2 - 8, 40), "right", true)
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 10, sy + 30, sw / 2 - 10, 34), "left", true)
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + sw / 2, sy + 30, sw / 2 - 10, 34), "right", true)
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
b_undo.add_theme_font_size_override("font_size", 13)
b_undo.visible = false
deck_label = Style.label(self, "", 13, Style.MUTED, L.DECK_LABEL, "center")
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
wait_label.visible = false
deck = Deck.new(L.DECK)
deck.clicked.connect(func(): on_click("deck", null))
add_child(deck)
opp_deck = Deck.new(L.OPP_DECK)
opp_deck.interactive = false
opp_deck.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(opp_deck)
hero_layer = _layer()
offer_layer = _layer()
tile_layer = _layer()
hand_layer = _layer()
preview_holder = _layer()
@@ -252,6 +198,28 @@ func _build_static() -> void:
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
log_overlay.add_child(log_box)
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
# Optionsmenü (Esc)
options = Style.overlay(self)
options.get_child(0).gui_input.connect(func(ev):
if ev is InputEventMouseButton and ev.pressed:
options.visible = false)
Style.panel(options, L.OPTIONS, Style.PANEL, 10, Style.GOLD, 2)
Style.label(options, "Optionen", 24, Style.GOLD, Rect2(L.OPTIONS.position.x, L.OPTIONS.position.y + 18, L.OPTIONS.size.x, 34), "center", true)
var oy := L.OPTIONS.position.y + 76
for entry in [["Verlauf", func():
options.visible = false
log_overlay.visible = true],
["Aufgeben", func():
options.visible = false
confirm.visible = true],
["Einstellungen", func():
options.visible = false
app.show_screen(SettingsScreen.new(), true)],
["Hauptmenü", leave],
["Beenden", func(): get_tree().quit()]]:
var ob := Style.button(options, entry[0], Rect2(L.OPTIONS.position.x + 60, oy, L.OPTIONS.size.x - 120, 48), entry[1])
ob.add_theme_font_size_override("font_size", 18)
oy += 60
# Spielende
overlay = _overlay()
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
@@ -261,8 +229,8 @@ func _build_static() -> void:
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
func toggle_log() -> void:
log_overlay.visible = not log_overlay.visible
func toggle_options() -> void:
options.visible = not options.visible
func _layer() -> Control:
@@ -277,8 +245,6 @@ func _overlay() -> Control:
func leave() -> void:
if is_online():
net.leave()
app.show_screen(MenuScreen.new())
@@ -336,24 +302,22 @@ func build_hero_boxes() -> void:
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, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 6, box.size.x - 16, 26), "center", true)
labels["name"] = Style.label(hero_layer, title, 16, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "center", true)
labels.name.clip_text = true
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
labels["life"] = Style.label(hero_layer, "", 30, Style.RED, Rect2(box.position.x + 8, box.position.y + 42, box.size.x / 2 - 8, 40), "left", true)
labels["stats"] = Style.label(hero_layer, "", 30, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 42, box.size.x / 2 - 8, 40), "right", true)
labels["life"] = Style.label(hero_layer, "", 26, Style.RED, Rect2(box.position.x + 10, box.position.y + 30, box.size.x / 2 - 10, 34), "left", true)
labels["stats"] = Style.label(hero_layer, "", 26, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 30, box.size.x / 2 - 10, 34), "right", true)
hero_labels[o] = labels
# --- Ablauf und Eingabe ------------------------------------------------------------------
func my_turn() -> bool:
if spectating():
return false
return game != null and game.winner == "" and game.current() == perspective
func any_overlay() -> bool:
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible or options.visible
func _input(event: InputEvent) -> void:
@@ -400,10 +364,12 @@ func _input(event: InputEvent) -> void:
close_menu()
elif log_overlay.visible:
log_overlay.visible = false
elif options.visible:
options.visible = false
elif selected != null or fuse_first != null:
cancel_all()
elif not any_overlay():
confirm.visible = true
options.visible = true
KEY_SPACE:
if my_turn() and not any_overlay():
end_turn()
@@ -451,8 +417,6 @@ func _process(delta: float) -> void:
_end_drag()
do_fuse(a, b)
after_action()
if is_online() and net.is_host:
net.tick()
if game.winner != "":
if not ended:
ended = true
@@ -461,8 +425,6 @@ func _process(delta: float) -> void:
var cur := game.current()
if cur.is_human:
return
if is_client():
return
if skip_ai:
ai_timer = 0.0
else:
@@ -506,15 +468,10 @@ func _exit_tree() -> void:
func _finish() -> void:
var won := game.winner == "human"
if counts_for_profile():
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, stage, factor)
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")
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, stage, factor)
app.save()
app.sfx.play("win" if won else "lose")
refresh()
@@ -631,10 +588,7 @@ func fusion_partners(target) -> Array:
func play(card: Cards.Card, face_down: bool = false) -> void:
if is_client():
net.send_action({"type": "play", "hand": perspective.hand.find(card), "face_down": face_down})
return
var before: Game = game.clone() if not is_online() else null
var before: Game = game.clone()
var log_len := game.log.size()
var from := _hand_rect_of(card)
var err := game.play_card(perspective, card, face_down)
@@ -655,9 +609,6 @@ func play(card: Cards.Card, face_down: bool = false) -> void:
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
func do_reveal(cr: Game.Creature) -> void:
clear_undo()
if is_client():
net.send_action({"type": "reveal", "uid": cr.uid})
return
var err := game.reveal(perspective, cr)
if err != "":
say(tr(err))
@@ -667,7 +618,7 @@ func do_reveal(cr: Game.Creature) -> void:
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
## (Gamepad, das nicht ziehen kann).
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
if spectating() or not my_turn():
if not my_turn():
return
cancel()
Style.clear(menu_layer)
@@ -685,7 +636,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
if full:
var partners := fusion_partners(target)
entries.append([tr("Verschmelzen mit … ◆ %d") % card.fusion_cost(), func():
entries.append([tr("Verschmelzen mit …"), func():
fuse_first = target
say(tr("Ziel wählen"))
refresh(), not partners.is_empty()])
@@ -729,22 +680,16 @@ func close_menu() -> void:
## Abwerfen: eine Handkarte gegen Mana loswerden.
func do_discard(card: Cards.Card) -> void:
clear_undo()
if is_client():
net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
return
var err := game.discard(perspective, card)
if err != "":
say(tr(err))
after_action()
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
func do_draw(index: int = 0) -> void:
## Ziehen: eine Karte vom eigenen Stapel, die erste im Zug frei.
func do_draw() -> void:
clear_undo()
if is_client():
net.send_action({"type": "draw", "index": index})
return
var err := game.draw(perspective, index)
var err := game.draw(perspective)
if err != "":
say(tr(err))
else:
@@ -755,12 +700,6 @@ func do_draw(index: int = 0) -> void:
func do_fuse(a, b) -> void:
cancel()
clear_undo()
if is_client():
if a is Cards.Card:
net.send_action({"type": "fuse", "hand": perspective.hand.find(a), "other_hand": perspective.hand.find(b)})
else:
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
return
var err := game.fuse(perspective, a, b)
if err != "":
say(tr(err))
@@ -784,8 +723,6 @@ func undo() -> void:
restored.say("Rückgängig.")
game = restored
perspective = game.players[idx]
if is_online() and net.is_host:
net.game = game
clear_undo()
cancel()
build_hero_boxes()
@@ -797,9 +734,6 @@ func end_turn() -> void:
return
cancel()
clear_undo()
if is_client():
net.send_action({"type": "end"})
return
game.end_turn()
ai_timer = ai_delay
after_action()
@@ -823,17 +757,14 @@ func on_click(kind: String, obj) -> void:
selected = null
if kind == "enemy" or kind == "hero":
clear_undo()
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(tr(err))
var err := g.attack(cr, obj)
if err != "":
say(tr(err))
after_action()
return
match kind:
"deck":
do_draw(int(obj) if obj != null else 0)
do_draw()
"hand":
var ok := g.can_play(perspective, obj)
if not ok[0]:
@@ -949,6 +880,41 @@ func update_attack_preview() -> void:
v.queue_redraw()
## Der eigene Stapel: Kartenstoß mit Kosten des nächsten Ziehens und Restzahl.
class Deck extends Control:
signal clicked
var enabled := true
var interactive := true # false: der Stapel des Gegners, nur Anzahl
var count := 0
var cost := 0
func _init(rect: Rect2) -> void:
position = rect.position
size = rect.size
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:
for k in range(2, -1, -1):
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12))
r.position.y += 6
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled and interactive else Style.MUTED, 2), r)
if k == 0:
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
if interactive:
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
else:
Style.text(self, str(count), 22, Style.TEXT, r.get_center(), "center", true)
if interactive and not enabled:
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
## Eine verdeckte Handkarte des Gegners in voller Größe.
class CardBack extends Control:
func _init(rect: Rect2) -> void:
@@ -967,51 +933,10 @@ class CardBack extends Control:
# --- 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[maxi(online_seat, 0)]
build_hero_boxes()
after_action()
## Host-Wechsel: das Spiel läuft jetzt auf diesem Rechner weiter.
func _on_migrated() -> void:
game = net.game
if not net.action_applied.is_connected(after_action):
net.action_applied.connect(after_action)
wait_label.visible = false
perspective = game.players[online_seat]
build_hero_boxes()
say("Du bist jetzt Host.")
after_action()
func _on_net_status() -> void:
wait_label.text = net.status
wait_label.visible = true
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()
var centers := {}
@@ -1138,23 +1063,12 @@ func refresh() -> void:
tiles.erase(uid)
fx.fade_out(t)
update_attack_preview()
Style.clear(offer_layer)
offer_views = []
draw_enabled = mine and g.can_draw(h)[0]
if not spectating():
for i in h.offer.size():
var oc: Cards.Card = h.offer[i]
var orect := Rect2(BattleLayout.OFFER_X, BattleLayout.OFFER_Y + i * (BattleLayout.HAND_H + BattleLayout.OFFER_GAP), BattleLayout.HAND_W, BattleLayout.HAND_H)
var ov := CardView.new(oc, orect.size, false, oc.rarity_in(h.looks))
ov.position = orect.position
ov.dim = not draw_enabled
var idx := i
ov.clicked.connect(func(_v): on_click("deck", idx))
ov.hover.connect(func(c, e): show_preview(c, e, h.looks, "", orect))
offer_layer.add_child(ov)
offer_views.append(ov)
deck_label.text = "%s ◆ %d · %d" % [tr("Ziehen"), g.draw_cost(h), g.deck.size()]
deck_label.visible = not spectating()
deck.enabled = mine and g.can_draw(h)[0]
deck.count = h.deck.size()
deck.cost = g.draw_cost(h)
deck.queue_redraw()
opp_deck.count = opponent().deck.size()
opp_deck.queue_redraw()
Style.clear(hand_layer)
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
@@ -1165,9 +1079,7 @@ func refresh() -> void:
back.rotation_degrees = arc[0]
back.position.y += arc[1]
hand_layer.add_child(back)
if spectating():
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
else:
if true:
for i in h.hand.size():
var card: Cards.Card = h.hand[i]
var playable: bool = mine and g.can_play(h, card)[0]
@@ -1199,10 +1111,8 @@ func refresh() -> void:
name_label.text = h.name
life_label.text = "♥ %d" % maxi(h.life, 0)
energy_label.text = "◆ %d" % h.energy
b_end.disabled = not mine
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
var ai_running: bool = g.winner == "" and not g.current().is_human
b_skip.visible = ai_running
b_end.visible = not ai_running and not spectating()
b_undo.visible = undo_state != null and mine
element_view.update_from(g, h)
opp_element_view.update_from(g, opponent())
@@ -1222,20 +1132,13 @@ func refresh() -> void:
pad.draw_cursor()
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"
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)
overlay.visible = true
confirm.visible = false

View File

@@ -137,13 +137,15 @@ func refresh() -> void:
elif revealed.has(i):
var res: Dictionary = cards[i]
var c: Cards.Card = res.card
var v := CardView.new(c, r.size, false, res.rarity)
var v := CardView.new(c.leveled(Cards.RARITIES.find(res.rarity)), r.size, false, res.rarity)
v.position = r.position
slots.add_child(v)
if fresh.has(i):
flip(v, fresh[i], res.rarity)
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
if res.upgraded:
if res.get("new", false):
Style.label(slots, "Neu!", 16, Style.GOLD, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
elif res.upgraded:
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
else:
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")

View File

@@ -1,7 +1,7 @@
class_name CollectionScreen
extends Control
## Sammlung: alle Karten gehören dir. Gesammelt wird die Seltenheitsstufe — reine Optik.
## Aufwerten kostet Staub, eine Stufe abgeben bringt Staub.
## Sammlung: alle Karten des Spiels, die fehlenden abgedunkelt. Eigene Karten lassen sich
## aufwerten (Staub) oder zerlegen (bringt Staub), fehlende herstellen (Staub).
var app
var filter := ""
@@ -15,6 +15,7 @@ var preview_holder: Control
var info_label: Label
var b_dismantle: Button
var b_upgrade: Button
var b_craft: Button
func _ready() -> void:
@@ -43,6 +44,7 @@ func _ready() -> void:
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
b_craft = Style.button(self, "", Rect2(840, 530, 380, 46), craft, Style.PRIMARY)
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
refresh()
@@ -57,7 +59,7 @@ func set_filter(e: String) -> void:
refresh()
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
## Eigene Karten zuerst (höhere Stufen vorn), dann die fehlenden, sonst in Kartenreihenfolge.
func cards() -> Array:
var out: Array = Cards.all().duplicate()
if filter != "":
@@ -69,7 +71,10 @@ func cards() -> Array:
var order := {}
for i in out.size():
order[out[i].id] = i
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
out.sort_custom(func(a, b):
if p.owns(a.id) != p.owns(b.id):
return p.owns(a.id)
return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
return out
@@ -111,11 +116,31 @@ func dismantle() -> void:
refresh()
func craft() -> void:
if selected == null:
return
var err: String = app.profile.craft(selected.id)
if err == "":
app.sfx.play("combo")
app.save()
say(tr("%s hergestellt") % selected.title(), true)
else:
say(err, false)
refresh()
func refresh() -> void:
var p: Profile = app.profile
dust_label.text = "Staub: %d" % p.dust
grid.fill(cards(), func(c): return p.rarity(c.id))
var shown := cards()
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
var views := grid.views()
for i in views.size():
views[i].dim = not p.owns(shown[i].id)
Style.clear(preview_holder)
b_craft.visible = false
b_upgrade.visible = true
b_dismantle.visible = true
if selected == null:
info_label.text = "Keine Karte gewählt"
b_upgrade.text = "Aufwerten"
@@ -123,11 +148,19 @@ func refresh() -> void:
b_upgrade.disabled = true
b_dismantle.disabled = true
return
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
var v := CardView.new(selected.leveled(p.level(selected.id)), Vector2(220, 308), false, p.rarity(selected.id))
v.position = Vector2(920, 152)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v)
var lvl := p.level(selected.id)
if not p.owns(selected.id):
info_label.text = tr("Nicht in der Sammlung")
b_upgrade.visible = false
b_dismantle.visible = false
b_craft.visible = true
b_craft.text = tr("Herstellen (%d Staub)") % Profile.CRAFT_COST
b_craft.disabled = p.dust < Profile.CRAFT_COST
return
info_label.text = "%s · %s" % [tr(selected.combo()), tr(p.rarity(selected.id))]
if lvl >= Cards.RARITIES.size() - 1:
b_upgrade.text = "Höchste Stufe"
@@ -136,5 +169,5 @@ func refresh() -> void:
var next: String = Cards.RARITIES[lvl + 1]
b_upgrade.text = "Aufwerten zu %s (%d Staub)" % [tr(next), Profile.DUST_UPGRADE[next]]
b_upgrade.disabled = p.dust < Profile.DUST_UPGRADE[next]
b_dismantle.text = "Zerlegen" if lvl == 0 else "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
b_dismantle.disabled = lvl == 0
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
b_dismantle.disabled = false

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

@@ -0,0 +1,93 @@
class_name DeckScreen
extends Control
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
## 40 bis 60 Karten; „Auffüllen“ nimmt die teuersten eigenen Karten des gewählten Filters.
var app
var filter := ""
var grid: CardGrid
var count_label: Label
var msg_label: Label
func _ready() -> void:
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
fb.add_theme_font_size_override("font_size", 14)
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
grid.card_clicked.connect(toggle)
add_child(grid)
Style.panel(self, Rect2(820, 86, 420, 696))
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
var hint := Style.label(self, "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.", 15, Style.MUTED, Rect2(840, 160, 380, 60), "center")
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up)
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28"))
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center")
refresh()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())
func set_filter(e: String) -> void:
filter = e
refresh()
func toggle(card: Cards.Card) -> void:
var err: String = app.profile.toggle_deck(card.id)
msg_label.text = err
if err == "":
app.sfx.play("click")
app.save()
refresh()
## Füllt mit den teuersten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
func fill_up() -> void:
var p: Profile = app.profile
var pool: Array = p.collection.keys()
if filter != "":
pool = pool.filter(func(id): return filter in Cards.by_id(id).elements)
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX)
app.save()
refresh()
func clear_deck() -> void:
app.profile.deck = []
app.save()
refresh()
## Eigene Karten, die im Stapel zuerst, sonst nach Kosten.
func cards() -> Array:
var p: Profile = app.profile
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
if filter != "":
out = out.filter(func(c): return filter in c.elements)
out.sort_custom(func(a, b):
var ia: bool = a.id in p.deck
var ib: bool = b.id in p.deck
if ia != ib:
return ia
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
return out
func refresh() -> void:
var p: Profile = app.profile
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
var shown := cards()
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
var views := grid.views()
for i in views.size():
views[i].dim = not (shown[i].id in p.deck)

View File

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

View File

@@ -1,10 +1,11 @@
class_name ElementView
extends Control
## Elementzähler der eigenen Seite, mittig: je Element zweimal das Symbol seines Sondereffekts in
## der Elementfarbe, leuchtend je offenem eigenen Wesen (beide: Effekt aktiv), daneben in der
## Elementfarbe das Mana der bisher ausgespielten Karten (je Game.ELEMENT_MANA gibt es +1 Angriff).
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
## offenem Wesen, beide: Effekt aktiv), die Zahl der ausgespielten Karten und ab
## Game.ELEMENT_COUNT der Bonus +1/+1.
const ENTRY_W := 180.0
const ROW := 32.0
var own := {} # offene eigene Wesen je Element
var played := {} # ausgespielte Karten je Element
@@ -31,37 +32,41 @@ func paired(e: String) -> bool:
func bonus(e: String) -> int:
return int(played.get(e, 0)) / Game.ELEMENT_MANA
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
func shown() -> Array:
return Cards.ELEMENTS.filter(func(e): return int(own.get(e, 0)) > 0 or int(played.get(e, 0)) > 0)
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
func text() -> String:
var parts: Array[String] = []
for e in Cards.ELEMENTS:
for e in shown():
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
if paired(e):
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
if bonus(e) > 0:
s += " (+%d)" % bonus(e)
s += " (+%d/+%d)" % [bonus(e), bonus(e)]
parts.append(s)
return " · ".join(parts)
func _draw() -> void:
var x0 := (size.x - ENTRY_W * Cards.ELEMENTS.size()) / 2
var cy := size.y / 2
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var y := ROW / 2
for e in shown():
var col: Color = Cards.ELEMENT_COLORS[e]
var active := paired(e) or bonus(e) > 0
var px := x0 + i * ENTRY_W + 10
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, cy), "midleft", active)
px += Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 10
draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4))
var px := 10.0
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
px = 76
for k in Game.ELEMENT_PAIR:
var filled := k < int(own.get(e, 0))
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, cy), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
px += Game.ELEMENT_PAIR * 24 + 8
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, cy), "midleft", true)
px += 20
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
if bonus(e) > 0:
Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
Style.text(self, "+%d/+%d" % [bonus(e), bonus(e)], 12, Style.GREEN, Vector2(size.x - 6, y), "midright", true)
y += ROW

View File

@@ -1,107 +0,0 @@
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_watch: Button
var b_start: Button
var addr_label: Label
func _ready() -> void:
net = app.net
net.my_name = "Spieler"
net.my_pick = {"looks": app.profile.looks()}
Style.header(self, "Netzwerkspiel", _back)
var col := Style.column(self, 760, 110, 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)
b_watch = Style.cbutton(hr, "Zuschauen", Vector2(150, 46), _watch)
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, 10)
var br := Style.row(col, 20)
br.alignment = BoxContainer.ALIGNMENT_CENTER
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
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()
## Als Zuschauer beitreten: sieht die Partie ohne Hände, spielt nicht mit.
func _watch() -> void:
net.spectator = true
net.join(ip_box.text.strip_edges())
refresh()
func _start() -> void:
net.start_battle()
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 ""])
for id in net.spectators:
lines.append("%s (%s)" % [net.spectators[id], tr("Zuschauer")])
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
b_host.disabled = net.connected
b_join.disabled = net.connected
b_watch.disabled = net.connected
ip_box.editable = not net.connected
var humans := ids.size()
b_start.disabled = not net.is_host
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
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

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

View File

@@ -5,7 +5,6 @@ var profile: Profile
var screen: Control
var save_path: String = Profile.SAVE_PATH
var sfx: Sfx
var net: Net
func _ready() -> void:
@@ -17,14 +16,9 @@ func _ready() -> void:
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)
apply_settings()
show_screen(MenuScreen.new())
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new())
func apply_settings() -> void:
@@ -36,20 +30,32 @@ func apply_settings() -> void:
theme = Style.make_theme()
## Neues Profil: Sammlung und Fortschritt werden verworfen.
## Neues Profil: Sammlung und Fortschritt werden verworfen, das Startdeck wird neu gewählt.
func reset_profile() -> void:
profile = Profile.starter()
save()
apply_settings()
show_screen(StartDeckScreen.new())
func set_language(code: String) -> void:
TranslationServer.set_locale(code)
func show_screen(s: Control) -> void:
var kept: Array = [] # vorige Bildschirme, zu denen back() zurückführt (Einstellungen aus dem Kampf)
## keep: der aktuelle Bildschirm bleibt erhalten und kommt mit back() zurück.
func show_screen(s: Control, keep: bool = false) -> void:
if screen != null:
screen.queue_free()
if keep:
remove_child(screen)
kept.append(screen)
else:
screen.queue_free()
for k in kept:
k.queue_free()
kept.clear()
screen = s
s.app = self
s.set_anchors_preset(Control.PRESET_FULL_RECT)
@@ -106,11 +112,17 @@ func _first_button(node: Node) -> Button:
return null
## Zurück: zum gemerkten Bildschirm, sonst ins Hauptmenü.
func back() -> void:
if kept.is_empty():
show_screen(MenuScreen.new())
return
var prev: Control = kept.pop_back()
screen.queue_free()
screen = prev
add_child(prev)
func save() -> void:
profile.save(save_path)
func _exit_tree() -> void:
if net != null and net.is_inside_tree():
net.leave()
net.queue_free()

View File

@@ -17,7 +17,7 @@ func _ready() -> void:
var y := 230
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
["Deck", func(): app.show_screen(DeckScreen.new())]]:
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
b.add_theme_font_size_override("font_size", 24)
y += 74
@@ -35,9 +35,9 @@ func _ready() -> void:
refresh()
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
## Drei eigene Karten, die höchsten Stufen zuerst, leicht gefächert.
func _showcase(p: Profile) -> void:
var cards: Array = Cards.all().duplicate()
var cards: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
cards.sort_custom(func(a, b):
if p.level(a.id) != p.level(b.id):
return p.level(a.id) > p.level(b.id)
@@ -45,7 +45,7 @@ func _showcase(p: Profile) -> void:
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
for i in 3:
var c: Cards.Card = cards[i]
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
var v := CardView.new(c.leveled(p.level(c.id)), Vector2(180, 252), false, p.rarity(c.id))
v.position = spots[i][0]
v.rotation = spots[i][1]
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -55,8 +55,8 @@ func _showcase(p: Profile) -> void:
func refresh() -> void:
var p: Profile = app.profile
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
coll_label.text = tr("Aufgewertet: %d von %d Karten · Kampagne: %d/%d") % [
p.upgraded_count(), Cards.all().size(), p.campaign_stage, Campaign.total()]
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d") % [
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()]
var notes := p.take_notices()
notice_label.text = " · ".join(notes)
if not notes.is_empty():

View File

@@ -16,7 +16,7 @@ var speed_label: Label
func _ready() -> void:
var s: Dictionary = app.profile.settings
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
Style.header(self, "Einstellungen", func(): app.back())
var col := Style.column(self, 700, 110, 24)
var grid := GridContainer.new()
grid.columns = 3
@@ -86,7 +86,6 @@ func _ready() -> void:
func _reset() -> void:
app.reset_profile()
app.show_screen(MenuScreen.new())
func _on_big_font(on: bool) -> void:
@@ -139,4 +138,4 @@ func _on_anim(on: bool) -> void:
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())
app.back()

View File

@@ -0,0 +1,37 @@
class_name StartDeckScreen
extends Control
## Erster Start: eines von vier Startdecks wählen, je eines mit einem Element im Mittelpunkt
## (25 Karten davon, je fünf der anderen). Danach geht es ins Menü.
var app
func _ready() -> void:
Style.label(self, tr("Wähle dein Startdeck"), 40, Style.GOLD, Rect2(0, 60, 1280, 60), "center", true)
Style.label(self, tr("25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest."), 17, Style.MUTED, Rect2(140, 124, 1000, 30), "center")
for i in Cards.ELEMENTS.size():
var e: String = Cards.ELEMENTS[i]
var x := 70 + i * 290
var col: Color = Cards.ELEMENT_COLORS[e]
Style.panel(self, Rect2(x, 190, 260, 470), Style.darker(col, 0.75), 12, col, 2)
var ids := DeckGen.start_deck(e)
var sample: Array = ids.filter(func(id): return Cards.by_id(id).elements[0] == e)
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
for k in 3:
var c: Cards.Card = Cards.by_id(sample[k * 7])
var v := CardView.new(c, Vector2(110, 140), false, "Common")
v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
v.rotation_degrees = -6 + k * 6
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(v)
Style.label(self, tr(e), 28, Style.TEXT, Rect2(x, 400, 260, 40), "center", true)
Style.label(self, tr(Cards.KEYWORD_TEXT[Cards.ELEMENT_KEYWORD[e]]), 15, Style.MUTED, Rect2(x + 16, 446, 228, 60), "center")
var b := Style.button(self, tr("%s wählen") % tr(e), Rect2(x + 30, 590, 200, 50), choose.bind(e), Style.PRIMARY)
b.add_theme_font_size_override("font_size", 18)
func choose(element: String) -> void:
app.profile.choose_start(element)
app.save()
app.sfx.play("combo")
app.show_screen(MenuScreen.new())

View File

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