Last active
August 21, 2025 02:52
-
-
Save alivesay/b7d4056cda84e54c12685c13b27b6917 to your computer and use it in GitHub Desktop.
Legion Script: Eat Everything!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # EatEverything.py | |
| # ----------------------------------------------------------------------------- | |
| # Eats all the defined food in your bag and drops bowls to ground. | |
| #------------------------------------------------------- | |
| import API | |
| import time | |
| # ================================ Config ================================ | |
| # Main gump position/size | |
| MAIN_X, MAIN_Y, MAIN_W, MAIN_H = 100, 100, 400, 260 | |
| # Persistence & behavior | |
| PERSIST_KEY = "EatEverything_Foods" # per-character persistent var | |
| FOOD_EAT_DELAY = 0.6 # seconds between uses | |
| TOO_FULL_LINES = ["you are simply too full", # journal lines that end eating | |
| "you are too full"] | |
| # Drop-after-eat settings | |
| TRASH_KEYWORDS = ["wooden bowl", "large wooden bowl", "pewter bowl", ] # case-insensitive contains | |
| OSI_SHARD = False # True on OSI shards | |
| MOVE_DELAY = 0.35 # seconds between item moves | |
| RECURSIVE_SEARCH = False # search subcontainers inside backpack? | |
| # Scatter config for ground drops (avoids item caps on a single tile) | |
| SCATTER_RADIUS = 2 # how far from your tile (in tiles) | |
| SCATTER_RANDOM = False # True = random scatter; False = spiral/round-robin | |
| # Layout inside gump | |
| HEADER_Y = 36 | |
| SA_X, SA_Y = 10, 58 | |
| SA_W, SA_H = 380, 150 | |
| BTN_Y = 214 | |
| # Grid sizing (with 2x icons) | |
| CELL_W, CELL_H = 92, 96 | |
| ICON_W, ICON_H = 88, 88 | |
| # =============================== UI State ============================== | |
| _gump = None | |
| _list_area = None | |
| _info_label = None | |
| _ctx_controls = [] # inline confirm overlay controls | |
| _food_graphics = set() # set[int] (graphics/types) | |
| _stop_requested = False | |
| # Double-click timing | |
| _last_click_t = 0.0 | |
| _last_click_key = None | |
| _DBL_WINDOW = 0.35 # seconds between clicks to count as double-click | |
| # ============================== Font Helper ============================ | |
| def Label(text, size=16, color="#FFFFFF", align="left", maxW=0): | |
| """Create Avadonia TTF label everywhere for consistent visuals.""" | |
| return API.CreateGumpTTFLabel( | |
| text, size, color, | |
| font="Avadonia", | |
| aligned=align, | |
| maxWidth=maxW if maxW else 0 | |
| ) | |
| # =========================== Persistence I/O =========================== | |
| def _load_foods(): | |
| saved = API.GetPersistentVar(PERSIST_KEY, "", API.PersistentVar.Char) | |
| if not saved: | |
| return | |
| for p in (x.strip() for x in saved.split(",") if x.strip()): | |
| try: | |
| val = int(p, 16) if p.lower().startswith("0x") else int(p) | |
| _food_graphics.add(val) | |
| except Exception: | |
| pass | |
| def _save_foods(): | |
| s = ",".join([f"0x{g:04X}" for g in sorted(_food_graphics)]) if _food_graphics else "" | |
| API.SavePersistentVar(PERSIST_KEY, s, API.PersistentVar.Char) | |
| # =============================== Utilities ============================= | |
| def _set_info(text: str): | |
| if _info_label: | |
| _info_label.SetText(text) | |
| def _close_context(): | |
| """Remove/destroy the inline confirmation overlay controls.""" | |
| global _ctx_controls | |
| if not _ctx_controls: | |
| return | |
| for ctl in _ctx_controls: | |
| try: | |
| ctl.Dispose() | |
| except Exception: | |
| pass | |
| _ctx_controls = [] | |
| def _attach_double_click(control, key, on_double): | |
| """ | |
| Simulate double-click using single-click timing. | |
| key: tuple identifying the same icon (e.g., ('food', gfx, idx)) | |
| """ | |
| def _on_click(): | |
| global _last_click_t, _last_click_key | |
| now = time.time() | |
| if _last_click_key == key and (now - _last_click_t) <= _DBL_WINDOW: | |
| _last_click_key = None | |
| _last_click_t = 0.0 | |
| on_double() | |
| else: | |
| _last_click_key = key | |
| _last_click_t = now | |
| _set_info("Double-click an icon to delete…") | |
| API.AddControlOnClick(control, _on_click, False) # any mouse button | |
| def _first_line_or_name(item) -> str: | |
| """Return item.Name or the first line of ItemNameAndProps, or empty string.""" | |
| try: | |
| n = (item.Name or "").strip() | |
| if n: | |
| return n | |
| except Exception: | |
| pass | |
| try: | |
| tip = API.ItemNameAndProps(item, True, 2) or "" | |
| return tip.split("\n", 1)[0].strip() | |
| except Exception: | |
| return "" | |
| def _is_trash(item) -> bool: | |
| base = _first_line_or_name(item).lower() | |
| return any(k in base for k in TRASH_KEYWORDS) | |
| # ---- scatter helpers ---- | |
| def _build_scatter_offsets(radius: int): | |
| offs = [] | |
| rmax = max(1, int(radius)) | |
| for r in range(1, rmax + 1): | |
| for x in range(-r, r + 1): offs.append((x, -r)) | |
| for y in range(-r + 1, r + 1): offs.append((r, y)) | |
| for x in range(r - 1, -r - 1, -1): offs.append((x, r)) | |
| for y in range(r - 1, -r, -1): offs.append((-r, y)) | |
| return [(1, 0)] if not offs else offs | |
| _SCATTER_OFFSETS = _build_scatter_offsets(SCATTER_RADIUS) | |
| _scatter_idx = 0 | |
| def _next_scatter_offset(): | |
| global _scatter_idx | |
| if SCATTER_RANDOM: | |
| i = API.Random.Next(len(_SCATTER_OFFSETS)) | |
| return _SCATTER_OFFSETS[i] | |
| off = _SCATTER_OFFSETS[_scatter_idx % len(_SCATTER_OFFSETS)] | |
| _scatter_idx += 1 | |
| return off | |
| def _drop_trash_to_ground() -> int: | |
| """Scan backpack and drop all matching trash items, scattered around your feet.""" | |
| items = API.ItemsInContainer(API.Backpack, RECURSIVE_SEARCH) | |
| if not items: | |
| return 0 | |
| dropped = 0 | |
| for it in items: | |
| try: | |
| if not _is_trash(it): | |
| continue | |
| while API.IsProcessingMoveQue() or API.IsGlobalCooldownActive(): | |
| API.Pause(0.15) | |
| ox, oy = _next_scatter_offset() | |
| API.MoveItemOffset(it, 0, ox, oy, 0, OSI_SHARD) # amt=0 => whole stack | |
| API.Pause(MOVE_DELAY) | |
| dropped += 1 | |
| except Exception: | |
| pass | |
| return dropped | |
| # ============================ Confirm Overlay ========================== | |
| def _open_delete_confirm_centered(gfx_hex: str): | |
| """Inline confirm overlay (centered within main gump).""" | |
| global _ctx_controls | |
| _close_context() | |
| box_w, box_h = 220, 96 | |
| ox = (MAIN_W // 2) - (box_w // 2) | |
| oy = (MAIN_H // 2) - (box_h // 2) | |
| dim = API.CreateGumpColorBox(0.35, "#000000") | |
| dim.SetRect(0, 0, MAIN_W, MAIN_H) | |
| _gump.Add(dim); _ctx_controls.append(dim) | |
| bg = API.CreateGumpColorBox(0.92, "#2E2E2E") | |
| bg.SetRect(ox, oy, box_w, box_h) | |
| _gump.Add(bg); _ctx_controls.append(bg) | |
| title = Label(f"Delete {gfx_hex}?", 18, "#FF6666", align="center", maxW=box_w) | |
| title.SetRect(ox, oy + 8, box_w, 22) | |
| _gump.Add(title); _ctx_controls.append(title) | |
| btn_yes = API.CreateSimpleButton("[Yes]", 80, 24) | |
| btn_yes.SetPos(ox + 20, oy + box_h - 36) | |
| def yes_cb(): | |
| try: | |
| g = int(gfx_hex, 16) | |
| if g in _food_graphics: | |
| _food_graphics.remove(g) | |
| _save_foods() | |
| _rebuild_list_area() | |
| _set_info(f"Removed {gfx_hex}.") | |
| except Exception: | |
| _set_info("Delete failed.") | |
| _close_context() | |
| API.AddControlOnClick(btn_yes, yes_cb) | |
| _gump.Add(btn_yes); _ctx_controls.append(btn_yes) | |
| btn_no = API.CreateSimpleButton("[No]", 80, 24) | |
| btn_no.SetPos(ox + box_w - 100, oy + box_h - 36) | |
| API.AddControlOnClick(btn_no, _close_context) | |
| _gump.Add(btn_no); _ctx_controls.append(btn_no) | |
| # =============================== Actions =============================== | |
| def action_add_food(): | |
| _set_info("Target a food item in your backpack…") | |
| targ = API.RequestAnyTarget(10) | |
| if not targ: | |
| _set_info("No target selected.") | |
| return | |
| try: | |
| serial = getattr(targ, "Serial", 0) | |
| except Exception: | |
| serial = 0 | |
| if not serial: | |
| _set_info("Could not read target serial.") | |
| return | |
| item = API.FindItem(serial) | |
| if not item: | |
| _set_info("Could not resolve item.") | |
| return | |
| try: | |
| if item.Container != API.Backpack: | |
| _set_info("Item is not in your backpack.") | |
| return | |
| except Exception: | |
| pass | |
| try: | |
| g = int(item.Graphic) | |
| except Exception: | |
| _set_info("Item has no graphic.") | |
| return | |
| name = _first_line_or_name(item) or "(unknown)" | |
| if g not in _food_graphics: | |
| _food_graphics.add(g) | |
| _save_foods() | |
| _rebuild_list_area() | |
| _set_info(f"Added {name} (0x{g:04X}).") | |
| else: | |
| _set_info(f"Already had {name} (0x{g:04X}).") | |
| def action_clear_all(): | |
| _food_graphics.clear() | |
| _save_foods() | |
| _rebuild_list_area() | |
| _set_info("Cleared all food types.") | |
| _close_context() | |
| def action_stop(): | |
| global _stop_requested | |
| _stop_requested = True | |
| _set_info("Stop requested…") | |
| def _finish_and_dump(prefix: str, total: int): | |
| """Shared epilogue: scatter trash on the ground and report.""" | |
| dropped = _drop_trash_to_ground() | |
| msg = f"{prefix} Ate {total}. Dumped {dropped} item(s) around your feet." | |
| _set_info(msg) | |
| API.SysMsg(msg, 32) | |
| def action_eat_now(): | |
| global _stop_requested | |
| _close_context() | |
| if not _food_graphics: | |
| _set_info("No food types saved.") | |
| return | |
| _stop_requested = False | |
| API.ClearJournal() | |
| total = 0 | |
| for gfx in sorted(_food_graphics): | |
| if _stop_requested: | |
| _finish_and_dump("Stopped.", total) | |
| return | |
| items = API.FindTypeAll(gfx, API.Backpack) # list[PyItem] or None | |
| if not items: | |
| continue | |
| for it in items: | |
| API.ProcessCallbacks() | |
| if _stop_requested: | |
| _finish_and_dump("Stopped.", total) | |
| return | |
| if API.IsGlobalCooldownActive() or API.IsProcessingUseItemQueue(): | |
| API.Pause(0.2) | |
| continue | |
| API.UseObject(it) | |
| API.Pause(FOOD_EAT_DELAY) | |
| total += 1 | |
| if API.InJournalAny(TOO_FULL_LINES): | |
| _finish_and_dump("Stopped (too full).", total) | |
| return | |
| _finish_and_dump("Done.", total) | |
| def action_close(): | |
| global _stop_requested | |
| _stop_requested = True | |
| _close_context() | |
| API.CloseGumps() | |
| API.Stop() | |
| # ============================= Scroll Grid ============================= | |
| def _rebuild_list_area(): | |
| """Rebuild the scroll area with 2x icons; double-click icon to delete.""" | |
| global _list_area | |
| try: | |
| if _list_area: | |
| _list_area.Dispose() | |
| except Exception: | |
| pass | |
| _list_area = API.CreateGumpScrollArea(SA_X, SA_Y, SA_W, SA_H) | |
| _gump.Add(_list_area) | |
| per_row = max(1, SA_W // CELL_W) | |
| foods = sorted(_food_graphics) | |
| if not foods: | |
| none_lbl = Label("(none added yet)", 16, "#AAAAAA") | |
| none_lbl.SetRect(0, 0, SA_W - 20, 20) | |
| _list_area.Add(none_lbl) | |
| return | |
| for idx, g in enumerate(foods): | |
| cx = (idx % per_row) * CELL_W | |
| cy = (idx // per_row) * CELL_H | |
| # Center the 2x icon in its cell (sprites anchor bottom-right) | |
| cell_center_x = cx + (CELL_W // 2) | |
| cell_center_y = cy + 44 | |
| icon_x = cell_center_x - (ICON_W // 2) | |
| icon_y = cell_center_y - (ICON_H // 2) | |
| pic = API.CreateGumpItemPic(g, ICON_W, ICON_H) | |
| pic.SetRect(icon_x, icon_y, ICON_W, ICON_H) | |
| _list_area.Add(pic) | |
| hexstr = f"0x{g:04X}" | |
| lbl = Label(hexstr, 14, "#FFD966", align="center", maxW=CELL_W) | |
| lbl.SetRect(cx, cy + CELL_H - 16, CELL_W, 16) | |
| _list_area.Add(lbl) | |
| # Double-click detector to open confirm overlay | |
| click_key = ("food", g, idx) | |
| def open_confirm(hx=hexstr): | |
| _open_delete_confirm_centered(hx) | |
| _attach_double_click(pic, click_key, open_confirm) | |
| # ============================== Build Gump ============================= | |
| def build_gump(): | |
| global _gump, _list_area, _info_label | |
| _gump = API.CreateGump(True, True, True) | |
| _gump.SetRect(MAIN_X, MAIN_Y, MAIN_W, MAIN_H) | |
| # Background | |
| bg = API.CreateGumpColorBox(0.85, "#1E1E1E") | |
| bg.SetRect(0, 0, MAIN_W, MAIN_H) | |
| _gump.Add(bg) | |
| # Title | |
| title = Label("Eat Everything!", 22, "#FFB000", align="center", maxW=MAIN_W) | |
| title.SetRect(0, 8, MAIN_W, 28) | |
| _gump.Add(title) | |
| # Static header (does not scroll) | |
| header = Label("Food Types (double-click to delete)", 16, "#FFFFFF") | |
| header.SetRect(10, HEADER_Y, SA_W, 20) | |
| _gump.Add(header) | |
| # Scroll grid | |
| _list_area = None | |
| _rebuild_list_area() | |
| # Buttons row | |
| W, H, GAP = 70, 24, 6 | |
| X0 = 10 | |
| x1 = X0 + (W + GAP) * 1 | |
| x2 = X0 + (W + GAP) * 2 | |
| x3 = X0 + (W + GAP) * 3 | |
| x4 = X0 + (W + GAP) * 4 | |
| btn_add = API.CreateSimpleButton("[Add]", W, H) | |
| btn_add.SetPos(X0, BTN_Y) | |
| API.AddControlOnClick(btn_add, action_add_food) | |
| _gump.Add(btn_add) | |
| btn_clear = API.CreateSimpleButton("[Clear]", W, H) | |
| btn_clear.SetPos(x1, BTN_Y) | |
| API.AddControlOnClick(btn_clear, action_clear_all) | |
| _gump.Add(btn_clear) | |
| btn_eat = API.CreateSimpleButton("[EAT!]", W, H) | |
| btn_eat.SetPos(x2, BTN_Y) | |
| API.AddControlOnClick(btn_eat, action_eat_now) | |
| _gump.Add(btn_eat) | |
| btn_stop = API.CreateSimpleButton("[STOP!]", W, H) | |
| btn_stop.SetPos(x3, BTN_Y) | |
| API.AddControlOnClick(btn_stop, action_stop) | |
| _gump.Add(btn_stop) | |
| btn_close = API.CreateSimpleButton("[Close]", W, H) | |
| btn_close.SetPos(x4, BTN_Y) | |
| API.AddControlOnClick(btn_close, action_close) | |
| _gump.Add(btn_close) | |
| # Status line | |
| _info_label = Label("", 14, "#C0C0C0") | |
| _info_label.SetRect(10, 240, 380, 16) | |
| _gump.Add(_info_label) | |
| API.AddGump(_gump) | |
| # ================================ Entry ================================ | |
| _load_foods() | |
| build_gump() | |
| _set_info("Add types, then EAT!") | |
| while True: | |
| API.ProcessCallbacks() | |
| API.Pause(0.1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment