UO Sagas: Halesluker's Sagas Bot

Created: 19 days ago on 05/26/2025, 09:02:23 PMUpdated: about 16 hours ago on 06/13/2025, 06:18:12 PM
FileType: LUA
Size: 60256
Category: Bot, Script

Description: Rolling my own bot, as everyone else should!

Currently has:

Simple player detection,

Bandaging and using health pots on low health

Disarm detection and rearming,

Cure poison with potion or bandage,

Scavenging,

Buffs, Song of Healing

Pop pouch,

Moongate,

Stamina drinking

Supports manually curing or bandaging while its running.

Configure what you want at the top.

Will update the list when more is added.

https://i.imgur.com/yKAkSA7.gif

-- Scripted by Halesluker, Knurr on Ultima Online Sagas --- --- CONFIG --- -- TODO: Use immutable fields and values local Config = { -- Adjust the ActionWaitTime if you experience issues, set it longer, ex. 1500 on high ping ActionWaitTime = 1000, -- in milliseconds, how long to wait for actions like using items, targeting etc. EnableOverheadMessages = true, -- Enables overhead messages, if false then messages will be printed in journal EnableCure = true, -- Cures poison with potions or bandages EnableBandage = true, -- Bandages player if HP is below BandageHP or if poisoned and no cure potions EnableScavenging = false, -- Scavenges items from the ground, only arrows, add more if needed EnablePopPouch = true, -- Pops pouch if you are paralyzed in PvP mode EnableDisarmDetection = true, -- Rearms your weapon if you are disarmed -- DO NOT STORE POTIONS IN BANK IF YOU ENABLE THIS, SCRIPT WILL LOOP TO TRY TO DRINK THEM EnableBuffs = true, -- Enables buffs like Stamina, Strength, Agility, Food and Song of Healing EnableMoongate = true, -- Opens moongate if you are near one EnableHunting = false, -- Alerts you when a player from the hunt list is visible EnableCancel = false, -- Stops the script from running by saying a unique command EnableEscape = false, -- Saying escape and the escape command in the escape config will port you EnableOpenCorpses = false, -- Opens corpses if you are near one, even if previously closed, not implemented yet EnableIdentification = false, -- Starts identifcation after standing still for 30 seconds, not implemented yet Debug = false, DebugTick = 1000, -- Overrides MainTick in debug mode DatePattern = "%H:%M:%S", InfoTextColor = 88, WarningTextColor = 34, ErrorTextColor = 53, DebugTextColor = 1153, MainTick = 60, -- in milliseconds JournalTick = 0, -- milliseconds, zero means immediate } local CancelConfig = { Command = "YOUR UNIQUE COMMAND HERE", -- The command to say, make it unique to you } local CureConfig = { Potions = {0x0f07}, Tick = 0, -- milliseconds, zero means immediate } local BandageConfig = { BandageHP = 99, -- in percentage, when to use bandage Bandages = { 0x00e21 }, HealPotionHP = 20, -- in percentage, when to use heal potion HealPotions = { 0x0f0c }, HealthPotionRecentTime = 15 * 1000, OverheadPauseTime = 0, -- in ms, zero means only when beginning bandage WarningPauseTime = 60 * 1000 } local HuntConfig = { Players = { "oFrizz", "FloodgateUO", "Lespunk Strange", "Vector", "BTK", "RDY", "BRG", "URK" }, AlertPauseTime = 10 * 1000 -- alert once per 10 seconds } local DisarmConfig = { AlwaysRearm = false, -- rearm without moving, warning will spam messages if you drag from hands AlertPauseTime = 10 * 1000, -- alert once per 10 seconds } local ScavengeConfig = { Tick = 0, -- milliseconds, zero means immediate items = { 0x0F3F } } local GateConfig = { LookupPauseTime = 1000, -- in milliseconds, how often to look for moongate gumpId = 585180759, OverheadPauseTime = 5 * 1000, } local EscapeConfig = { Command = "I shall return!", -- The command to say, make it unique to you Callback = function() -- Use the assistant and record your way of escaping and paste it below Player.UseObject('1110433901') Gumps.WaitForGump(1498407526, 1000) Gumps.PressButton(1498407526, 26) return true end, } local BuffsConfig = { buffs = { SongOfHealing = { Enable = true, FailWait = 30 * 1000, -- in ms, how long to retry if already under effects by manual cast }, Stamina = { Enable = true, DrinkBelowPercentage = 50, -- in percentage, when to drink stamina potion Potions = {0x0f0b}, }, Strength = { Enable = true, Potions = {0x0f09}, }, Agility = { Enable = true, Potions = {0x0f08}, }, NightSight = { Enable = false, }, Food = { Enable = true, EatCooldown = 15 * 60 * 1000, -- in ms, how often to eat food }, }, debuffs = { Peacemaking = { Enable = false, }, }, Instruments = {"Drum", "Lute", "Tambourine", "Lap Harp" } } --- --- STATE --- -- TODO: Use immutable fields and mutable values local state = { currentTickTime = math.floor(os.clock() * 1000), lastJournalTickTime = 0, flaggedForPvp = false, disarmed = nil, cure = { useBandages = false, }, bandage = { lastTickTime = nil, lastOverheadTime = 0, isBandaging = false, lastPotionDrinkTime = 0, }, hunt = { lastTickTime = nil, lastOverheadTime = 0, }, scavenge = { lastTickTime = nil, }, -- TODO: Implement states below inventory = { bandages = nil, healthPotions = nil, curePotions = nil, agilityPotions = nil, strengthPotions = nil, nightSightPotions = nil, }, buffs = { songOfHealing = { isActive = false, startTime = nil, endTime = nil, duration = 163 * 1000, -- Need to calculate based on music skill? lastWarningTickTime = nil, instrument = nil, }, strength = { lastStr = 999, isActive = false, startTime = nil, endTime = nil, duration = 120 * 1000, -- Need to calculate based on alchemy skill }, agility = { lastDex = 999, isActive = false, startTime = nil, endTime = nil, duration = 120 * 1000, -- Need to calculate based on alchemy skill }, nightSight = { isActive = false, startTime = nil, endTime = nil, duration = 120, }, }, debuffs = { peacemaking = { enemies = { --[[ { serial = "12345678", -- Serial ID of the enemy name = "Enemy Name", -- Name of the enemy lastTickTime = timestamp, -- Last time we checked this enemy endTime = timestamp, -- When the Peacemaking effect ends }, ]] }, isActive = false, startTime = nil, endTime = nil, duration = 163, -- Need to calculate based on music skill? lastWarningTickTime = nil, }, }, moongate = { lastTickTime = nil, lastOverheadTime = nil, serial = nil, gateChangeDistance = 0, }, food = { lastEatTime = nil, } } --- --- DATA --- local data = { foods = { [0x1602] = "Bowl Of Potatoes", [0x1601] = "Bowl Of Peas", [0x1600] = "Bowl Of Lettuce", [0x15ff] = "Bowl Of Corn", [0x15fc] = "Bowl Of Peas", [0x15fb] = "Bowl Of Lettuce", [0x15fa] = "Bowl Of Corn", [0x15f9] = "Bowl Of Carrots", [0x9ec] = "Jar Of Honey", [0x9bb] = "Roast Pig", [0x1606] = "Tomato Soup", [0x1604] = "Bowl Of Stew", [0x15f8] = "Wooden Bowl", [0x1608] = "Chicken Leg", [0x160a] = "Leg Of Lamb", [0x9b7] = "Cooked Bird", [0x97e] = "Wheel Of Cheese", [0x9eb] = "Muffins", [0x9e9] = "Cake", [0x1041] = "Baked Apple Pie", [0x103b] = "Bread Loaf", [0x09d0] = "Apple", [0x09d1] = "Grape Bunch", [0x09d2] = "Peach", [0x0994] = "Pear", [0x0c5c] = "Watermelon", [0x0c64] = "Gourd", [0x0c6a] = "Pumpkin", [0x0c6d] = "Onion", [0x0c70] = "Head Of Lettuce", [0x0c72] = "Squash", [0x0c74] = "Honeydew Melon", [0x0c78] = "Carrot", [0x0c79] = "Canteloupe", [0x0c7b] = "Head Of Cabbage", [0x09ad] = "Pitcher Of Milk", [0x09b5] = "Eggs" } } --- --- PRINT UTILS --- local function message(text, color, sendFunc) if not text or (type(text) ~= "string" and type(text) ~= "table") then text = "Variabel message to print needs to be a string or table" end if not color or type(color) ~= "number" then color = Config.InfoTextColor end if type(text) == "table" then text = table.concat(text, ", ") end sendFunc(text, color, Player.Serial) end local function print(text, color) message(text, color, Messages.Print) end local function overhead(text, color) local printFn = Messages.Overhead if not Config.EnableOverheadMessages then printFn = Messages.Print end message(text, color, printFn) end local function info(text) overhead(text, Config.InfoTextColor) end local function warning(text) overhead(text, Config.WarningTextColor) end ---@diagnostic disable-next-line: unused-local, unused-function local function error(text) overhead(text, Config.ErrorTextColor) end local function debug(text) if not Config.Debug then return end local ok, timestamp = pcall(function() return os.date(Config.DatePattern, os.time()) .. "." .. string.format("%03d", os.clock() * 1000 % 1000) end) if not ok then timestamp = os.time() end if Config.DebugTick > Config.MainTick then Pause(Config.DebugTick - Config.MainTick) end print("[" .. timestamp .. "] " .. text, Config.DebugTextColor) end --- --- UTILITY FUNCTIONS --- local function pauseUntil(callback, interval, timeout) local startTime = math.floor(os.clock() * 1000) while (math.floor(os.clock() * 1000) - startTime) < timeout do if callback() then return true end Pause(interval) end return false end local function findInInventory(itemType) local items = Items.FindByFilter({ graphics = itemType, onground = false }) if not items or #items == 0 then return nil end -- Filter out items that are not on player for i = #items, 1, -1 do if items[i].RootContainer ~= Player.Serial then table.remove(items, i) end end return items end ---@diagnostic disable-next-line: unused-local, unused-function local function findInSurroundings(itemType, range) local items = Items.FindByFilter({ graphics = itemType, range = range }) if not items or #items == 0 then return nil end return items[1] end local function exceedsDuration(startTime, endTime, duration) if startTime == nil then return true end if endTime == nil then endTime = math.floor(os.clock() * 1000) end if duration == nil then duration = 1000 end return (endTime - startTime) >= duration end local function bandageEndTime(start) local delayMs = math.ceil((9.0 + 0.85 * ((130 - Player.Dex) / 20)) * 1000) local baseTime = start or state.currentTickTime or math.floor(os.clock() * 1000) return baseTime + delayMs end local function disarmPlayer() if state.disarmed then debug("Player is already disarmed, skipping disarm.") return state.disarmed end local disarmState = { weapon = nil, hand = nil } local weapon = Items.FindByLayer(2) if weapon then Player.ClearHands("right") -- Wait for hands to be cleared Pause(Config.ActionWaitTime) end disarmState = { weapon = weapon, hand = function() return Items.FindByLayer(2) end } state.disarmed = disarmState return disarmState end local function rearmPlayer() if not state.disarmed or not state.disarmed.weapon then debug("Player is not disarmed, skipping rearm.") return end while state.disarmed.hand() == nil do Player.Equip(state.disarmed.weapon.Serial) Pause(Config.ActionWaitTime) -- Wait for weapon to be equipped end state.disarmed = nil end --- --- CURE --- local function cure() if not Config.EnableCure then return end debug("Trying to cure...") if Player.IsHidden then debug("Player is hiding, skipping cure.") return end debug("Player is poisoned: " .. tostring(Player.IsPoisoned)) if not Player.IsPoisoned then if state.cure.isPoisoned then info("Cured") end state.cure.isPoisoned = false state.cure.useBandages = false debug("Player is not poisoned") return end state.cure.isPoisoned = true if state.cure.useBandages then debug("Using bandages to cure poison.") return end warning("Poisoned") local tick = CureConfig.Tick > 0 and CureConfig.Tick or 1000 if not exceedsDuration(state.bandage.lastPotionDrinkTime, state.currentTickTime, tick) then debug("A potion was recently used, skipping.") return end debug("Player is poisoned, looking for cure potion.") local potions = findInInventory(CureConfig.Potions) if not potions or #potions == 0 then if not state.cure.useBandages then warning("No cure potions") end state.cure.useBandages = true return else state.cure.useBandages = false debug("Found " .. #potions .. " cure potions in inventory.") end local alchemySkill = Skills.GetValue("Alchemy") if alchemySkill and alchemySkill < 80 then debug("Alchemy skill is below 80, disarming weapon to use health potion.") disarmPlayer() end local hasUsedPotion = false for _, potion in ipairs(potions) do if not potion and not potion.Serial then goto endcure end debug("Using cure potion: " .. (potion.Name or "No Potion Name")) if not Player.UseObject(potion.Serial) then debug("Failed to use cure potion: " .. (potion.Name or "No Potion Name")) goto endcure end local cured = pauseUntil(function () return Journal.Contains("You feel cured of poison") end, 50, 500) if cured then debug("Successfully cured poison.") hasUsedPotion = true state.bandage.lastPotionDrinkTime = state.currentTickTime break end :: endcure :: end if not hasUsedPotion then warning("Cure failed") state.cure.useBandages = true end debug("Cure process completed.") end --- --- BANDAGE --- local function getHpPercentage() return (Player.Hits / Player.HitsMax) * 100 end local function getHealthPotions() local healthPotions = findInInventory(BandageConfig.HealPotions) if not healthPotions or #healthPotions == 0 then if exceedsDuration(state.bandage.lastOverHeadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then warning("No health potions") state.bandage.lastOverHeadTime = state.currentTickTime end end return healthPotions end local function drinkHealthPotion(forced) forced = forced or false if not Config.EnableBandage then return end local playerHpPercentage = getHpPercentage() if not forced and (playerHpPercentage > BandageConfig.HealPotionHP) then debug("Player HP is above health potion threshold, skipping health potion.") return end debug("Player HP is below health potion threshold, drinking health potion.") if Player.IsPoisoned then debug("Player is poisoned, skipping health potion.") return end debug("Drinking health potion...") local healthPotions = getHealthPotions() if not healthPotions or #healthPotions == 0 then if exceedsDuration(state.bandage.lastOverHeadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then warning("No health potions") state.bandage.lastOverHeadTime = state.currentTickTime end return end if not exceedsDuration(state.bandage.lastPotionDrinkTime, state.currentTickTime, BandageConfig.HealthPotionRecentTime) then debug("Health potion recently drunk, skipping.") return end local alchemySkill = Skills.GetValue("Alchemy") if alchemySkill and alchemySkill < 80 then debug("Alchemy skill is below 80, disarming weapon to use health potion.") disarmPlayer() end for _, potion in ipairs(healthPotions) do if not potion or not potion.Serial then goto enddrink end debug("Using health potion: " .. (potion.Name or "No Potion Name")) if not Player.UseObject(potion.Serial) then debug("Failed to use health potion: " .. (potion.Name or "No Potion Name")) goto enddrink end local drank = pauseUntil(function () return Journal.Contains("You feel better") end, 50, Config.ActionWaitTime) if drank then debug("Successfully drank health potion.") info("Drank health pot") state.bandage.lastPotionDrinkTime = state.currentTickTime break end :: enddrink :: end debug("Health potion process completed.") end local function bandage() if Config.EnableBandage == false then return end debug("Bandage running with main tick time") if Player.IsHidden then debug("Player is hiding, skipping bandage.") return end drinkHealthPotion() local bandageState = state and state.bandage if bandageState.isBandaging then debug("Already healing, skipping bandage.") local timeLeft = bandageState.bandageTimeEnd - state.currentTickTime if timeLeft > 0 and BandageConfig.OverheadPauseTime > 0 then if exceedsDuration(bandageState.lastOverHeadTime, state.currentTickTime, BandageConfig.OverheadPauseTime) then local countdown = math.floor(timeLeft / 1000) if countdown >= 1 then info("Bandaging " .. countdown .. "s") end bandageState.lastOverHeadTime = state.currentTickTime end end if state.currentTickTime > bandageState.bandageTimeEnd then bandageState.isBandaging = false end return end debug("Checking if bandaging is needed...") if Player.IsDead then debug("Cannot bandage while dead.") return end if Journal.Contains("You begin applying the bandages") then debug("Already manually bandaging, skipping.") bandageState.bandageTimeEnd = bandageEndTime(state.currentTickTime) bandageState.isBandaging = true return end local playerHpPercentage = getHpPercentage() if not Player.IsPoisoned and (playerHpPercentage >= BandageConfig.BandageHP) then debug("Player not poisoned or HP is above threshold, no bandage needed.") return end if Player.IsPoisoned and bandageState.useBandages then debug("Using bandages due to previous poison.") info("Curing with bandage") bandageState.useBandages = false end debug("Looking for bandages...") local bandages = findInInventory(0x00E21) if not bandages or #bandages == 0 then if exceedsDuration(bandageState.lastOverheadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then warning("No bandages found") bandageState.lastOverheadTime = state.currentTickTime end return end debug("Attempting to bandage...") -- Print size of bandages or 1 if only one item local bandageCount = #bandages > 1 and #bandages or 1 debug("Bandaging with " .. bandageCount .. " bandage(s)...") -- Loop in case you got item from bank or other "player" container local isBandagingSuccessful = false for _, item in ipairs(bandages) do if not Player.UseObject(item.Serial) then debug("Unable to use bandage item.") goto continue end if not Targeting.WaitForTarget(500) then debug("Targeting failed, unable to bandage.") goto continue end if Targeting.TargetSelf() then isBandagingSuccessful = true break end :: continue :: end if not isBandagingSuccessful then debug("Failed to bandage, the bandages found are probably in bank?") return end local bandaging = pauseUntil(function() return Journal.Contains("You begin applying the bandages.") end, 50, 500) if not bandaging then bandageState.isBandaging = false bandageState.lastBandageStart = nil return end debug("Bandaging") if BandageConfig.OverheadPauseTime == 0 then info("Bandaging...") bandageState.lastOverHeadTime = state.currentTickTime end bandageState.isBandaging = bandaging bandageState.lastBandageStart = state.currentTickTime bandageState.bandageTimeEnd = bandageEndTime(state.bandage.lastBandageStart) end --- --- PLAYER DETECTION --- local function hunt() if Config.EnableHunting == false then return end debug("Hunting for players...") state.hunt.lastTickTime = state.currentTickTime local isWarningTimeExceeded = exceedsDuration(state.hunt.lastOverheadTime, state.currentTickTime, HuntConfig.AlertPauseTime) if not isWarningTimeExceeded then debug("Last player detection notification was too recent, skipping") return end for index, playerName in ipairs(HuntConfig.Players) do debug("Looking for player " .. index .. "... ") if Journal.Contains(playerName) then info("Hunted player " .. playerName) state.hunt.lastOverheadTime = state.currentTickTime end end end --- --- DISARM DETECTION --- -- TODO: Use config and state local LAYER_ONE_HANDED = 1 local LAYER_TWO_HANDED = 2 local lastRightHand = nil local lastLeftHand = nil local disarm = { x = 0, y = 0 } local function disarmed() if not Config.EnableDisarmDetection then return end debug("Disarm detection running") if lastRightHand == nil then local rightHand = Items.FindByLayer(LAYER_ONE_HANDED) if not rightHand then debug("No weapon in right hand") else lastRightHand = rightHand debug("Weapon " .. (rightHand.Name or "No Weapon Name") .. " used as right hand") end else local rightHand = Items.FindByLayer(LAYER_ONE_HANDED) if rightHand and rightHand.Serial ~= lastRightHand.Serial then debug("Right hand weapon changed from: " .. (lastRightHand.Name or "No Weapon Name") .. " to: " .. (rightHand.Name or "No Weapon Name")) -- Since user changed weapon we are not disarmed and need to reset both hands in case of two hander lastRightHand = nil lastLeftHand = nil disarm.x = 0 disarm.y = 0 end end if lastLeftHand == nil then local leftHand = Items.FindByLayer(LAYER_TWO_HANDED) if not leftHand then debug("No weapon in left hand") else lastLeftHand = leftHand debug("Weapon " .. (leftHand.Name or "No Weapon Name") .. " used as left hand") end else local leftHand = Items.FindByLayer(LAYER_TWO_HANDED) if leftHand and leftHand.Serial ~= lastLeftHand.Serial then debug("Left hand weapon changed from: " .. (lastLeftHand.Name or "No Weapon Name") .. " to: " .. (leftHand.Name or "No Weapon Name")) -- Since user changed weapon we are not disarmed and need to reset both hands in case of two hander lastLeftHand = nil lastRightHand = nil disarm.x = 0 disarm.y = 0 end end local isDisarmed = disarm.x > 0 or disarm.y > 0 local playerMoved = (Player.X ~= disarm.x or Player.Y ~= disarm.y) or DisarmConfig.AlwaysRearm if isDisarmed and playerMoved then -- Right hand local alreadyHasRightHand = Items.FindByLayer(LAYER_ONE_HANDED) if alreadyHasRightHand then debug("Weapon " .. ((lastRightHand and lastRightHand.Name) or "No Weapon Name") .. " already equipped in right hand") end local canEquipRightHand = not alreadyHasRightHand and lastRightHand and lastRightHand.Serial if canEquipRightHand then debug("Trying to re-equip right hand weapon: " .. ((lastRightHand and lastRightHand.Name) or "No Weapon Name")) ---@diagnostic disable-next-line: need-check-nil if Player.Equip(lastRightHand.Serial) then info("Equipping right hand") disarm.x = 0 disarm.y = 0 --lastDisarmRightHand = nil -- To refresh serial Pause(Config.ActionWaitTime) -- Wait to allow the weapon to equip else warning("Equipping right hand failed") end end -- Left hand local alreadyHasLeftHand = Items.FindByLayer(LAYER_TWO_HANDED) if alreadyHasLeftHand then debug("Weapon " .. ((lastLeftHand and lastLeftHand.Name) or "No Weapon Name") .. " already equipped in left hand") end local canEquipLeftHand = not alreadyHasLeftHand and lastLeftHand and lastLeftHand.Serial if canEquipLeftHand then debug("Trying to re-equip left hand weapon: " .. ((lastLeftHand and lastLeftHand.Name) or "No Weapon Name")) ---@diagnostic disable-next-line: need-check-nil if Player.Equip(lastLeftHand.Serial) then info("Equipping left hand") disarm.x = 0 disarm.y = 0 --lastDisarmLeftHand = nil -- To refresh serial Pause(Config.ActionWaitTime) else warning("Equipping left hand failed") end end end if not isDisarmed then if lastRightHand and not Items.FindByLayer(LAYER_ONE_HANDED) then warning("Right hand disarmed, move to equip") disarm.x = Player.X disarm.y = Player.Y elseif lastLeftHand and not Items.FindByLayer(LAYER_TWO_HANDED) then warning("Left hand disarmed, move to equip") disarm.x = Player.X disarm.y = Player.Y end end end --- --- Scavenging --- local function scavenge() if not Config.EnableScavenging then return end if not exceedsDuration(state.scavenge.lastTickTime, state.currentTickTime, ScavengeConfig.Tick) then debug("Scavenging is not ready yet, skipping this tick.") return end debug("Trying to scavenge...") state.scavenge.lastTickTime = state.currentTickTime local filter = { onground = true, rangemax = 2, graphics = ScavengeConfig.items } local list = Items.FindByFilter(filter) for _, item in ipairs(list) do if not Player.PickUp(item.Serial, 1000) then debug("Scavenging failed to pick up item: " .. (item.Name or "No Item Name")) goto continue end Pause(250) if not Player.DropInBackpack() then debug("Scavenging failed to drop item in backpack: " .. (item.Name or "No Item Name")) goto continue end debug("Scavenged item: " .. (item.Name or "No Item Name")) Pause(250) ::continue:: end end --- --- BUFFS --- local function startBuff(buffState, duration) buffState.isActive = true buffState.startTime = state.currentTickTime buffState.endTime = buffState.startTime + duration end local function recentCast() return Journal.Contains("You play your hypnotic music, stopping the battle.") or Journal.Contains("You must wait a few seconds before you can play another song.") or Journal.Contains("Your song creates a healing aura around you.") end local function eatFood() local FoodConfig = BuffsConfig and BuffsConfig.buffs.Food local foodState = state and state.food if not exceedsDuration(foodState.lastEatTime, state.currentTickTime, Config.ActionWaitTime) then debug("Food check time is not ready, skipping.") return end if not exceedsDuration(foodState.lastEatTime, state.currentTickTime, FoodConfig.EatCooldown) then debug("Eat cooldown not met, skipping.") return end debug("Lookin for food...") local foodItems = {} for graphic, name in pairs(data.foods) do local found = findInInventory({graphic}) if found and #found > 0 then for _, item in ipairs(found) do table.insert(foodItems, item) end end end if #foodItems == 0 then debug("No food items found in inventory.") return end debug("Starting to eat") for _, item in ipairs(foodItems) do debug("Attempting to eat: " .. (item.Name or "Unknown")) Player.UseObject(item.Serial) local full = pauseUntil(function() return Journal.Contains("You are simply too full") end, 50, Config.ActionWaitTime) if full then debug("Player is full.") break end end info("Finished eating") foodState.lastEatTime = state.currentTickTime end local function stamina() local StaminaConfig = BuffsConfig and BuffsConfig.buffs.Stamina if not StaminaConfig or not StaminaConfig.Enable then return end -- Debug stamina and max stamina debug("Player stamina: " .. Player.Stam .. ", Max Stamina: " .. Player.MaxStam) if Player.Stam >= Player.MaxStam then debug("Player stamina is full, skipping stamina buff.") return end local staminaPercentage = (Player.Stam / Player.MaxStam) * 100 if staminaPercentage > StaminaConfig.DrinkBelowPercentage then debug("Player stamina is above " .. staminaPercentage .. "%, skipping stamina buff.") return end debug("Player stamina is below " .. staminaPercentage .. "%, looking for stamina potion...") local staminaPotions = findInInventory(StaminaConfig.Potions) if not staminaPotions or #staminaPotions == 0 then debug("No stamina potions found in inventory.") return end debug("Found " .. #staminaPotions .. " stamina potions in inventory.") local alchemySkill = Skills.GetValue("Alchemy") if alchemySkill and alchemySkill < 80 then debug("Alchemy skill is below 80, disarming weapon to use stamina potion.") disarmPlayer() end for _, potion in ipairs(staminaPotions) do if not potion then goto endstamina end debug("Using stamina potion: " .. (potion.Name or "No Potion Name")) if not Player.UseObject(potion.Serial) then debug("Failed to use stamina potion: " .. (potion.Name or "No Potion Name")) goto endstamina end local drank = pauseUntil(function() return Journal.Contains("You feel invigorated") end, 50, Config.ActionWaitTime) if drank then debug("Successfully drank stamina potion.") break end :: endstamina :: end end local function agility() local AgilityConfig = BuffsConfig and BuffsConfig.buffs.Agility if not AgilityConfig or not AgilityConfig.Enable then return end local buffState = state and state.buffs and state.buffs.agility if Player.Dex < buffState.lastDex then debug("Detected dexterity debuff.") buffState.lastDex = Player.Dex else debug("Player dexterity is above last known dexterity, skipping agility buff.") return end debug("Looking for agility potion...") local agilityPotions = findInInventory(AgilityConfig.Potions) if not agilityPotions or #agilityPotions == 0 then debug("No agility potions found in inventory.") return end debug("Found " .. #agilityPotions .. " agility potions in inventory.") local alchemySkill = Skills.GetValue("Alchemy") if alchemySkill and alchemySkill < 80 then debug("Alchemy skill is below 80, disarming weapon to use agility potion.") disarmPlayer() end for _, potion in ipairs(agilityPotions) do if not potion and not potion.Serial then goto endagility end debug("Using agility potion: " .. (potion.Name or "No Potion Name")) if not Player.UseObject(potion.Serial) then debug("Failed to use agility potion: " .. (potion.Name or "No Potion Name")) goto endagility end local buffed = pauseUntil(function() return Player.Dex > buffState.lastDex end, 50, Config.ActionWaitTime) if buffed then debug("Successfully drank agility potion.") Pause(Config.ActionWaitTime) break end :: endagility :: end buffState.lastDex = Player.Dex end local function strength() local StrengthConfig = BuffsConfig and BuffsConfig.buffs.Strength if not StrengthConfig or not StrengthConfig.Enable then return end local buffState = state and state.buffs and state.buffs.strength debug("Checking if strength is debuffed or dropped") if Player.Str < buffState.lastStr then debug("Detected strength debuff") buffState.lastStr = Player.Str else debug("Player strength is above last known strength, skipping strength buff.") return end debug("Looking for strength potion...") local strengthPotions = findInInventory(StrengthConfig.Potions) if not strengthPotions or #strengthPotions == 0 then debug("No strength potions found in inventory.") return end debug("Found " .. #strengthPotions .. " strength potions in inventory.") local alchemySkill = Skills.GetValue("Alchemy") if alchemySkill and alchemySkill < 80 then debug("Alchemy skill is below 80, disarming weapon to use strength potion.") disarmPlayer() end for _, potion in ipairs(strengthPotions) do if (not potion) and (not potion.Serial) then goto endstrength end debug("Using strength potion: " .. (potion.Name or "No Potion Name")) if not Player.UseObject(potion.Serial) then debug("Failed to use strength potion: " .. (potion.Name or "No Potion Name")) goto endstrength end local buffed = pauseUntil(function() return Player.Str > buffState.lastStr end, 50, Config.ActionWaitTime) if buffed then debug("Successfully drank strength potion.") Pause(Config.ActionWaitTime) drinkHealthPotion(true) break end :: endstrength :: end buffState.lastStr = Player.Str end local function songOfHealing() local SongConfig = BuffsConfig and BuffsConfig.buffs.SongOfHealing if not SongConfig.Enable then return end local musicSkill = Skills.GetValue("Musicianship") if musicSkill and musicSkill > 0 then debug("Musicianship skill is " .. musicSkill .. ", proceeding with Song of Healing.") else debug("Musicianship skill is 0, skipping Song of Healing.") return end local songState = state and state.buffs and state.buffs.songOfHealing if songState.isActive then if state.currentTickTime > songState.endTime then songState.isActive = false debug("Song of Healing ended.") return end debug("Waiting for Song of Healing to end in: " .. ((songState.endTime - state.currentTickTime) / 1000) .. " seconds.") return end if recentCast() then debug("Buff was recently cast, wait to retry") songState.isActive = true songState.startTime = state.currentTickTime local recastWaitTime = 8 songState.endTime = songState.startTime + recastWaitTime return end if not songState.instrument then local instrument = nil for _, instrumentName in ipairs(BuffsConfig.Instruments) do debug("Looking for instrument: " .. instrumentName) instrument = Items.FindByName(instrumentName) if instrument then debug("Found instrument: " .. instrument.Name) break end end if not instrument then debug("No instrument found in inventory") return end songState.instrument = instrument end debug("Casting Song of Healing...") if not Spells.Cast("SongOfHealing") then if exceedsDuration(songState.lastWarningTickTime, state.currentTickTime, SongConfig.FailWait) then info("Recasting Song of Healing") debug("Failed to cast Song of Healing, waiting " .. (SongConfig.FailWait / 1000) .. " seconds to retry.") songState.lastWarningTickTime = state.currentTickTime end startBuff(songState, SongConfig.FailWait) return end local castSuccess = pauseUntil(function() if Journal.Contains("You are already under the effects") then debug("Song was already active.") return false elseif Journal.Contains("Your song creates a healing aura around you.") then return true elseif Journal.Contains("What instrument shall you play?") then debug("Instrument depleeted, will look for a new one") songState.instrument = nil return false end return false end, 50, Config.ActionWaitTime) if not castSuccess then if exceedsDuration(songState.lastWarningTickTime, state.currentTickTime, SongConfig.FailWait) then info("Recasting Song of Healing") debug("Journal did not contain expectations for Song of Healing, waiting " .. (SongConfig.FailWait / 1000) .. " seconds to retry.") songState.lastWarningTickTime = state.currentTickTime end startBuff(songState, SongConfig.FailWait) return end startBuff(songState, songState.duration) info("Casted Song of Healing") debug("Song of Healing started.") end local function buffs() if not Config.EnableBuffs then return end if Player.IsDead then debug("Player is dead, skipping buffs.") return end if Player.IsHidden then debug("Player is hiding, skipping buffs.") return end songOfHealing() stamina() strength() agility() eatFood() end local function peacemaking() local PeaceConfig = BuffsConfig and BuffsConfig.debuffs.Peacemaking if not PeaceConfig.Enable then return end local skill = Skills.GetValue("Peacemaking") if not skill or not (skill > 0) then debug("No skill in peacemaking...") return end local songState = state and state.debuffs and state.debuffs.peacemaking local target = Player.Serial if recentCast() then debug("Resent cast, waiting to retry peacemaking.") songState.isActive = true songState.startTime = state.currentTickTime local recastWaitTime = 8 songState.endTime = songState.startTime + recastWaitTime return end -- Whom do you wish to calm? if not Journal.Contains("You begin to play a soothing melody") or not Journal.Contains("That creature is already being calmed.") then debug("No Peacemaking song in progress, starting...") Spells.Cast("Peacemaking") end end local function debuffs() if not Config.EnableDebuffs then return end debug("Buffs running") if Player.IsHidden then debug("Player is hiding, skipping buffs.") return end peacemaking() end --- --- POP POUCH --- local function popPouch() if not Config.EnablePopPouch then return end if Journal.Contains("You are now PvP-Combat flagged!") then state.flaggedForPvp = true end if Journal.Contains("You are no longer PvP-Combat flagged!") then state.flaggedForPvp = false end if state.flaggedForPvp and Player.IsParalyzed and Journal.Contains("You cannot move!") then debug("Player is paralyzed, popping pouch.") info("Popping pouch") Player.PopPouch() end end --- --- Moongate --- -- Based on Jase's moongate script: https://uoaddicts.com/script/escape-moongate-m-cm6micvh local function moongate() if not Config.EnableMoongate then return end local gateState = state and state.moongate if not exceedsDuration(gateState.lastTickTime, state.currentTickTime, 1000) then debug("Moongate check is not ready yet, skipping") return end gateState.lastTickTime = state.currentTickTime local gate = Items.FindByName('Moongate') if not gate then gateState.serial = nil gateState.previousDistance = nil gateState.messageShown = false return end if gateState.serial ~= gate.Serial then gateState.serial = gate.Serial gateState.previousDistance = gate.Distance gateState.messageShown = false return end if gateState.previousDistance == nil then gateState.previousDistance = gate.Distance end local movingTowardGate = gate.Distance < gateState.previousDistance local isNearGate = gate.Distance <= 10 local movedAway = gate.Distance > 10 if movedAway and gateState.messageShown then gateState.messageShown = false debug("Moved away from moongate, resetting message flag") end if (movingTowardGate or isNearGate) and not gateState.messageShown then if not exceedsDuration(gateState.lastMessageTime, state.currentTickTime, GateConfig.OverheadPauseTime) then info("Found moongate") end gateState.messageShown = true end gateState.previousDistance = gate.Distance if gate.Distance > 2 then debug("Moongate is too far away, skipping") return end if Gumps.IsActive(GateConfig.gumpId) then info("Click destination") else Player.UseObject(gate.Serial) end if Gumps.WaitForGump(GateConfig.gumpId, Config.ActionWaitTime) then info("Trying to travel") Gumps.PressButton(GateConfig.gumpId, 1) end end --- --- ESCAPE --- local function escape() if not Config.EnableEscape then return end if Player.IsDead then debug("Player is dead, skipping escape.") return end if Player.IsHidden then debug("Player is hiding, skipping escape.") return end local command = EscapeConfig.Command if not Journal.Contains(command) then return end local callback = EscapeConfig.Callback if callback and type(callback) == "function" then debug("Running escape callback function") pauseUntil(callback, 50, Config.ActionWaitTime) end end --- --- CANCEL --- local function cancel() return Journal.Contains(CancelConfig.Command) end --- --- MAIN LOOP --- info("Halesluker's Sagas Bot") debug("Halesluker's Sagas Bot started") -- Debug messages has timestamps if Config.Debug then print("Debug mode is enabled.") for key, _ in pairs(Config) do if type(Config[key]) == "boolean" then Config[key] = false end end Config.Debug = true Config.EnableCure = true Config.EnableBandage = true Config.EnableBuffs = true end Journal.Clear() -- Start with a clean journal while true do state.currentTickTime = math.floor(os.clock() * 1000) debug("Main tick loop start") if Player.IsDead then debug("Player is dead, skipping main loop.") goto mainloopend end -- Journal dependent functions debug("Before journal tick.") if exceedsDuration(state.lastJournalTickTime, state.currentTickTime, Config.JournalTick) then debug("Journal tick time exceeded, processing journal...") if cancel() then debug("Cancel command detected, exiting main loop.") return end popPouch() escape() cure() bandage() buffs() rearmPlayer() hunt() state.lastJournalTickTime = state.currentTickTime end -- Journal independent functions disarmed() scavenge() moongate() debug("Main tick loop end") :: mainloopend :: Journal.Clear() Pause(Config.MainTick) end

Version History

Version 1 - 6/13/2025, 6:18:12 PM - about 16 hours ago

Halesluker's Sagas Bot

Original Version Saved - 6/13/2025, 6:18:12 PM - about 16 hours ago

Halesluker's Sagas Bot

No changes to display
View list of scripts
Disclaimer: This is a fan made site and is not directly associated with Ultima Online or UO staff.