extends Node2D # ─── Atlas UI ──────────────────────────────────────────────────────────────── # Beschreibt alle kosmischen Objekte, Schiffe und Events mit Live-Preview. # Previews verwenden die echten Spielobjekte (draw-Methoden identisch zum Spiel). # Sichtbar über das Hauptmenü (ATLAS). Sendet `closed` wenn beendet. signal closed const CosmicObjects = preload("res://scripts/cosmic_objects.gd") const BlackHoleClass = preload("res://scripts/black_hole.gd") const PlanetClass = preload("res://scripts/planet.gd") # NOVA-1 Palette — exakt aus main.gd SHIPS[0] const PAL_NOVA1 := { "nose": Color("#ffffff"), "bright": Color("#dddddd"), "mid": Color("#cccccc"), "dim": Color("#aaaaaa"), "accent": Color("#88aaff"), "edge": Color("#888888"), "shadow": Color("#666688"), "trail": Color(0.533, 0.667, 1.0, 0.251), "thrustHot": Color("#ffcc44"), "thrustCool": Color(1.0, 0.533, 0.267, 0.533) } const COL_BG := Color(0.0, 0.0, 0.04, 0.90) 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) var W: float = 960.0 var H: float = 600.0 var _blink: float = 0.0 var _cursor: int = 0 var _scroll: int = 0 var _time: float = 0.0 # Preview box center (fixed for 960×600 viewport) # right_x=300, pbox_x=318, pbox_y=116, pbox_sz=180 const PREV_CX := 408.0 const PREV_CY := 206.0 # Preview objects — real game instances var _preview_obj = null # single object (most kinds) var _preview_list: Array = [] # multiple objects (antimatter) var _preview_kind: String = "" # Entry schema: # kind: id used by preview dispatcher # cat: category key (atlas_cat_*) # name_key: translation key for name # desc_key: translation key for description # props: Array of [label_key, value_string] (value is NOT translated) const ENTRIES: Array = [ {"kind": "star", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_star", "desc_key": "atlas_d_star", "props": [ ["atlas_prop_size", "1–7 px"], ["atlas_prop_speed", "0.05–0.3 px/f"], ["atlas_prop_spawn", "~150–300"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_terr", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_terr", "desc_key": "atlas_d_planet_terr", "props": [ ["atlas_prop_size", "5–10 px"], ["atlas_prop_orbit", "19–72 px"], ["atlas_prop_effect", "Wolken/Polkappen"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_desert", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_desert", "desc_key": "atlas_d_planet_desert", "props": [ ["atlas_prop_size", "5–10 px"], ["atlas_prop_orbit", "19–72 px"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_gas", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_gas", "desc_key": "atlas_d_planet_gas", "props": [ ["atlas_prop_size", "12–19 px"], ["atlas_prop_orbit", "19–72 px"], ["atlas_prop_effect", "Ringe, Monde, Sturm"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_ice", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_ice", "desc_key": "atlas_d_planet_ice", "props": [ ["atlas_prop_size", "5–10 px"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_lava", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_lava", "desc_key": "atlas_d_planet_lava", "props": [ ["atlas_prop_size", "5–10 px"], ["atlas_prop_effect", "Halo + Glut"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "planet_toxic", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_toxic", "desc_key": "atlas_d_planet_toxic", "props": [ ["atlas_prop_size", "5–10 px"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "nebula", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_nebula", "desc_key": "atlas_d_nebula", "props": [ ["atlas_prop_size", "120–220 px"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "comet", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_comet", "desc_key": "atlas_d_comet", "props": [ ["atlas_prop_speed", "1–4 px/f"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "galaxy", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_galaxy", "desc_key": "atlas_d_galaxy", "props": [ ["atlas_prop_size", "~80 px"], ["atlas_prop_effect", "SMBH-Trigger"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "blackhole", "cat": "atlas_cat_exotic", "name_key": "atlas_n_blackhole", "desc_key": "atlas_d_blackhole", "props": [ ["atlas_prop_size", "14–40 px"], ["atlas_prop_effect", "Pull 160 px"], ["atlas_prop_hazard", "atlas_hazard_deadly"], ]}, {"kind": "whitehole", "cat": "atlas_cat_exotic", "name_key": "atlas_n_whitehole", "desc_key": "atlas_d_whitehole", "props": [ ["atlas_prop_size", "~16 px"], ["atlas_prop_effect", "Push 340 px"], ["atlas_prop_life", "~60 s"], ["atlas_prop_hazard", "atlas_hazard_low"], ]}, {"kind": "neutron", "cat": "atlas_cat_exotic", "name_key": "atlas_n_neutron", "desc_key": "atlas_d_neutron", "props": [ ["atlas_prop_size", "~6 px"], ["atlas_prop_effect", "Pulsar-Beam"], ["atlas_prop_hazard", "atlas_hazard_low"], ]}, {"kind": "quasar", "cat": "atlas_cat_exotic", "name_key": "atlas_n_quasar", "desc_key": "atlas_d_quasar", "props": [ ["atlas_prop_life", "~30 s"], ["atlas_prop_hazard", "atlas_hazard_none"], ]}, {"kind": "antimatter", "cat": "atlas_cat_anti", "name_key": "atlas_n_antimatter", "desc_key": "atlas_d_antimatter", "props": [ ["atlas_prop_size", "2 px"], ["atlas_prop_hazard", "atlas_hazard_deadly"], ]}, {"kind": "antistar", "cat": "atlas_cat_anti", "name_key": "atlas_n_antistar", "desc_key": "atlas_d_antistar", "props": [ ["atlas_prop_life", "~50 s"], ["atlas_prop_effect", "Push"], ["atlas_prop_hazard", "atlas_hazard_deadly"], ]}, {"kind": "player", "cat": "atlas_cat_ships", "name_key": "atlas_n_player", "desc_key": "atlas_d_player", "props": [ ["atlas_prop_hp", "1 (+Schilde)"], ["atlas_prop_speed", "7.5 Max"], ["atlas_prop_damage", "1.0 (Pierce ≥2)"], ]}, {"kind": "enemy", "cat": "atlas_cat_ships", "name_key": "atlas_n_enemy", "desc_key": "atlas_d_enemy", "props": [ ["atlas_prop_hp", "1"], ["atlas_prop_reward", "15 Credits"], ["atlas_prop_hazard", "atlas_hazard_mid"], ]}, {"kind": "wraith", "cat": "atlas_cat_ships", "name_key": "atlas_n_wraith", "desc_key": "atlas_d_wraith", "props": [ ["atlas_prop_hp", "20"], ["atlas_prop_reward", "150 Credits"], ["atlas_prop_hazard", "atlas_hazard_high"], ]}, {"kind": "leviathan", "cat": "atlas_cat_ships", "name_key": "atlas_n_leviathan", "desc_key": "atlas_d_leviathan", "props": [ ["atlas_prop_hp", "50"], ["atlas_prop_reward", "300 Credits"], ["atlas_prop_hazard", "atlas_hazard_deadly"], ]}, {"kind": "bullet", "cat": "atlas_cat_events", "name_key": "atlas_n_bullet", "desc_key": "atlas_d_bullet", "props": [ ["atlas_prop_speed", "9.6 px/f"], ["atlas_prop_life", "240 f"], ]}, {"kind": "bigwipe", "cat": "atlas_cat_events", "name_key": "atlas_n_bigwipe", "desc_key": "atlas_d_bigwipe", "props": [ ["atlas_prop_effect", "> 500 Objekte"], ["atlas_prop_reward", "25 Credits"], ]}, ] func open() -> void: visible = true _cursor = 0 _scroll = 0 _time = 0.0 _rebuild_preview() queue_redraw() func close() -> void: visible = false _preview_obj = null _preview_list = [] _preview_kind = "" closed.emit() # ─── Loop ──────────────────────────────────────────────────────────────────── func _process(delta: float) -> void: if not visible: return _blink += delta _time += delta _update_preview(delta) queue_redraw() func _update_preview(delta: float) -> void: match _preview_kind: "star": if _preview_obj: _preview_obj.update(delta, 99999.0, 99999.0) _preview_obj.x = PREV_CX _preview_obj.y = PREV_CY "galaxy": if _preview_obj: _preview_obj.update(delta, 99999.0, 99999.0) _preview_obj.x = PREV_CX _preview_obj.y = PREV_CY "blackhole": if _preview_obj: _preview_obj.update(delta, [], 99999.0, 99999.0) _preview_obj.x = PREV_CX _preview_obj.y = PREV_CY "whitehole": if _preview_obj: _preview_obj.update(delta) _preview_obj.x = PREV_CX _preview_obj.y = PREV_CY "neutron", "quasar": if _preview_obj: _preview_obj.update(delta) "antistar": if _preview_obj: _preview_obj.update(delta) _preview_obj.x = PREV_CX _preview_obj.y = PREV_CY "antimatter": for k: int in _preview_list.size(): var am = _preview_list[k] var ang := float(k) * TAU / float(_preview_list.size()) + _time * 0.6 var r := 20.0 + sin(_time + float(k)) * 12.0 am.x = PREV_CX + cos(ang) * r am.y = PREV_CY + sin(ang) * r am.update(99999.0, 99999.0, delta, []) # reset position after update (velocity might shift it) am.x = PREV_CX + cos(ang) * r am.y = PREV_CY + sin(ang) * r "wraith", "leviathan": if _preview_obj: _preview_obj.update(delta, PREV_CX, PREV_CY) "planet_terr", "planet_desert", "planet_gas", "planet_ice", "planet_lava", "planet_toxic": if _preview_obj and _preview_obj.has_method("update"): _preview_obj.update(delta) func _unhandled_input(event: InputEvent) -> void: if not visible: return if event.is_action_pressed("ui_up"): _cursor = (_cursor - 1 + ENTRIES.size()) % ENTRIES.size() _ensure_cursor_visible() _rebuild_preview() get_viewport().set_input_as_handled() elif event.is_action_pressed("ui_down"): _cursor = (_cursor + 1) % ENTRIES.size() _ensure_cursor_visible() _rebuild_preview() get_viewport().set_input_as_handled() elif event.is_action_pressed("ui_cancel") or event.is_action_pressed("ui_accept"): close() get_viewport().set_input_as_handled() func _ensure_cursor_visible() -> void: var visible_rows := 14 if _cursor < _scroll: _scroll = _cursor elif _cursor >= _scroll + visible_rows: _scroll = _cursor - visible_rows + 1 func _rebuild_preview() -> void: var kind: String = ENTRIES[_cursor]["kind"] _preview_kind = kind _preview_obj = null _preview_list = [] _time = 0.0 match kind: "star": var s := CosmicObjects.Star.new() s.init(PREV_CX, PREV_CY, 99999.0, 99999.0) _preview_obj = s "nebula": var n := CosmicObjects.Nebula.new() n.init(99999.0, 99999.0) n.x = PREV_CX n.y = PREV_CY _preview_obj = n "comet": var c := CosmicObjects.Comet.new() c.init(99999.0, 99999.0) # Position it so it travels across the preview box area c.x = PREV_CX - 70.0 c.y = PREV_CY - 40.0 _preview_obj = c "galaxy": var g := CosmicObjects.Galaxy.new() g.init(PREV_CX, PREV_CY) _preview_obj = g "blackhole": var bh := BlackHoleClass.new() bh.init(PREV_CX, PREV_CY, false) _preview_obj = bh "whitehole": var wh := CosmicObjects.WhiteHole.new() wh.init(PREV_CX, PREV_CY) _preview_obj = wh "neutron": var ns := CosmicObjects.NeutronStar.new() ns.init(PREV_CX, PREV_CY) _preview_obj = ns "quasar": var q := CosmicObjects.Quasar.new() q.init(PREV_CX, PREV_CY) _preview_obj = q "antimatter": for k: int in 10: var am := CosmicObjects.Antimatter.new() var ang := float(k) * TAU / 10.0 var r := 22.0 am.init(PREV_CX + cos(ang) * r, PREV_CY + sin(ang) * r, ang, 0.0) _preview_list.append(am) "antistar": var a := CosmicObjects.AntimatterStar.new() a.init(PREV_CX, PREV_CY) _preview_obj = a "player": var sp := Spaceship.new() sp.init(PREV_CX, PREV_CY, PAL_NOVA1, 0) sp.heading = -PI * 0.5 # zeigt nach oben _preview_obj = sp "enemy": var en := EnemyShip.new() en.init(PREV_CX, PREV_CY, 0) # idx=0 → roter Gegner en.heading = PI * 0.5 _preview_obj = en "wraith": var boss := BossShip.new() boss.init_miniboss(PREV_CX, PREV_CY) _preview_obj = boss "leviathan": var boss := BossShip.new() boss.init_boss(PREV_CX, PREV_CY) _preview_obj = boss "planet_terr", "planet_desert", "planet_gas", "planet_ice", "planet_lava", "planet_toxic": var p := PlanetClass.new() p.init(0.0, 0.0, 960.0, 600.0) p.ptype = _ptype_for(kind) p._setup_palette() p._setup_noise() p._setup_animation() if kind == "planet_gas": p.radius = 22.0 else: p.radius = 18.0 p.initial_radius = p.radius p.color = p.palette[0] if kind == "planet_gas": p.ring = true p.ring_count = 2 p.ring_inner = p.radius + 6.0 p.ring_outer = p.ring_inner + 8.0 p.ring_tilt = 0.28 p.ring_colors = [p.palette[0], p.palette[2]] p.ring_gaps = [] else: p.ring = false p.moons.clear() _preview_obj = p # bullet / bigwipe: no live object, drawn procedurally below func _ptype_for(kind: String) -> int: match kind: "planet_terr": return PlanetClass.PType.TERRESTRIAL "planet_desert": return PlanetClass.PType.DESERT "planet_gas": return PlanetClass.PType.GAS_GIANT "planet_ice": return PlanetClass.PType.ICE "planet_lava": return PlanetClass.PType.LAVA "planet_toxic": return PlanetClass.PType.TOXIC return 0 # ─── Drawing ───────────────────────────────────────────────────────────────── func _draw() -> void: var vs: Vector2 = get_viewport_rect().size W = vs.x; H = vs.y # Full-screen darkening draw_rect(Rect2(0, 0, W, H), COL_BG) # Outer brackets _draw_brackets(12.0, 12.0, W - 12.0, H - 12.0, COL_DIM, 18.0) # Title header _draw_text_c(Tr.t("atlas_title"), W * 0.5, 26.0, 14, COL_PRIMARY) var line_y := 54.0 draw_line(Vector2(W * 0.25, line_y), Vector2(W * 0.75, line_y), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35), 1.0) # Layout var top := 70.0 var bottom := H - 40.0 var list_x := 24.0 var list_w := 260.0 var right_x := list_x + list_w + 16.0 var right_w := W - right_x - 24.0 _draw_list(list_x, top, list_w, bottom - top) _draw_details(right_x, top, right_w, bottom - top) # Footer _draw_text_c(Tr.t("atlas_footer"), W * 0.5, H - 22.0, 8, COL_DIM) func _draw_list(x: float, y: float, w: float, h: float) -> void: _draw_terminal_box(x, y, w, h) var visible_rows := 14 var row_h := 22.0 var last_cat := "" var draw_y := y + 14.0 var i := _scroll var rows_left := visible_rows while i < ENTRIES.size() and rows_left > 0: var entry: Dictionary = ENTRIES[i] var cat: String = entry["cat"] if cat != last_cat: var ch := Tr.t(cat) _draw_text(ch, x + 14.0, draw_y, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.55)) draw_y += 14.0 last_cat = cat var is_sel := (i == _cursor) var pulse := 0.6 + 0.4 * sin(_blink * 3.0) var col: Color if is_sel: col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse) else: col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.60) var prefix := "▶ " if is_sel else " " _draw_text(prefix + Tr.t(entry["name_key"]), x + 20.0, draw_y, 10, col) draw_y += row_h - 2.0 i += 1 rows_left -= 1 # Scroll indicators if _scroll > 0: _draw_text(" ▲", x + w - 24.0, y + 6.0, 10, COL_DIM) if _scroll + visible_rows < ENTRIES.size(): _draw_text(" ▼", x + w - 24.0, y + h - 22.0, 10, COL_DIM) func _draw_details(x: float, y: float, w: float, h: float) -> void: _draw_terminal_box(x, y, w, h) var entry: Dictionary = ENTRIES[_cursor] # Name header var name_y := y + 18.0 _draw_text(Tr.t(entry["name_key"]), x + 18.0, name_y, 14, COL_ACCENT) # Preview box (left sub-area) var pbox_sz := 180.0 var pbox_x := x + 18.0 var pbox_y := name_y + 28.0 _draw_preview_box(pbox_x, pbox_y, pbox_sz, pbox_sz, entry["kind"]) # Props (right of preview) var prop_x := pbox_x + pbox_sz + 24.0 var prop_y := pbox_y _draw_text(Tr.t("atlas_props"), prop_x, prop_y, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.65)) prop_y += 16.0 draw_line(Vector2(prop_x, prop_y), Vector2(x + w - 24.0, prop_y), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0) prop_y += 8.0 var props: Array = entry["props"] for pair in props: var lbl: String = Tr.t(pair[0]) var val_raw: String = pair[1] var val := val_raw if val_raw.begins_with("atlas_hazard_"): val = Tr.t(val_raw) _draw_text(lbl, prop_x, prop_y, 10, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, 0.55)) _draw_text(val, prop_x + 100.0, prop_y, 10, COL_WHITE) prop_y += 20.0 # Description (full width below) var desc_y := pbox_y + pbox_sz + 22.0 _draw_text(Tr.t("atlas_desc"), x + 18.0, desc_y, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.65)) desc_y += 16.0 draw_line(Vector2(x + 18.0, desc_y), Vector2(x + w - 18.0, desc_y), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0) desc_y += 8.0 var text: String = Tr.t(entry["desc_key"]) _draw_wrapped(text, x + 18.0, desc_y, w - 36.0, 11, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, 0.85)) # ─── Preview dispatcher ─────────────────────────────────────────────────────── func _draw_preview_box(px: float, py: float, pw: float, ph: float, kind: String) -> void: # Dark frame draw_rect(Rect2(px, py, pw, ph), Color(0.02, 0.02, 0.06, 0.85)) draw_rect(Rect2(px, py, pw, ph), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), false, 1.0) var cx := px + pw * 0.5 var cy := py + ph * 0.5 match kind: "antimatter": # Multiple real Antimatter objects for am in _preview_list: am.draw(self) "player": if _preview_obj: _preview_obj.x = cx _preview_obj.y = cy _preview_obj.draw(self, 0) # frame=0 → nie blinken "comet": # Comet trail needs manual positioning within the box _prev_comet(px, py, pw, ph) "bullet": _prev_bullet(cx, cy) "bigwipe": _prev_bigwipe(px, py, pw, ph) _: # Alle anderen: echtes Spielobjekt an Preview-Mitte positionieren und zeichnen if _preview_obj: _preview_obj.x = cx _preview_obj.y = cy _preview_obj.draw(self) # ─── Custom previews (keine Entsprechung als einzelnes Spielobjekt) ─────────── func _prev_comet(px: float, py: float, pw: float, ph: float) -> void: var tip_x := px + 30.0 + fmod(_time * 80.0, pw - 60.0) var tip_y := py + 30.0 + fmod(_time * 40.0, ph - 60.0) var tail := 26 for i: int in tail: var t := float(i) / float(tail) var tx := tip_x - cos(0.5) * float(i) * 3.0 var ty := tip_y - sin(0.5) * float(i) * 3.0 var a := (1.0 - t) draw_rect(Rect2(tx, ty, 2, 2), Color(0.9, 0.95, 1.0, a * 0.85)) draw_rect(Rect2(tip_x - 2, tip_y - 2, 4, 4), Color(1, 1, 1, 1.0)) func _prev_bullet(cx: float, cy: float) -> void: # Player bullet draw_rect(Rect2(cx - 24, cy - 12, 4, 4), Color(0.7, 0.9, 1.0, 1.0)) draw_rect(Rect2(cx - 26, cy - 13, 6, 2), Color(0.7, 0.9, 1.0, 0.4)) # Pierce (white) draw_rect(Rect2(cx + 2, cy - 12, 4, 4), Color(1, 1, 1, 1)) draw_rect(Rect2(cx - 8, cy - 8, 6, 2), Color(1, 1, 1, 0.3)) # Enemy bullet draw_rect(Rect2(cx - 24, cy + 10, 4, 4), Color(1.0, 0.4, 0.4, 1.0)) # Boss bullet draw_rect(Rect2(cx + 2, cy + 10, 4, 4), Color(1.0, 0.6, 0.2, 1.0)) _draw_text("player", cx - 52, cy - 18, 8, Color(0.7, 0.9, 1.0, 0.7)) _draw_text("pierce", cx + 12, cy - 18, 8, Color(1, 1, 1, 0.7)) _draw_text("enemy", cx - 52, cy + 4, 8, Color(1, 0.4, 0.4, 0.7)) _draw_text("boss", cx + 12, cy + 4, 8, Color(1, 0.6, 0.2, 0.7)) func _prev_bigwipe(px: float, py: float, pw: float, ph: float) -> void: var flash := 0.5 + 0.5 * sin(_time * 6.0) draw_rect(Rect2(px + 4, py + 4, pw - 8, ph - 8), Color(0.0, 0.0, 0.05, 0.75)) for i: int in 18: var ly := py + 6.0 + float(i) * 10.0 if ly > py + ph - 6: continue draw_line(Vector2(px + 4, ly), Vector2(px + pw - 4, ly), Color(1, 1, 1, 0.08 + 0.12 * flash), 1.0) var cx := px + pw * 0.5; var cy := py + ph * 0.5 draw_circle(Vector2(cx, cy), 22.0 + flash * 8.0, Color(1, 1, 1, 0.25 * flash)) _draw_text_c("[ N ]", cx, cy + 48.0, 14, Color(1, 1, 1, 0.7 + 0.3 * flash)) _draw_text_c("HOLD", cx, cy + 66.0, 8, Color(1, 1, 1, 0.6)) # ─── Drawing helpers ────────────────────────────────────────────────────────── func _draw_terminal_box(bx: float, by: float, bw: float, bh: float) -> void: draw_rect(Rect2(bx, by, bw, bh), Color(0.0, 0.04, 0.02, 0.95)) var bc := Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35) draw_line(Vector2(bx, by), Vector2(bx+bw, by), bc, 1.0) draw_line(Vector2(bx, by+bh), Vector2(bx+bw, by+bh), bc, 1.0) draw_line(Vector2(bx, by), Vector2(bx, by+bh), bc, 1.0) draw_line(Vector2(bx+bw, by), Vector2(bx+bw, by+bh), bc, 1.0) var cl := 12.0 for cx: float in [bx, bx+bw]: for cy: float in [by, by+bh]: var sx := 1.0 if cx == bx else -1.0 var sy := 1.0 if cy == by else -1.0 draw_line(Vector2(cx, cy), Vector2(cx + sx*cl, cy), COL_PRIMARY, 1.5) draw_line(Vector2(cx, cy), Vector2(cx, cy + sy*cl), COL_PRIMARY, 1.5) func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void: var bw := 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) func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void: draw_string(ThemeDB.fallback_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 tw := _text_w(text, sz) draw_string(ThemeDB.fallback_font, Vector2(x - tw * 0.5, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col) func _text_w(text: String, sz: int) -> float: return ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x func _draw_wrapped(text: String, x: float, y: float, w: float, sz: int, col: Color) -> void: var words := text.split(" ", false) var line := "" var row_y := y for word: String in words: var trial := (line + " " + word) if line != "" else word if _text_w(trial, sz) > w and line != "": _draw_text(line, x, row_y, sz, col) row_y += float(sz) + 4.0 line = word else: line = trial if line != "": _draw_text(line, x, row_y, sz, col)