diff --git a/config/params.ini b/config/params.ini index 914b80a..d4662b4 100644 --- a/config/params.ini +++ b/config/params.ini @@ -443,6 +443,7 @@ buff_2= [advanced_options] ; startup hotkeys +select_runs_key=f6 restore_settings_from_backup_key=f7 settings_backup_key=f8 auto_settings_key=f9 diff --git a/src/config.py b/src/config.py index 92240b5..2b7cada 100644 --- a/src/config.py +++ b/src/config.py @@ -359,6 +359,7 @@ class Config: "auto_settings_key": self._select_val("advanced_options", "auto_settings_key"), "restore_settings_from_backup_key": self._select_val("advanced_options", "restore_settings_from_backup_key"), "settings_backup_key": self._select_val("advanced_options", "settings_backup_key"), + "select_runs_key": self._select_val("advanced_options", "select_runs_key"), "graphic_debugger_key": self._select_val("advanced_options", "graphic_debugger_key"), "hwnd_window_title": _default_iff(Config()._select_val("advanced_options", "hwnd_window_title"), ''), "hwnd_window_process": _default_iff(Config()._select_val("advanced_options", "hwnd_window_process"), ''), diff --git a/src/main.py b/src/main.py index a62c2fe..504bbf7 100644 --- a/src/main.py +++ b/src/main.py @@ -24,6 +24,7 @@ from game_controller import GameController from utils.graphic_debugger import GraphicDebuggerController from utils.misc import restore_d2r_window_visibility from utils.auto_settings import adjust_settings, backup_settings, restore_settings_from_backup +from ui.run_selector import open_run_selector @dataclass class Controllers(): @@ -104,6 +105,7 @@ def main(): print("\nFor gettings started and documentation\nplease read https://github.com/aeon0/botty\n") table = BeautifulTable() table.set_style(BeautifulTable.STYLE_BOX_ROUNDED) + table.rows.append([Config().advanced_options['select_runs_key'], "Select boss / farm runs"]) table.rows.append([Config().advanced_options['restore_settings_from_backup_key'], "Restore D2R settings from backup"]) table.rows.append([Config().advanced_options['settings_backup_key'], "Backup D2R current settings"]) table.rows.append([Config().advanced_options['auto_settings_key'], "Adjust D2R settings"]) @@ -114,6 +116,7 @@ def main(): print(table) print("\n") + keyboard.add_hotkey(Config().advanced_options['select_runs_key'], lambda: open_run_selector(Config())) keyboard.add_hotkey(Config().advanced_options['auto_settings_key'], lambda: adjust_settings()) keyboard.add_hotkey(Config().advanced_options['graphic_debugger_key'], lambda: start_or_stop_graphic_debugger(controllers)) keyboard.add_hotkey(Config().advanced_options['restore_settings_from_backup_key'], lambda: restore_settings_from_backup()) diff --git a/src/ui/run_selector.py b/src/ui/run_selector.py new file mode 100644 index 0000000..c8fcb0a --- /dev/null +++ b/src/ui/run_selector.py @@ -0,0 +1,135 @@ +""" +Run-selector GUI — opened via hotkey from the botty console. +Updates Config().routes and Config().routes_order in-memory; no file changes. +Takes effect on the next game (Bot is re-created each game in run_bot()). +""" +import tkinter as tk +from tkinter import messagebox + +# All supported runs in display order, with friendly labels +ALL_RUNS = [ + ("run_pindle", "Pindleskin (A5)"), + ("run_eldritch_shenk", "Eldritch + Shenk (A5)"), + ("run_nihlathak", "Nihlathak (A5)"), + ("run_trav", "Travincal (A3)"), + ("run_mephisto", "Mephisto (A3)"), + ("run_arcane", "Arcane Sanctuary (A2)"), + ("run_vizier", "Vizier (A4 CS)"), + ("run_diablo", "Diablo (A4 CS)"), + ("run_andariel", "Andariel (A1)"), + ("run_countess", "Countess (A1)"), + ("run_baal", "Baal (A5)"), +] + +# Internal key aliases: eldritch_shenk maps to shenk in routes_order +_ROUTE_ALIASES = { + "run_eldritch_shenk": "run_shenk", + "run_eldritch": "run_shenk", +} + + +def _currently_enabled(config) -> set: + """Return the set of display-keys (from ALL_RUNS) that are currently active.""" + enabled = set() + for key, _ in ALL_RUNS: + internal = _ROUTE_ALIASES.get(key, key) + if config.routes.get(key) or config.routes.get(internal): + enabled.add(key) + if internal in config.routes_order or key in config.routes_order: + enabled.add(key) + return enabled + + +def open_run_selector(config) -> None: + """ + Open the run-selector window (non-blocking from the hotkey perspective — + tkinter mainloop blocks the calling thread, which is fine since the hotkey + callback runs in its own thread via the keyboard library). + + Mutates config.routes and config.routes_order on confirmation and prints + the new selection to the console. + """ + currently_on = _currently_enabled(config) + + root = tk.Tk() + root.title("Botty — Select Runs") + root.resizable(False, False) + # Keep the window on top so it's visible over D2R + root.attributes("-topmost", True) + + # ── header ────────────────────────────────────────────────────────────── + header = tk.Frame(root, bg="#1a1a2e", pady=8) + header.pack(fill=tk.X) + tk.Label( + header, text="Select boss / farm runs", + bg="#1a1a2e", fg="#e2b96e", + font=("Segoe UI", 13, "bold"), + ).pack() + tk.Label( + header, text="(takes effect on next game)", + bg="#1a1a2e", fg="#888", + font=("Segoe UI", 8), + ).pack() + + # ── checkboxes ────────────────────────────────────────────────────────── + body = tk.Frame(root, bg="#16213e", padx=20, pady=12) + body.pack(fill=tk.BOTH) + + vars_: list[tuple[str, tk.BooleanVar]] = [] + for key, label in ALL_RUNS: + var = tk.BooleanVar(value=(key in currently_on)) + cb = tk.Checkbutton( + body, text=label, variable=var, + bg="#16213e", fg="#d4d4d4", selectcolor="#0f3460", + activebackground="#16213e", activeforeground="#e2b96e", + font=("Segoe UI", 10), + anchor="w", + ) + cb.pack(fill=tk.X, pady=1) + vars_.append((key, var)) + + # ── buttons ───────────────────────────────────────────────────────────── + btn_frame = tk.Frame(root, bg="#1a1a2e", pady=8) + btn_frame.pack(fill=tk.X) + + def on_apply(): + selected_keys = [k for k, v in vars_ if v.get()] + if not selected_keys: + messagebox.showwarning("No runs selected", "Please select at least one run.", parent=root) + return + + # Build new routes dict and order list + new_routes = {} + new_order = [] + for key in selected_keys: + new_routes[key] = True + internal = _ROUTE_ALIASES.get(key, key) + new_routes[internal] = True + if internal not in new_order: + new_order.append(internal) + + config.routes = new_routes + config.routes_order = new_order + + friendly = [label for k, label in ALL_RUNS if k in selected_keys] + print(f"\n[Run Selector] Runs updated: {', '.join(friendly)}") + print("(takes effect on next game)\n") + + root.destroy() + + def on_cancel(): + root.destroy() + + tk.Button( + btn_frame, text="Apply", command=on_apply, + bg="#e63946", fg="white", activebackground="#c1121f", + font=("Segoe UI", 10, "bold"), relief=tk.FLAT, padx=16, pady=4, + ).pack(side=tk.RIGHT, padx=12) + + tk.Button( + btn_frame, text="Cancel", command=on_cancel, + bg="#444", fg="white", activebackground="#333", + font=("Segoe UI", 10), relief=tk.FLAT, padx=12, pady=4, + ).pack(side=tk.RIGHT, padx=4) + + root.mainloop()