update
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
93
scripts/ui/deck_screen.gd
Normal 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)
|
||||
1
scripts/ui/deck_screen.gd.uid
Normal file
1
scripts/ui/deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctt36od26uyea
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bhnnxjh6rsxnf
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
37
scripts/ui/start_deck_screen.gd
Normal file
37
scripts/ui/start_deck_screen.gd
Normal 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())
|
||||
1
scripts/ui/start_deck_screen.gd.uid
Normal file
1
scripts/ui/start_deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bxl6isdrgqb2b
|
||||
Reference in New Issue
Block a user