edc40f9008
- 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>
511 lines
18 KiB
GDScript
511 lines
18 KiB
GDScript
extends Node2D
|
|
|
|
# Hauptmenü — erscheint beim Start.
|
|
# Hintergrund-Simulation läuft durch (game_world im Menu-Modus).
|
|
|
|
signal mode_selected(is_multiplayer: bool)
|
|
signal atlas_requested
|
|
signal quit_requested
|
|
|
|
var W: float = 960.0
|
|
var H: float = 600.0
|
|
|
|
const COL_BG := Color(0.0, 0.0, 0.04, 0.65)
|
|
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)
|
|
|
|
# 0 = Hauptmenü, 1 = Optionen, 2 = Steuerung, 3 = Leaderboard
|
|
var _screen: int = 0
|
|
var _cursor: int = 0
|
|
var _blink: float = 0.0
|
|
var _opt_cursor: int = 0
|
|
var _ctrl_cursor: int = 0
|
|
var _ctrl_waiting: bool = false # wartet auf Taste-Drücken zum Binden
|
|
|
|
# Leaderboard state
|
|
var _lb_view: int = 0 # 0=lokal, 1=online
|
|
var _lb_online_data: Array = []
|
|
var _lb_loading: bool = false
|
|
var _lb_error: String = ""
|
|
var _online_lb_node: Node = null
|
|
|
|
# Abwärtskompatibilität
|
|
var _show_options: bool:
|
|
get: return _screen == 1
|
|
set(v): _screen = 1 if v else 0
|
|
|
|
func _ready() -> void:
|
|
pass
|
|
|
|
func _items() -> Array:
|
|
return [Tr.t("menu_single"), Tr.t("menu_vs"), Tr.t("menu_leaderboard"), Tr.t("menu_options"), Tr.t("menu_atlas"), Tr.t("menu_quit")]
|
|
|
|
func _opt_items() -> Array:
|
|
return [Tr.t("opt_sfx"), Tr.t("opt_music"), Tr.t("opt_mute"), Tr.t("opt_fullscreen"),
|
|
Tr.t("opt_nebula"), Tr.t("opt_stars"), Tr.t("opt_language"), Tr.t("opt_touch"),
|
|
Tr.t("opt_controls"), Tr.t("opt_back")]
|
|
|
|
func _ctrl_labels() -> Array:
|
|
return [Tr.t("ctrl_thrust"), Tr.t("ctrl_left"), Tr.t("ctrl_right"),
|
|
Tr.t("ctrl_shoot"), Tr.t("ctrl_wipe")]
|
|
|
|
func _process(delta: float) -> void:
|
|
if not visible: return
|
|
_blink += delta
|
|
queue_redraw()
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not visible: return
|
|
match _screen:
|
|
0: _input_menu(event)
|
|
1: _input_options(event)
|
|
2: _input_controls(event)
|
|
3: _input_leaderboard(event)
|
|
|
|
func _input_menu(event: InputEvent) -> void:
|
|
var items := _items()
|
|
if event.is_action_pressed("ui_up"):
|
|
_cursor = (_cursor - 1 + items.size()) % items.size()
|
|
get_viewport().set_input_as_handled()
|
|
elif event.is_action_pressed("ui_down"):
|
|
_cursor = (_cursor + 1) % items.size()
|
|
get_viewport().set_input_as_handled()
|
|
elif event.is_action_pressed("ui_accept"):
|
|
# Consume the event FIRST so the same ENTER doesn't bubble up to
|
|
# main.gd._unhandled_input and auto-confirm the next state's UI.
|
|
get_viewport().set_input_as_handled()
|
|
match _cursor:
|
|
0: mode_selected.emit(false)
|
|
1: mode_selected.emit(true)
|
|
2: _screen = 3; _lb_view = 0; _lb_online_data = []; _lb_error = ""; _lb_loading = false
|
|
3: _screen = 1; _opt_cursor = 0
|
|
4: atlas_requested.emit()
|
|
5: quit_requested.emit()
|
|
|
|
func _input_options(event: InputEvent) -> void:
|
|
var opts := _opt_items()
|
|
if event.is_action_pressed("ui_up"):
|
|
_opt_cursor = (_opt_cursor - 1 + opts.size()) % opts.size()
|
|
elif event.is_action_pressed("ui_down"):
|
|
_opt_cursor = (_opt_cursor + 1) % opts.size()
|
|
elif event.is_action_pressed("ui_left"):
|
|
_change_option(_opt_cursor, -1)
|
|
elif event.is_action_pressed("ui_right"):
|
|
_change_option(_opt_cursor, 1)
|
|
elif event.is_action_pressed("ui_accept"):
|
|
if _opt_cursor == opts.size() - 1: # Back
|
|
_screen = 0
|
|
elif _opt_cursor == opts.size() - 2: # Controls
|
|
_screen = 2; _ctrl_cursor = 0; _ctrl_waiting = false
|
|
else:
|
|
_change_option(_opt_cursor, 1)
|
|
elif event.is_action_pressed("ui_cancel"):
|
|
_screen = 0
|
|
|
|
func _input_controls(event: InputEvent) -> void:
|
|
var labels := _ctrl_labels()
|
|
var total := labels.size() + 2 # actions + Reset + Back
|
|
var reset_idx := labels.size()
|
|
var back_idx := labels.size() + 1
|
|
|
|
if _ctrl_waiting:
|
|
if event is InputEventKey and event.pressed:
|
|
var kc: int = event.physical_keycode
|
|
# Ignore pure modifier keys
|
|
if kc in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META, KEY_CAPSLOCK]:
|
|
return
|
|
if kc == KEY_ESCAPE:
|
|
_ctrl_waiting = false
|
|
return
|
|
_rebind_key(Settings.REBIND_ACTIONS[_ctrl_cursor], kc)
|
|
_ctrl_waiting = false
|
|
get_viewport().set_input_as_handled()
|
|
elif event is InputEventJoypadButton and event.pressed:
|
|
_rebind_joy(Settings.REBIND_ACTIONS[_ctrl_cursor], event.button_index)
|
|
_ctrl_waiting = false
|
|
get_viewport().set_input_as_handled()
|
|
return # block all other input while waiting
|
|
|
|
if event.is_action_pressed("ui_up"):
|
|
_ctrl_cursor = (_ctrl_cursor - 1 + total) % total
|
|
elif event.is_action_pressed("ui_down"):
|
|
_ctrl_cursor = (_ctrl_cursor + 1) % total
|
|
elif event.is_action_pressed("ui_accept"):
|
|
if _ctrl_cursor == back_idx:
|
|
_screen = 1
|
|
elif _ctrl_cursor == reset_idx:
|
|
Settings.reset_key_bindings()
|
|
else:
|
|
_ctrl_waiting = true
|
|
elif event.is_action_pressed("ui_cancel"):
|
|
_screen = 1
|
|
|
|
func _input_leaderboard(event: InputEvent) -> void:
|
|
if _lb_loading:
|
|
return
|
|
if event.is_action_pressed("ui_cancel"):
|
|
_screen = 0
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
# 'O' key toggles online/local view
|
|
if event is InputEventKey and (event as InputEventKey).pressed:
|
|
var phk := (event as InputEventKey).physical_keycode
|
|
if phk == KEY_O:
|
|
if _lb_view == 0:
|
|
_fetch_online()
|
|
else:
|
|
_lb_view = 0
|
|
_lb_error = ""
|
|
get_viewport().set_input_as_handled()
|
|
|
|
func _fetch_online() -> void:
|
|
if _online_lb_node == null:
|
|
_online_lb_node = load("res://scripts/online_leaderboard.gd").new()
|
|
add_child(_online_lb_node)
|
|
_online_lb_node.scores_fetched.connect(_on_online_scores_fetched)
|
|
_lb_loading = true
|
|
_lb_error = ""
|
|
_online_lb_node.fetch_scores()
|
|
|
|
func _on_online_scores_fetched(scores: Array, error: String) -> void:
|
|
_lb_loading = false
|
|
if error != "":
|
|
_lb_error = Tr.t(error)
|
|
_lb_view = 1
|
|
else:
|
|
_lb_online_data = scores
|
|
_lb_view = 1
|
|
|
|
func _rebind_key(action: String, physical_keycode: int) -> void:
|
|
if not InputMap.has_action(action): return
|
|
# Remove existing keyboard events
|
|
for ev in InputMap.action_get_events(action):
|
|
if ev is InputEventKey:
|
|
InputMap.action_erase_event(action, ev)
|
|
var new_ev := InputEventKey.new()
|
|
new_ev.physical_keycode = physical_keycode
|
|
InputMap.action_add_event(action, new_ev)
|
|
Settings.key_bindings[action] = physical_keycode
|
|
Settings.save_settings()
|
|
|
|
func _rebind_joy(action: String, button_index: int) -> void:
|
|
if not InputMap.has_action(action): return
|
|
for ev in InputMap.action_get_events(action):
|
|
if ev is InputEventJoypadButton:
|
|
InputMap.action_erase_event(action, ev)
|
|
var new_ev := InputEventJoypadButton.new()
|
|
new_ev.button_index = button_index
|
|
InputMap.action_add_event(action, new_ev)
|
|
Settings.key_bindings[action + "_joy"] = button_index
|
|
Settings.save_settings()
|
|
|
|
func _change_option(idx: int, dir: int) -> void:
|
|
match idx:
|
|
0: # SFX
|
|
Settings.master_volume = clamp(Settings.master_volume + dir * 0.1, 0.0, 1.0)
|
|
Settings.apply_volume()
|
|
1: # Music
|
|
Settings.music_volume = clamp(Settings.music_volume + dir * 0.1, 0.0, 1.0)
|
|
Settings.apply_music_volume()
|
|
2: # Mute
|
|
Settings.sfx_muted = not Settings.sfx_muted
|
|
Settings.apply_volume()
|
|
3: # Fullscreen
|
|
Settings.fullscreen = not Settings.fullscreen
|
|
if Settings.fullscreen:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
else:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
4: # Nebula
|
|
Settings.nebula_enabled = not Settings.nebula_enabled
|
|
5: # Stars
|
|
Settings.star_density = (Settings.star_density + dir + 3) % 3
|
|
6: # Language
|
|
Settings.language = "en" if Settings.language == "de" else "de"
|
|
7: # Touch mode
|
|
Settings.touch_mode = (Settings.touch_mode + 1) % 3
|
|
# 8 = Controls → handled in _input_options (opens sub-screen)
|
|
Settings.save_settings()
|
|
|
|
# ── Drawing ───────────────────────────────────────────────────────────────────
|
|
|
|
func _draw() -> void:
|
|
var vs: Vector2 = get_viewport_rect().size
|
|
W = vs.x; H = vs.y
|
|
draw_rect(get_viewport_rect(), COL_BG)
|
|
match _screen:
|
|
0: _draw_main()
|
|
1: _draw_options()
|
|
2: _draw_controls()
|
|
3: _draw_leaderboard()
|
|
|
|
func _draw_main() -> void:
|
|
# Title
|
|
var title_y := H * 0.22
|
|
_draw_text_c("S P A C E L", W * 0.5, title_y, 38, COL_PRIMARY)
|
|
var sub_alpha := 0.4 + 0.2 * sin(_blink * 1.5)
|
|
_draw_text_c(Tr.t("subtitle"), W * 0.5, title_y + 52.0, 9,
|
|
Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, sub_alpha))
|
|
|
|
# Separator line
|
|
var lx1 := W * 0.5 - 120.0; var lx2 := W * 0.5 + 120.0
|
|
draw_line(Vector2(lx1, title_y + 70.0), Vector2(lx2, title_y + 70.0),
|
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
|
|
|
# Menu entries
|
|
var items := _items()
|
|
var menu_y := H * 0.50
|
|
for i in items.size():
|
|
var is_sel := i == _cursor
|
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
|
|
|
var col: Color
|
|
if i == items.size() - 1: # Quit in warning colour
|
|
col = Color(COL_WARN.r, COL_WARN.g, COL_WARN.b,
|
|
pulse if is_sel else 0.45)
|
|
elif 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.55)
|
|
|
|
var sz := 16 if is_sel else 13
|
|
var prefix := "▶ " if is_sel else " "
|
|
_draw_text_c(prefix + items[i], W * 0.5, menu_y + i * 42.0, sz, col)
|
|
|
|
# Corner brackets
|
|
_draw_brackets(12.0, 12.0, W - 12.0, H - 12.0, COL_DIM, 18.0)
|
|
|
|
# Footer — adapts to active input device
|
|
_draw_text_c(Tr.hint("footer_nav"), W * 0.5, H - 22.0, 8, COL_DIM)
|
|
|
|
func _draw_options() -> void:
|
|
var bw := 480.0; var bh := 380.0
|
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
|
_draw_terminal_box(bx, by, bw, bh)
|
|
_draw_text_c(Tr.t("opt_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
|
|
|
var star_labels := [Tr.t("star_low"), Tr.t("star_mid"), Tr.t("star_high")]
|
|
var touch_labels := [Tr.t("touch_auto"), Tr.t("touch_on"), Tr.t("touch_off")]
|
|
var opts := _opt_items()
|
|
var values: Array = [
|
|
_volume_bar(Settings.master_volume),
|
|
_volume_bar(Settings.music_volume),
|
|
Tr.t("yes") if Settings.sfx_muted else Tr.t("no"),
|
|
Tr.t("yes") if Settings.fullscreen else Tr.t("no"),
|
|
Tr.t("yes") if Settings.nebula_enabled else Tr.t("no"),
|
|
star_labels[Settings.star_density],
|
|
Settings.language.to_upper(),
|
|
touch_labels[Settings.touch_mode],
|
|
"→", # Controls sub-screen
|
|
"" # Back (rendered specially)
|
|
]
|
|
|
|
var item_y := by + 52.0
|
|
for i in opts.size():
|
|
var is_sel := i == _opt_cursor
|
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
|
var row_y := item_y + i * 36.0
|
|
|
|
if i == opts.size() - 1: # Back
|
|
_draw_text_c("[ %s ]" % opts[i], W * 0.5, row_y, 12,
|
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
|
pulse if is_sel else 0.4))
|
|
else:
|
|
var label_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
|
pulse) if is_sel else Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
|
0.90 if is_sel else 0.50)
|
|
var prefix := "▶ " if is_sel else " "
|
|
_draw_text(prefix + opts[i], bx + 28.0, row_y, 11, label_col)
|
|
if values[i] != "":
|
|
var val_str: String
|
|
if values[i] == "→":
|
|
val_str = "→" # Sub-screen indicator — no ◄► arrows
|
|
else:
|
|
val_str = "◄ %s ►" % values[i]
|
|
_draw_text(val_str, bx + bw - 28.0 - _text_w(val_str, 11), row_y, 11, val_col)
|
|
|
|
_draw_text_c(Tr.hint("opt_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
|
|
|
func _draw_controls() -> void:
|
|
var bw := 400.0; var bh := 310.0
|
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
|
_draw_terminal_box(bx, by, bw, bh)
|
|
_draw_text_c(Tr.t("ctrl_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
|
|
|
var labels := _ctrl_labels()
|
|
var actions := Settings.REBIND_ACTIONS
|
|
var reset_idx := labels.size()
|
|
var back_idx := labels.size() + 1
|
|
var total := labels.size() + 2
|
|
|
|
var item_y := by + 52.0
|
|
for i in total:
|
|
var is_sel := i == _ctrl_cursor
|
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
|
var row_y := item_y + i * 36.0
|
|
|
|
if i == back_idx:
|
|
_draw_text_c("[ %s ]" % Tr.t("opt_back"), W * 0.5, row_y, 12,
|
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse if is_sel else 0.4))
|
|
elif i == reset_idx:
|
|
_draw_text_c("[ %s ]" % Tr.t("ctrl_reset"), W * 0.5, row_y, 12,
|
|
Color(COL_WARN.r, COL_WARN.g, COL_WARN.b, pulse if is_sel else 0.35))
|
|
else:
|
|
var label_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
|
pulse) if is_sel else Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
|
0.90 if is_sel else 0.50)
|
|
var prefix := "▶ " if is_sel else " "
|
|
|
|
var key_str: String
|
|
if _ctrl_waiting and is_sel:
|
|
key_str = Tr.t("ctrl_waiting")
|
|
val_col = Color(COL_WARN.r, COL_WARN.g, COL_WARN.b,
|
|
0.5 + 0.5 * sin(_blink * 6.0))
|
|
else:
|
|
key_str = "[ %s ]" % _action_key_name(actions[i])
|
|
|
|
_draw_text(prefix + labels[i], bx + 28.0, row_y, 11, label_col)
|
|
_draw_text(key_str, bx + bw - 28.0 - _text_w(key_str, 11), row_y, 11, val_col)
|
|
|
|
_draw_text_c(Tr.hint("ctrl_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
|
|
|
func _draw_leaderboard() -> void:
|
|
var bw := 500.0; var bh := 420.0
|
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
|
_draw_terminal_box(bx, by, bw, bh)
|
|
|
|
# Header
|
|
var view_label := Tr.t("lb_online") if _lb_view == 1 else Tr.t("lb_local")
|
|
_draw_text_c(Tr.t("lb_title") + " " + view_label, W * 0.5, by + 16.0, 10, COL_DIM)
|
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 38.0)
|
|
|
|
# Loading / error
|
|
if _lb_loading:
|
|
var pulse := 0.5 + 0.5 * sin(_blink * 4.0)
|
|
_draw_text_c(Tr.t("lb_loading"), W * 0.5, by + 200.0, 13,
|
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
|
return
|
|
|
|
if _lb_error != "":
|
|
_draw_text_c(_lb_error, W * 0.5, by + 200.0, 11, COL_WARN)
|
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
|
return
|
|
|
|
# Column headers
|
|
var col_x_rank := bx + 30.0
|
|
var col_x_name := bx + 70.0
|
|
var col_x_score := bx + 260.0
|
|
var col_x_wave := bx + 390.0
|
|
var header_y := by + 50.0
|
|
var dim := Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, 0.8)
|
|
_draw_text("#", col_x_rank, header_y, 9, dim)
|
|
_draw_text("NAME", col_x_name, header_y, 9, dim)
|
|
_draw_text("SCORE", col_x_score, header_y, 9, dim)
|
|
_draw_text("WAVE", col_x_wave, header_y, 9, dim)
|
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 66.0)
|
|
|
|
# Rows
|
|
var scores: Array = Leaderboard.get_scores() if _lb_view == 0 else _lb_online_data
|
|
if scores.is_empty():
|
|
_draw_text_c(Tr.t("lb_empty"), W * 0.5, by + 200.0, 11,
|
|
Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, 0.6))
|
|
else:
|
|
for i: int in min(scores.size(), 10):
|
|
var e: Dictionary = scores[i]
|
|
var row_y := by + 76.0 + i * 30.0
|
|
var row_col := COL_ACCENT if i == 0 else \
|
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.85 - i * 0.05)
|
|
_draw_text("%d." % (i + 1), col_x_rank, row_y, 10, row_col)
|
|
_draw_text(str(e.get("name", "???")), col_x_name, row_y, 10, row_col)
|
|
_draw_text(str(e.get("score", 0)), col_x_score, row_y, 10, row_col)
|
|
var wave_str := Tr.t("lb_wave") + str(e.get("wave", 1))
|
|
_draw_text(wave_str, col_x_wave, row_y, 10, row_col)
|
|
|
|
# Footer
|
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + bh - 36.0)
|
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
func _draw_hline(x1: float, x2: float, y: float) -> void:
|
|
draw_line(Vector2(x1, y), Vector2(x2, y),
|
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
|
|
|
# Returns a short human-readable name for the first keyboard or joypad event of an action.
|
|
func _action_key_name(action: String) -> String:
|
|
if not InputMap.has_action(action): return "?"
|
|
for ev in InputMap.action_get_events(action):
|
|
if ev is InputEventKey:
|
|
var kev := ev as InputEventKey
|
|
var kc: int = kev.physical_keycode if kev.physical_keycode != KEY_NONE else kev.keycode
|
|
return OS.get_keycode_string(kc)
|
|
elif ev is InputEventJoypadButton:
|
|
var jev := ev as InputEventJoypadButton
|
|
return _joy_btn_label(jev.button_index)
|
|
return "?"
|
|
|
|
func _joy_btn_label(idx: int) -> String:
|
|
match idx:
|
|
JOY_BUTTON_A: return "A"
|
|
JOY_BUTTON_B: return "B"
|
|
JOY_BUTTON_X: return "X"
|
|
JOY_BUTTON_Y: return "Y"
|
|
JOY_BUTTON_LEFT_SHOULDER: return "LB"
|
|
JOY_BUTTON_RIGHT_SHOULDER: return "RB"
|
|
JOY_BUTTON_LEFT_STICK: return "L3"
|
|
JOY_BUTTON_RIGHT_STICK: return "R3"
|
|
_: return "BTN%d" % idx
|
|
|
|
func _volume_bar(vol: float) -> String:
|
|
var filled := int(round(vol * 10.0))
|
|
var bar := ""
|
|
for i in 10:
|
|
bar += "█" if i < filled else "░"
|
|
return bar
|
|
|
|
# ── 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
|