Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bdab104
feat(targetbot): integrate HuntContext for enhanced targeting priorit…
mCodex Mar 10, 2026
b71bba3
fix(walking): reduce keyboard step threshold for improved responsiveness
mCodex Mar 10, 2026
f75b2c6
fix(pathfinding): enhance lookahead logic for goto action to improve …
mCodex Mar 10, 2026
517b9c7
feat(inspector): tabbed UI revamp + fix patterns always showing None
mCodex Mar 10, 2026
e04a01e
fix(cavebot): track lookahead target in regression detector to preven…
mCodex Mar 10, 2026
58795aa
feat(inspector): fix live tracking display + add Hunt Analyzer tabbed…
mCodex Mar 11, 2026
c7033aa
fix(cavebot): improve timeout and regression tolerance logic for navi…
mCodex Mar 11, 2026
3ca030a
fix(AttackBot, monster_inspector): add nil check for widget.id and fo…
mCodex Mar 11, 2026
02cfc44
fix(cavebot): reduce RECOVERY_IDLE_TIMEOUT from 5 min to 12s for quic…
mCodex Mar 11, 2026
8ad9d8c
fix(cavebot): enhance resetWaypointEngine to clear blacklists and pre…
mCodex Mar 11, 2026
9eb566c
fix(cavebot): enhance waypoint processing to skip blacklisted and flo…
mCodex Mar 11, 2026
27e7643
fix(monster_ai, monster_inspector, monster_patterns): enhance trackin…
mCodex Mar 11, 2026
1ad4b6b
fix(cavebot): improve handling of Z-level changes and waypoint proces…
mCodex Mar 11, 2026
d9d2c66
fix(monster_inspector): update formatting in live and patterns tab me…
mCodex Mar 11, 2026
d99a73b
fix(cavebot, waypoint_navigator): improve handling of floor changes a…
mCodex Mar 11, 2026
c33ab66
fix(cavebot): prevent oscillation near stairs by rejecting floor-chan…
mCodex Mar 11, 2026
72f15f3
fix(cavebot): enhance walking logic to prefer raw pathfinder directio…
mCodex Mar 11, 2026
d2cdf93
fix(cavebot): enhance lookahead logic to reject floor-change tiles an…
mCodex Mar 11, 2026
d00445d
refactor(AttackBot): improve UI structure and enhance control binding…
mCodex Mar 13, 2026
f5ed455
refactor(AttackBot, HealBot): streamline UI elements and enhance layo…
mCodex Mar 13, 2026
2928048
Enhance CaveBot navigation and combat mechanics
mCodex Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _Loader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ loadCategory("tools_legacy", {
loadCategory("analytics", {
"analyzer",
"smart_hunt",
"hunt_context",
"spy_level",
"supplies",
"depositer_config",
Expand Down
56 changes: 51 additions & 5 deletions cavebot/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -610,11 +610,57 @@ CaveBot.registerAction("goto", "#46e6a6", function(value, retries, prev)
walkParams.ignoreFields = true
end

-- ========== RESOLVE WALK TARGET ==========
-- Use Pure Pursuit lookahead when the route is built: walk to a point 10 tiles
-- ahead on the route instead of the exact waypoint position. This carries the
-- player through waypoints without stopping — arrival is detected by the
-- hasPassedWaypoint() check above (fires every 150ms during walk).
-- Floor-change waypoints bypass lookahead: they require exact tile precision.
-- Use Pure Pursuit lookahead only on clean (retry=0) attempts.
-- The lookahead is a geometric interpolation and may land on impassable tiles;
-- when blocked (retries > 0) fall back to destPos so progressive escalation
-- (ignoreCreatures, ignoreFields, attack blocker) works against a guaranteed-
-- walkable recorded position.
local walkTarget = destPos
if retries == 0
and not isFloorChange
and WaypointNavigator
and type(WaypointNavigator.isRouteBuilt) == "function"
and WaypointNavigator.isRouteBuilt()
and type(WaypointNavigator.getLookaheadTarget) == "function" then
local lookahead = WaypointNavigator.getLookaheadTarget(playerPos)
if lookahead and lookahead.z == playerPos.z then
local lhDist = math.max(
math.abs(lookahead.x - playerPos.x),
math.abs(lookahead.y - playerPos.y)
)
if lhDist >= 3 then
-- Gate 1: reject floor-change tiles (walkTo redirects to adjacent tile
-- with allowFloorChange=false, causing oscillation near the stair).
local lookaheadIsStair = (FloorItems and FloorItems.isFloorChangeTile)
and FloorItems.isFloorChangeTile(lookahead)
-- Gate 2: reject unreachable targets behind walls. The lookahead is a
-- geometric interpolation that ignores map topology; validate that A*
-- can actually find a path before committing. Uses ignoreCreatures
-- (creatures are transient) and precision=1 (don't need exact tile).
local lookaheadReachable = true
if not lookaheadIsStair then
local lhPath = findPath(playerPos, lookahead, maxDist, {
ignoreNonPathable = true,
ignoreCreatures = true,
precision = 1,
})
lookaheadReachable = lhPath and #lhPath > 0
end
if not lookaheadIsStair and lookaheadReachable then
walkTarget = lookahead
end
end
end
end

-- ========== ATTEMPT WALK ==========
-- Walk directly to destPos. The A* pathfinder computes optimal smooth paths
-- around obstacles. No lookahead target needed — smooth movement comes from
-- the widened arrival precision (player advances to next WP before stopping).
local walkResult = CaveBot.walkTo(destPos, maxDist, walkParams)
local walkResult = CaveBot.walkTo(walkTarget, maxDist, walkParams)
if walkResult == "nudge" then
-- Nudge only — count as retry so progressive strategies activate
if CaveBot.setCurrentWaypointTarget then
Expand All @@ -628,7 +674,7 @@ CaveBot.registerAction("goto", "#46e6a6", function(value, retries, prev)
CaveBot.setCurrentWaypointTarget(destPos, precision)
end
if CaveBot.setWalkingToWaypoint then
CaveBot.setWalkingToWaypoint(destPos)
CaveBot.setWalkingToWaypoint(walkTarget)
end
local walkDelay = dist <= 3 and 0 or dist <= 8 and 25 or dist <= 15 and 50 or 75
if walkDelay > 0 then CaveBot.delay(walkDelay) end
Expand Down
121 changes: 98 additions & 23 deletions cavebot/cavebot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ local function shouldSkipExecution()
local elapsed = now - walkState.walkStartTime
local HARD_TIMEOUT = 8000 -- 8 seconds absolute maximum
local expectedDur = walkState.walkExpectedDuration or 5000 -- Fallback 5s if nil
local softTimeout = expectedDur * 1.5
-- Pure Pursuit lookahead targets may be close in straight-line but far in actual
-- winding-corridor path length. Use a floor of 5s to avoid premature timeout.
local softTimeout = math.max(expectedDur * 2.0, 5000)

if elapsed > HARD_TIMEOUT or elapsed > softTimeout then
-- Walking too long — stop and let macro recompute
Expand Down Expand Up @@ -188,8 +190,11 @@ local function shouldSkipExecution()
if not walkState.minDist or curDist < walkState.minDist then
walkState.minDist = curDist
end
-- Scale regression tolerance: generous for U-shaped cave corridors
local tolerance = math.max(3, math.floor((walkState.walkStartDist or 20) * 0.6))
-- Pure Pursuit lookahead can be geometrically close (small Chebyshev) but
-- require a long winding path. Tolerance = max(startDist, 8) so regression
-- only fires when the player has gone FURTHER from the lookahead than they
-- started — a reliable signal that navigation truly went backward.
local tolerance = math.max(walkState.walkStartDist or 5, 8)
if walkState.minDist and curDist > walkState.minDist + tolerance then
-- Getting farther from closest point — stop and recompute
if player.stopAutoWalk then
Expand All @@ -201,11 +206,13 @@ local function shouldSkipExecution()
walkState.targetPos = nil
return false
end
-- Elapsed-progress check: if walking > 3s with zero distance decrease, stuck
-- Disabled for short walks (≤8 tiles) — the no-progress timer handles those
-- Elapsed-progress check: if walking > 6s with zero distance decrease, stuck.
-- 6s floor handles winding corridors where the player navigates away from the
-- lookahead before looping back around; HARD_TIMEOUT (8s) still catches true stucks.
-- Disabled for short walks (≤8 tiles) — the no-progress timer handles those.
if walkState.walkStartTime and walkState.walkStartDist and (walkState.walkStartDist or 99) > 8 then
local elapsed = now - walkState.walkStartTime
if elapsed > 3000 and curDist >= walkState.walkStartDist then
if elapsed > 6000 and curDist >= walkState.walkStartDist then
if player.stopAutoWalk then
pcall(player.stopAutoWalk, player)
end
Expand Down Expand Up @@ -321,7 +328,7 @@ WaypointEngine = {
recoveryJustFocused = false, -- suppress actionRetries reset after recovery focus
lastRecoverySearch = 0, -- throttle recovery searches (1/sec)
recoveryStartedAt = 0, -- when current recovery session began
RECOVERY_IDLE_TIMEOUT = 300000,-- 5 min: clear blacklists if completely stuck
RECOVERY_IDLE_TIMEOUT = 12000, -- 12s: clear blacklists if all WPs exhausted

-- Drift detection: proactive refocus to nearest WP when player drifts too far
-- NOTE: Corridor enforcement (WaypointNavigator) is now the primary drift detector.
Expand Down Expand Up @@ -640,7 +647,7 @@ local function runWaypointEngine()
return false
end

-- Reset engine state
-- Reset engine state (clears blacklists too — matches full-restart behavior)
resetWaypointEngine = function()
WaypointEngine.state = "NORMAL"
WaypointEngine.failureCount = 0
Expand All @@ -652,6 +659,7 @@ resetWaypointEngine = function()
WaypointEngine.wasTargetBotBlocking = false
WaypointEngine.postCombatUntil = 0
lastDispatchedChild = nil
clearWaypointBlacklist()
end

-- Cache TargetBot function references (avoid repeated table lookups)
Expand Down Expand Up @@ -726,22 +734,63 @@ cavebotMacro = macro(75, function() -- 75ms for smooth, responsive walking
if not buildWaypointCache then return end

-- Z-LEVEL CHANGE: Must run BEFORE shouldSkipExecution so stale delays
-- from the old floor can't block rescue. All Z changes handled identically
-- (no intended/accidental distinction).
-- from the old floor can't block rescue.
local playerPos = player and player:getPosition()
if playerPos and lastPlayerFloor and playerPos.z ~= lastPlayerFloor then
-- Clear ALL stale state from old floor
-- Determine whether this floor change was intentional (the focused WP is
-- already on the new floor, meaning goto navigated the stairs on purpose)
-- or accidental (player changed floor while targeting a different-floor WP).
local focusedChild = ui and ui.list and ui.list:getFocusedChild()
local focusedIdx = focusedChild and ui.list:getChildIndex(focusedChild)
local focusedWp = focusedIdx and waypointPositionCache[focusedIdx]
-- Case 1: WP is already on the new floor (e.g. the goto navigated to a stair
-- tile whose destination is explicitly recorded with the new floor's z value).
local intentional = focusedWp and focusedWp.isGoto and focusedWp.z == playerPos.z

-- Case 2: Stair-triggered change — focused WP is a floor-change tile on the
-- OLD floor. The goto walked the player onto a hole/ladder/rope and the
-- server teleported them to the adjacent floor. This is intentional; using
-- findNearestSameFloorGoto here would snap to a WP *before* the stair
-- entrance and create an infinite loop (Wyrm / Banuta routes).
local stairUsed = false
if not intentional and focusedWp and focusedWp.isGoto and focusedWp.z == lastPlayerFloor then
local wpPos = { x = focusedWp.x, y = focusedWp.y, z = focusedWp.z }
if FloorItems and FloorItems.isFloorChangeTile then
stairUsed = FloorItems.isFloorChangeTile(wpPos)
end
end

-- Always clear stale walk state regardless of intent
walkState.delayUntil = 0
cavebotMacro.delay = nil
clearWaypointBlacklist()
cavebotMacro.delay = nil
safeResetWalking()
resetWaypointEngine()
-- Focus nearest same-Z goto WP (pure distance, no path validation)
local child, idx = findNearestSameFloorGoto(playerPos, playerPos.z, CaveBot.getMaxGotoDistance())
if child then
print("[CaveBot] Z-change (" .. lastPlayerFloor .. "→" .. playerPos.z .. "): focusing WP" .. idx)
focusWaypointForRecovery(child, idx)

if intentional then
-- Intentional stair use: the current WP is already on this floor.
-- Don't refocus — let the goto action complete normally.
-- Clear blacklists so fresh state on new floor, but keep engine in NORMAL.
clearWaypointBlacklist()
WaypointEngine.failureCount = 0
print("[CaveBot] Z-change (" .. lastPlayerFloor .. "→" .. playerPos.z .. "): intentional, continuing WP" .. (focusedIdx or "?"))
elseif stairUsed then
-- Stair tile on the old floor caused this change (goto-driven stair use).
-- Don't snap to nearest — the goto for this WP will instantFail (floor
-- mismatch) and the Z-mismatch guard will then advance to the next
-- same-floor goto naturally, preserving correct route order.
clearWaypointBlacklist()
WaypointEngine.failureCount = 0
print("[CaveBot] Z-change (" .. lastPlayerFloor .. "→" .. playerPos.z .. "): stair at WP" .. (focusedIdx or "?") .. ", advancing via Z-mismatch guard")
else
-- Accidental floor change: reset fully and snap to nearest same-floor WP.
clearWaypointBlacklist()
resetWaypointEngine()
local child, idx = findNearestSameFloorGoto(playerPos, playerPos.z, CaveBot.getMaxGotoDistance())
if child then
print("[CaveBot] Z-change (" .. lastPlayerFloor .. "→" .. playerPos.z .. "): accidental, focusing WP" .. idx)
focusWaypointForRecovery(child, idx)
end
end

lastPlayerFloor = playerPos.z
return -- Consume this tick for the Z transition
end
Expand Down Expand Up @@ -906,7 +955,7 @@ cavebotMacro = macro(75, function() -- 75ms for smooth, responsive walking
for _ = 1, actionCount do
scanIdx = (scanIdx % actionCount) + 1
local wp = waypointPositionCache[scanIdx]
if wp and wp.isGoto and wp.z == playerPos.z then
if wp and wp.isGoto and wp.z == playerPos.z and not isWaypointBlacklisted(wp.child) then
focusWaypointForRecovery(wp.child, scanIdx)
found = true
break
Expand Down Expand Up @@ -1035,10 +1084,36 @@ cavebotMacro = macro(75, function() -- 75ms for smooth, responsive walking

local nextChild = uiList:getChildByIndex(nextIndex)
if nextChild then
-- Skip blacklisted (stuck/unreachable) waypoints
if isWaypointBlacklisted(nextChild) then
-- Skip blacklisted WPs, and floor-mismatched WPs only when they form a
-- *trailing rescue block* — i.e. from this WP to the end of the list there
-- are no same-floor WPs (Banuta-style rescue WPs appended at end of route).
-- Intentional multi-floor routes (Wyrm-style) always have same-floor WPs
-- further ahead and are therefore NOT skipped.
local function isTrailingRescueBlock(startIdx)
if not playerPos then return false end
for i = startIdx + 1, actionCount do
local wp = waypointPositionCache[i]
if wp and wp.isGoto and wp.z == playerPos.z then
return false -- same-floor WP exists ahead → intentional transition
end
end
return true -- no same-floor WP until end of route → rescue block
end

local function shouldSkipNext(child)
if isWaypointBlacklisted(child) then return true end
if child.action == "goto" and playerPos then
local idx2 = uiList:getChildIndex(child)
local wp2 = waypointPositionCache[idx2]
if wp2 and wp2.z ~= playerPos.z then
return isTrailingRescueBlock(idx2)
end
end
return false
end
if shouldSkipNext(nextChild) then
local skipped = 0
while isWaypointBlacklisted(nextChild) and skipped < actionCount do
while shouldSkipNext(nextChild) and skipped < actionCount do
nextIndex = (nextIndex % actionCount) + 1
nextChild = uiList:getChildByIndex(nextIndex)
skipped = skipped + 1
Expand Down
16 changes: 10 additions & 6 deletions cavebot/walking.lua
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ end
-- DISPATCH: KEYBOARD STEP vs AUTOWALK
-- ============================================================================

local KEYBOARD_THRESHOLD = 12
local KEYBOARD_THRESHOLD = 3

--- Walk a single keyboard step along the path. Returns true on success.
local function keyboardStep(path, playerPos, curIdx)
Expand Down Expand Up @@ -353,15 +353,19 @@ CaveBot.walkTo = function(dest, maxDist, params)
local manhattan = distX + distY

if manhattan <= 3 then
-- Close: precise keyboard steps
-- Close: precise keyboard steps. Prefer the raw pathfinder direction
-- (precision=0 must land on the exact tile); fall back to smoothed only
-- when the raw direction is blocked (creature, pushable).
local fcPath = PS().findPath(playerPos, dest, {ignoreNonPathable = true, precision = 0})
if fcPath and #fcPath > 0 then
local dir = fcPath[1]
local smoothed = PS().smoothDirection(dir, true) or dir
if canWalkDirection(smoothed) then
PS().walkStep(smoothed)
elseif canWalkDirection(dir) then
if canWalkDirection(dir) then
PS().walkStep(dir)
else
local smoothed = PS().smoothDirection(dir, true) or dir
if smoothed ~= dir and canWalkDirection(smoothed) then
PS().walkStep(smoothed)
end
end
end
return true
Expand Down
6 changes: 4 additions & 2 deletions core/AttackBot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,10 @@ end
widget:setText(params.description)
if params.itemId > 0 then
widget.spell:setVisible(false)
widget.id:setVisible(true)
widget.id:setItemId(params.itemId)
if widget.id then
widget.id:setVisible(true)
widget.id:setItemId(params.itemId)
end
end
widget:setTooltip(params.tooltip)
widget.remove.onClick = function()
Expand Down
Loading
Loading