extends Node2D # ─── Werkstatt UI ───────────────────────────────────────────────────────────── # Zwei Phasen: # PHASE_ATTR — 3 kostenlose Attribut-Upgrades (Pflicht, eins auswählen) # PHASE_SHOP — Werkstatt: beliebig viele Items kaufen, SPACE beendet # # Items kommen aus ItemDB.roll_werkstatt() (Plugin-System unter res://items/). # Gekaufte Items werden via stats.apply_item_def(def) angewendet und erscheinen # visuell am Schiff-Preview im Zentrum. # Emits: shop_closed(remaining_credits, updated_stats, owned_item_ids) signal shop_closed(remaining_credits: int, new_stats: ShipStats, items: Array, reroll_count: int) # ─── Konstanten ─────────────────────────────────────────────────────────────── const REROLL_BASE_COST := 60 const REROLL_STEP_COST := 42 const SHOP_CARD_COUNT := 4 const ATTR_CARD_COUNT := 3 # Cockpit palette const COL_BG := Color(0.0, 0.0, 0.04, 0.92) const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0) const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0) const COL_DIM := Color(0.0, 0.27, 0.13, 0.55) const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90) const COL_WARN := Color(1.0, 0.67, 0.0, 0.90) const COL_POS := Color(0.3, 1.0, 0.4, 1.0) const COL_NEG := Color(1.0, 0.35, 0.35, 1.0) # ─── Attribut-Pool (Phase 1 — gratis, reine Buffs) ──────────────────────────── # Jedes Attribut ist ein Dictionary wie ein ItemDef-Effect. const ATTR_POOL: Array = [ { "id": "a_speed", "name": "Schubverstärker", "name_en": "Thruster", "desc": "+8% Geschwindigkeit", "desc_en": "+8% Speed", "effects": { "speed_mult": 1.08 } }, { "id": "a_turn", "name": "Agilität", "name_en": "Agility", "desc": "+12% Wendigkeit", "desc_en": "+12% Handling", "effects": { "turn_mult": 1.12 } }, { "id": "a_fire", "name": "Schnellfeuer", "name_en": "Rapid Fire", "desc": "+12% Feuerrate", "desc_en": "+12% Fire Rate", "effects": { "fire_rate_mult": 1.12 } }, { "id": "a_damage", "name": "Schwere Ladung", "name_en": "Heavy Load", "desc": "+12% Schaden", "desc_en": "+12% Damage", "effects": { "damage_mult": 1.12 } }, { "id": "a_proj", "name": "Lineare Ballistik","name_en": "Linear Ballistics", "desc": "+12% Projektilgeschw.", "desc_en": "+12% Projectile Speed", "effects": { "bullet_speed_mult": 1.12 } }, { "id": "a_shield", "name": "Panzerplatte", "name_en": "Armor Plate", "desc": "+1 Schutzladung", "desc_en": "+1 Shield Charge", "effects": { "shield_charges": 1 } }, { "id": "a_invuln", "name": "Schildgenerator", "name_en": "Shield Generator", "desc": "+18% Unverwundbarkeit", "desc_en": "+18% Invulnerability", "effects": { "invuln_mult": 1.18 } }, { "id": "a_credits", "name": "Profitoptimierer", "name_en": "Profit Optimizer", "desc": "+8% Kreditgewinn", "desc_en": "+8% Credit Gain", "effects": { "credit_bonus": 1.08 } }, ] # ─── Phasen-State ───────────────────────────────────────────────────────────── enum Phase { ATTR, SHOP } var _phase: int = Phase.ATTR # ─── Session-State ──────────────────────────────────────────────────────────── var _lives: int = 3 var _credits: int = 0 var _stats: ShipStats = null var _owned_ids: Array = [] var _player_label: String = "" var _ship_palette: Dictionary = {} var _wave: int = 1 var _player_idx: int = 1 # 1 = P1 (Pfeiltasten/Enter/Space), 2 = P2 (ADWEF/Q) # Phase 1 var _attr_choices: Array = [] # 3 zufällige Attribute var _attr_cursor: int = 0 # Phase 2 var _shop_cards: Array = [] # aktuelle 4 ItemDef-Objekte var _shop_cursor: int = 0 var _reroll_count: int = 0 # UI var _blink: float = 0.0 var W: float = 960.0 var H: float = 600.0 # Touch state (direct InputEventScreenTouch handling) var _tc_id: int = -1 var _tc_start: Vector2 = Vector2.ZERO # ────────────────────────────────────────────────────────────────────────────── func open(lives: int, credits: int, stats: ShipStats, owned: Array, player_label: String = "", ship_palette: Dictionary = {}, wave: int = 1, reroll_count: int = 0, player_idx: int = 1) -> void: _lives = lives _credits = credits _stats = stats if stats != null else ShipStats.new() _owned_ids = owned.duplicate() _player_label = player_label _ship_palette = ship_palette _wave = wave # Attribut-Phase nur in ungeraden Wellen (1, 3, 5, ...). Gerade Wellen # starten direkt im Shop — langsamere Power-Kurve. _phase = Phase.ATTR if (wave % 2) == 1 else Phase.SHOP _attr_cursor = 0 _shop_cursor = 0 # Reroll-Counter persistiert über Wellen hinweg (von main.gd durchgereicht). _reroll_count = reroll_count _player_idx = player_idx _blink = 0.0 if _phase == Phase.ATTR: _roll_attr() _roll_shop() visible = true func _roll_attr() -> void: var pool := ATTR_POOL.duplicate() pool.shuffle() _attr_choices = pool.slice(0, ATTR_CARD_COUNT) func _roll_shop() -> void: # Exclude already-owned item IDs so you don't see the same thing twice in one roll _shop_cards = ItemDB.roll_werkstatt(SHOP_CARD_COUNT, _owned_ids) func _process(delta: float) -> void: if not visible: return _blink += delta queue_redraw() # ─── Input ──────────────────────────────────────────────────────────────────── func _input(event: InputEvent) -> void: if not visible: return if not event is InputEventScreenTouch: return if event.pressed: if _tc_id == -1: _tc_id = event.index _tc_start = event.position elif event.index == _tc_id: _tc_id = -1 var d: Vector2 = event.position - _tc_start if d.length() < 32.0: _touch_tap(event.position) elif abs(d.x) >= abs(d.y): _touch_nav("ui_left" if d.x < 0 else "ui_right") else: _touch_nav("ui_up" if d.y < 0 else "ui_down") get_viewport().set_input_as_handled() func _touch_tap(p: Vector2) -> void: if _phase == Phase.ATTR: var card_w := 230.0; var card_h := 140.0 var n := _attr_choices.size() var total_w: float = card_w * n + 20.0 * (n - 1) var start_x: float = (W - total_w) * 0.5 var cy: float = H * 0.5 - 30.0 - card_h * 0.5 + 40.0 for i in n: var cx: float = start_x + i * (card_w + 20.0) if Rect2(cx, cy, card_w, card_h).has_point(p): _attr_cursor = i _confirm_attr() return else: var card_w := 250.0; var card_h := 140.0 var card_pos: Array = [ Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0), Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0), Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0), Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0), ] for i in min(_shop_cards.size(), card_pos.size()): if Rect2(card_pos[i].x, card_pos[i].y, card_w, card_h).has_point(p): _shop_cursor = i _try_buy(i) return if p.y >= H - 52.0: if p.x < W * 0.5: _try_reroll() else: _close() func _touch_nav(action: String) -> void: if _phase == Phase.ATTR: match action: "ui_left": _attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size() "ui_right": _attr_cursor = (_attr_cursor + 1) % _attr_choices.size() else: match action: "ui_left": _move_shop_cursor(-1) "ui_right": _move_shop_cursor(1) "ui_up": _move_shop_cursor(-2) "ui_down": _move_shop_cursor(2) func _unhandled_input(event: InputEvent) -> void: if not visible: return if _phase == Phase.ATTR: _handle_attr_input(event) else: _handle_shop_input(event) func _handle_attr_input(event: InputEvent) -> void: if _player_idx == 2: if event.is_action_pressed("p2_left"): _attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size() elif event.is_action_pressed("p2_right"): _attr_cursor = (_attr_cursor + 1) % _attr_choices.size() elif event.is_action_pressed("p2_shoot"): _confirm_attr() else: if event.is_action_pressed("ui_left"): _attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size() elif event.is_action_pressed("ui_right"): _attr_cursor = (_attr_cursor + 1) % _attr_choices.size() elif event is InputEventKey and event.pressed and event.keycode == KEY_SPACE: _confirm_attr() elif event.is_action_pressed("ui_accept"): _confirm_attr() func _handle_shop_input(event: InputEvent) -> void: if _player_idx == 2: if event.is_action_pressed("p2_wipe") or event.is_action_pressed("ui_cancel"): _close() elif event.is_action_pressed("p2_left"): _move_shop_cursor(-1) elif event.is_action_pressed("p2_right"): _move_shop_cursor(1) elif event.is_action_pressed("p2_thrust"): _move_shop_cursor(-2) elif event.is_action_pressed("p2_boost"): _try_reroll() elif event.is_action_pressed("p2_shoot"): _try_buy(_shop_cursor) else: if event is InputEventKey and event.pressed: if event.keycode == KEY_SPACE or event.keycode == KEY_ESCAPE: _close() return elif event.keycode == KEY_ENTER: _try_buy(_shop_cursor) return elif event.keycode == KEY_R: _try_reroll() return if event.is_action_pressed("ui_cancel"): _close() elif event.is_action_pressed("ui_left"): _move_shop_cursor(-1) elif event.is_action_pressed("ui_right"): _move_shop_cursor(1) elif event.is_action_pressed("ui_up"): _move_shop_cursor(-2) elif event.is_action_pressed("ui_down"): _move_shop_cursor(2) elif event.is_action_pressed("ui_accept"): _try_buy(_shop_cursor) func _move_shop_cursor(delta: int) -> void: if _shop_cards.is_empty(): return _shop_cursor = (_shop_cursor + delta + _shop_cards.size()) % _shop_cards.size() func _confirm_attr() -> void: if _attr_cursor >= _attr_choices.size(): return var choice: Dictionary = _attr_choices[_attr_cursor] _stats.apply_item(choice["effects"]) _phase = Phase.SHOP func _try_buy(idx: int) -> void: if idx < 0 or idx >= _shop_cards.size(): return var def: ItemDef = _shop_cards[idx] if _credits < def.cost: return _credits -= def.cost _stats.apply_item_def(def) _owned_ids.append(def.id) _shop_cards.remove_at(idx) if _shop_cards.is_empty(): _shop_cursor = 0 else: _shop_cursor = min(_shop_cursor, _shop_cards.size() - 1) func _try_reroll() -> void: var cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST if _credits < cost: return _credits -= cost _reroll_count += 1 _roll_shop() _shop_cursor = 0 func _close() -> void: visible = false shop_closed.emit(_credits, _stats, _owned_ids, _reroll_count) # ═══════════════════════════════════════════════════════════════════════════════ # Drawing # ═══════════════════════════════════════════════════════════════════════════════ func _draw() -> void: var vs: Vector2 = get_viewport_rect().size W = vs.x; H = vs.y draw_rect(get_viewport_rect(), COL_BG) _draw_header() _draw_brackets(8, 8, W - 8, H - 8, COL_DIM, 16) if _phase == Phase.ATTR: _draw_attr_phase() else: _draw_shop_phase() _draw_footer() # ── Header ──────────────────────────────────────────────────────────────────── func _draw_header() -> void: draw_rect(Rect2(0, 0, W, 50.0), Color(0.0, 0.04, 0.02, 0.95)) draw_line(Vector2(0, 50), Vector2(W, 50), COL_DIM, 1.0) var phase_key: String = "werk_phase_attr" if _phase == Phase.ATTR else "werk_phase_shop" var wave_str := Tr.t("werk_wave") % _wave if _player_label != "": _draw_text_c(_player_label, W * 0.5, 10.0, 9, COL_DIM) _draw_text_c(Tr.t(phase_key) + wave_str, W * 0.5, 26.0, 13, COL_PRIMARY) # Lives + credits oben rechts var lives_str := "" for li in 3: lives_str += ("●" if li < _lives else "○") _draw_text(lives_str + " CR: %d" % _credits, W - 190.0, 14.0, 11, COL_PRIMARY) # ── Attribut-Phase ──────────────────────────────────────────────────────────── func _draw_attr_phase() -> void: var y_mid := H * 0.5 - 30.0 _draw_text_c(Tr.t("werk_attr_prompt"), W * 0.5, 76.0, 11, COL_ACCENT) var card_w := 230.0; var card_h := 140.0 var n: int = _attr_choices.size() var total_w: float = card_w * n + 20.0 * (n - 1) var start_x: float = (W - total_w) * 0.5 for i in n: var choice: Dictionary = _attr_choices[i] var cx: float = start_x + i * (card_w + 20.0) var cy: float = y_mid - card_h * 0.5 + 40.0 var is_sel: bool = i == _attr_cursor var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0) draw_rect(Rect2(cx, cy, card_w, card_h), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.10)) var bc: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse) if is_sel else COL_DIM _draw_brackets(cx, cy, cx + card_w, cy + card_h, bc, 14) _draw_text_c(Tr.t("werk_attr_tag"), cx + card_w * 0.5, cy + 10.0, 8, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6)) var _en := Settings.language == "en" _draw_text_c(choice["name_en"] if _en else choice["name"], cx + card_w * 0.5, cy + 32.0, 13, COL_WHITE) _draw_text_c(choice["desc_en"] if _en else choice["desc"], cx + card_w * 0.5, cy + 62.0, 10, COL_POS) if is_sel: var hint_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse) var pick_key: String = "werk_pick_p2" if _player_idx == 2 else "werk_pick" _draw_text_c(Tr.hint(pick_key), cx + card_w * 0.5, cy + card_h - 24.0, 10, hint_col) # ── Werkstatt-Phase ─────────────────────────────────────────────────────────── func _draw_shop_phase() -> void: # Schiff-Preview in der Mitte (leicht nach oben verschoben) _draw_ship_preview(W * 0.5, H * 0.5 - 15.0) # 2×2 Karten um das Schiff herum — nach oben geschoben, damit Vorschau-Panel Platz hat var card_w := 250.0; var card_h := 140.0 var positions: Array = [ Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0), # Top-left y=110 Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0), # Top-right y=110 Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0), # Bottom-left y=290 Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0), # Bottom-right y=290 ] for i in _shop_cards.size(): var def: ItemDef = _shop_cards[i] var p: Vector2 = positions[i] if i < positions.size() else Vector2(20, 80 + i * 40) _draw_shop_card(def, p.x, p.y, card_w, card_h, i == _shop_cursor) # Vorschau-Panel — unterhalb der unteren Kartenreihe (y=430), mit 8px Abstand if not _shop_cards.is_empty() and _shop_cursor < _shop_cards.size(): _draw_preview_panel(_shop_cards[_shop_cursor], W * 0.5, H - 119.0) # Owned items Banner (oben unter dem Header) if _owned_ids.size() > 0: _draw_owned_banner(W * 0.5, 60.0) func _draw_shop_card(def: ItemDef, x: float, y: float, w: float, h: float, is_sel: bool) -> void: var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0) var can_afford: bool = _credits >= def.cost var rarity_col: Color = ItemDB.RARITY_COLORS.get(def.rarity, COL_PRIMARY) var bg_a: float = 0.18 if can_afford else 0.08 draw_rect(Rect2(x, y, w, h), Color(rarity_col.r, rarity_col.g, rarity_col.b, bg_a)) var border_col: Color if is_sel: border_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse) else: border_col = Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.35 if can_afford else 0.15) _draw_brackets(x, y, x + w, y + h, border_col, 12) # Icon + Kategorie var _item_en := Settings.language == "en" var disp_cat := def.category_en if _item_en and def.category_en != "" else def.category var disp_name := def.name_en if _item_en and def.name_en != "" else def.name _draw_text(def.icon + " " + disp_cat, x + 12.0, y + 8.0, 8, Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.7 if can_afford else 0.3)) # Name var name_a: float = 0.9 if can_afford else 0.35 _draw_text(disp_name, x + 12.0, y + 26.0, 13, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, name_a)) # Effekte (bis zu 4 Zeilen, + grün / − rot) var line_y: float = y + 58.0 var count := 0 for key: String in def.effects: if count >= 4: break var val = def.effects[key] var is_pos: bool = ItemDB.is_positive_effect(key, val) var sign_str: String = "+ " if is_pos else "− " var col: Color = COL_POS if is_pos else COL_NEG if not can_afford: col = Color(col.r, col.g, col.b, 0.35) var line: String = "%s%s %s" % [sign_str, ItemDB.stat_display_name(key), _fmt_val(key, val)] _draw_text(line, x + 16.0, line_y, 9, col) line_y += 13.0 count += 1 # Cost unten rechts var cost_col: Color = COL_WARN if not can_afford else Color(1.0, 1.0, 0.6, 0.95) _draw_text("%d CR" % def.cost, x + w - 60.0, y + h - 20.0, 12, cost_col) # Hint auf aktiver Karte if is_sel: var hint_col: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse) var hint: String = Tr.hint("werk_buy_hint_p2" if _player_idx == 2 else "werk_buy_hint_p1") if can_afford else Tr.t("shop_no_credits") _draw_text(hint, x + 12.0, y + h - 20.0, 9, hint_col) # ── Preview-Panel: Vorher/Nachher-Diff ──────────────────────────────────────── func _draw_preview_panel(def: ItemDef, cx: float, cy: float) -> void: var pw := 420.0; var ph := 86.0 var px := cx - pw * 0.5; var py := cy - ph * 0.5 draw_rect(Rect2(px, py, pw, ph), Color(0.0, 0.04, 0.02, 0.9)) _draw_brackets(px, py, px + pw, py + ph, COL_DIM, 10) _draw_text_c(Tr.t("werk_preview") + " · " + def.name, cx, py + 6.0, 9, COL_ACCENT) # Liste: key: before → after var col_x: float = px + 14.0 var line_y: float = py + 26.0 var count := 0 for key: String in def.effects: if count >= 3: break var val = def.effects[key] var before: float = _stat_value(key) var after: float = _project_stat(key, val) var pos: bool = ItemDB.is_positive_effect(key, val) var col: Color = COL_POS if pos else COL_NEG var line := "%s: %.2f → %.2f" % [ItemDB.stat_display_name(key), before, after] _draw_text(line, col_x, line_y, 10, col) line_y += 14.0 count += 1 # Hull-Size Warnung if def.hull_size_bonus > 0.0: _draw_text(Tr.t("werk_grows"), px + pw - 150.0, py + ph - 22.0, 9, COL_WARN) func _stat_value(key: String) -> float: match key: "speed_mult": return _stats.speed_mult "turn_mult": return _stats.turn_mult "fire_rate_mult": return _stats.fire_rate_mult "damage_mult": return _stats.damage_mult "bullet_speed_mult": return _stats.bullet_speed_mult "bullet_count": return float(_stats.bullet_count) "shield_charges": return float(_stats.shield_charges) "invuln_mult": return _stats.invuln_mult "bh_resist": return _stats.bh_resist "wipe_mult": return _stats.wipe_mult "credit_bonus": return _stats.credit_bonus return 0.0 func _project_stat(key: String, val) -> float: match key: "speed_mult","turn_mult","fire_rate_mult","damage_mult", \ "bullet_speed_mult","invuln_mult","wipe_mult","credit_bonus": return _stat_value(key) * float(val) "bullet_count","shield_charges": return _stat_value(key) + float(val) "bh_resist": return min(0.9, _stat_value(key) + float(val)) return _stat_value(key) func _fmt_val(key: String, val) -> String: match key: "bullet_count": return "+%d" % int(val) "shield_charges": return ("+%d" % int(val)) if int(val) >= 0 else "%d" % int(val) "bh_resist": return "+%.0f%%" % (float(val) * 100.0) _: var f: float = float(val) var pct: float = (f - 1.0) * 100.0 return "%+.0f%%" % pct # ── Ship preview im Zentrum ─────────────────────────────────────────────────── func _draw_ship_preview(cx: float, cy: float) -> void: var bw := 180.0; var bh := 180.0 draw_rect(Rect2(cx - bw * 0.5, cy - bh * 0.5, bw, bh), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.05)) _draw_brackets(cx - bw * 0.5, cy - bh * 0.5, cx + bw * 0.5, cy + bh * 0.5, COL_DIM, 14) _draw_text_c(Tr.t("werk_ship_preview"), cx, cy - bh * 0.5 + 6.0, 8, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55)) # Schiff zeichnen — gleich wie Spaceship.HULL_PIXELS, skaliert var hs: float = _stats.hull_scale if _stats != null else 3.0 var preview_scale: float = 3.5 # Extra-Zoom für Preview var off_mul: float = hs / 3.0 * preview_scale var rect_sz: float = hs * preview_scale * 0.9 var rect_half: float = rect_sz * 0.5 var heading: float = -PI * 0.5 # Schiff zeigt nach oben var cos_h: float = cos(heading); var sin_h: float = sin(heading) for px_data in Spaceship.HULL_PIXELS: var lx: float = px_data[0] * off_mul var ly: float = px_data[1] * off_mul var col: Color = _ship_palette.get(px_data[2], COL_WHITE) var rx: float = cx + lx * cos_h - ly * sin_h var ry: float = cy + lx * sin_h + ly * cos_h draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col) # Attachments aus owned_items var seen: Dictionary = {} for id: String in _owned_ids: var def: ItemDef = ItemDB.get_def_by_id(id) if def == null or def.visual_pixels.is_empty(): continue var stack: int = int(seen.get(id, 0)) seen[id] = stack + 1 var stack_off: float = float(stack) * 0.6 for vpx in def.visual_pixels: var lx: float = (float(vpx[0]) + stack_off) * off_mul var ly: float = float(vpx[1]) * off_mul var col: Color = _ship_palette.get(vpx[2], COL_WHITE) var rx: float = cx + lx * cos_h - ly * sin_h var ry: float = cy + lx * sin_h + ly * cos_h draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col) func _draw_owned_banner(cx: float, cy: float) -> void: var names: Array = [] for id: String in _owned_ids: var def: ItemDef = ItemDB.get_def_by_id(id) if def != null: names.append(def.name) if names.is_empty(): return var line := Tr.t("shop_owned") + " " + " · ".join(names) if line.length() > 100: line = line.substr(0, 97) + "…" _draw_text_c(line, cx, cy, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.5)) # ── Footer ──────────────────────────────────────────────────────────────────── func _draw_footer() -> void: draw_rect(Rect2(0, H - 52.0, W, 52.0), Color(0.0, 0.04, 0.02, 0.95)) draw_line(Vector2(0, H - 52.0), Vector2(W, H - 52.0), COL_DIM, 1.0) var p2: bool = _player_idx == 2 var is_touch: bool = Settings.last_input_device == "touch" var pulse: float = 0.5 + 0.4 * sin(_blink * 2.5) if _phase == Phase.ATTR: if is_touch: _draw_text_c("● TAP", W * 0.5, H - 26.0, 11, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)) else: _draw_text_c(Tr.hint("werk_attr_footer_p2" if p2 else "werk_attr_footer"), W * 0.5, H - 48.0, 9, COL_DIM) _draw_text_c(Tr.hint("werk_confirm_p2" if p2 else "werk_confirm"), W * 0.5, H - 26.0, 11, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)) else: var reroll_cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST var reroll_col: Color = COL_DIM if _credits < reroll_cost else COL_PRIMARY if is_touch: # Touch: left half = REROLL tap zone, right half = SKIP tap zone draw_rect(Rect2(0, H - 52.0, W * 0.5 - 1, 52.0), Color(reroll_col.r, reroll_col.g, reroll_col.b, 0.06)) draw_rect(Rect2(W * 0.5 + 1, H - 52.0, W * 0.5 - 1, 52.0), Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.06)) var reroll_str: String = ("REROLL %d CR" if Settings.language == "en" else "REROLL %d CR") % reroll_cost _draw_text_c(reroll_str, W * 0.25, H - 26.0, 10, reroll_col) var skip_str: String = "SKIP ►" if Settings.language == "en" else "WEITER ►" _draw_text_c(skip_str, W * 0.75, H - 26.0, 11, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)) else: _draw_text_c(Tr.hint("werk_shop_footer_p2" if p2 else "werk_shop_footer"), W * 0.5, H - 48.0, 9, COL_DIM) var reroll_key: String = "Q" if p2 else "R" _draw_text_c(Tr.t("werk_reroll") % [reroll_key, reroll_cost], W * 0.30, H - 26.0, 10, reroll_col) _draw_text_c(Tr.hint("werk_continue_p2" if p2 else "shop_continue"), W * 0.72, H - 26.0, 11, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)) # ═══════════════════════════════════════════════════════════════════════════════ # Text + Shape Helpers # ═══════════════════════════════════════════════════════════════════════════════ func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void: var font := ThemeDB.fallback_font draw_string(font, Vector2(x, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col) func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void: var font := ThemeDB.fallback_font var tw: float = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x draw_string(font, Vector2(x - tw * 0.5, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col) func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void: var bw: float = 1.5 draw_line(Vector2(x1, y1), Vector2(x1 + arm, y1), col, bw) draw_line(Vector2(x1, y1), Vector2(x1, y1 + arm), col, bw) draw_line(Vector2(x2, y1), Vector2(x2 - arm, y1), col, bw) draw_line(Vector2(x2, y1), Vector2(x2, y1 + arm), col, bw) draw_line(Vector2(x1, y2), Vector2(x1 + arm, y2), col, bw) draw_line(Vector2(x1, y2), Vector2(x1, y2 - arm), col, bw) draw_line(Vector2(x2, y2), Vector2(x2 - arm, y2), col, bw) draw_line(Vector2(x2, y2), Vector2(x2, y2 - arm), col, bw)