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>
This commit is contained in:
2026-04-21 14:38:09 +02:00
commit edc40f9008
108 changed files with 10068 additions and 0 deletions
+673
View File
@@ -0,0 +1,673 @@
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)