Files
spacel/scripts/atlas_ui.gd
T
alpacaman edc40f9008 Initial commit — Godot space roguelite source
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay)
- ItemDB: static preload list instead of DirAccess scan (export fix)
- All 18 items with EN localisation (name_en, desc_en, category_en)
- Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank
- Quasar: SMBH visual, jet boost, merge, push, BH-eating
- Atlas & UI text updated EN+DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 14:38:09 +02:00

674 lines
23 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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", "17 px"],
["atlas_prop_speed", "0.050.3 px/f"],
["atlas_prop_spawn", "~150300"],
["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", "510 px"],
["atlas_prop_orbit", "1972 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", "510 px"],
["atlas_prop_orbit", "1972 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", "1219 px"],
["atlas_prop_orbit", "1972 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", "510 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", "510 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", "510 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", "120220 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", "14 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", "1440 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)