1220 lines
46 KiB
GDScript
1220 lines
46 KiB
GDScript
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 Handkarte oder ein eigenes Wesen auf ein anderes ziehen und loslassen; das
|
||
## Ergebnis (eine neue Handkarte) erscheint über dem Ziel, darunter Bestätigen oder Abbrechen.
|
||
## Mit Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||
## Modi: Einzelspieler und Kampagne.
|
||
|
||
var app
|
||
var stage := -1 # Kampagnenstufe oder -1
|
||
var game: Game
|
||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||
var fx: BattleFx
|
||
var selected = null # Game.Creature
|
||
var hover_target = null # fremdes Wesen unter der Maus: Vorschau des Angriffs mit dem gewählten Wesen
|
||
var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
|
||
var fuse_pending = null # [a, b] nach dem Loslassen über einem Ziel, wartet auf Bestätigung
|
||
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
|
||
var press_view: Control = null
|
||
var press_pos := Vector2.ZERO
|
||
var drag_ghost: Control = null # schwebende Kopie der gezogenen Karte
|
||
var drag_target = null # mögliches Verschmelzungsziel unter dem Zeiger
|
||
var drag_target_view: Control = null
|
||
var msg_t := 0.0
|
||
var ai_delay := 0.75
|
||
var ai_timer := 0.0
|
||
var ai_task := -1 # WorkerThreadPool-Aufgabe der laufenden KI-Berechnung
|
||
var ai_copy: Game # Spielkopie, auf der die KI rechnet
|
||
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
|
||
var reward := -1 # gutgeschriebene Münzen (mit Kampagnenbonus)
|
||
var earned := 0 # Münzen aus Zügen, Karten und Tempo (Profile.battle_coins)
|
||
var fast_turns := 0 # eigene Züge unter Profile.FAST_TURN_SEC
|
||
var turn_start := 0 # Beginn des laufenden eigenen Zugs (ms)
|
||
var ended := false
|
||
var pad: BattleGamepad
|
||
var gp_zone: String:
|
||
get:
|
||
return pad.zone
|
||
var gp_index: int:
|
||
get:
|
||
return pad.index
|
||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||
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 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 deck: Deck
|
||
var opp_deck: Deck # Stapel des Gegners, nur zum Ansehen
|
||
var log_overlay: Control
|
||
var options: Control # Optionsmenü (Esc): Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden
|
||
var status_panel: Panel
|
||
var fx_layer: Control
|
||
var log_box: RichTextLabel
|
||
var preview_holder: Control
|
||
var name_label: Label
|
||
var life_label: Label
|
||
var energy_label: Label
|
||
var msg_label: Label
|
||
var element_view: ElementView # eigene Elementleiste
|
||
var opp_element_view: ElementView # die des Gegners darüber
|
||
var info_label: Label # Vorschau: Merkmale, Eigenschaften
|
||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||
var hand_arrivals := {} # Karte -> Rect2: neue Handkarte, die aus der Verschmelzung in die Hand gleitet
|
||
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||
var overlay: Control
|
||
var result_label: Label
|
||
var reward_label: Label
|
||
var detail_label: Label # Aufschlüsselung der Münzen
|
||
var notice_label: Label
|
||
var confirm: Control
|
||
var cursor_layer: Control
|
||
var b_skip: Button
|
||
var b_undo: Button
|
||
|
||
|
||
func _init(campaign_stage: int = -1) -> void:
|
||
stage = campaign_stage
|
||
|
||
|
||
func _ready() -> void:
|
||
var s: Dictionary = app.profile.settings
|
||
fx = BattleFx.new(self)
|
||
fx.enabled = s.animations
|
||
pad = BattleGamepad.new(self)
|
||
ai_delay = 0.75 / float(s.ai_speed)
|
||
_build_static()
|
||
var r := Game.make_rng()
|
||
var opp: Game.Player = Campaign.opponent(stage) if stage >= 0 else app.profile.single_opponent(r)
|
||
game = Game.new_battle_with(opp, r, false, own_rules())
|
||
smart_ai = bool(app.profile.difficulty().smart)
|
||
_begin()
|
||
|
||
|
||
## Eigener Stapel und Kartenstufen und das eigene Merkmal (nach Kapitel 1).
|
||
func own_rules() -> Dictionary:
|
||
var p: Profile = app.profile
|
||
var rules := {"looks": p.looks(), "deck": p.deck_cards(), "bonus_attack": p.skill("stark"), "bonus_health": p.skill("zaeh"), "cheap_from": Skills.cheap_from(p.skill("sparsam"))}
|
||
var perks := p.battle_perks()
|
||
if not perks.is_empty():
|
||
rules["human_perks"] = perks
|
||
return rules
|
||
|
||
|
||
func _begin() -> void:
|
||
game.events.clear()
|
||
perspective = game.human
|
||
build_hero_boxes()
|
||
refresh()
|
||
announce_turn()
|
||
|
||
|
||
func _build_static() -> void:
|
||
var L := BattleLayout
|
||
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
|
||
# Elementzähler beider Seiten links neben ihrem Brett
|
||
opp_element_view = ElementView.new(L.OPP_ELEMENTS)
|
||
add_child(opp_element_view)
|
||
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
|
||
var slots := Control.new()
|
||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
slots.draw.connect(func():
|
||
for j in Game.MAX_BOARD:
|
||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.05), 2, 8)
|
||
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||
add_child(slots)
|
||
status_panel = Style.panel(self, L.STATUS)
|
||
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, "", 16, Style.TEXT, Rect2(sx + 8, sy + 4, sw - 16, 22), "center", true)
|
||
name_label.clip_text = 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 = 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()
|
||
tile_layer = _layer()
|
||
hand_layer = _layer()
|
||
preview_holder = _layer()
|
||
info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
|
||
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
info_label.visible = false
|
||
fx_layer = _layer()
|
||
# Kartenmenü: durchsichtiger Fänger, der das Menü beim Klick daneben schließt
|
||
menu_layer = Control.new()
|
||
menu_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
menu_layer.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
menu_layer.visible = false
|
||
menu_layer.gui_input.connect(func(ev):
|
||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||
close_menu())
|
||
add_child(menu_layer)
|
||
cursor_layer = _layer()
|
||
# Verlauf als Überlagerung
|
||
log_overlay = Style.overlay(self)
|
||
log_overlay.get_child(0).gui_input.connect(func(ev):
|
||
if ev is InputEventMouseButton and ev.pressed:
|
||
log_overlay.visible = false)
|
||
Style.panel(log_overlay, L.LOG_PANEL, Style.PANEL, 10, Style.MUTED, 2)
|
||
Style.label(log_overlay, "Verlauf", 20, Style.GOLD, Rect2(L.LOG_PANEL.position + Vector2(20, 12), Vector2(300, 30)), "left", true)
|
||
log_box = RichTextLabel.new()
|
||
log_box.position = L.LOG_PANEL.position + Vector2(20, 50)
|
||
log_box.size = L.LOG_PANEL.size - Vector2(40, 110)
|
||
log_box.bbcode_enabled = true
|
||
log_box.scroll_following = true
|
||
log_box.add_theme_font_size_override("normal_font_size", 14)
|
||
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)
|
||
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
||
detail_label = Style.label(overlay, "", 17, Style.MUTED, Rect2(0, 372, 1280, 28), "center")
|
||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 405, 1280, 70), "center")
|
||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||
|
||
|
||
func toggle_options() -> void:
|
||
options.visible = not options.visible
|
||
|
||
|
||
func _layer() -> Control:
|
||
var c := Control.new()
|
||
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(c)
|
||
return c
|
||
|
||
|
||
func _overlay() -> Control:
|
||
return Style.overlay(self)
|
||
|
||
|
||
func leave() -> void:
|
||
app.show_screen(MenuScreen.new())
|
||
|
||
|
||
# --- Layout-Helfer ------------------------------------------------------------------
|
||
|
||
## Der Gegner aus Sicht der eigenen Perspektive (Zuschauer: der zweite Spieler).
|
||
func opponent() -> Game.Player:
|
||
return game.enemy_of(perspective)
|
||
|
||
|
||
func others() -> Array:
|
||
return [opponent()]
|
||
|
||
|
||
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
||
if p == perspective:
|
||
return BattleLayout.own_slot(j)
|
||
return BattleLayout.opp_slot(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.STATUS.get_center()
|
||
if hero_panels.has(p):
|
||
var panel: Panel = hero_panels[p]
|
||
return panel.position + panel.size / 2
|
||
return Vector2(-1, -1)
|
||
|
||
|
||
## Kasten des Gegners links oben, gebaut wie der eigene; ein Klick zielt auf den Helden.
|
||
func build_hero_boxes() -> void:
|
||
Style.clear(hero_layer)
|
||
hero_panels = {}
|
||
hero_labels = {}
|
||
var o := opponent()
|
||
var box := BattleLayout.OPP_BOX
|
||
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, 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, "", 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:
|
||
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 or options.visible
|
||
|
||
|
||
func _input(event: InputEvent) -> void:
|
||
if game == null:
|
||
return
|
||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||
var under := get_viewport().gui_get_hovered_control()
|
||
if menu_layer.visible:
|
||
close_menu()
|
||
elif under is CardView and under in hand_views and my_turn():
|
||
open_menu(under.card, Rect2(under.position, under.size))
|
||
elif under is CreatureView and under in own_views and my_turn():
|
||
open_menu(under.creature, Rect2(under.slot, under.size))
|
||
elif selected != null or fuse_first != null or any_overlay() or not my_turn():
|
||
cancel_all()
|
||
else:
|
||
show_menu(tr("Zug %d") % game.turn, [[tr("Zug beenden"), end_turn, true]], Rect2(event.position, Vector2.ZERO))
|
||
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
||
return
|
||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||
var under := get_viewport().gui_get_hovered_control()
|
||
if under is CardView and under in hand_views and my_turn() and not any_overlay():
|
||
cancel()
|
||
play(under.card, true)
|
||
after_action()
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
if event is InputEventMouseMotion:
|
||
_drag_motion(event.position)
|
||
return
|
||
if event is InputEventMouseButton and not event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||
_release()
|
||
return
|
||
if event is InputEventJoypadButton and event.pressed:
|
||
pad.button(event.button_index)
|
||
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 menu_layer.visible:
|
||
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():
|
||
options.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 overlay.visible:
|
||
leave()
|
||
_:
|
||
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])
|
||
elif event.keycode == KEY_Z and my_turn() and not any_overlay():
|
||
on_click("deck", null)
|
||
|
||
|
||
func cancel_all() -> void:
|
||
if confirm.visible:
|
||
confirm.visible = false
|
||
close_menu()
|
||
cancel()
|
||
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 undo_t > 0.0:
|
||
undo_t -= delta
|
||
if undo_t <= 0.0:
|
||
clear_undo()
|
||
if game.winner != "":
|
||
if not ended:
|
||
ended = true
|
||
_finish()
|
||
return
|
||
var cur := game.current()
|
||
if cur.is_human:
|
||
return
|
||
if skip_ai:
|
||
ai_timer = 0.0
|
||
else:
|
||
ai_timer -= delta
|
||
var action: Dictionary
|
||
if smart_ai:
|
||
# Die Vorausschau rechnet im Hintergrund auf einer Kopie, schon während der Wartezeit,
|
||
# damit Animationen nicht stocken.
|
||
if ai_task < 0:
|
||
_start_ai(cur)
|
||
if ai_timer > 0 or not WorkerThreadPool.is_task_completed(ai_task):
|
||
return
|
||
WorkerThreadPool.wait_for_task_completion(ai_task)
|
||
ai_task = -1
|
||
action = AI.map_action(game, ai_copy, ai_box.action)
|
||
ai_copy = null
|
||
else:
|
||
if ai_timer > 0:
|
||
return
|
||
action = AI.choose_action_greedy(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 _start_ai(cur: Game.Player) -> void:
|
||
ai_copy = game.masked_clone(cur) # verdeckte Wesen des Menschen kennt die KI nicht
|
||
var copy := ai_copy
|
||
var idx := game.current_idx
|
||
var box := ai_box
|
||
ai_task = WorkerThreadPool.add_task(func(): box["action"] = AI.choose_action(copy, copy.players[idx]))
|
||
|
||
|
||
func _exit_tree() -> void:
|
||
if ai_task >= 0:
|
||
WorkerThreadPool.wait_for_task_completion(ai_task)
|
||
ai_task = -1
|
||
|
||
|
||
func _finish() -> void:
|
||
var won := game.winner == "human"
|
||
if game.current() == game.human:
|
||
_close_turn() # der Zug, der das Spiel entschied
|
||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||
earned = Profile.battle_coins(won, game.human.turns, game.human.cards_played, fast_turns, factor)
|
||
reward = app.profile.record_battle(won, earned, stage)
|
||
app.save()
|
||
app.sfx.play("win" if won else "lose")
|
||
refresh()
|
||
|
||
|
||
func say(m: String) -> void:
|
||
msg_t = 2.5
|
||
msg_label.text = m
|
||
|
||
|
||
func cancel() -> void:
|
||
selected = null
|
||
fuse_first = null
|
||
press_target = null
|
||
_end_drag()
|
||
|
||
|
||
# --- Ziehen mit der Maus: Klick beim Loslassen; losgelassen über einem Ziel fragt nach dem Verschmelzen ---
|
||
|
||
func _press(obj, view: Control, pos: Vector2) -> void:
|
||
_end_drag()
|
||
press_target = obj
|
||
press_view = view
|
||
press_pos = pos
|
||
|
||
|
||
func _drag_motion(pos: Vector2) -> void:
|
||
if press_target == null:
|
||
return
|
||
if drag_ghost == null:
|
||
if pos.distance_to(press_pos) < 8.0 or not my_turn() or any_overlay():
|
||
return
|
||
_start_drag(pos)
|
||
drag_ghost.position = pos - drag_ghost.size / 2
|
||
var t = _fusion_target_at(pos)
|
||
if t != drag_target:
|
||
_mark_target(false)
|
||
drag_target = t
|
||
drag_target_view = _view_of(t)
|
||
_mark_target(true)
|
||
|
||
|
||
## Loslassen ohne Ziehen ist der Klick; über einem Ziel öffnet es die Verschmelzungsfrage.
|
||
func _release() -> void:
|
||
var obj = press_target
|
||
var target = drag_target
|
||
var target_rect := _rect_of(drag_target_view) if drag_target_view != null else Rect2()
|
||
var dragged := drag_ghost != null
|
||
press_target = null
|
||
_end_drag()
|
||
if obj == null:
|
||
return
|
||
if dragged:
|
||
if target != null:
|
||
open_fuse_confirm(obj, target, target_rect)
|
||
else:
|
||
refresh()
|
||
elif obj is Cards.Card:
|
||
on_click("hand", obj)
|
||
else:
|
||
on_tile_click(obj)
|
||
|
||
|
||
func _start_drag(pos: Vector2) -> void:
|
||
show_preview(null, false)
|
||
if press_target is Cards.Card:
|
||
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H) * 0.8
|
||
drag_ghost = CardView.new(press_target, size, false, press_target.rarity_in(perspective.looks))
|
||
else:
|
||
drag_ghost = CreatureView.new(press_target, BattleLayout.own_slot(0).size)
|
||
drag_ghost.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
drag_ghost.modulate.a = 0.85
|
||
drag_ghost.position = pos - drag_ghost.size / 2
|
||
fx_layer.add_child(drag_ghost)
|
||
if press_view != null:
|
||
press_view.modulate.a = 0.4
|
||
|
||
|
||
func _end_drag() -> void:
|
||
if drag_ghost != null:
|
||
drag_ghost.queue_free()
|
||
drag_ghost = null
|
||
if is_instance_valid(press_view):
|
||
press_view.modulate.a = 1.0
|
||
_mark_target(false)
|
||
drag_target = null
|
||
drag_target_view = null
|
||
|
||
|
||
func _mark_target(on: bool) -> void:
|
||
if is_instance_valid(drag_target_view):
|
||
drag_target_view.drop = on
|
||
drag_target_view.queue_redraw()
|
||
|
||
|
||
## Platz einer Ansicht: Kacheln zählen an ihrem Platz, auch während sie dorthin gleiten.
|
||
func _rect_of(v: Control) -> Rect2:
|
||
return Rect2(v.slot, v.size) if v is CreatureView else v.get_global_rect()
|
||
|
||
|
||
## Handkarte oder eigenes Wesen unter pos, mit dem sich die gezogene Karte verschmelzen lässt.
|
||
func _fusion_target_at(pos: Vector2):
|
||
for v in hand_views + own_views:
|
||
var obj = v.card if v is CardView else v.creature
|
||
if obj != press_target and _rect_of(v).has_point(pos) and game.can_fuse(perspective, press_target, obj)[0]:
|
||
return obj
|
||
return null
|
||
|
||
|
||
func _view_of(obj) -> Control:
|
||
if obj is Cards.Card:
|
||
for v in hand_views:
|
||
if v.card == obj:
|
||
return v
|
||
elif obj is Game.Creature and tiles.has(obj.uid):
|
||
return tiles[obj.uid]
|
||
return null
|
||
|
||
|
||
## Mögliche Partner für „Verschmelzen mit …“: alle anderen Handkarten und eigenen Wesen.
|
||
func fusion_partners(target) -> Array:
|
||
var pool: Array = perspective.hand + perspective.board
|
||
return pool.filter(func(o): return o != target and game.can_fuse(perspective, target, o)[0])
|
||
|
||
|
||
func play(card: Cards.Card, face_down: bool = false) -> void:
|
||
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)
|
||
if err != "":
|
||
say(tr(err))
|
||
return
|
||
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
|
||
arrivals[perspective.board[-1].uid] = from
|
||
# Rückgängig nur, wenn nichts weiter passiert ist (kein Wesen gestorben)
|
||
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()
|
||
|
||
|
||
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
|
||
func do_reveal(cr: Game.Creature) -> void:
|
||
clear_undo()
|
||
var err := game.reveal(perspective, cr)
|
||
if err != "":
|
||
say(tr(err))
|
||
after_action()
|
||
|
||
|
||
## 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 not my_turn():
|
||
return
|
||
cancel()
|
||
Style.clear(menu_layer)
|
||
menu_target = target
|
||
var entries := [] # [Text, Callable, aktiv]
|
||
var is_card: bool = target is Cards.Card
|
||
var card: Cards.Card = target if is_card else target.card
|
||
if is_card:
|
||
var ok := game.can_play(perspective, card)
|
||
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
||
play(card, true)
|
||
after_action(), ok[0]])
|
||
entries.append([tr("Abwerfen +%d ◆") % Game.DISCARD_GAIN, func(): do_discard(card), game.can_discard(perspective, card)[0]])
|
||
elif not is_card and target.hidden:
|
||
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 …"), func():
|
||
fuse_first = target
|
||
say(tr("Ziel wählen"))
|
||
refresh(), not partners.is_empty()])
|
||
if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen
|
||
close_menu()
|
||
return
|
||
show_menu(card.title(), entries, anchor)
|
||
|
||
|
||
## Kleines Menü neben anchor: Überschrift und Knöpfe [Text, Callable, aktiv].
|
||
func show_menu(title: String, entries: Array, anchor: Rect2) -> void:
|
||
Style.clear(menu_layer)
|
||
var w := 230.0
|
||
var h := 18.0 + entries.size() * 44 + 34
|
||
var x := clampf(anchor.end.x + 8, 10, 1270 - w)
|
||
if x + w > anchor.position.x and x < anchor.end.x:
|
||
x = clampf(anchor.position.x - 8 - w, 10, 1270 - w)
|
||
var y := clampf(anchor.position.y, 10, 790 - h)
|
||
var box := Rect2(x, y, w, h)
|
||
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||
Style.label(menu_layer, title, 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
||
var by := box.position.y + 36
|
||
for e in entries:
|
||
var act: Callable = e[1]
|
||
var b := Style.button(menu_layer, e[0], Rect2(box.position.x + 12, by, w - 24, 38), func():
|
||
close_menu()
|
||
act.call())
|
||
b.add_theme_font_size_override("font_size", 15)
|
||
b.disabled = not e[2]
|
||
by += 44
|
||
menu_layer.visible = true
|
||
|
||
|
||
func close_menu() -> void:
|
||
if menu_layer.visible:
|
||
menu_layer.visible = false
|
||
Style.clear(menu_layer)
|
||
menu_target = null
|
||
fuse_pending = null
|
||
|
||
|
||
## Nach dem Loslassen über einem Ziel: die neue Handkarte schwebt über dem Ziel, darunter
|
||
## Bestätigen und Abbrechen; beide Ausgangskarten bleiben sichtbar. Klick daneben oder Esc bricht ab.
|
||
func open_fuse_confirm(a, b, target: Rect2) -> void:
|
||
cancel()
|
||
Style.clear(menu_layer)
|
||
fuse_pending = [a, b]
|
||
var card := Game.fusion_result(perspective, a, b)
|
||
var size := Vector2(BattleLayout.HAND_W, BattleLayout.HAND_H)
|
||
var bw := 150.0
|
||
var x := clampf(target.get_center().x - size.x / 2, 10 + bw - size.x / 2, 1270 - bw - size.x / 2)
|
||
var y := maxf(10.0, target.position.y - size.y - 48)
|
||
var v := CardView.new(card, size, false, Cards.RARITIES[card.look])
|
||
v.position = Vector2(x, y)
|
||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
menu_layer.add_child(v)
|
||
var by := y + size.y + 6
|
||
var bx := x + size.x / 2 - bw - 3
|
||
var yes := Style.button(menu_layer, tr("Verschmelzen"), Rect2(bx, by, bw, 36), func():
|
||
var pair: Array = fuse_pending
|
||
close_menu()
|
||
do_fuse(pair[0], pair[1], Rect2(v.position, v.size))
|
||
after_action(), Color("285a3c"))
|
||
yes.add_theme_font_size_override("font_size", 15)
|
||
var no := Style.button(menu_layer, tr("Abbrechen"), Rect2(bx + bw + 6, by, bw, 36), func():
|
||
close_menu()
|
||
refresh())
|
||
no.add_theme_font_size_override("font_size", 15)
|
||
menu_layer.visible = true
|
||
|
||
|
||
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||
func do_discard(card: Cards.Card) -> void:
|
||
clear_undo()
|
||
var err := game.discard(perspective, card)
|
||
if err != "":
|
||
say(tr(err))
|
||
after_action()
|
||
|
||
|
||
## Ziehen: eine Karte vom eigenen Stapel, die erste im Zug frei.
|
||
func do_draw() -> void:
|
||
clear_undo()
|
||
var err := game.draw(perspective)
|
||
if err != "":
|
||
say(tr(err))
|
||
else:
|
||
fx.sfx("buy")
|
||
|
||
|
||
## Verschmilzt zwei eigene Karten zur neuen Handkarte (siehe Game.fuse). from: von dort gleitet
|
||
## die neue Karte in die Hand, Eltern auf der Hand blenden aus (Wesen tut das der Abgleich).
|
||
func do_fuse(a, b, from: Rect2 = Rect2()) -> void:
|
||
cancel()
|
||
clear_undo()
|
||
var before: Array = perspective.hand.duplicate()
|
||
var err := game.fuse(perspective, a, b)
|
||
if err != "":
|
||
say(tr(err))
|
||
return
|
||
if from.size.x <= 0:
|
||
return
|
||
for x in [a, b]:
|
||
var pv := _view_of(x)
|
||
if pv is CardView:
|
||
pv.reparent(fx_layer)
|
||
fx.fade_view(pv)
|
||
for c in perspective.hand:
|
||
if not (c in before):
|
||
hand_arrivals[c] = from
|
||
|
||
|
||
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]
|
||
clear_undo()
|
||
cancel()
|
||
build_hero_boxes()
|
||
after_action()
|
||
|
||
|
||
func end_turn() -> void:
|
||
if not my_turn():
|
||
return
|
||
cancel()
|
||
clear_undo()
|
||
_close_turn()
|
||
game.end_turn()
|
||
ai_timer = ai_delay
|
||
after_action()
|
||
|
||
|
||
func on_click(kind: String, obj) -> void:
|
||
var g := game
|
||
if not my_turn() or any_overlay():
|
||
return
|
||
if fuse_first != null:
|
||
var first = fuse_first
|
||
fuse_first = null
|
||
if obj != null and obj != first and g.can_fuse(perspective, first, obj)[0]:
|
||
var tv := _view_of(obj)
|
||
do_fuse(first, obj, _rect_of(tv) if tv != null else Rect2())
|
||
else:
|
||
say(tr("Abgebrochen"))
|
||
after_action()
|
||
return
|
||
if selected != null:
|
||
var cr = selected
|
||
selected = null
|
||
if kind == "enemy" or kind == "hero":
|
||
clear_undo()
|
||
var err := g.attack(cr, obj)
|
||
if err != "":
|
||
say(tr(err))
|
||
after_action()
|
||
return
|
||
match kind:
|
||
"deck":
|
||
do_draw()
|
||
"hand":
|
||
var ok := g.can_play(perspective, obj)
|
||
if not ok[0]:
|
||
say(tr(ok[1]))
|
||
else:
|
||
play(obj)
|
||
"own":
|
||
if obj.hidden:
|
||
do_reveal(obj)
|
||
return
|
||
if obj.can_attack():
|
||
selected = obj
|
||
say(tr("Wähle ein Angriffsziel"))
|
||
elif not obj.ready:
|
||
say(tr("%s greift erst im nächsten Zug an") % obj.card.title())
|
||
else:
|
||
say(tr("%s kann nicht angreifen") % obj.card.title())
|
||
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)
|
||
|
||
|
||
## Zählt den laufenden eigenen Zug als schnell, wenn er unter Profile.FAST_TURN_SEC blieb.
|
||
func _close_turn() -> void:
|
||
if Time.get_ticks_msec() - turn_start < Profile.FAST_TURN_SEC * 1000:
|
||
fast_turns += 1
|
||
|
||
|
||
## Aufschlüsselung der Münzen fürs Ergebnisbild.
|
||
func reward_text() -> String:
|
||
var h := game.human
|
||
var s := "%s · %s · %s = %d" % [tr("%d Züge") % h.turns, tr("%d Karten") % h.cards_played, tr("%d schnelle Züge") % fast_turns,
|
||
Profile.COINS_TURN * h.turns + Profile.COINS_CARD * h.cards_played + Profile.COINS_FAST * fast_turns]
|
||
if game.winner == "human":
|
||
s += " · " + tr("Sieg ×2")
|
||
var d: Dictionary = app.profile.difficulty()
|
||
if stage < 0 and float(d.reward) != 1.0:
|
||
s += " · %s %s" % [tr(d.name), Style.factor(float(d.reward))]
|
||
var boosted := int(round(earned * app.profile.coin_factor()))
|
||
if boosted != earned:
|
||
s += " · %s %s" % [tr("Münzsegen"), Style.factor(app.profile.coin_factor())]
|
||
if reward > boosted:
|
||
s += " · " + tr("Kampagne +%d") % (reward - boosted)
|
||
return s
|
||
|
||
|
||
## Zugansage, wenn der eigene Zug beginnt; ab hier läuft die Zeit für den schnellen Zug.
|
||
func announce_turn() -> void:
|
||
if game == null or not my_turn() or banner_turn == game.turn:
|
||
return
|
||
banner_turn = game.turn
|
||
turn_start = Time.get_ticks_msec()
|
||
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||
fx.sfx("turn")
|
||
|
||
|
||
func _hand_rect_of(card: Cards.Card) -> Rect2:
|
||
for v in hand_views:
|
||
if v.card == card:
|
||
return Rect2(v.position, v.size)
|
||
return Rect2()
|
||
|
||
|
||
## Vorschau schwebt neben der gezeigten Karte (anchor), darunter die Zusatzzeile (info).
|
||
func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: String = "", anchor: Rect2 = Rect2()) -> void:
|
||
Style.clear(preview_holder)
|
||
info_label.visible = entered and info != ""
|
||
info_label.text = info if entered else ""
|
||
if entered and card != null:
|
||
var r := BattleLayout.preview_rect(anchor)
|
||
var v := CardView.new(card, r.size, false, card.rarity_in(owner_looks))
|
||
v.position = r.position
|
||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
preview_holder.add_child(v)
|
||
var ir := BattleLayout.info_rect(r)
|
||
info_label.position = ir.position
|
||
info_label.size = ir.size
|
||
|
||
|
||
## Eigene Handkarte in der Vorschau.
|
||
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||
if drag_ghost != null:
|
||
return
|
||
show_preview(card, entered, perspective.looks, "", anchor)
|
||
|
||
|
||
## Was ein Wesen gerade hebt, was es kann und ob es schon angreift.
|
||
func creature_info(cr: Game.Creature) -> String:
|
||
var parts: Array[String] = []
|
||
var perk := cr.power() - cr.attack
|
||
if perk > 0:
|
||
parts.append(tr("Merkmal +%d") % perk)
|
||
if cr.card.is_neutral():
|
||
parts.append(tr(Cards.NEUTRAL_TEXT))
|
||
for k in cr.keywords():
|
||
if not (k in cr.card.keywords):
|
||
parts.append(tr("%s durch das Element") % tr(k))
|
||
if cr.hidden:
|
||
parts.append(tr("verdeckt"))
|
||
elif not cr.ready and not cr.has("Flink") and cr.owner() == perspective:
|
||
parts.append(tr("greift erst im nächsten Zug an"))
|
||
return " · ".join(parts)
|
||
|
||
|
||
## Konter eines möglichen Ziels gegen den gewählten Angreifer (Verschiebung durch sein Element);
|
||
## gegen Verdeckte unbekannt.
|
||
func matchup_for(cr: Game.Creature) -> int:
|
||
if selected != null and not cr.hidden and game.can_attack(selected, cr)[0]:
|
||
return Game.matchup_bonus(cr, selected)
|
||
return 0
|
||
|
||
|
||
## Vorschau für den Angriff auf das Wesen unter der Maus: der Angreifer zeigt seinen Angriff mit
|
||
## Elementvorteil, und wer dabei stirbt, bekommt die Marke. Verdeckte Ziele bleiben unbekannt.
|
||
func update_attack_preview() -> void:
|
||
var target = hover_target
|
||
if selected == null or target == null or target.hidden or not game.can_attack(selected, target)[0]:
|
||
target = null
|
||
for v in enemy_views:
|
||
v.doomed = target != null and v.creature == target and selected.power() + Game.matchup_bonus(selected, target) >= target.health
|
||
v.queue_redraw()
|
||
for v in own_views:
|
||
var mine: bool = v.creature == selected
|
||
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
|
||
var counter: int = maxi(0, target.power() + Game.matchup_bonus(target, selected)) if target != null else 0
|
||
v.doomed = mine and target != null and counter >= selected.health
|
||
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:
|
||
position = rect.position
|
||
size = rect.size
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
|
||
func _draw() -> void:
|
||
var r := Rect2(Vector2.ZERO, size)
|
||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 2), r)
|
||
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||
Style.text(self, "?", 40, Style.GOLD, size / 2, "center", true)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# --- Ereignisse ----------------------------------------------------------------------------
|
||
|
||
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
||
func after_action() -> void:
|
||
var evs := game.events.duplicate()
|
||
game.events.clear()
|
||
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)
|
||
announce_turn()
|
||
|
||
|
||
# --- 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()
|
||
hand_views = []
|
||
own_views = []
|
||
enemy_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 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)
|
||
labels.stats.text = "◆ %d" % o.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: # erst die Mindestgröße, sonst bleibt die Kachel groß
|
||
tile.custom_minimum_size = rect.size
|
||
tile.size = rect.size
|
||
tile.pivot_offset = rect.size / 2
|
||
tile.set_process(Style.rarity_animated(tile.rarity()))
|
||
else:
|
||
tile = CreatureView.new(cr, rect.size)
|
||
tile.position = rect.position
|
||
tile.slot = rect.position
|
||
tiles[cr.uid] = tile
|
||
tile_layer.add_child(tile)
|
||
var looks: Dictionary = p.looks
|
||
tile.hover.connect(func(card, entered):
|
||
hover_target = tile.creature if entered else null
|
||
update_attack_preview()
|
||
if tile.secret:
|
||
show_preview(null, false)
|
||
else:
|
||
show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
||
var uid := cr.uid
|
||
tile.pressed.connect(func(_v, pos):
|
||
var c := game.find_creature(uid)
|
||
if c != null:
|
||
_press(c, tile, pos))
|
||
tile.double_clicked.connect(func(_v):
|
||
var c := game.find_creature(uid)
|
||
if c != null and c.owner() == perspective and my_turn() and not any_overlay():
|
||
selected = c # Doppelklick: gleich auf das Leben des Gegners
|
||
on_click("hero", opponent()))
|
||
if arrivals.has(cr.uid):
|
||
fx.fly_in(tile, arrivals[cr.uid])
|
||
arrivals.erase(cr.uid)
|
||
else:
|
||
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 or cr == fuse_first:
|
||
b = Style.YELLOW
|
||
w = 4
|
||
elif fuse_first != null and p == h and mine and g.can_fuse(h, fuse_first, cr)[0]:
|
||
b = Style.PURPLE
|
||
w = 3
|
||
elif 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.secret = cr.hidden and p != h
|
||
tile.matchup = matchup_for(cr)
|
||
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)
|
||
update_attack_preview()
|
||
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
|
||
for i in o.hand.size():
|
||
var back := CardBack.new(BattleLayout.opp_hand_rect(o.hand.size(), i))
|
||
var arc: Array = BattleLayout.hand_arc(o.hand.size(), i, true)
|
||
back.pivot_offset = back.size / 2
|
||
back.rotation_degrees = arc[0]
|
||
back.position.y += arc[1]
|
||
hand_layer.add_child(back)
|
||
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]
|
||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||
var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
|
||
v.position = r.position
|
||
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
|
||
v.rotation_degrees = arc[0]
|
||
v.position.y += arc[1]
|
||
if hand_arrivals.has(card):
|
||
fx.glide(v, hand_arrivals[card])
|
||
hand_arrivals.erase(card)
|
||
v.lift = mine
|
||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||
v.border = Style.PURPLE
|
||
elif card == fuse_first:
|
||
v.border = Style.YELLOW
|
||
elif playable:
|
||
v.border = Style.GREEN
|
||
v.dim = not playable and card != fuse_first
|
||
v.drag_enabled = true
|
||
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
||
v.pressed.connect(func(_view, pos): _press(card, v, pos))
|
||
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
|
||
status_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" % h.energy
|
||
var ai_running: bool = g.winner == "" and not g.current().is_human
|
||
b_skip.visible = ai_running
|
||
b_undo.visible = undo_state != null and mine
|
||
element_view.update_from(g, h)
|
||
opp_element_view.update_from(g, opponent())
|
||
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 " verschmelzen " in line or " wird aufgedeckt." in line:
|
||
lines.append("[color=#8c78dc]%s[/color]" % line)
|
||
elif " bekommt " in line or " regeneriert sich." in line:
|
||
lines.append("[color=#5ad282]%s[/color]" % line)
|
||
else:
|
||
lines.append(line)
|
||
log_box.text = "\n".join(lines)
|
||
pad.draw_cursor()
|
||
|
||
if g.winner != "":
|
||
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)
|
||
detail_label.text = reward_text()
|
||
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
|
||
|