extends Node3D ## Spielszene: 3D-Arena plus Bedienoberfläche. ## Alles wird im Code aus primitiven Meshes und Controls gebaut. const TOP_H := 46.0 const LEFT_W := 290.0 ## links: Elemente, Einheit, Rundenlog const RIGHT_W := 210.0 ## rechts: Spielerliste const BOTTOM_H := 138.0 const TEXT_W := 244.0 const BG := Color(0.09, 0.10, 0.13) const PANEL_BG := Color(0.14, 0.15, 0.19) const SLOT_BG := Color(0.20, 0.21, 0.26) const CAM_FOV := 44.0 const CAM_PITCH := 52.0 ## Grad über der Waagerechten var game := Game.new() var board: BoardView3D var camera: Camera3D var ui_layer: CanvasLayer var ui_root: Control var hud: HudOverlay var combat_speed := 1.0 # Kopfzeile var lbl_round: Label var lbl_gold: Label var lbl_hp: Label var lbl_slots: Label var lbl_streak: Label var lbl_phase: Label var btn_ready: Button var btn_speed: Button # Panels var players_box: VBoxContainer var traits_box: VBoxContainer var info_box: VBoxContainer var log_box: VBoxContainer var shop_row: HBoxContainer var lbl_fodder: Label var btn_reroll: Button var lbl_hint: Label # Auswahl und Ziehen var sel_slot := {} var drag_src := {} var drag_avatar: Avatar = null var drag_moved := false # Element-Auswahl var picker: PopupPanel var picker_grid: GridContainer var picker_title: Label var picker_sub: Label var picker_cb: Callable var overlay: Control func _ready() -> void: _build_world() ui_layer = CanvasLayer.new() add_child(ui_layer) hud = HudOverlay.new() hud.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) hud.board = board ui_layer.add_child(hud) ui_root = Control.new() ui_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) ui_root.mouse_filter = Control.MOUSE_FILTER_IGNORE ui_layer.add_child(ui_root) _build_top() _build_left() _build_right() _build_bottom() _build_picker() game.start() board.game = game _refresh() set_process(true) func _process(delta: float) -> void: if game.phase == Game.Phase.COMBAT: var before := game.phase game.update_combat(delta * combat_speed) _refresh_header() if game.phase != before: _refresh() func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: _on_mouse_move(event.position) return if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: _begin_drag(event.position) else: _end_drag(event.position) return if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: _on_right_click(event.position) return return if not (event is InputEventKey and event.pressed and not event.echo): return match event.keycode: KEY_D: _on_reroll() KEY_SPACE, KEY_F: _on_ready() KEY_ESCAPE: _cancel_drag() sel_slot = {} _refresh() func _on_mouse_move(pos: Vector2) -> void: var slot := _pick_slot(pos) board.hover_slot = slot if drag_avatar != null: board.drag_world = _drag_point(pos, slot) drag_moved = true ## Hebt einen Avatar an, wenn unter dem Zeiger einer liegt. func _begin_drag(pos: Vector2) -> void: if game.phase != Game.Phase.PLANNING: return var slot := _pick_slot(pos) var a: Avatar = _avatar_at(slot) if a == null: sel_slot = {} _refresh() return drag_src = slot drag_avatar = a drag_moved = false board.drag_active = true board.drag_avatar_id = a.id board.drag_world = _drag_point(pos, slot) # Während des Ziehens zeigt das Info-Panel den Avatar in der Hand. sel_slot = slot _refresh() func _end_drag(pos: Vector2) -> void: if drag_avatar == null: return var dst := _pick_slot(pos) var src := drag_src _cancel_drag() if dst.is_empty() or BoardView3D._same_slot(src, dst): # Kein Weg zurückgelegt: das war eine Auswahl. sel_slot = src _refresh() return if dst["kind"] == "board" and not board.is_own_half(BoardView3D.from_board(dst["hex"])): _refresh() return if not game.human.move_slot(src, dst, game.round_no): if dst["kind"] == "board": _hint("Kaderplätze voll (%d)." % game.human.team_slots(game.round_no)) else: sel_slot = dst _refresh() func _cancel_drag() -> void: drag_avatar = null drag_src = {} board.drag_active = false board.drag_avatar_id = -1 func _on_right_click(pos: Vector2) -> void: if game.phase != Game.Phase.PLANNING: return _cancel_drag() var slot := _pick_slot(pos) if slot.is_empty(): return if slot["kind"] == "bench": game.human.sell_bench(slot["index"], game.pool) else: game.human.unplace(slot["hex"]) sel_slot = {} _refresh() func _avatar_at(slot: Dictionary): if slot.is_empty(): return null if slot["kind"] == "bench": return game.human.bench[slot["index"]] return game.human.board.get(slot["hex"]) # --- Aufbau ---------------------------------------------------------------- func _add_panel(pos: Vector2, sz: Vector2) -> VBoxContainer: var p := UiKit.panel(PANEL_BG) p.position = pos p.size = sz ui_root.add_child(p) var v := VBoxContainer.new() v.add_theme_constant_override("separation", 4) p.add_child(v) return v func _build_top() -> void: var bar := ColorRect.new() bar.color = PANEL_BG bar.position = Vector2.ZERO bar.size = Vector2(1600, TOP_H) bar.mouse_filter = Control.MOUSE_FILTER_IGNORE ui_root.add_child(bar) var h := HBoxContainer.new() h.position = Vector2(14, 9) h.size = Vector2(1570, 30) h.add_theme_constant_override("separation", 22) ui_root.add_child(h) lbl_round = UiKit.label("Runde 1", 18) lbl_phase = UiKit.label("Planung", 16, Color(0.7, 0.85, 1.0)) lbl_hp = UiKit.label("Leben 100", 16, Color(0.5, 0.9, 0.55)) lbl_gold = UiKit.label("Gold 4", 16, Color(1.0, 0.85, 0.35)) lbl_slots = UiKit.label("Kader 0/3", 16) lbl_streak = UiKit.label("Serie 0", 16, Color(0.85, 0.7, 1.0)) for l in [lbl_round, lbl_phase, lbl_hp, lbl_gold, lbl_slots, lbl_streak]: h.add_child(l) var spacer := Control.new() spacer.custom_minimum_size = Vector2(360, 0) h.add_child(spacer) btn_speed = UiKit.button("Tempo 1x", Color(0.25, 0.28, 0.36)) btn_speed.custom_minimum_size = Vector2(96, 28) btn_speed.pressed.connect(_on_speed) h.add_child(btn_speed) btn_ready = UiKit.button("Kampf starten [Leer]", Color(0.24, 0.52, 0.34)) btn_ready.custom_minimum_size = Vector2(190, 28) btn_ready.pressed.connect(_on_ready) h.add_child(btn_ready) ## Linke Spalte: Elemente, gewählte Einheit und Rundenlog, scrollbar. func _build_left() -> void: var w := LEFT_W - 16.0 var h := 900.0 - TOP_H - BOTTOM_H - 20.0 var p := UiKit.panel(PANEL_BG) p.position = Vector2(8, TOP_H + 8) p.size = Vector2(w, h) p.clip_contents = true ui_root.add_child(p) var sc := ScrollContainer.new() sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED p.add_child(sc) var col := VBoxContainer.new() col.add_theme_constant_override("separation", 10) col.size_flags_horizontal = Control.SIZE_EXPAND_FILL sc.add_child(col) traits_box = VBoxContainer.new() info_box = VBoxContainer.new() log_box = VBoxContainer.new() for b in [traits_box, info_box, log_box]: b.add_theme_constant_override("separation", 3) b.size_flags_horizontal = Control.SIZE_EXPAND_FILL col.add_child(b) col.add_child(HSeparator.new()) func _build_world() -> void: var we := WorldEnvironment.new() var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = BG env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.44, 0.48, 0.62) env.ambient_light_energy = 0.9 we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-58.0, -35.0, 0.0) sun.light_energy = 1.15 sun.shadow_enabled = true add_child(sun) var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-22.0, 140.0, 0.0) fill.light_energy = 0.4 fill.light_color = Color(0.72, 0.80, 1.0) add_child(fill) board = BoardView3D.new() add_child(board) camera = Camera3D.new() camera.fov = CAM_FOV camera.look_at_from_position(Vector3(0.0, 16.0, 13.0), Vector3.ZERO, Vector3.UP) add_child(camera) board.camera = camera get_viewport().size_changed.connect(_fit_camera) _fit_camera() ## Legt die Kamera so, dass das Spielfeld genau in die freie Bildmitte passt. ## Links, rechts und unten liegen Bedienpanels darüber. func _fit_camera() -> void: var vp := Vector2(get_viewport().get_visible_rect().size) var target := Rect2(LEFT_W + 16.0, TOP_H + 16.0, vp.x - LEFT_W - RIGHT_W - 32.0, vp.y - TOP_H - BOTTOM_H - 32.0) if target.size.x <= 0.0 or target.size.y <= 0.0: return var pts := board.corner_points() var pitch := deg_to_rad(CAM_PITCH) var dir := Vector3(0.0, sin(pitch), cos(pitch)) var dist := 20.0 var center := Vector3.ZERO for i in 8: camera.look_at_from_position(center + dir * dist, center, Vector3.UP) var r := _project_rect(pts) if r.size.x <= 0.0 or r.size.y <= 0.0: return dist *= maxf(r.size.x / target.size.x, r.size.y / target.size.y) camera.look_at_from_position(center + dir * dist, center, Vector3.UP) r = _project_rect(pts) var err := r.get_center() - target.get_center() var o := camera.unproject_position(center) var kx := camera.unproject_position(center + Vector3(1.0, 0.0, 0.0)).x - o.x var kz := camera.unproject_position(center + Vector3(0.0, 0.0, 1.0)).y - o.y if absf(kx) < 0.001 or absf(kz) < 0.001: break center += Vector3(err.x / kx, 0.0, err.y / kz) if err.length() < 1.0 and absf(1.0 - maxf(r.size.x / target.size.x, r.size.y / target.size.y)) < 0.01: break camera.look_at_from_position(center + dir * dist, center, Vector3.UP) func _project_rect(points: Array) -> Rect2: var lo := Vector2(INF, INF) var hi := Vector2(-INF, -INF) for p in points: if camera.is_position_behind(p): return Rect2() var q := camera.unproject_position(p) lo = Vector2(minf(lo.x, q.x), minf(lo.y, q.y)) hi = Vector2(maxf(hi.x, q.x), maxf(hi.y, q.y)) return Rect2(lo, hi - lo) ## Punkt auf der Feldebene unter einer Bildschirmposition, oder null. func _ground_point(screen_pos: Vector2): if camera == null: return null var from := camera.project_ray_origin(screen_pos) var dir := camera.project_ray_normal(screen_pos) if absf(dir.y) < 0.0001: return null var t := (BoardView3D.TILE_H * 0.5 - from.y) / dir.y if t < 0.0: return null return board.to_local(from + dir * t) ## Position des gezogenen Avatars: auf Feldhöhe, damit er nicht schwebt. func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3: var hit = _ground_point(screen_pos) if hit == null: return board.slot_world(fallback) return Vector3(hit.x, 0.0, hit.z) ## Feld oder Bankplatz unter einer Bildschirmposition. func _pick_slot(screen_pos: Vector2) -> Dictionary: var hit = _ground_point(screen_pos) if hit == null: return {} return board.slot_at_world(hit) ## Rechte Spalte: die acht Spieler mit Leben und Serie. func _build_right() -> void: players_box = _add_panel(Vector2(1600.0 - RIGHT_W + 8.0, TOP_H + 8), Vector2(RIGHT_W - 16, 900 - TOP_H - BOTTOM_H - 20)) players_box.add_child(UiKit.label("Spieler", 16, Color(0.75, 0.8, 0.95))) func _build_bottom() -> void: var y := 900.0 - BOTTOM_H var p := UiKit.panel(PANEL_BG) p.position = Vector2(8, y) p.size = Vector2(1584, BOTTOM_H - 10) ui_root.add_child(p) var v := VBoxContainer.new() v.add_theme_constant_override("separation", 6) p.add_child(v) var head := HBoxContainer.new() head.add_theme_constant_override("separation", 16) v.add_child(head) head.add_child(UiKit.label("Shop", 13, Color(0.7, 0.72, 0.8))) btn_reroll = UiKit.button("Neu würfeln %d Gold [D]" % Data.REROLL_COST, Color(0.30, 0.32, 0.44)) btn_reroll.custom_minimum_size = Vector2(200, 26) btn_reroll.pressed.connect(_on_reroll) head.add_child(btn_reroll) lbl_fodder = UiKit.label("", 13, Color(0.7, 0.72, 0.8)) head.add_child(lbl_fodder) lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45)) head.add_child(lbl_hint) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL head.add_child(spacer) head.add_child(UiKit.label( "Avatare zwischen Bank und Feld ziehen · Rechtsklick: verkaufen bzw. zurück", 13, Color(0.6, 0.62, 0.7))) shop_row = HBoxContainer.new() shop_row.add_theme_constant_override("separation", 8) v.add_child(shop_row) func _build_picker() -> void: picker = PopupPanel.new() picker.add_theme_stylebox_override("panel", UiKit.flat(Color(0.16, 0.17, 0.22))) var v := VBoxContainer.new() v.add_theme_constant_override("separation", 8) picker.add_child(v) picker_title = UiKit.label("Element wählen", 16) v.add_child(picker_title) picker_sub = UiKit.label("", 13, Color(0.75, 0.8, 0.95)) v.add_child(picker_sub) picker_grid = GridContainer.new() picker_grid.columns = 3 picker_grid.add_theme_constant_override("h_separation", 6) picker_grid.add_theme_constant_override("v_separation", 6) v.add_child(picker_grid) ui_root.add_child(picker) ## Element-Auswahl. Mit `previews` steht neben jedem Knopf, was sich ändert. func _open_picker(title: String, options: Array, cb: Callable, previews: Array = [], subtitle: String = "") -> void: picker_title.text = title picker_sub.text = subtitle picker_sub.visible = subtitle != "" for c in picker_grid.get_children(): c.queue_free() picker_cb = cb var counts := game.human.trait_counts() var with_preview := not previews.is_empty() picker_grid.columns = 2 if with_preview else 3 for i in options.size(): var e: int = options[i] var col: Color = Data.ELEMENT_COLORS[e] var b := UiKit.button("%s\n(%d im Team)" % [Data.ELEMENT_NAMES[e], counts.get(e, 0)], col, UiKit.ink(col)) b.custom_minimum_size = Vector2(120, 52 if with_preview else 46) b.pressed.connect(func(): picker.hide() picker_cb.call(e)) picker_grid.add_child(b) if with_preview: var l := UiKit.label(previews[i], 12, Color(0.85, 0.88, 0.95)) l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER picker_grid.add_child(l) picker.reset_size() picker.popup_centered() # --- Aktionen -------------------------------------------------------------- func _on_ready() -> void: if game.phase == Game.Phase.PLANNING: _cancel_drag() sel_slot = {} game.start_combat() _refresh() elif game.phase == Game.Phase.COMBAT: game.skip_combat() _refresh() func _on_speed() -> void: combat_speed = 1.0 if combat_speed >= 4.0 else combat_speed * 2.0 btn_speed.text = "Tempo %dx" % int(combat_speed) func _on_reroll() -> void: if game.phase != Game.Phase.PLANNING: return game.human.reroll(game.pool, game.rand) _refresh() func _on_buy(index: int) -> void: if game.phase != Game.Phase.PLANNING: return var p := game.human if index < 0 or index >= p.shop.size(): return var entry: ShopEntry = p.shop[index] if entry.sold: return if p.free_bench_slot() < 0: _hint("Bank ist voll. Erst etwas verkaufen.") return if p.gold < entry.price(): return if entry.element >= 0: if not p.buy(index, game.pool, entry.element): _hint("Kauf nicht möglich (Vorrat leer).") _refresh() return # Neutraler Avatar: Spezialisierung schon beim Kauf wählen. var free: Array[int] = [] for e in Data.ELEMENT_COUNT: if game.pool.element_left[e] > 0: free.append(e) if free.is_empty(): _hint("Kein Element mehr im Vorrat.") return _open_picker("Spezialisierung wählen", free, func(e): if not game.human.buy(index, game.pool, e): _hint("Kauf nicht möglich (Vorrat leer).") _refresh()) func _on_upgrade() -> void: var p := game.human var hex := selected_board_hex() if hex.x < 0 or not p.can_upgrade(hex): return var a: Avatar = p.board[hex] var options := p.available_new_elements(hex) var previews := [] for e in options: previews.append("+ " + _element_effect(e) + " (halbe Stärke)") _open_picker("Zweite/dritte Spezialisierung", options, func(e): game.human.upgrade(hex, e, game.pool) _refresh(), previews, "Fähigkeit wird zu: " + _star_effect(a.role, a.star + 1)) func _star_effect(role: int, star: int) -> String: match role: Data.Role.TANK: if star == 2: return "Schild und spottet 3 Gegner" return "Schild, Spott und 25% Reflexion" Data.Role.DPS: if star == 2: return "Ziel plus Splash auf Nachbarn" return "Ziel plus Fläche im Radius 2" _: if star == 2: return "heilt 3 Verbündete" return "heilt alle und schildet sie" func _element_effect(element: int) -> String: match element: Data.Element.FEUER: return "Brand" Data.Element.WASSER: return "Mana und stärkere Heilung" Data.Element.WIND: return "Tempo-Schub" Data.Element.BLITZ: return "Kettenblitz" Data.Element.ERDE: return "Betäubung" _: return "Verlangsamung" ## Gewähltes Board-Feld, oder (-1,-1) wenn nichts oder ein Bankplatz gewählt ist. func selected_board_hex() -> Vector2i: if sel_slot.is_empty() or sel_slot["kind"] != "board": return Vector2i(-1, -1) return sel_slot["hex"] func _hint(text: String) -> void: lbl_hint.text = text get_tree().create_timer(2.5).timeout.connect(func(): if is_instance_valid(lbl_hint) and lbl_hint.text == text: lbl_hint.text = "") # --- Anzeige --------------------------------------------------------------- func _refresh() -> void: _refresh_header() _refresh_players() _refresh_traits() _refresh_info() _refresh_log() _refresh_fodder() _refresh_shop() board.selected_slot = sel_slot if game.phase == Game.Phase.GAMEOVER: _show_gameover() func _refresh_header() -> void: var p := game.human lbl_round.text = "Runde %d" % game.round_no lbl_hp.text = "Leben %d" % p.hp var interest := Data.interest(p.gold) lbl_gold.text = "Gold %d (+%d Zins)" % [p.gold, interest] if interest > 0 else "Gold %d" % p.gold lbl_slots.text = "Kader %d/%d" % [p.board.size(), p.team_slots(game.round_no)] var s := p.streak lbl_streak.text = "Serie %s%d (+%d Gold)" % ["+" if s > 0 else "", s, Data.streak_gold(s)] match game.phase: Game.Phase.PLANNING: lbl_phase.text = "Planung" btn_ready.text = "Kampf starten [Leer]" btn_ready.disabled = false Game.Phase.COMBAT: var opp := game.human_opponent() lbl_phase.text = "Kampf gegen %s" % (opp.pname if opp else "?") btn_ready.text = "Kampf überspringen" btn_ready.disabled = false _: lbl_phase.text = "Spiel vorbei" btn_ready.disabled = true ## Label für das rechte Panel: bricht um und bleibt in der Panelbreite. func _wrapped(text: String, size: int, col: Color) -> Label: var l := UiKit.label(text, size, col) l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART l.custom_minimum_size = Vector2(TEXT_W, 0) l.size_flags_horizontal = Control.SIZE_FILL return l func _clear(box: Node, keep: int = 0) -> void: var kids := box.get_children() for i in range(keep, kids.size()): box.remove_child(kids[i]) kids[i].queue_free() func _refresh_players() -> void: _clear(players_box, 1) for p in game.ranking(): var row := VBoxContainer.new() row.add_theme_constant_override("separation", 1) var top := HBoxContainer.new() var col := Color(0.9, 0.9, 0.95) if p.alive else Color(0.5, 0.5, 0.55) if p == game.human: col = Color(0.6, 0.9, 1.0) var nm := UiKit.label(p.pname, 14, col) nm.custom_minimum_size = Vector2(88, 0) top.add_child(nm) top.add_child(UiKit.label("%d" % p.hp, 14, col)) if p.streak != 0: top.add_child(UiKit.label(" Serie %s%d" % ["+" if p.streak > 0 else "", p.streak], 12, Color(0.85, 0.7, 1.0))) row.add_child(top) var bar := ColorRect.new() bar.color = Color(0.12, 0.13, 0.16) bar.custom_minimum_size = Vector2(RIGHT_W - 32, 6) var fill := ColorRect.new() fill.color = Color(0.35, 0.8, 0.45) if p.alive else Color(0.4, 0.4, 0.45) fill.size = Vector2((RIGHT_W - 32) * (float(p.hp) / float(Data.START_HP)), 6) fill.position = Vector2.ZERO bar.add_child(fill) row.add_child(bar) players_box.add_child(row) var opp := game.human_opponent() if opp != null and game.phase == Game.Phase.COMBAT: players_box.add_child(UiKit.label("\nGegner: " + opp.pname, 13, Color(1.0, 0.6, 0.6))) func _refresh_traits() -> void: _clear(traits_box) traits_box.add_child(UiKit.label("Elemente im Team", 15, Color(0.75, 0.8, 0.95))) var counts := game.human.trait_counts() var any := false for e in Data.ELEMENT_COUNT: var c: int = counts.get(e, 0) if c == 0: continue any = true var tier := Data.trait_tier(e, c) var col: Color = Data.ELEMENT_COLORS[e] var row := HBoxContainer.new() var dot := ColorRect.new() dot.color = col if tier > 0 else col.darkened(0.55) dot.custom_minimum_size = Vector2(12, 12) row.add_child(dot) var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c] if tier > 0: txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1] else: txt += " (ab 2 aktiv)" var el := UiKit.label(txt, 13, Color(0.9, 0.9, 0.95) if tier > 0 else Color(0.55, 0.55, 0.6)) el.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART el.custom_minimum_size = Vector2(TEXT_W - 20.0, 0) row.add_child(el) traits_box.add_child(row) if not any: traits_box.add_child(_wrapped("Noch nichts aufgestellt.", 13, Color(0.55, 0.55, 0.6))) traits_box.add_child(UiKit.label("\nKombinationen", 15, Color(0.75, 0.8, 0.95))) var active := game.human.active_combos() var keys := [] for c in active: keys.append(c["key"]) for c in Data.COMBO_TRAITS: var on: bool = keys.has(c["key"]) var t := "%s (%s+%s): %s" % [c["name"], Data.ELEMENT_NAMES[c["a"]], Data.ELEMENT_NAMES[c["b"]], c["desc"]] traits_box.add_child(_wrapped(t, 12, Color(0.6, 0.95, 0.7) if on else Color(0.45, 0.45, 0.5))) func _refresh_info() -> void: _clear(info_box) info_box.add_child(UiKit.label("Einheit", 15, Color(0.75, 0.8, 0.95))) var p := game.human var a: Avatar = _selected_avatar() if a == null: info_box.add_child(_wrapped("Einheit auf dem Feld oder der Bank anklicken.", 13, Color(0.55, 0.55, 0.6))) return var s := a.base_stats() info_box.add_child(_wrapped("%s %d-Star" % [a.display_name(), a.star], 14, a.color())) info_box.add_child(_wrapped("Leben %d Angriff %d Rüstung %d%%\nTempo %.2f Reichweite %d" % [int(s["hp"]), int(s["atk"]), int(s["armor"] * 100.0), s["aspd"], s["range"]], 12, Color(0.9, 0.9, 0.95))) info_box.add_child(_wrapped(a.ability_text(), 12, Color(0.8, 0.85, 1.0))) var hex := selected_board_hex() if hex.x < 0: info_box.add_child(_wrapped("Zum Aufwerten aufs Feld ziehen.", 12, Color(0.85, 0.85, 0.9))) return var cost := a.upgrade_cost() if cost.is_empty(): info_box.add_child(_wrapped("Maximale Stufe.", 12, Color(1.0, 0.85, 0.35))) return var missing := p.upgrade_missing(hex) var need := [] for e in cost: need.append("%dx %s" % [cost[e], Data.ELEMENT_NAMES[e]]) info_box.add_child(_wrapped("Aufwerten braucht: " + ", ".join(need), 12, Color(0.85, 0.85, 0.9))) if missing.is_empty(): var b := UiKit.button("Aufwerten", Color(0.28, 0.55, 0.36)) b.custom_minimum_size = Vector2(140, 26) b.pressed.connect(_on_upgrade) info_box.add_child(b) else: var miss := [] for e in missing: miss.append("%dx %s" % [missing[e], Data.ELEMENT_NAMES[e]]) info_box.add_child(_wrapped("Es fehlt: " + ", ".join(miss), 12, Color(0.95, 0.6, 0.5))) func _selected_avatar(): return _avatar_at(sel_slot) func _refresh_log() -> void: _clear(log_box) log_box.add_child(UiKit.label("Letzte Runde", 15, Color(0.75, 0.8, 0.95))) if game.last_round_log.is_empty(): log_box.add_child(UiKit.label("-", 12, Color(0.55, 0.55, 0.6))) for line in game.last_round_log: log_box.add_child(_wrapped(line, 12, Color(0.8, 0.8, 0.86))) func _refresh_fodder() -> void: var fodder := game.human.fodder_counts() var parts := [] for e in fodder: parts.append("%dx %s" % [fodder[e], Data.ELEMENT_NAMES[e]]) lbl_fodder.text = "Futter auf der Bank: " + (", ".join(parts) if parts else "-") func _refresh_shop() -> void: _clear(shop_row) var p := game.human for i in p.shop.size(): var e: ShopEntry = p.shop[i] var col := e.color() var txt := "verkauft" var b: Button if e.sold: b = UiKit.button(txt, SLOT_BG.darkened(0.3)) b.disabled = true else: var note: String = "Element wählen" if e.element < 0 \ else "noch %d im Vorrat" % game.pool.element_left[e.element] b = UiKit.button("%s\n%d Gold\n%s" % [e.label(), e.price(), note], col, UiKit.ink(col)) b.disabled = p.gold < e.price() or game.phase != Game.Phase.PLANNING b.pressed.connect(_on_buy.bind(i)) b.custom_minimum_size = Vector2(196, 74) shop_row.add_child(b) var mine := [] for el in game.human.shop_elements(game.pool): mine.append("%s %d" % [Data.ELEMENT_NAMES[el], game.pool.element_left[el]]) shop_row.add_child(UiKit.label(" Dein Shop zeigt Neutral\n und deine Elemente:\n %s" % [", ".join(mine) if mine else "noch keine aufgestellt"], 12, Color(0.7, 0.72, 0.8))) func _show_gameover() -> void: if overlay != null: return overlay = ColorRect.new() overlay.color = Color(0, 0, 0, 0.72) overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) ui_root.add_child(overlay) var v := VBoxContainer.new() v.position = Vector2(620, 320) v.add_theme_constant_override("separation", 10) overlay.add_child(v) var alive := game.alive_players() var win: String = alive[0].pname if not alive.is_empty() else "-" v.add_child(UiKit.label("Spiel vorbei", 32)) v.add_child(UiKit.label("Sieger: %s" % win, 20, Color(1.0, 0.85, 0.35))) var place := game.human.placement if game.human.placement > 0 else 1 v.add_child(UiKit.label("Dein Platz: %d von 8" % place, 18, Color(0.6, 0.95, 0.7) if place <= 4 else Color(0.95, 0.6, 0.6))) var b := UiKit.button("Neues Spiel", Color(0.28, 0.5, 0.36)) b.custom_minimum_size = Vector2(180, 36) b.pressed.connect(func(): overlay.queue_free() overlay = null game = Game.new() game.start() board.game = game _cancel_drag() sel_slot = {} _refresh()) v.add_child(b)