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 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 challenge := {} # wöchentliche Herausforderung oder leer var preset_opponents: Array = [] # aus der Gegnervorschau übernommene Gegner var skip_ai := false # Gegnerzüge im Schnelldurchlauf var smart_ai := true # Vorausschau-KI (Leicht: einfache KI) var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion var undo_log_len := 0 var undo_t := 0.0 # Ansichten (auch für Tests) var tiles := {} var hand_views: Array = [] var own_views: Array = [] var enemy_views: Array = [] var 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: 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 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(part: int = 1) -> BattleScreen: var b := BattleScreen.new(1, -1) b.tutorial = true b.tutorial_part = part return b static func against(opps: Array) -> BattleScreen: var b := BattleScreen.new(opps.size(), -1) b.preset_opponents = opps 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 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) 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: 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.human.hand) elif not challenge.is_empty(): var rules: Dictionary = challenge.rules.duplicate() rules["target_rating"] = Game.deck_rating(app.profile.deck_cards()) game = Game.new_battle(app.profile.deck_cards(), n_opp, null, rules) 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()) elif not preset_opponents.is_empty(): game = Game.new_battle_with(app.profile.deck_cards(), preset_opponents, null, 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[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() 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 = _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: 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 - (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.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 handover.visible: take_over() elif 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 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.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) if smart_ai else 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 _finish() -> void: var won := game.winner == "human" if counts_for_profile(): var factor: float = float(app.profile.difficulty().reward) if stage < 0 and challenge.is_empty() and not tutorial else 1.0 reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, 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") 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.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.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_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) 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) 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 handover.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())