class_name BattleScreen extends Control ## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx. ## Modi: Einzelspieler, Kampagne, Anleitung, Netzwerk (Host, Mitspieler oder Zuschauer). var app var n_opp: int var stage := -1 # Kampagnenstufe oder -1 var tutorial := false 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 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 played_ids: Array = [] # vom Menschen gespielte Karten (Statistik) var guide: BattleTutorial var tutorial_part := 1 var tutorial_step: int: get: return guide.step if guide != null else 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 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 b_save_leave: Button var mulligan: BattleMulligan var mulligan_box: Control: get: return mulligan var mulligan_layer: Control: get: return mulligan.layer var b_keep: Button: get: return mulligan.b_keep var b_swap: Button: get: return mulligan.b_swap var mulligan_selected: Array: get: return mulligan.selected var cursor_layer: Control 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) 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 static func guided(part: int = 1) -> BattleScreen: var b := BattleScreen.new(1, -1) b.tutorial = true b.tutorial_part = part 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.played_ids = Array(data.get("played_ids", [])) 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 tutorial: guide = BattleTutorial.new(self, tutorial_part) var r := Game.make_rng() var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards())) opps[0].perks = [] game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true) guide.inject_cards(game) elif stage >= 0: var r := Game.make_rng() game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules()) else: game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules()) smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile() _begin() ## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar) und Zielstärke der Gegner. func own_rules() -> Dictionary: var p: Profile = app.profile var rules := {"target_rating": p.target_rating(), "tier": str(p.difficulty().tier)} 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, 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() cursor_layer = _layer() # Mulligan mulligan = BattleMulligan.new() mulligan.decided.connect(do_mulligan) add_child(mulligan) # 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.label(confirm, tr("Aufgeben beendet den Kampf ohne Münzen.") + "\n" + tr("Speichern hebt ihn für später auf."), 15, Style.MUTED, Rect2(340, 356, 600, 48), "center") 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() and not tutorial func save_battle() -> void: if not resumable() or game == null or game.winner != "": return var d := {"version": 1, "n_opp": n_opp, "stage": stage, "played_ids": played_ids, "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 if tutorial: if tutorial_part == 1: app.profile.tutorial_done = true else: app.profile.tutorial2_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 - (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 mulligan.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 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 mulligan.visible: do_mulligan([]) 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 not spectating() and cur.turns == 1 and not cur.mulligan_done and not mulligan.visible: show_mulligan() 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_for(cur) 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 and not tutorial else 1.0 reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life) if tutorial: if tutorial_part == 1: app.profile.tutorial_done = true else: app.profile.tutorial2_done = true 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 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.open(perspective.hand) func do_mulligan(idxs: Array) -> void: mulligan.visible = false 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.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) -> 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.card.title()) 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.card.title() 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[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() 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) save_battle() 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_next() -> void: if guide != null and guide.next(): app.sfx.play("click") refresh() func _tutorial_refresh() -> void: guide.advance() tutorial_text.text = guide.text() b_tut_next.visible = not guide.finished() func _tutorial_highlights(mine: bool) -> void: Style.clear(highlight_layer) for r in guide.highlight_rects(mine): 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) # --- 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) 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) 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]) 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 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) 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 mulligan.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())