Gem transmute: use cube from stash (PERSONAL tab) + ctrl+shift to send gems to cube

- _open_cube_from_stash(): switch to PERSONAL tab, right-click cube from stash
- _ctrl_shift_click_monitor(): ctrl+shift+click sends item from active stash tab
  directly into the open cube (no inventory roundtrip)
- convert_all_gems_to_perfect: open cube once per gem type from stash, switch to
  GEMS tab, ctrl+shift+click gem stack 3x -> into cube, transmute, ctrl+click
  result -> back to GEMS tab; close cube when type is done
- Keeps _open_cube_from_inventory as fallback for other callers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexpolo1
2026-06-17 07:45:47 +02:00
parent 0e9c18e3e7
commit fffc0a4b56

View File

@@ -199,14 +199,19 @@ class Transmute:
# ── New-UI (D2R named-tab stash) gem helpers ──────────────────────────
# Approximate screen position of the GEMS tab button in the new stash UI.
# The stash left panel spans x=33..415 at 1280x720. With 5 named tabs
# (PERSONAL, SHARED, GEMS, MATERIALS, RUNES) each ~76px wide:
# Named-tab stash UI (1280x720). 5 tabs ~76px wide starting at x=33:
# PERSONAL≈71 SHARED≈147 GEMS≈223 MATERIALS≈299 RUNES≈375
# Hermes measured GEMS at ~175 so we split the difference at ~200, y≈100.
PERSONAL_TAB_X = 71
GEMS_TAB_X = 200
GEMS_TAB_Y = 100
def _switch_to_personal_tab(self) -> None:
x, y = convert_screen_to_monitor((self.PERSONAL_TAB_X, self.GEMS_TAB_Y))
mouse.move(x, y)
self._wait()
mouse.click("left")
wait(0.4, 0.5)
def _switch_to_gems_tab(self) -> None:
x, y = convert_screen_to_monitor((self.GEMS_TAB_X, self.GEMS_TAB_Y))
mouse.move(x, y)
@@ -214,6 +219,18 @@ class Transmute:
mouse.click("left")
wait(0.4, 0.5)
def _open_cube_from_stash(self) -> bool:
"""Switch to PERSONAL tab and right-click the Horadric Cube to open its UI."""
self._switch_to_personal_tab()
if (match := detect_screen_object(ScreenObjects.CubeStash)).valid:
mouse.move(*match.center_monitor)
self._wait()
mouse.click("right")
wait(0.3, 0.4)
return True
Logger.error("Cube not found in PERSONAL stash tab")
return False
def _open_cube_from_inventory(self) -> bool:
if (match := wait_until_visible(ScreenObjects.CubeInventory, 3.0)).valid:
mouse.move(*match.center_monitor)
@@ -234,6 +251,19 @@ class Transmute:
keyboard.release('ctrl')
self._wait()
def _ctrl_shift_click_monitor(self, x: int, y: int, label: str = "") -> None:
"""Ctrl+Shift+Click — moves item from current stash tab into the open cube."""
Logger.info(f" ctrl+shift+click {label} @ monitor ({x},{y})")
mouse.move(x, y)
self._wait()
keyboard.send('shift', do_release=False)
keyboard.send('ctrl', do_release=False)
self._wait()
mouse.click("left")
keyboard.release('ctrl')
keyboard.release('shift')
self._wait()
def _gems_tab_pull_3(self, gems_in: list) -> int:
"""Find a gem type in the GEMS tab (stacked) and ctrl+click it 3 times to pull 3 gems."""
img = grab()
@@ -477,61 +507,49 @@ class Transmute:
total = sum(n for n, *_ in plan)
Logger.info(f"convert_all_gems: {len(plan)} gem types to process, {total} total transmutes planned")
# Step 3: execute — for each planned gem type, do N transmutes
# Step 3: execute.
# Flow per gem type: open cube from PERSONAL stash tab once, switch to
# GEMS tab (cube UI stays open), then for each batch:
# ctrl+shift+click gem stack 3x -> gems land directly in open cube
# transmute
# ctrl+click result from cube -> goes back to active GEMS tab
# Close cube when done with this gem type.
done = 0
for n_transmutes, gem_template, gems_out, tier_name in plan:
Logger.info(f"convert_all_gems: executing {n_transmutes}x {gem_template}")
for i in range(n_transmutes):
self._switch_to_gems_tab()
Logger.info(f"convert_all_gems: {n_transmutes}x {gem_template}")
# Pull 3 of this specific gem from GEMS tab.
# GEMS tab uses stacked slots: one slot = many gems.
# Click the same stack position 3 times to pull 3 individual gems.
# Open cube from PERSONAL tab
if not self._open_cube_from_stash():
Logger.error(" cube not found in stash — aborting")
return
# Switch to GEMS tab (cube UI stays open)
self._switch_to_gems_tab()
for i in range(n_transmutes):
img = grab()
roi = Config().ui_roi["left_inventory"]
matches = template_finder.search_all(gem_template, img, threshold=0.85, roi=roi)
if not matches:
Logger.warning(f" no {gem_template} found in GEMS tab, stopping this type")
Logger.info(f" no {gem_template} left in GEMS tab, stopping")
break
Logger.info(f" [{i+1}/{n_transmutes}] pulling 3x {gem_template} from stack @ {matches[0].center_monitor}")
Logger.info(f" [{i+1}/{n_transmutes}] ctrl+shift+click {gem_template} x3 -> cube @ {matches[0].center_monitor}")
for _ in range(3):
self._ctrl_click_monitor(*matches[0].center_monitor, label=gem_template)
self._ctrl_shift_click_monitor(*matches[0].center_monitor, label=gem_template)
wait(0.15, 0.2)
# Open cube, load gems via search_all on right inventory, transmute, pick result
if not self._open_cube_from_inventory():
Logger.error(" cube missing from inventory — aborting")
return
right_roi = Config().ui_roi["right_inventory"]
img = grab()
inv_hits = template_finder.search_all(gem_template, img, threshold=0.80, roi=right_roi)
loaded = 0
for m in inv_hits[:3]:
Logger.info(f" inv->cube: {gem_template}")
self._ctrl_click_monitor(*m.center_monitor, label=gem_template)
loaded += 1
Logger.info(f" transmuting ({loaded} gems in cube)")
Logger.info(f" [{i+1}/{n_transmutes}] transmuting")
self.transmute()
wait(0.5, 0.5)
# Find result in cube and take it
# ctrl+click result from cube -> goes to active GEMS tab
if not self._pick_result_from_cube(gems_out):
Logger.warning(f" result not found in cube, skipping pickup")
self.close_cube()
wait(0.3, 0.4)
# Put result back to GEMS tab via search_all on right inventory
self._switch_to_gems_tab()
img = grab()
result_hits = []
for gem in gems_out:
for m in template_finder.search_all(gem, img, threshold=0.80, roi=right_roi):
result_hits.append((gem, m))
Logger.info(f" putting {len(result_hits)} result gem(s) back to GEMS tab")
for gem, m in result_hits:
self._ctrl_click_monitor(*m.center_monitor, label=f"result {gem}")
Logger.warning(f" [{i+1}/{n_transmutes}] result not found in cube")
done += 1
self.close_cube()
wait(0.3, 0.4)
Logger.info(f"convert_all_gems: done — {done}/{total} transmutes completed")
def should_transmute(self) -> bool: