extends Node2D # Pause-Menü + Optionen-Menü im Cockpit-Stil. # Node braucht process_mode = PROCESS_MODE_ALWAYS (wird in _ready gesetzt). signal resume_requested signal quit_to_menu_requested var W: float = 960.0 var H: float = 600.0 const COL_BG := Color(0.0, 0.0, 0.04, 0.75) 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=Pause, 1=Optionen, 2=Steuerung var _screen: int = 0 var _cursor: int = 0 var _blink: float = 0.0 var _ctrl_cursor: int = 0 var _ctrl_waiting: bool = false func _pause_items() -> Array: return [Tr.t("pause_resume"), Tr.t("menu_options"), Tr.t("pause_main_menu"), 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 _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS func open() -> void: _screen = 0 _cursor = 0 _blink = 0.0 _ctrl_waiting = false visible = true 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_pause(event) 1: _input_options(event) 2: _input_controls(event) func _input_pause(event: InputEvent) -> void: var pitems := _pause_items() if event.is_action_pressed("ui_up"): _cursor = (_cursor - 1 + pitems.size()) % pitems.size() elif event.is_action_pressed("ui_down"): _cursor = (_cursor + 1) % pitems.size() elif event.is_action_pressed("ui_accept"): match _cursor: 0: _do_resume() 1: _screen = 1; _cursor = 0 2: _do_quit_menu() 3: get_tree().quit() elif event.is_action_pressed("ui_cancel"): _do_resume() func _input_options(event: InputEvent) -> void: var opts := _opt_items() if event.is_action_pressed("ui_up"): _cursor = (_cursor - 1 + opts.size()) % opts.size() elif event.is_action_pressed("ui_down"): _cursor = (_cursor + 1) % opts.size() elif event.is_action_pressed("ui_left"): _change_option(_cursor, -1) elif event.is_action_pressed("ui_right"): _change_option(_cursor, 1) elif event.is_action_pressed("ui_accept"): if _cursor == opts.size() - 1: # Back _screen = 0; _cursor = 1 elif _cursor == opts.size() - 2: # Controls _screen = 2; _ctrl_cursor = 0; _ctrl_waiting = false else: _change_option(_cursor, 1) elif event.is_action_pressed("ui_cancel"): _close_options() func _input_controls(event: InputEvent) -> void: var labels := _ctrl_labels() var reset_idx := labels.size() var back_idx := labels.size() + 1 var total := labels.size() + 2 if _ctrl_waiting: if event is InputEventKey and event.pressed: var kc: int = event.physical_keycode 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 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 _rebind_key(action: String, physical_keycode: int) -> void: if not InputMap.has_action(action): return 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 volume Settings.master_volume = clamp(Settings.master_volume + dir * 0.1, 0.0, 1.0) Settings.apply_volume() 1: # Music volume 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 Settings.save_settings() func _do_resume() -> void: visible = false resume_requested.emit() func _close_options() -> void: _screen = 0 _cursor = 1 func _do_quit_menu() -> void: visible = false quit_to_menu_requested.emit() # ── 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_pause() 1: _draw_options() 2: _draw_controls() func _draw_pause() -> void: var bw := 300.0; var bh := 255.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("pause_title"), W * 0.5, by + 16.0, 10, COL_DIM) var pitems := _pause_items() var item_y := by + 55.0 for i in pitems.size(): var pulse := 0.6 + 0.4 * sin(_blink * 3.0) var col: Color if i == _cursor: 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.65) var prefix := "▶ " if i == _cursor else " " _draw_text_c(prefix + pitems[i], W * 0.5, item_y + i * 40.0, 14, col) _draw_text_c(Tr.hint("pause_footer"), W * 0.5, by + bh - 20.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 == _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 var p := pulse if is_sel else 0.4 _draw_text_c("[ %s ]" % opts[i], W * 0.5, row_y, 12, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, p)) else: var label_col: Color if is_sel: label_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse) else: label_col = 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 = "→" 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 _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 # ── 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_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