Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
1093 lines
40 KiB
GDScript
1093 lines
40 KiB
GDScript
class_name BattleScreen
|
|
extends Control
|
|
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
|
## Modi: Einzelspieler, Kampagne, Anleitung, Hotseat (Perspektivwechsel), Netzwerk (Host oder Mitspieler).
|
|
|
|
var app
|
|
var n_opp: int
|
|
var stage := -1 # Kampagnenstufe oder -1
|
|
var tutorial := false
|
|
var hotseat_decks: Array = []
|
|
var hotseat_ai := 0
|
|
var online_seat := -1 # Netzwerk: eigener Platz, sonst -1
|
|
var net: Net
|
|
var game: Game
|
|
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
|
var fx: BattleFx
|
|
var selected = null # Game.Creature
|
|
var pending = null # Cards.Card, wartet auf Ziel
|
|
var msg_t := 0.0
|
|
var ai_delay := 0.75
|
|
var ai_timer := 0.0
|
|
var reward := -1
|
|
var ended := false
|
|
var zoom_player = null
|
|
var preview_timer := 0.0
|
|
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
|
var mulligan_selected: Array = []
|
|
var tutorial_step := 0
|
|
var challenge := {} # wöchentliche Herausforderung oder leer
|
|
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
|
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
|
var undo_log_len := 0
|
|
var undo_t := 0.0
|
|
|
|
# Ansichten (auch für Tests)
|
|
var tiles := {}
|
|
var hand_views: Array = []
|
|
var own_views: Array = []
|
|
var enemy_views: Array = []
|
|
var trap_views: Array = []
|
|
var hero_panels := {} # Player -> Panel
|
|
var hero_labels := {}
|
|
var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
|
|
|
var hero_layer: Control
|
|
var tile_layer: Control
|
|
var hand_layer: Control
|
|
var trap_layer: Control
|
|
var zoom_layer: Control
|
|
var fx_layer: Control
|
|
var log_box: RichTextLabel
|
|
var preview_holder: Control
|
|
var preview_hint: Label
|
|
var bar_panel: Panel
|
|
var name_label: Label
|
|
var life_label: Label
|
|
var energy_label: Label
|
|
var deck_label: Label
|
|
var status_label: Label
|
|
var msg_label: Label
|
|
var b_end: Button
|
|
var b_giveup: Button
|
|
var overlay: Control
|
|
var result_label: Label
|
|
var reward_label: Label
|
|
var notice_label: Label
|
|
var confirm: Control
|
|
var handover: Control
|
|
var handover_label: Label
|
|
var mulligan_box: Control
|
|
var mulligan_layer: Control
|
|
var b_keep: Button
|
|
var b_swap: Button
|
|
var tutorial_panel: Panel
|
|
var tutorial_text: RichTextLabel
|
|
var b_tut_next: Button
|
|
var wait_label: Label
|
|
var b_skip: Button
|
|
var b_undo: Button
|
|
var highlight_layer: Control
|
|
|
|
|
|
## Pulsierender Rahmen für die Anleitung.
|
|
class Highlight extends Control:
|
|
var t := 0.0
|
|
|
|
func _process(delta: float) -> void:
|
|
t += delta
|
|
queue_redraw()
|
|
|
|
func _draw() -> void:
|
|
var a := 0.55 + 0.45 * sin(t * 6.0)
|
|
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
|
|
|
|
const TUTORIAL_STEPS := [
|
|
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
|
|
"played_creature"],
|
|
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
|
|
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
|
|
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig +1 Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
|
|
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an alle gegnerischen Wesen.", "combo"],
|
|
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
|
|
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
|
|
]
|
|
|
|
|
|
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
|
n_opp = n
|
|
stage = campaign_stage
|
|
|
|
|
|
static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
|
|
var b := BattleScreen.new(0, -1)
|
|
b.hotseat_decks = decks
|
|
b.hotseat_ai = n_ai
|
|
return b
|
|
|
|
|
|
static func online(seat: int) -> BattleScreen:
|
|
var b := BattleScreen.new(0, -1)
|
|
b.online_seat = seat
|
|
return b
|
|
|
|
|
|
static func guided() -> BattleScreen:
|
|
var b := BattleScreen.new(1, -1)
|
|
b.tutorial = true
|
|
return b
|
|
|
|
|
|
static func weekly(ch: Dictionary) -> BattleScreen:
|
|
var b := BattleScreen.new(int(ch.n_opp), -1)
|
|
b.challenge = ch
|
|
return b
|
|
|
|
|
|
func is_hotseat() -> bool:
|
|
return game != null and game.humans.size() > 1 and online_seat < 0
|
|
|
|
|
|
func is_online() -> bool:
|
|
return online_seat >= 0
|
|
|
|
|
|
func is_client() -> bool:
|
|
return is_online() and not net.is_host
|
|
|
|
|
|
func counts_for_profile() -> bool:
|
|
return not is_hotseat() and not is_online()
|
|
|
|
|
|
func _ready() -> void:
|
|
var s: Dictionary = app.profile.settings
|
|
fx = BattleFx.new(self)
|
|
fx.enabled = s.animations
|
|
ai_delay = 0.75 / float(s.ai_speed)
|
|
_build_static()
|
|
if is_online():
|
|
net = app.net
|
|
net.ended.connect(_on_net_ended)
|
|
if net.is_host:
|
|
game = net.game
|
|
net.action_applied.connect(after_action)
|
|
else:
|
|
net.snapshot_received.connect(_on_snapshot)
|
|
wait_label.visible = true
|
|
return # der erste Zustand kommt vom Host
|
|
elif not hotseat_decks.is_empty():
|
|
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
|
elif tutorial:
|
|
var r := Game.make_rng()
|
|
var opps := Campaign.opponents(0, r)
|
|
opps[0].perks = []
|
|
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
|
elif not challenge.is_empty():
|
|
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, challenge.rules)
|
|
elif stage >= 0:
|
|
var r := Game.make_rng()
|
|
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r, false, own_rules())
|
|
else:
|
|
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
|
_begin()
|
|
|
|
|
|
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar).
|
|
func own_rules() -> Dictionary:
|
|
var p: Profile = app.profile
|
|
if p.perk != "" and p.perk_unlocked():
|
|
return {"human_perks": [p.perk]}
|
|
return {}
|
|
|
|
|
|
func _begin() -> void:
|
|
n_opp = game.players.size() - 1
|
|
if is_online():
|
|
perspective = game.players[online_seat]
|
|
else:
|
|
game.events.clear()
|
|
perspective = game.current() if game.current().is_human else game.humans[0]
|
|
build_hero_boxes()
|
|
if is_online() and net.is_host:
|
|
net.broadcast()
|
|
game.events.clear()
|
|
refresh()
|
|
|
|
|
|
func _build_static() -> void:
|
|
Style.panel(self, Rect2(10, 205, 320, 340))
|
|
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20))
|
|
log_box = RichTextLabel.new()
|
|
log_box.position = Vector2(18, 230)
|
|
log_box.size = Vector2(304, 308)
|
|
log_box.bbcode_enabled = true
|
|
log_box.scroll_following = true
|
|
log_box.add_theme_font_size_override("normal_font_size", 13)
|
|
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(log_box)
|
|
if tutorial:
|
|
tutorial_panel = Style.panel(self, Rect2(10, 205, 320, 150), Style.PANEL, 8, Style.GOLD, 2)
|
|
tutorial_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
Style.label(self, "Anleitung", 14, Style.GOLD, Rect2(20, 209, 200, 20), "left", true)
|
|
tutorial_text = RichTextLabel.new()
|
|
tutorial_text.position = Vector2(20, 230)
|
|
tutorial_text.size = Vector2(300, 90)
|
|
tutorial_text.bbcode_enabled = false
|
|
tutorial_text.scroll_active = false
|
|
tutorial_text.add_theme_font_size_override("normal_font_size", 13)
|
|
tutorial_text.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(tutorial_text)
|
|
b_tut_next = Style.button(self, "Weiter", Rect2(230, 318, 90, 30), tutorial_next)
|
|
b_tut_next.add_theme_font_size_override("font_size", 14)
|
|
log_box.position = Vector2(18, 365)
|
|
log_box.size = Vector2(304, 173)
|
|
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
|
Style.panel(self, Rect2(950, 205, 320, 380))
|
|
preview_hint = Style.label(self, "Karte überfahren für Details", 14, Style.MUTED, Rect2(950, 345, 320, 24), "center")
|
|
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10)
|
|
bar_panel = Style.panel(self, BattleLayout.BAR)
|
|
name_label = Style.label(self, "Du", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
|
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true)
|
|
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true)
|
|
deck_label = Style.label(self, "", 16, Style.MUTED, Rect2(780, 424, 150, 24), "right")
|
|
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
|
|
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
|
|
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
|
|
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
|
|
b_skip.add_theme_font_size_override("font_size", 14)
|
|
b_skip.visible = false
|
|
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
|
|
b_undo.visible = false
|
|
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
|
wait_label.visible = false
|
|
hero_layer = _layer()
|
|
tile_layer = _layer()
|
|
trap_layer = _layer()
|
|
hand_layer = _layer()
|
|
preview_holder = _layer()
|
|
highlight_layer = _layer()
|
|
zoom_layer = _layer()
|
|
fx_layer = _layer()
|
|
# Mulligan
|
|
mulligan_box = _overlay()
|
|
Style.panel(mulligan_box, Rect2(140, 250, 1000, 320), Style.PANEL, 10, Style.GOLD, 2)
|
|
Style.label(mulligan_box, "Hand tauschen?", 26, Style.TEXT, Rect2(140, 262, 1000, 40), "center", true)
|
|
Style.label(mulligan_box, "Klicke Karten an die du zurücklegen willst. Einmal pro Partie.", 15, Style.MUTED, Rect2(140, 300, 1000, 24), "center")
|
|
mulligan_layer = Control.new()
|
|
mulligan_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
mulligan_box.add_child(mulligan_layer)
|
|
b_keep = Style.button(mulligan_box, "Behalten", Rect2(430, 515, 200, 44), func(): do_mulligan(false), Color("285a3c"))
|
|
b_swap = Style.button(mulligan_box, "Tauschen", Rect2(650, 515, 200, 44), func(): do_mulligan(true))
|
|
# Spielende
|
|
overlay = _overlay()
|
|
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
|
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
|
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
|
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
|
# Aufgeben bestätigen
|
|
confirm = _overlay()
|
|
Style.panel(confirm, Rect2(390, 300, 500, 180), Style.PANEL, 10, Style.MUTED, 2)
|
|
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(390, 320, 500, 40), "center", true)
|
|
Style.label(confirm, "Der Kampf endet ohne Münzen.", 16, Style.MUTED, Rect2(390, 365, 500, 24), "center")
|
|
Style.button(confirm, "Ja, aufgeben", Rect2(420, 410, 200, 46), leave, Color("5a2828"))
|
|
Style.button(confirm, "Weiterspielen", Rect2(660, 410, 200, 46), func(): confirm.visible = false)
|
|
# Übergabe zwischen Menschen (Hotseat)
|
|
handover = _overlay()
|
|
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
|
handover_label = Style.label(handover, "", 30, Style.TEXT, Rect2(340, 310, 600, 44), "center", true)
|
|
Style.label(handover, "Maus weitergeben, dann weiter. Die Hand des Vorgängers ist verdeckt.", 16, Style.MUTED, Rect2(340, 362, 600, 24), "center")
|
|
Style.button(handover, "Weiter", Rect2(540, 420, 200, 50), take_over, Color("285a3c"))
|
|
|
|
|
|
func _layer() -> Control:
|
|
var c := Control.new()
|
|
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(c)
|
|
return c
|
|
|
|
|
|
func _overlay() -> Control:
|
|
var o := Control.new()
|
|
o.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
o.visible = false
|
|
var dark := ColorRect.new()
|
|
dark.color = Color(0, 0, 0, 0.67)
|
|
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
dark.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
o.add_child(dark)
|
|
add_child(o)
|
|
return o
|
|
|
|
|
|
func leave() -> void:
|
|
if is_online():
|
|
net.leave()
|
|
if tutorial and not app.profile.tutorial_done:
|
|
app.profile.tutorial_done = true
|
|
app.save()
|
|
app.show_screen(MenuScreen.new())
|
|
|
|
|
|
# --- Layout-Helfer ------------------------------------------------------------------
|
|
|
|
func others() -> Array:
|
|
var out := []
|
|
var n := game.players.size()
|
|
var start := game.players.find(perspective)
|
|
for i in range(1, n):
|
|
out.append(game.players[(start + i) % n])
|
|
return out
|
|
|
|
|
|
func compact() -> bool:
|
|
return BattleLayout.compact(others().size())
|
|
|
|
|
|
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
|
if p == perspective:
|
|
return BattleLayout.own_slot(j)
|
|
var os := others()
|
|
return BattleLayout.other_slot(os.size(), os.find(p), j)
|
|
|
|
|
|
func ref_key(ref: Dictionary) -> String:
|
|
return "c%d" % ref.uid if ref.kind == "creature" else "p%d" % ref.index
|
|
|
|
|
|
func target_center(ref: Dictionary) -> Vector2:
|
|
if ref.kind == "creature":
|
|
if tiles.has(ref.uid):
|
|
var t: CreatureView = tiles[ref.uid]
|
|
return t.slot + t.size / 2
|
|
return Vector2(-1, -1)
|
|
var p: Game.Player = game.players[ref.index]
|
|
if p == perspective:
|
|
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
|
|
if hero_panels.has(p):
|
|
var panel: Panel = hero_panels[p]
|
|
return panel.position + panel.size / 2
|
|
return Vector2(-1, -1)
|
|
|
|
|
|
func build_hero_boxes() -> void:
|
|
Style.clear(hero_layer)
|
|
hero_panels = {}
|
|
hero_labels = {}
|
|
var os := others()
|
|
var c := compact()
|
|
for i in os.size():
|
|
var o: Game.Player = os[i]
|
|
var box := BattleLayout.hero_box_rect(os.size(), i)
|
|
var panel := Style.panel(hero_layer, box)
|
|
panel.gui_input.connect(func(ev):
|
|
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
|
on_click("hero", o))
|
|
if not o.perks.is_empty():
|
|
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
|
hero_panels[o] = panel
|
|
var labels := {}
|
|
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
|
if not o.perks.is_empty():
|
|
title += " ★"
|
|
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
|
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (44 if c else 20), 26), "left", true)
|
|
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
|
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
|
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
|
Rect2(box.position.x + 90, box.position.y + 38, box.size.x - 100, 20), "right")
|
|
hero_labels[o] = labels
|
|
if c:
|
|
var b := Style.button(hero_layer, "Lupe", Rect2(box.end.x - 44, box.position.y + 5, 38, 22), func():
|
|
zoom_player = o
|
|
refresh())
|
|
b.add_theme_font_size_override("font_size", 11)
|
|
|
|
|
|
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
|
|
|
func my_turn() -> bool:
|
|
return game != null and game.winner == "" and game.current() == perspective
|
|
|
|
|
|
func any_overlay() -> bool:
|
|
return confirm.visible or handover.visible or mulligan_box.visible or overlay.visible
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if game == null:
|
|
return
|
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
|
cancel_all()
|
|
return
|
|
if not (event is InputEventKey and event.pressed and not event.echo):
|
|
return
|
|
match event.keycode:
|
|
KEY_ESCAPE:
|
|
if confirm.visible:
|
|
confirm.visible = false
|
|
elif selected != null or pending != null or zoom_player != null:
|
|
cancel_all()
|
|
elif not any_overlay():
|
|
confirm.visible = true
|
|
KEY_SPACE:
|
|
if my_turn() and not any_overlay():
|
|
end_turn()
|
|
elif b_skip.visible:
|
|
skip_ai = true
|
|
KEY_U:
|
|
if b_undo.visible:
|
|
undo()
|
|
KEY_ENTER, KEY_KP_ENTER:
|
|
if handover.visible:
|
|
take_over()
|
|
elif mulligan_box.visible:
|
|
do_mulligan(false)
|
|
elif overlay.visible:
|
|
leave()
|
|
elif tutorial and b_tut_next.visible:
|
|
tutorial_next()
|
|
_:
|
|
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
|
|
var idx: int = event.keycode - KEY_1
|
|
if idx < perspective.hand.size():
|
|
on_click("hand", perspective.hand[idx])
|
|
|
|
|
|
func cancel_all() -> void:
|
|
if confirm.visible:
|
|
confirm.visible = false
|
|
cancel()
|
|
zoom_player = null
|
|
refresh()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if game == null:
|
|
return
|
|
msg_t = maxf(0.0, msg_t - delta)
|
|
if msg_t <= 0.0 and msg_label.text != "":
|
|
msg_label.text = ""
|
|
if preview_timer > 0.0:
|
|
preview_timer -= delta
|
|
if preview_timer <= 0.0:
|
|
show_preview(null, false)
|
|
if undo_t > 0.0:
|
|
undo_t -= delta
|
|
if undo_t <= 0.0:
|
|
clear_undo()
|
|
if is_online() and net.is_host:
|
|
net.tick()
|
|
if game.winner != "":
|
|
if not ended:
|
|
ended = true
|
|
_finish()
|
|
return
|
|
var cur := game.current()
|
|
if cur.is_human:
|
|
if cur != perspective and is_hotseat() and not handover.visible:
|
|
handover_label.text = "%s ist dran" % cur.name
|
|
handover.visible = true
|
|
hand_layer.visible = false
|
|
trap_layer.visible = false
|
|
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan_box.visible and not handover.visible:
|
|
show_mulligan()
|
|
return
|
|
if is_client():
|
|
return
|
|
if skip_ai:
|
|
ai_timer = 0.0
|
|
else:
|
|
ai_timer -= delta
|
|
if ai_timer > 0:
|
|
return
|
|
var action := AI.choose_action(game, cur)
|
|
AI.apply_action(game, cur, action)
|
|
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
|
|
if game.current().is_human:
|
|
skip_ai = false
|
|
after_action()
|
|
|
|
|
|
func _finish() -> void:
|
|
var won := game.winner == "human"
|
|
if counts_for_profile():
|
|
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge)
|
|
if tutorial:
|
|
app.profile.tutorial_done = true
|
|
app.save()
|
|
app.sfx.play("win" if won else "lose")
|
|
else:
|
|
var wp := game.winner_player
|
|
var mine: bool = wp != null and wp == perspective
|
|
app.sfx.play("win" if mine else "lose")
|
|
refresh()
|
|
|
|
|
|
func take_over() -> void:
|
|
handover.visible = false
|
|
hand_layer.visible = true
|
|
trap_layer.visible = true
|
|
perspective = game.current()
|
|
cancel()
|
|
zoom_player = null
|
|
build_hero_boxes()
|
|
refresh()
|
|
|
|
|
|
func say(m: String) -> void:
|
|
msg_t = 2.5
|
|
msg_label.text = m
|
|
|
|
|
|
func cancel() -> void:
|
|
selected = null
|
|
pending = null
|
|
|
|
|
|
func play(card: Cards.Card, target) -> void:
|
|
if is_client():
|
|
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
|
|
return
|
|
var before: Game = game.clone() if card.is_creature() and not is_online() else null
|
|
var log_len := game.log.size()
|
|
var err := game.play_card(perspective, card, target)
|
|
if err != "":
|
|
say(tr(err))
|
|
return
|
|
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte, Kombos, Fallen)
|
|
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
|
undo_state = before
|
|
undo_log_len = log_len
|
|
undo_t = 6.0
|
|
else:
|
|
clear_undo()
|
|
|
|
|
|
func clear_undo() -> void:
|
|
undo_state = null
|
|
undo_t = 0.0
|
|
b_undo.visible = false
|
|
|
|
|
|
## Nimmt das letzte Ausspielen eines Wesens zurück (nur solange nichts Verdecktes offenbart wurde).
|
|
func undo() -> void:
|
|
if undo_state == null:
|
|
return
|
|
var idx := game.players.find(perspective)
|
|
var restored := undo_state
|
|
restored.quiet = false
|
|
restored.rng = game.rng
|
|
restored.log = game.log.slice(0, undo_log_len)
|
|
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()
|
|
after_action()
|
|
|
|
|
|
func end_turn() -> void:
|
|
if not my_turn():
|
|
return
|
|
cancel()
|
|
clear_undo()
|
|
zoom_player = null
|
|
if is_client():
|
|
net.send_action({"type": "end"})
|
|
return
|
|
game.end_turn()
|
|
ai_timer = ai_delay
|
|
after_action()
|
|
|
|
|
|
func show_mulligan() -> void:
|
|
mulligan_selected = []
|
|
mulligan_box.visible = true
|
|
Style.clear(mulligan_layer)
|
|
var hand: Array = perspective.hand
|
|
for i in hand.size():
|
|
var r := BattleLayout.hand_rect(hand.size(), i)
|
|
var v := CardView.new(hand[i], r.size)
|
|
v.position = Vector2(r.position.x, 322)
|
|
var idx := i
|
|
v.clicked.connect(func(_view): toggle_mulligan(idx))
|
|
mulligan_layer.add_child(v)
|
|
_mulligan_borders()
|
|
|
|
|
|
func toggle_mulligan(idx: int) -> void:
|
|
if idx in mulligan_selected:
|
|
mulligan_selected.erase(idx)
|
|
else:
|
|
mulligan_selected.append(idx)
|
|
_mulligan_borders()
|
|
|
|
|
|
func _mulligan_borders() -> void:
|
|
var views := mulligan_layer.get_children()
|
|
for i in views.size():
|
|
views[i].border = Style.YELLOW if i in mulligan_selected else null
|
|
views[i].queue_redraw()
|
|
b_swap.text = "Tauschen (%d)" % mulligan_selected.size() if not mulligan_selected.is_empty() else "Tauschen"
|
|
|
|
|
|
func do_mulligan(swap: bool) -> void:
|
|
mulligan_box.visible = false
|
|
var idxs: Array = mulligan_selected.duplicate() if swap else []
|
|
if is_client():
|
|
net.send_action({"type": "mulligan", "hand": idxs})
|
|
return
|
|
var cards := []
|
|
for i in idxs:
|
|
cards.append(perspective.hand[i])
|
|
game.mulligan(perspective, cards)
|
|
after_action()
|
|
|
|
|
|
func on_click(kind: String, obj) -> void:
|
|
var g := game
|
|
if not my_turn() or any_overlay():
|
|
return
|
|
if pending != null:
|
|
var card = pending
|
|
pending = null
|
|
if obj in g.valid_targets(perspective, card.target_type()):
|
|
play(card, obj)
|
|
else:
|
|
say(tr("Abgebrochen"))
|
|
zoom_player = null
|
|
after_action()
|
|
return
|
|
if selected != null:
|
|
var cr = selected
|
|
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))
|
|
zoom_player = null
|
|
after_action()
|
|
return
|
|
match kind:
|
|
"hand":
|
|
var ok := g.can_play(perspective, obj)
|
|
if not ok[0]:
|
|
say(tr(ok[1]))
|
|
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
|
|
pending = obj
|
|
say(tr("Wähle ein Ziel"))
|
|
else:
|
|
play(obj, null)
|
|
"own":
|
|
if obj.can_attack():
|
|
selected = obj
|
|
say(tr("Wähle ein Angriffsziel"))
|
|
else:
|
|
say(tr("%s kann nicht angreifen") % obj.name)
|
|
after_action()
|
|
|
|
|
|
func on_tile_click(cr: Game.Creature) -> void:
|
|
var o = cr.owner()
|
|
if o == null:
|
|
return
|
|
if o == perspective:
|
|
on_click("own", cr)
|
|
elif o.alive():
|
|
on_click("enemy", cr)
|
|
|
|
|
|
func show_preview(card, entered: bool) -> void:
|
|
Style.clear(preview_holder)
|
|
if entered and card != null:
|
|
var v := CardView.new(card, Vector2(210, 294))
|
|
v.position = Vector2(1005, 215)
|
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
preview_holder.add_child(v)
|
|
preview_hint.visible = not (entered and card != null)
|
|
|
|
|
|
## Vorschau einer Handkarte mit Hinweisen auf Resonanz und Feldkombos mit dem eigenen Feld.
|
|
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
|
|
Style.clear(preview_holder)
|
|
preview_hint.visible = not entered
|
|
if not entered:
|
|
return
|
|
var hints: Array[String] = combo_hints(card)
|
|
var v := CardView.new(card, Vector2(190, 266) if not hints.is_empty() else Vector2(210, 294))
|
|
v.position = Vector2(1015, 212) if not hints.is_empty() else Vector2(1005, 215)
|
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
preview_holder.add_child(v)
|
|
if not hints.is_empty():
|
|
var box := RichTextLabel.new()
|
|
box.position = Vector2(958, 480)
|
|
box.size = Vector2(304, 46)
|
|
box.bbcode_enabled = true
|
|
box.scroll_active = false
|
|
box.fit_content = false
|
|
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
box.add_theme_font_size_override("normal_font_size", 11)
|
|
box.add_theme_color_override("default_color", Style.GOLD)
|
|
box.text = "[center]" + "\n".join(hints.slice(0, 2)) + "[/center]"
|
|
preview_holder.add_child(box)
|
|
|
|
|
|
func combo_hints(card: Cards.Card) -> Array[String]:
|
|
var out: Array[String] = []
|
|
if not card.is_creature():
|
|
return out
|
|
var partners: Array[String] = []
|
|
var combos := {}
|
|
for other in perspective.board:
|
|
if other.card.elements.any(func(e): return e in card.elements):
|
|
partners.append(other.name)
|
|
for e1 in card.elements:
|
|
for e2 in other.card.elements:
|
|
if e1 != e2:
|
|
var name := Cards.combo_name([e1, e2])
|
|
if not combos.has(name):
|
|
combos[name] = other.name
|
|
if not partners.is_empty():
|
|
out.append("Resonanz +%d mit %s" % [mini(partners.size(), Game.RESONANCE_MAX), ", ".join(partners)])
|
|
for name in combos:
|
|
out.append("Feldkombo %s mit %s: %s" % [name, combos[name], Cards.FIELD_COMBO_TEXT[name]])
|
|
return out
|
|
|
|
|
|
# --- Netzwerk -------------------------------------------------------------------------------
|
|
|
|
func _on_snapshot(snap: Dictionary) -> void:
|
|
var first := game == null
|
|
game = Game.from_snapshot(snap)
|
|
wait_label.visible = false
|
|
if first:
|
|
_begin()
|
|
return
|
|
perspective = game.players[online_seat]
|
|
build_hero_boxes()
|
|
after_action()
|
|
|
|
|
|
func _on_net_ended(reason: String) -> void:
|
|
if game == null or game.winner == "":
|
|
say(reason)
|
|
await get_tree().create_timer(1.5).timeout
|
|
if is_inside_tree():
|
|
app.show_screen(MenuScreen.new())
|
|
|
|
|
|
# --- Ereignisse ----------------------------------------------------------------------------
|
|
|
|
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
|
func after_action() -> void:
|
|
if is_online() and net.is_host:
|
|
net.broadcast()
|
|
var evs := game.events.duplicate()
|
|
game.events.clear()
|
|
if counts_for_profile():
|
|
_note_events(evs)
|
|
var centers := {}
|
|
for ev in evs:
|
|
for key in ["target", "attacker", "source"]:
|
|
if ev.has(key):
|
|
centers[ref_key(ev[key])] = target_center(ev[key])
|
|
refresh()
|
|
fx.play_events(evs, centers)
|
|
|
|
|
|
func _note_events(evs: Array) -> void:
|
|
var hi := game.players.find(game.human)
|
|
for ev in evs:
|
|
if ev.get("player", -1) != hi:
|
|
continue
|
|
match ev.type:
|
|
"play", "spell":
|
|
played_ids.append(ev.card.id)
|
|
if ev.type == "play":
|
|
app.profile.daily_event("creature_played")
|
|
"trap_set":
|
|
app.profile.daily_event("trap_set")
|
|
"combo":
|
|
app.profile.daily_event("combo")
|
|
|
|
|
|
# --- Anleitung ------------------------------------------------------------------------------
|
|
|
|
func tutorial_condition(kind: String) -> bool:
|
|
var h := game.human
|
|
match kind:
|
|
"played_creature":
|
|
return game.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
|
"turn_passed":
|
|
return h.turns >= 2
|
|
"attacked":
|
|
return game.log.any(func(l): return l.ends_with("greift Fenn an.") or (" greift " in l and l.begins_with(h.board[0].name) if not h.board.is_empty() else false))
|
|
"resonance":
|
|
return h.board.any(func(c): return c.resonance > 0)
|
|
"combo":
|
|
return game.log.any(func(l): return l.begins_with("Feldkombo"))
|
|
"spell":
|
|
return game.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
|
return false
|
|
|
|
|
|
func tutorial_next() -> void:
|
|
if tutorial_step < TUTORIAL_STEPS.size() - 1:
|
|
tutorial_step += 1
|
|
app.sfx.play("click")
|
|
refresh()
|
|
|
|
|
|
## Hebt hervor, was der aktuelle Schritt verlangt.
|
|
func _tutorial_highlights(mine: bool) -> void:
|
|
Style.clear(highlight_layer)
|
|
if not mine or any_overlay():
|
|
return
|
|
var g := game
|
|
var h := perspective
|
|
var rects: Array[Rect2] = []
|
|
match TUTORIAL_STEPS[tutorial_step][1]:
|
|
"played_creature":
|
|
for v in hand_views:
|
|
if v.card.is_creature() and g.can_play(h, v.card)[0]:
|
|
rects.append(Rect2(v.position, v.size))
|
|
"turn_passed":
|
|
rects.append(b_end.get_rect())
|
|
"attacked":
|
|
for v in own_views:
|
|
if v.creature.can_attack():
|
|
rects.append(Rect2(v.slot, v.size))
|
|
if not own_views.is_empty() and own_views.any(func(v): return v.creature.can_attack()):
|
|
for o in others():
|
|
if o.alive() and hero_panels.has(o):
|
|
rects.append(hero_panels[o].get_rect())
|
|
"resonance":
|
|
for v in hand_views:
|
|
var c: Cards.Card = v.card
|
|
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
|
|
rects.append(Rect2(v.position, v.size))
|
|
"combo":
|
|
for v in hand_views:
|
|
var c: Cards.Card = v.card
|
|
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
|
|
rects.append(Rect2(v.position, v.size))
|
|
"spell":
|
|
for v in hand_views:
|
|
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
|
|
rects.append(Rect2(v.position, v.size))
|
|
for r in rects:
|
|
var hl := Highlight.new()
|
|
hl.position = r.position - Vector2(4, 4)
|
|
hl.size = r.size + Vector2(8, 8)
|
|
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
highlight_layer.add_child(hl)
|
|
|
|
|
|
func _tutorial_refresh() -> void:
|
|
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
|
|
tutorial_step += 1
|
|
tutorial_text.text = "%d/%d %s" % [tutorial_step + 1, TUTORIAL_STEPS.size(), TUTORIAL_STEPS[tutorial_step][0]]
|
|
b_tut_next.visible = tutorial_step < TUTORIAL_STEPS.size() - 1
|
|
|
|
|
|
# --- Ansicht angleichen --------------------------------------------------------------------
|
|
|
|
func refresh() -> void:
|
|
if game == null:
|
|
return
|
|
var g := game
|
|
var h := perspective
|
|
var now := Time.get_ticks_msec()
|
|
var mine := my_turn()
|
|
var valid: Array = g.valid_targets(h, pending.target_type()) if pending != null else []
|
|
hand_views = []
|
|
own_views = []
|
|
enemy_views = []
|
|
trap_views = []
|
|
|
|
for o in others():
|
|
var panel: Panel = hero_panels[o]
|
|
var bcol = null
|
|
var bw := 0
|
|
if g.current() == o:
|
|
bcol = Style.BLUE
|
|
bw = 3
|
|
if o in valid or (selected != null and o.alive() and g.can_attack(selected, o)[0]):
|
|
bcol = Style.ORANGE
|
|
bw = 3
|
|
if int(flash_until.get(g.players.find(o), 0)) > now:
|
|
bcol = Style.RED
|
|
bw = 4
|
|
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if o.alive() else Style.darker(Style.PANEL, 0.6), 8, bcol, bw))
|
|
panel.mouse_filter = Control.MOUSE_FILTER_STOP if o.alive() else Control.MOUSE_FILTER_IGNORE
|
|
var labels: Dictionary = hero_labels[o]
|
|
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
|
if o.alive():
|
|
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
|
var traps_txt := (" F %d" if compact() else " Fallen %d") % o.traps.size() if not o.traps.is_empty() else ""
|
|
labels.stats.text = ("H %d D %d%s ◆ %d" if compact() else "Hand %d Deck %d%s ◆ %d") % [o.hand.size(), o.deck.size(), traps_txt, o.max_energy]
|
|
else:
|
|
labels.life.text = "besiegt"
|
|
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
|
labels.stats.text = ""
|
|
|
|
var seen := {}
|
|
for p in g.players:
|
|
for j in p.board.size():
|
|
var cr: Game.Creature = p.board[j]
|
|
var rect := slot_rect(p, j)
|
|
seen[cr.uid] = true
|
|
var tile: CreatureView
|
|
if tiles.has(cr.uid):
|
|
tile = tiles[cr.uid]
|
|
tile.creature = cr
|
|
if tile.size != rect.size:
|
|
tile.size = rect.size
|
|
tile.custom_minimum_size = rect.size
|
|
tile.pivot_offset = rect.size / 2
|
|
else:
|
|
tile = CreatureView.new(cr, rect.size)
|
|
tile.position = rect.position
|
|
tile.slot = rect.position
|
|
tiles[cr.uid] = tile
|
|
tile_layer.add_child(tile)
|
|
tile.hover.connect(show_preview)
|
|
var uid := cr.uid
|
|
tile.clicked.connect(func(_v):
|
|
var c := game.find_creature(uid)
|
|
if c != null:
|
|
on_tile_click(c))
|
|
fx.pop_in(tile)
|
|
tile.slot = rect.position
|
|
if not tile.busy and tile.position != rect.position:
|
|
fx.move_to(tile, rect.position)
|
|
var b = null
|
|
var w := 2
|
|
if cr == selected:
|
|
b = Style.YELLOW
|
|
w = 4
|
|
elif cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
|
b = Style.ORANGE
|
|
w = 3
|
|
elif p == h and mine and cr.can_attack():
|
|
b = Style.GREEN
|
|
w = 3
|
|
tile.border = b
|
|
tile.width = w
|
|
tile.queue_redraw()
|
|
if p == h:
|
|
own_views.append(tile)
|
|
else:
|
|
enemy_views.append(tile)
|
|
for uid in tiles.keys():
|
|
if not seen.has(uid):
|
|
var t: CreatureView = tiles[uid]
|
|
tiles.erase(uid)
|
|
fx.fade_out(t)
|
|
|
|
Style.clear(trap_layer)
|
|
if not h.traps.is_empty():
|
|
Style.label(trap_layer, "Fallen", 13, Style.MUTED, Rect2(335, 492, 60, 20))
|
|
for i in h.traps.size():
|
|
var tv := CardView.new(h.traps[i], BattleLayout.trap_rect(i).size)
|
|
tv.position = BattleLayout.trap_rect(i).position
|
|
tv.hover.connect(show_preview)
|
|
trap_layer.add_child(tv)
|
|
trap_views.append(tv)
|
|
|
|
Style.clear(hand_layer)
|
|
for i in h.hand.size():
|
|
var card: Cards.Card = h.hand[i]
|
|
var playable: bool = mine and g.can_play(h, card)[0]
|
|
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
|
var v := CardView.new(card, r.size)
|
|
v.position = r.position
|
|
if card == pending:
|
|
v.border = Style.YELLOW
|
|
elif playable:
|
|
v.border = Style.GREEN
|
|
v.dim = not playable
|
|
v.hover.connect(show_hand_preview)
|
|
v.clicked.connect(func(_view): on_click("hand", card))
|
|
hand_layer.add_child(v)
|
|
hand_views.append(v)
|
|
|
|
var bar_border = Style.BLUE if mine else null
|
|
if int(flash_until.get(g.players.find(h), 0)) > now:
|
|
bar_border = Style.RED
|
|
bar_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
|
name_label.text = h.name
|
|
life_label.text = "♥ %d" % maxi(h.life, 0)
|
|
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
|
|
deck_label.text = "Deck %d" % h.deck.size()
|
|
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
|
b_end.disabled = not mine
|
|
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
|
b_skip.visible = ai_running
|
|
b_end.visible = not ai_running
|
|
b_undo.visible = undo_state != null and mine
|
|
var lines: Array[String] = []
|
|
var start := maxi(0, g.log.size() - 60)
|
|
for i in range(start, g.log.size()):
|
|
var line := g.log[i]
|
|
if line.begins_with("---"):
|
|
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
|
elif line.begins_with("Feldkombo") or line.begins_with("Falle von"):
|
|
lines.append("[color=#f5c850]%s[/color]" % line)
|
|
else:
|
|
lines.append(line)
|
|
log_box.text = "\n".join(lines)
|
|
if tutorial:
|
|
_tutorial_refresh()
|
|
_tutorial_highlights(mine)
|
|
|
|
Style.clear(zoom_layer)
|
|
if zoom_player != null and zoom_player.alive():
|
|
build_zoom(valid)
|
|
|
|
if g.winner != "":
|
|
var wp := g.winner_player
|
|
if counts_for_profile():
|
|
var won := g.winner == "human"
|
|
result_label.text = "Sieg!" if won else "Niederlage"
|
|
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
|
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
|
var notes: Array[String] = app.profile.take_notices()
|
|
if not notes.is_empty():
|
|
notice_label.text = "\n".join(notes)
|
|
else:
|
|
var mine_win: bool = wp != null and wp == perspective
|
|
result_label.text = ("%s gewinnt!" % wp.name) if wp != null and wp.is_human else "Die KI gewinnt"
|
|
result_label.add_theme_color_override("font_color", Style.GREEN if mine_win else Style.RED)
|
|
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
|
overlay.visible = true
|
|
confirm.visible = false
|
|
handover.visible = false
|
|
mulligan_box.visible = false
|
|
|
|
|
|
func build_zoom(valid: Array) -> void:
|
|
var o: Game.Player = zoom_player
|
|
var g := game
|
|
var box := Rect2(190, 190, 900, 300)
|
|
var back := Style.panel(zoom_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
|
back.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d Deck %d Fallen %d ◆ %d" % [o.name, o.title, o.life, o.hand.size(), o.deck.size(), o.traps.size(), o.max_energy],
|
|
18, Style.TEXT, Rect2(box.position.x + 20, box.position.y + 12, 800, 30), "left", true)
|
|
for j in o.board.size():
|
|
var cr: Game.Creature = o.board[j]
|
|
var b = null
|
|
if cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
|
b = Style.ORANGE
|
|
var v := CreatureView.new(cr, Vector2(110, 140), b, 3 if b != null else 2)
|
|
v.position = Vector2(box.position.x + 20 + j * 122, box.position.y + 56)
|
|
v.hover.connect(show_preview)
|
|
v.clicked.connect(func(_x): on_tile_click(cr))
|
|
zoom_layer.add_child(v)
|
|
if o.board.is_empty():
|
|
Style.label(zoom_layer, "Keine Wesen auf dem Feld", 16, Style.MUTED, Rect2(box.position.x + 20, box.position.y + 110, 600, 30))
|
|
var hero_ok: bool = (o in valid) or (selected != null and g.can_attack(selected, o)[0])
|
|
var bh := Style.button(zoom_layer, "Held als Ziel", Rect2(box.position.x + 20, box.end.y - 60, 180, 44), func(): on_click("hero", o), Color("5a3c28"))
|
|
bh.disabled = not hero_ok
|
|
Style.button(zoom_layer, "Schließen", Rect2(box.end.x - 180, box.end.y - 60, 160, 44), func():
|
|
zoom_player = null
|
|
refresh())
|