HerbTracker

Edit

-- ============================================================
-- Herb Tracker for LegendMUD (Mudlet)

-- Tallies herbs carried by your character: loose items flagged
-- "herb", and stacked herb bundles (type "BUNDLE", using bundle.count).
-- Also opens any CONTAINER items (bags) to catch herbs stored inside.

-- Recipes for poultices etc. are NOT exposed over GMCP (LegendMUD
-- deliberately excludes them), so this shows raw herb counts only -
-- you'll still need to do the "how many poultices" math yourself,
-- but the counts here should make that quick.
-- ============================================================

herbTracker = herbTracker or {}

-- ---- Reload-safe cleanup ----
if herbTracker.container then
herbTracker.container:hide()
herbTracker.container = nil
end
if herbTracker.invHandlerID then killAnonymousEventHandler(herbTracker.invHandlerID) end
if herbTracker.pollTimerID then killTimer(herbTracker.pollTimerID) end
if herbTracker.aliasIDs then
for _, id in pairs(herbTracker.aliasIDs) do killAlias(id) end
end

-- ---- CONFIG ----
herbTracker.config = {
x = "76%", y = "58%", width = "22%", height = "220px",
pollSeconds = 30, -- how often to auto-rescan
maxContainers = 20, -- safety cap on nested bags opened per scan
}

-- ---- STATE ----
herbTracker.state = {
scanning = false,
data = {}, -- herb name -> quantity
pendingContainers = 0,
processedOids = {}, -- containers already opened this scan
}

-- ---- Build window ----
herbTracker.container = Geyser.Container:new({
name = "herbTracker_container",
x = herbTracker.config.x, y = herbTracker.config.y,
width = herbTracker.config.width, height = herbTracker.config.height,
})

herbTracker.title = Geyser.Label:new({
name = "herbTracker_title", x = 0, y = 0, width = "100%", height = "18px",
}, herbTracker.container)
herbTracker.title:setStyleSheet("background-color: rgba(20,60,20,200); color:white; font-weight:bold;")
herbTracker.title:echo("Herb Inventory")

herbTracker.console = Geyser.MiniConsole:new({
name = "herbTracker_console", x = 0, y = "18px", width = "100%", height = "202px",
}, herbTracker.container)

-- ---- Rendering ----
local function renderHerbs()
herbTracker.console:clear()

local names = {}
for name in pairs(herbTracker.state.data) do table.insert(names, name) end
table.sort(names)

if #names == 0 then
herbTracker.console:cecho("<grey>No herbs found.\n")
return
end

local total = 0
for _, name in ipairs(names) do
local qty = herbTracker.state.data[name]
total = total + qty
herbTracker.console:cecho(string.format("<lime_green>%-28s <white>x%d\n", name, qty))
end
herbTracker.console:cecho(string.format("<grey>---\nTotal: %d\n", total))
end

-- ---- Item processing ----
local function processItem(item)
local isHerb = false
if item.flags then
for _, f in ipairs(item.flags) do
if f == "herb" then isHerb = true end
end
end

local key = item.short or item.name or "unknown herb"

if isHerb then
herbTracker.state.data[key] = (herbTracker.state.data[key] or 0) + 1
elseif item.type == "BUNDLE" and item.bundle then
herbTracker.state.data[key] = (herbTracker.state.data[key] or 0) + (item.bundle.count or 1)
end

-- Queue any container for a follow-up request, if we haven't opened it
-- yet this scan and we're under the safety cap.
if item.type == "CONTAINER" and item.oid
and not herbTracker.state.processedOids[item.oid]
and herbTracker.state.pendingContainers < herbTracker.config.maxContainers then
herbTracker.state.processedOids[item.oid] = true
herbTracker.state.pendingContainers = herbTracker.state.pendingContainers + 1
sendGMCP('char.inventory {"oid":"' .. item.oid .. '"}')
end
end

local function finishIfDone()
if herbTracker.state.pendingContainers <= 0 then
herbTracker.state.scanning = false
renderHerbs()
end
end

-- ---- GMCP handler (handles BOTH base inventory and container replies -
-- Mudlet routes both through the same char.inventory event; a container
-- reply is distinguished by having an "oid" key) ----
function herbTracker.onInventory()
local reply = gmcp.char and gmcp.char.inventory
if not reply then return end

local items = reply.items or {}
for _, item in ipairs(items) do
processItem(item)
end

if reply.oid then
-- this was a container-contents reply
herbTracker.state.pendingContainers = herbTracker.state.pendingContainers - 1
end

finishIfDone()
end

herbTracker.invHandlerID = registerAnonymousEventHandler("gmcp.char.inventory", "herbTracker.onInventory")

-- ---- Scan control ----
function herbTracker.scan()
if herbTracker.state.scanning then return end
herbTracker.state.scanning = true
herbTracker.state.data = {}
herbTracker.state.processedOids = {}
herbTracker.state.pendingContainers = 0
sendGMCP("char.inventory")
end

herbTracker.pollTimerID = tempTimer(herbTracker.config.pollSeconds, herbTracker.scan, true)

-- ---- Alias for a manual rescan ----
herbTracker.aliasIDs = {}
herbTracker.aliasIDs.scan = tempAlias("^herbs scan$", [[herbTracker.scan()]])

-- ---- Initial scan ----
herbTracker.scan()

cecho("<cyan>[HerbTracker] <white>Loaded. Auto-rescans every " ..
herbTracker.config.pollSeconds .. "s. Manual rescan: 'herbs scan'.\n")