Created: 6 days ago on 05/31/2025, 07:41:40 PMUpdated: 5 days ago on 06/01/2025, 05:17:07 PM
FileType: No file type provided
Size: 22312
Category: No category provided
Skills: No skills provided
Hotkey: No hotkey provided
Tags: No tags provided
--[[
__ __
___ ________ ___ _ ___ ___ ____ / / ___ ___ / /____
\\\\---- / _ `/ __/ _ `/ ' \/ _ \/ _ `(_-</ _ \/ _ \/ _ \/ __(_-< ----\
////---- \_, /_/ \_,_/_/_/_/ .__/\_,_/___/_//_/\___/\___/\__/___/ ----/
/___/ /_/
Bully Bard (full auto) v1.2
Requirements:
-------------
• Peacemaking/Musicianship skills (to successfully calm hostile creatures)
• At least one tambourine (graphic ID 3741) in your backpack (take a few to be safe)
• A few hundred bandages (graphic ID 0x0E21) in your backpack
• A safe roaming area where you can draw out and reset hostiles (newb dungeon is perfect)
• See “UO Sagas Assistant” Wiki for more info: https://github.com/uosagas/assistant
Usage:
------
This script automates your Bard’s Peacemaking routine in UO Sagas. It will:
1. Continuously scan for the nearest hostile (Gray/Criminal) creature within SEARCH_RANGE.
2. Automatically cast “Song of Peacemaking” on that target as soon as the cooldown allows.
3. Immediately engage (single‐swing attack) the target to pull it under “calm” effect.
4. Bandage your character whenever you take damage or become poisoned.
5. Dynamically switch to any closer hostile if it enters range.
6. Abandon a target and retarget if the target dies, moves out of view/range, or cannot be calmed.
7. Continue looping indefinitely—even if no creatures are nearby—so you can walk around and re‐engage new mobs.
8. Stop automatically if you run out of bandages or a tambourine, or if your character dies.
9. If health falls below 20% of max, an overhead “RUN AWAY!” warning appears and the script pauses until health recovers.
Configuration:
--------------
• SEARCH_RANGE: Maximum tile radius to search for hostiles (default: 12).
• Color constants: Adjust COLOR_INFO, COLOR_ALERT, COLOR_SUCCESS, COLOR_HINT as desired.
• MSG table: Customize any overhead text strings at the top of the script.
Notes:
------
• Make sure you are in a location where hostiles can reset (e.g., near a spawn point).
• Keep your tambourine and bandages in your backpack at all times.
• The script runs in an infinite loop—press your macro’s stop key to end it.
• It will not exit when no targets are found; simply walk into a new area to re‐engage new hostiles.
• This script does NOT handle any criminal‐act gump confirmations—close those manually if they appear.
]]
--------------------------------------------------------------------------------
-- CONFIGURABLE MESSAGES
--------------------------------------------------------------------------------
local MSG = {
instrumentNotFound = "Instrument not found! Script stopping.",
noHostiles = "No hostile monsters within %d tiles.",
switchTo = "Switching to: %s",
targetDied = "Target died: %s",
peacemakingSucceeded = "Peacemaking succeeded: %s",
stillCalm = "Still Calm!",
cantSee = "Can't see target!",
tooFar = "Too far away!",
cannotCalmHere = "Cannot calm here!",
cannotCalmThat = "Cannot calm that creature!",
peacemakingFailed = "Peacemaking failed!",
peacemakingUnknown = "Peacemaking result unknown",
noBandages = "No bandages! Script stopping.",
runAway = "RUN AWAY!"
}
--------------------------------------------------------------------------------
-- CONFIGURATION: Other constants
--------------------------------------------------------------------------------
local SEARCH_RANGE = 12 -- Max tile radius for finding hostiles
local BANDAGE_GRAPHIC = 0x0E21 -- Item ID for bandages
local TAMB_GRAPHIC = 3741 -- Item ID for tambourine
-- Color constants for overhead text
local COLOR_INFO = 93 -- Blue
local COLOR_ALERT = 33 -- Red
local COLOR_SUCCESS = 73 -- Green
local COLOR_HINT = 53 -- Yellow
--------------------------------------------------------------------------------
-- Cooldown helper (for bandaging and Peacemaking)
--------------------------------------------------------------------------------
local Cooldown = {} do
local data = {}
setmetatable(Cooldown, {
__call = function(t, key, value)
if not value then
-- READ remaining ms for “key”
local cd = data[key]
if not cd then
return nil
end
local elapsed = (os.clock() - cd.clock) * 1000
local remaining = cd.delay - elapsed
if remaining <= 0 then
data[key] = nil
return nil
end
return remaining
else
-- SET new cooldown in ms for “key”
if value <= 0 then
data[key] = nil
return
end
data[key] = { clock = os.clock(), delay = value }
end
end,
__index = function() return nil end,
__newindex = function() error("Use Cooldown(key, value) instead") end
})
end
--------------------------------------------------------------------------------
-- Fetch & sort all valid hostile mobiles (Gray or Criminal)
--------------------------------------------------------------------------------
local function GetSortedValidHostiles(range)
local raw = Mobiles.FindByFilter({
range = { max = range },
human = false,
notorieties = { 3, 4 }
})
local valid = {}
for _, m in ipairs(raw) do
-- Exclude self and any tamed/vendor pets (IsRenamable == true)
if m.Serial ~= Player.Serial and not m.IsRenamable then
table.insert(valid, m)
end
end
table.sort(valid, function(a, b)
return a.Distance < b.Distance
end)
return valid
end
--------------------------------------------------------------------------------
-- Calculate the Peacemaking cooldown based on Player.Dex
-- rawDelay = 10 - floor(Dex / 10), minimum = 6 seconds
--------------------------------------------------------------------------------
local function ComputePeacemakingCooldown()
local dex = Player.Dex or 100
local rawDelay = 10 - math.floor(dex / 10)
if rawDelay < 6 then rawDelay = 6 end
return rawDelay
end
--------------------------------------------------------------------------------
-- If hurt or poisoned and no bandage CD, use a bandage on self.
-- BandageCD = (8 + 0.85 * ((130 - Dex) / 20)) * 1000 ms
--------------------------------------------------------------------------------
local function TryBandageSelf()
if Player.Hits < Player.HitsMax or Player.IsPoisoned then
if not Cooldown("BandageSelf") then
local b = Items.FindByType(BANDAGE_GRAPHIC)
if b then
Messages.Overhead("Bandaging self...", COLOR_HINT, Player.Serial)
Player.UseObject(b.Serial)
if Targeting.WaitForTarget(500) then
Targeting.TargetSelf()
end
local dex = Player.Dex or 100
local delay = (8.0 + 0.85 * ((130 - dex) / 20)) * 1000
Cooldown("BandageSelf", delay)
else
Messages.Overhead(MSG.noBandages, COLOR_ALERT, Player.Serial)
return false
end
end
end
return true
end
--------------------------------------------------------------------------------
-- Attempt a single‐swing attack on the given serial.
-- If Player.Attack exists, use it. Otherwise, do a bare‐hand punch
--------------------------------------------------------------------------------
local function TryEngageTarget(serial)
if Player.Attack then
Player.Attack(serial)
else
if Targeting.WaitForTarget(500) then
Targeting.Target(serial)
end
end
end
--------------------------------------------------------------------------------
-- INITIAL CHECK: Ensure instrument and bandage exist before starting
--------------------------------------------------------------------------------
local function InitialChecks()
local tamb = Items.FindByType(TAMB_GRAPHIC)
if not tamb then
Messages.Overhead(MSG.instrumentNotFound, COLOR_ALERT, Player.Serial)
return false
end
local band = Items.FindByType(BANDAGE_GRAPHIC)
if not band then
Messages.Overhead(MSG.noBandages, COLOR_ALERT, Player.Serial)
return false
end
return true
end
if not InitialChecks() then
return -- Stop script if initial requirements fail
end
--------------------------------------------------------------------------------
-- Variables to track the current target and next allowable Peacemaking time
--------------------------------------------------------------------------------
local currentTarget = nil
local nextPeaceTime = 0
local peaceCooldown = ComputePeacemakingCooldown()
--------------------------------------------------------------------------------
-- MAIN LOOP
--------------------------------------------------------------------------------
while true do
--------------------------------------------------------------------------------
-- STOP IF PLAYER DEAD
--------------------------------------------------------------------------------
if Player.IsDead then
Messages.Overhead("You are dead. Script stopping.", COLOR_ALERT, Player.Serial)
return
end
--------------------------------------------------------------------------------
-- WARN IF HEALTH BELOW 20%
--------------------------------------------------------------------------------
local hpPercent = (Player.Hits / (Player.HitsMax > 0 and Player.HitsMax or 1)) * 100
if hpPercent < 20 then
Messages.Overhead(MSG.runAway, COLOR_ALERT, Player.Serial)
Pause(1000)
goto continue_loop
end
--------------------------------------------------------------------------------
-- 1) BANDAGE PRIORITY EACH ITERATION
--------------------------------------------------------------------------------
if not TryBandageSelf() then
return -- Stop script if no bandages left
end
--------------------------------------------------------------------------------
-- 2) VERIFY INSTRUMENT STILL PRESENT
--------------------------------------------------------------------------------
local tamb = Items.FindByType(TAMB_GRAPHIC)
if not tamb then
Messages.Overhead(MSG.instrumentNotFound, COLOR_ALERT, Player.Serial)
return -- Stop script if tambourine lost
end
--------------------------------------------------------------------------------
-- 3) GATHER SORTED HOSTILES & PICK NEAREST
--------------------------------------------------------------------------------
local hostiles = GetSortedValidHostiles(SEARCH_RANGE)
if #hostiles == 0 then
currentTarget = nil
Messages.Overhead(string.format(MSG.noHostiles, SEARCH_RANGE), COLOR_ALERT, Player.Serial)
Pause(500)
goto continue_loop
end
local nearest = hostiles[1]
--------------------------------------------------------------------------------
-- 4) SWITCH TO NEW OR CLOSER HOSTILE
--------------------------------------------------------------------------------
if not currentTarget or nearest.Serial ~= currentTarget.Serial then
currentTarget = nearest
Messages.Overhead(string.format(MSG.switchTo, currentTarget.Name or "<unknown>"),
COLOR_INFO, currentTarget.Serial)
-- Immediately attempt to engage this new target:
TryEngageTarget(currentTarget.Serial)
end
--------------------------------------------------------------------------------
-- 5) CONFIRM TARGET STILL EXISTS & IS ALIVE
--------------------------------------------------------------------------------
local tgtData = Mobiles.FindBySerial(currentTarget.Serial)
if not tgtData or tgtData.IsDead then
Messages.Overhead(string.format(MSG.targetDied, currentTarget.Name or "<unknown>"),
COLOR_HINT, Player.Serial)
currentTarget = nil
Pause(200)
goto continue_loop
end
--------------------------------------------------------------------------------
-- 6) ATTEMPT PEACEMAKING WHEN OFF COOLDOWN
--------------------------------------------------------------------------------
local now = os.clock()
if now >= nextPeaceTime then
-- Clear journal for fresh messages
Journal.Clear()
-- Cast “Song of Peacemaking”
Player.UseObject(tamb.Serial)
Spells.Cast("SongOfPeacemaking")
Pause(250)
-- Re‐check if target died mid‐cast
tgtData = Mobiles.FindBySerial(currentTarget.Serial)
if not tgtData or tgtData.IsDead then
Messages.Overhead(string.format(MSG.targetDied, currentTarget.Name or "<unknown>"),
COLOR_ALERT, Player.Serial)
nextPeaceTime = now + peaceCooldown
currentTarget = nil
Pause(200)
goto continue_loop
end
-- Proceed to target creature for Peacemaking effect
if Targeting.WaitForTarget(1000) then
Targeting.Target(currentTarget.Serial)
else
Messages.Overhead(MSG.peacemakingUnknown, COLOR_ALERT, Player.Serial)
nextPeaceTime = now + peaceCooldown
Pause(200)
goto continue_loop
end
-- Wait for journal to populate
Pause(500)
-- Inspect journal for EXACT messages in priority order:
if Journal.Contains("You play your hypnotic music, stopping the battle.") then
-- Success
Messages.Overhead(string.format(MSG.peacemakingSucceeded, currentTarget.Name or "<unknown>"),
COLOR_SUCCESS, currentTarget.Serial)
-- Engage again in case the first punch didn't register
TryEngageTarget(currentTarget.Serial)
nextPeaceTime = now + peaceCooldown
elseif Journal.Contains("That creature is already being calmed.") then
-- Already calm
Messages.Overhead(MSG.stillCalm, COLOR_HINT, currentTarget.Serial)
nextPeaceTime = now + peaceCooldown
elseif Journal.Contains("You can’t see that") or Journal.Contains("You can't see that") then
-- Cannot see: abandon target
Messages.Overhead(MSG.cantSee, COLOR_ALERT, currentTarget.Serial)
currentTarget = nil
nextPeaceTime = now + peaceCooldown
Pause(200)
goto continue_loop
elseif Journal.Contains("That is too far away") then
-- Too far: abandon target
Messages.Overhead(MSG.tooFar, COLOR_ALERT, currentTarget.Serial)
currentTarget = nil
nextPeaceTime = now + peaceCooldown
Pause(200)
goto continue_loop
elseif Journal.Contains("You may not do that in this area") then
Messages.Overhead(MSG.cannotCalmHere, COLOR_ALERT, currentTarget.Serial)
nextPeaceTime = now + peaceCooldown
elseif Journal.Contains("You cannot calm that") then
-- Explicit “cannot calm that” → abandon target
Messages.Overhead(MSG.cannotCalmThat, COLOR_ALERT, currentTarget.Serial)
currentTarget = nil
nextPeaceTime = now + peaceCooldown
Pause(200)
goto continue_loop
elseif Journal.Contains("You play poorly, and there is no effect") then
-- Failed Peacemaking
Messages.Overhead(MSG.peacemakingFailed, COLOR_ALERT, currentTarget.Serial)
nextPeaceTime = now + peaceCooldown
else
-- Any other/unrecognized line
Messages.Overhead(MSG.peacemakingUnknown, COLOR_ALERT, currentTarget.Serial)
nextPeaceTime = now + peaceCooldown
end
end
--------------------------------------------------------------------------------
-- 7) SMALL PAUSE BEFORE NEXT ITERATION (allows bandaging & retarget checks)
--------------------------------------------------------------------------------
Pause(200)
::continue_loop::
end