Files
cards/scripts/ui/battle_screen.gd
Team3 dfcd3cb713
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled
update
2026-09-22 13:23:02 +02:00

981 lines
34 KiB
GDScript

class_name BattleScreen
extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
var app
var n_opp: int
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
var selected = null # Game.Creature
var pending = null # Cards.Card, wartet auf Ziel
var fusing := false # Verschmelzen gewählt: zwei eigene Wesen anklicken
var fuse_first = null # Game.Creature, erstes Wesen der Verschmelzung
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
var ended := false
var zoom_player = null
var preview_timer := 0.0
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 saved_game: Game = null # fortgesetzter Kampf (siehe resume)
var undo_log_len := 0
var undo_t := 0.0
# Ansichten (auch für Tests)
var tiles := {}
var hand_views: Array = []
var shop_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 shop_layer: Control
var zoom_layer: Control
var fx_layer: Control
var log_box: RichTextLabel
var preview_holder: Control
var bar_panel: Panel
var name_label: Label
var life_label: Label
var energy_label: Label
var income_label: Label
var status_label: Label
var msg_label: Label
var b_end: Button
var b_fuse: Button
var b_reroll: Button
var b_giveup: Button
var field_label: Label
var overlay: Control
var result_label: Label
var reward_label: Label
var notice_label: Label
var confirm: Control
var b_save_leave: Button
var cursor_layer: Control
var wait_label: Label
var b_skip: Button
var b_undo: Button
func _init(n: int = 2, campaign_stage: int = -1) -> void:
n_opp = n
stage = campaign_stage
static func online(seat: int) -> BattleScreen:
var b := BattleScreen.new(0, -1)
b.net_play = true
b.online_seat = seat
return b
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
static func resume(data: Dictionary) -> BattleScreen:
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
s.saved_game = Game.from_snapshot(data.game)
return s
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)
fx.enabled = s.animations
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 saved_game != null:
game = saved_game
elif stage >= 0:
var r := Game.make_rng()
game = Game.new_battle_with(Campaign.opponents(stage), r, false, own_rules())
else:
game = Game.new_battle(n_opp, Game.make_rng(), own_rules())
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
_begin()
## Kartenoptik, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
func own_rules() -> Dictionary:
var p: Profile = app.profile
var rules := {"looks": p.looks()}
if stage < 0:
rules["opponent_life"] = int(p.difficulty().life)
if p.perk != "" and p.perk_unlocked():
rules["human_perks"] = [p.perk]
return rules
func _begin() -> void:
n_opp = game.players.size() - 1
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]
build_hero_boxes()
if is_online() and net.is_host:
net.broadcast()
game.events.clear()
b_save_leave.visible = resumable()
save_battle()
refresh()
func _build_static() -> void:
Style.panel(self, Rect2(10, 205, 320, 290))
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, 258)
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)
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
Style.panel(self, Rect2(950, 205, 320, 380))
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, "", 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)
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 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
field_label = Style.label(self, "", 15, Style.MUTED, Rect2(340, 211, 600, 24), "center")
b_fuse = Style.button(self, "Verschmelzen", Rect2(10, 503, 150, 40), toggle_fuse, Color("3c2858"))
b_fuse.visible = false
b_reroll = Style.button(self, "", Rect2(170, 503, 150, 40), do_reroll, Color("285a5a"))
b_reroll.add_theme_font_size_override("font_size", 14)
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()
hand_layer = _layer()
shop_layer = _layer()
preview_holder = _layer()
zoom_layer = _layer()
fx_layer = _layer()
cursor_layer = _layer()
# 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 = Style.overlay(self)
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
b_save_leave.add_theme_font_size_override("font_size", 14)
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
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)
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
func resumable() -> bool:
return counts_for_profile()
func save_battle() -> void:
if not resumable() or game == null or game.winner != "":
return
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "game": game.snapshot(-1)}
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(d))
f.close()
func save_and_leave() -> void:
save_battle()
app.show_screen(MenuScreen.new())
func leave() -> void:
if is_online():
net.leave()
if resumable() and game != null and game.winner == "":
app.clear_battle_save() # aufgegeben
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 - (58 if c else 20), 26), "left", true)
labels.name.clip_text = true
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
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:
if spectating():
return false
return game != null and game.winner == "" and game.current() == perspective
func any_overlay() -> bool:
return confirm.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 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 selected != null or pending != null or fusing 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 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])
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:
return
if is_client():
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.clone()
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 counts_for_profile():
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, n_opp, stage, factor)
app.save()
app.sfx.play("win" if won else "lose")
app.clear_battle_save()
else:
var wp := game.winner_player
var mine: bool = wp != null and wp == perspective
app.sfx.play("win" if mine else "lose")
refresh()
func say(m: String) -> void:
msg_t = 2.5
msg_label.text = m
func cancel() -> void:
selected = null
pending = null
fusing = false
fuse_first = 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 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 und keine Kombos)
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()
## Verschmelzen an- und abwählen (zwei eigene Wesen, siehe Game.fuse).
func toggle_fuse() -> void:
var on := not fusing
cancel()
fusing = on
say(tr("Zwei eigene Wesen wählen") if on else tr("Abgebrochen"))
after_action()
## Kauft ein Angebot aus dem Laden auf die Hand.
func do_buy(slot: int) -> void:
clear_undo()
if is_client():
net.send_action({"type": "buy", "slot": slot})
return
var err := game.buy(perspective, slot)
if err != "":
say(tr(err))
## Würfelt den Laden neu (kostet Mana).
func do_reroll() -> void:
if not my_turn() or any_overlay():
return
clear_undo()
if is_client():
net.send_action({"type": "reroll"})
after_action()
return
var err := game.reroll(perspective)
if err != "":
say(tr(err))
after_action()
func do_fuse(a, b) -> void:
cancel()
clear_undo()
if is_client():
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
return
var err := game.fuse(perspective, a, b)
if err != "":
say(tr(err))
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 on_click(kind: String, obj) -> void:
var g := game
if not my_turn() or any_overlay():
return
if fusing:
if kind != "own":
cancel()
say(tr("Abgebrochen"))
elif fuse_first == null:
fuse_first = obj
say(tr("Zweites Wesen wählen"))
elif obj == fuse_first:
fuse_first = null
else:
do_fuse(fuse_first, obj)
after_action()
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:
"shop":
do_buy(int(obj))
"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.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)
func show_preview(card, entered: bool, owner_looks: Dictionary = {}) -> void:
Style.clear(preview_holder)
if entered and card != null:
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
v.position = Vector2(1005, 215)
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview_holder.add_child(v)
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
show_preview(card, entered, perspective.looks)
# --- 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 := {}
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)
save_battle()
# --- 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 = []
shop_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 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)
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, 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)
var looks: Dictionary = p.looks
tile.hover.connect(func(card, entered): show_preview(card, entered, looks))
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 or cr == fuse_first:
b = Style.YELLOW
w = 4
elif fusing and p == h and mine:
b = Style.PURPLE
w = 3
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(shop_layer)
if not spectating():
for i in h.shop.size():
var offer = h.shop[i]
var sr := BattleLayout.shop_rect(i)
if offer == null:
Style.panel(shop_layer, sr, Style.darker(Style.PANEL, 0.8), 8, Style.PANEL_LIGHT, 2)
continue
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
sv.position = sr.position
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
sv.hover.connect(show_hand_preview)
var slot := i
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
sv.clicked.connect(func(_view): on_click("shop", slot))
shop_layer.add_child(sv)
shop_views.append(sv)
Style.clear(hand_layer)
if spectating():
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center")
else:
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, Cards.RARITIES[int(h.looks.get(card.id, 0))])
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" % h.energy
income_label.text = tr("+%d je Zug") % h.max_energy
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])
if spectating():
status_label.text = tr("Zuschauer") + " · " + status_label.text
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 and not spectating()
b_undo.visible = undo_state != null and mine
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
b_reroll.visible = mine and not spectating()
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST
b_reroll.disabled = h.energy < Game.REROLL_COST
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
if g.field_element == "":
var parts: Array[String] = []
for e in Cards.ELEMENTS:
parts.append("%s %d" % [tr(e), int(g.field_charge.get(e, 0))])
field_label.text = tr("Feld: neutral") + " · " + " ".join(parts)
field_label.add_theme_color_override("font_color", Style.MUTED)
else:
field_label.text = "%s: %s" % [tr(Cards.FIELD_NAMES[g.field_element]), tr(Cards.FIELD_TEXT[g.field_element])]
field_label.add_theme_color_override("font_color", Cards.ELEMENT_COLORS[g.field_element])
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("Das Feld wird"):
lines.append("[color=#8c78dc]%s[/color]" % line)
elif line.begins_with("Feldkombo"):
lines.append("[color=#f5c850]%s[/color]" % line)
else:
lines.append(line)
log_box.text = "\n".join(lines)
pad.draw_cursor()
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
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 ◆ %d (+%d)" % [o.name, o.title, o.life, o.hand.size(), o.energy, 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())