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