• You've discovered RedGuides, an EverQuest multi-boxing and scripting community 🧙‍♀️⚙️. We want you to play several EQ characters at once, come join us and say hello! 👋

Question - Cleric CH Rotation

Status
Not open for further replies.
Unfortunately, my friend, it is the same answer as your last question.
Yes, it is possible (but you'd have to modify a function in casting.Lua).
No, we don't have a quick toggle for it nor do we plan to.
I am one of those people that kind of leans into thinking that CH rotations should just use a script that does CH rotations, because that's all it needs to do.
 
Unfortunately, my friend, it is the same answer as your last question.
Yes, it is possible (but you'd have to modify a function in casting.lua).
No, we don't have a quick toggle for it nor do we plan to.
I am one of those people that kind of leans into thinking that CH rotations should just use a script that does CH rotations, because that's all it needs to do.
The Events gets the job done but if I could duck casts and save mana, I could shave 1-2 clerics. Oh well.
 
The Events gets the job done but if I could duck casts and save mana, I could shave 1-2 clerics. Oh well.
This almost sounds like someting you could just script outside, tbh. Slightly more painful then "Let them make a toggle for me", but not sure inserting this into Mercs would be a good call, either. Up to you what those one-two clerics are worth i guess.
 
Code:
  CancelHealIfTargetHealthy:
    action: /stopcast
    condition: >-
      ${Me.Casting.ID} && ${Target.ID} && ${Target.PctHPs}>=92 && ${Me.CastTimeLeft.TotalSeconds}<=1
      && ${Me.Casting.Name.Equal[Remedy]} || ${Me.Casting.ID} && ${Target.ID}
      && ${Target.PctHPs}>=92 && ${Me.CastTimeLeft.TotalSeconds}<=1 &&
      ${Me.Casting.Name.Equal[Divine Light]} || ${Me.Casting.ID} && ${Target.ID}
      && ${Target.PctHPs}>=92 && ${Me.CastTimeLeft.TotalSeconds}<=1 &&
      ${Me.Casting.Name.Equal[Chloroblast]} || ${Me.Casting.ID} && ${Target.ID}
      && ${Target.PctHPs}>=92 && ${Me.CastTimeLeft.TotalSeconds}<=1 &&
      ${Me.Casting.Name.Equal[Nature's Touch]}

add in complete heal and any other heals.

Code:
local mq = require('mq')
local ImGui = require('ImGui')

-- CHChainGUI_SitAfterCast.lua
-- Adds large right-side visual heal monitor with top chain controls.
-- No ImGui.Checkbox, no forced window size, no mq.gettime, no mq.delay from GUI callbacks.

local S = {
  spellName='Complete Heal', spellGemNumber=1,
  chainDelaySeconds=5, spellCastSeconds=10, recoveryBufferSeconds=1.5, memDelaySeconds=12,
  stopIfChainTooFast=true, autoSaveOnExit=true, stopOtherLuasOnClerics=false,
  cwtnPauseCommand='/clr pause on', cwtnUnpauseCommand='/clr pause off',
  enableLateHealInterrupt=true, lateHealInterruptPct=90, lateHealInterruptSecondsLeft=2.0, lateHealInterruptCheckIntervalMs=500, showInterruptEcho=false,
  forceSitAfterCast=true, sitDelayAfterHealSeconds=1.0,
  openGUI=true, scriptRunning=true, chainRunning=false, preparingChain=false, prepFinishTime=0, prepStartAfterPrep=false,
  clerics={}, tanks={}, newClericName='', newTankName='', currentClericIndex=1, currentTankIndex=1,
  nextCastTime=0, lastLateHealInterruptCheckTime=0,
  activeCasts={}, completedCasts={}, maxCompletedCasts=8, remoteCastStartDelaySeconds=0.8, visualFontScale=1.8,
  statusMessage='Idle.', lastCastMessage='None yet.', lastCommandSent='None.', lastErrorMessage='None.', lastEventMessage='None.',
  configFile='CHChainGUI_Config.lua', debugLogFile='CHChainGUI_Debug.log',
  debugMode=true, debugEchoToMQ=true, debugLogToFile=true, debugLogPulseChecks=false,
  commandCounter=0, errorCounter=0, guiErrorCounter=0
}

local function now() return os.clock() end
local function nowMs() return math.floor(os.clock()*1000) end
local function trim(v) if v==nil then return '' end return tostring(v):match('^%s*(.-)%s*$') end
local function q(v) return string.format('%q', tostring(v or '')) end
local function stripSlash(c) return tostring(c or ''):gsub('^/','') end
local function fileExists(p) local f=io.open(p,'r'); if f then f:close(); return true end return false end
local function ts() return os.date('%Y-%m-%d %H:%M:%S') end

local function currentTank()
  if #S.tanks==0 then return '' end
  if S.currentTankIndex<1 then S.currentTankIndex=1 end
  if S.currentTankIndex>#S.tanks then S.currentTankIndex=1 end
  return S.tanks[S.currentTankIndex] or ''
end
local function fullCycle() return #S.clerics*S.chainDelaySeconds end
local function minCycle() return S.spellCastSeconds+S.recoveryBufferSeconds end
local function cancelGap() return S.chainDelaySeconds+S.lateHealInterruptSecondsLeft end
local function fmtSec(v) v=tonumber(v) or 0; if v<0 then v=0 end; return string.format('%.1fs',v) end
local function pruneCompleted() S.completedCasts=S.completedCasts or {}; S.maxCompletedCasts=S.maxCompletedCasts or 8; while #S.completedCasts>S.maxCompletedCasts do table.remove(S.completedCasts,1) end end
local function addCastVisual(clericName,tankName)
  if S.remoteCastStartDelaySeconds==nil then S.remoteCastStartDelaySeconds=0.8 end
  local startTime=now()+S.remoteCastStartDelaySeconds
  S.activeCasts=S.activeCasts or {}; table.insert(S.activeCasts,{clericName=tostring(clericName or ''),tankName=tostring(tankName or ''),spellName=tostring(S.spellName or ''),castStartTime=startTime,landTime=startTime+S.spellCastSeconds})
end
local function pulseCastVisuals()
  S.activeCasts=S.activeCasts or {}; S.completedCasts=S.completedCasts or {}
  local t=now(); local i=1
  while i<=#S.activeCasts do
    local c=S.activeCasts[i]
    if t>=c.landTime then
      table.insert(S.completedCasts,{clericName=c.clericName,tankName=c.tankName,spellName=c.spellName,landedTime=t})
      table.remove(S.activeCasts,i); pruneCompleted()
    else
      i=i+1
    end
  end
end

local function writeLog(m)
  if not S.debugMode then return end
  m=tostring(m or '')
  if S.debugEchoToMQ then mq.cmdf('/echo [CH Debug] %s', m) end
  if S.debugLogToFile then
    local f,err=io.open(S.debugLogFile,'a')
    if f then f:write(string.format('[%s] %s\n', ts(), m)); f:close() else S.lastErrorMessage='Could not write debug log: '..tostring(err) end
  end
end
local function logEvent(m) S.lastEventMessage=tostring(m or ''); writeLog('EVENT: '..S.lastEventMessage) end
local function logError(m) S.errorCounter=S.errorCounter+1; S.lastErrorMessage=tostring(m or ''); writeLog('ERROR: '..S.lastErrorMessage) end
local function echo(m) m=tostring(m or ''); mq.cmdf('/echo [CH Chain GUI] %s', m); writeLog('ECHO: '..m) end
local function safe(label, fn) local ok,err=pcall(fn); if not ok then logError(label..': '..tostring(err)) end end

local function summary()
  return string.format('Status=%s | LastEvent=%s | LastCommand=%s | LastError=%s | Commands=%d | Errors=%d | GuiErrors=%d | Running=%s | Preparing=%s | Clerics=%d | Tanks=%d | CurrentTank=%s | CurrentCleric=%d | Delay=%.1f | Spell=%s | Gem=%d',
    tostring(S.statusMessage), tostring(S.lastEventMessage), tostring(S.lastCommandSent), tostring(S.lastErrorMessage), S.commandCounter, S.errorCounter, S.guiErrorCounter, tostring(S.chainRunning), tostring(S.preparingChain), #S.clerics, #S.tanks, tostring(currentTank()), S.currentClericIndex, S.chainDelaySeconds, tostring(S.spellName), S.spellGemNumber)
end
local function clearLog()
  local f,err=io.open(S.debugLogFile,'w')
  if f then f:write(string.format('[%s] Debug log cleared.\n',ts())); f:close(); S.lastErrorMessage='None.'; S.errorCounter=0; S.guiErrorCounter=0 else S.lastErrorMessage='Could not clear debug log: '..tostring(err) end
end

local function inputText(label,val) local old=tostring(val or ''); local a,b=ImGui.InputText(label,old); if type(b)=='string' then return b end; if type(a)=='string' then return a end; return old end
local function inputFloat(label,val,step,fast,fmt) local old=tonumber(val) or 0; local a,b=ImGui.InputFloat(label,old,step or 0.1,fast or 1.0,fmt or '%.2f'); if type(b)=='number' then return b end; if type(a)=='number' then return a end; return old end
local function inputInt(label,val) local old=tonumber(val) or 0; local a,b=ImGui.InputInt(label,old); if type(b)=='number' then return b end; if type(a)=='number' then return a end; return old end
local function button(label) local ok,clicked=pcall(function() return ImGui.Button(label) end); if not ok then S.guiErrorCounter=S.guiErrorCounter+1; S.lastErrorMessage='Button failed ['..tostring(label)..']: '..tostring(clicked); return false end; return clicked==true end
local function sameLine() pcall(function() ImGui.SameLine() end) end
local function toggle(label,val) local old=val==true; ImGui.Text(label..': '..(old and 'ON' or 'OFF')); sameLine(); if button('Toggle##'..label) then return not old end; return old end

local function bc(char,cmd,suppress)
  char=trim(char); cmd=stripSlash(cmd)
  if char=='' then logError('bc blocked: blank character. Command='..tostring(cmd)); return end
  if cmd=='' then logError('bc blocked: blank command. Character='..tostring(char)); return end
  local full=string.format('/noparse /bct %s //%s',char,cmd)
  S.commandCounter=S.commandCounter+1
  if not suppress then S.lastCommandSent=full; writeLog('COMMAND #'..S.commandCounter..': '..full) end
  mq.cmd(full)
end

local function inRaid(name)
  name=trim(name); if name=='' then return false end
  local n=mq.TLO.Raid.Members(); if n==nil or n<=0 then return false end
  local ok,rn=pcall(function() return mq.TLO.Raid.Member(name).Name() end)
  return ok and rn~=nil and rn~=''
end
local function validateRaid()
  local missing={}
  for _,n in ipairs(S.clerics) do if not inRaid(n) then table.insert(missing,'Cleric not found in raid: '..n) end end
  for _,n in ipairs(S.tanks) do if not inRaid(n) then table.insert(missing,'Tank not found in raid: '..n) end end
  if #missing>0 then S.statusMessage=table.concat(missing,' | '); echo(S.statusMessage); logError(S.statusMessage); return false end
  return true
end

local function timingStatus()
  if #S.clerics<=0 then return false,'No clerics are configured.' end
  if #S.tanks<=0 then return false,'No tanks are configured.' end
  if S.chainDelaySeconds<=0 then return false,'Chain delay must be greater than 0.' end
  if fullCycle()<minCycle() then return false,string.format('TOO FAST: full cycle %.1fs, minimum %.1fs. Short by %.1fs.',fullCycle(),minCycle(),minCycle()-fullCycle()) end
  return true,string.format('Safe: full cycle %.1fs, minimum %.1fs.',fullCycle(),minCycle())
end

local function saveSettings()
  local f,err=io.open(S.configFile,'w')
  if not f then S.statusMessage='Failed to save settings: '..tostring(err); echo(S.statusMessage); logError(S.statusMessage); return end
  f:write('return {\n')
  local fields={'spellName','spellGemNumber','chainDelaySeconds','spellCastSeconds','recoveryBufferSeconds','memDelaySeconds','stopIfChainTooFast','autoSaveOnExit','stopOtherLuasOnClerics','cwtnPauseCommand','cwtnUnpauseCommand','currentTankIndex','enableLateHealInterrupt','lateHealInterruptPct','lateHealInterruptSecondsLeft','lateHealInterruptCheckIntervalMs','debugMode','debugEchoToMQ','debugLogToFile','debugLogPulseChecks','forceSitAfterCast','sitDelayAfterHealSeconds'}
  for _,k in ipairs(fields) do local v=S[k]; if type(v)=='string' then f:write(string.format('\t%s = %s,\n',k,q(v))) else f:write(string.format('\t%s = %s,\n',k,tostring(v))) end end
  f:write('\tclerics = {\n'); for _,n in ipairs(S.clerics) do f:write(string.format('\t\t%s,\n',q(n))) end; f:write('\t},\n')
  f:write('\ttanks = {\n'); for _,n in ipairs(S.tanks) do f:write(string.format('\t\t%s,\n',q(n))) end; f:write('\t},\n')
  f:write('}\n'); f:close(); S.statusMessage='Settings saved.'; writeLog('Settings saved to '..S.configFile)
end
local function loadSettings()
  if not fileExists(S.configFile) then S.statusMessage='No saved settings found. Configure in GUI, then click Save Settings.'; return end
  local lf,le=loadfile(S.configFile); if not lf then S.statusMessage='Could not load settings file: '..tostring(le); echo(S.statusMessage); logError(S.statusMessage); return end
  local ok,cfg=pcall(lf); if not ok or type(cfg)~='table' then S.statusMessage='Saved settings file exists, but could not load.'; echo(S.statusMessage); logError(S.statusMessage); return end
  for k,v in pairs(cfg) do
    if k=='clerics' and type(v)=='table' then S.clerics=v
    elseif k=='tanks' and type(v)=='table' then S.tanks=v
    elseif S[k]~=nil then
      if type(S[k])=='boolean' and type(v)=='boolean' then S[k]=v
      elseif type(S[k])=='number' then S[k]=tonumber(v) or S[k]
      elseif type(S[k])=='string' and type(v)=='string' then S[k]=v end
    end
  end
  if S.currentTankIndex<1 then S.currentTankIndex=1 end; if S.currentTankIndex>#S.tanks then S.currentTankIndex=1 end
  S.statusMessage='Settings loaded.'
end
local function resetDefaults()
  S.spellName='Complete Heal'; S.spellGemNumber=1; S.chainDelaySeconds=5; S.spellCastSeconds=10; S.recoveryBufferSeconds=1.5; S.memDelaySeconds=12
  S.stopIfChainTooFast=true; S.autoSaveOnExit=true; S.stopOtherLuasOnClerics=false; S.cwtnPauseCommand='/clr pause on'; S.cwtnUnpauseCommand='/clr pause off'
  S.enableLateHealInterrupt=true; S.lateHealInterruptPct=90; S.lateHealInterruptSecondsLeft=2.0; S.lateHealInterruptCheckIntervalMs=500
  S.debugMode=true; S.debugEchoToMQ=true; S.debugLogToFile=true; S.debugLogPulseChecks=false; S.statusMessage='Defaults restored. Click Save Settings to keep them.'
end

local function addCleric() local n=trim(S.newClericName); if n=='' then S.statusMessage='Enter a cleric name first.'; return end; if not inRaid(n) then S.statusMessage='Cleric not found in raid: '..n; echo(S.statusMessage); logError(S.statusMessage); return end; table.insert(S.clerics,n); S.newClericName=''; S.statusMessage='Added cleric: '..n; logEvent(S.statusMessage); saveSettings() end
local function addTank() local n=trim(S.newTankName); if n=='' then S.statusMessage='Enter a tank name first.'; return end; if not inRaid(n) then S.statusMessage='Tank not found in raid: '..n; echo(S.statusMessage); logError(S.statusMessage); return end; table.insert(S.tanks,n); if #S.tanks==1 then S.currentTankIndex=1 end; S.newTankName=''; S.statusMessage='Added tank: '..n; logEvent(S.statusMessage); saveSettings() end
local function moveUp(list,i) if i<=1 then return end; list[i],list[i-1]=list[i-1],list[i]; saveSettings() end
local function moveDown(list,i) if i>=#list then return end; list[i],list[i+1]=list[i+1],list[i]; saveSettings() end
local function remCleric(i) table.remove(S.clerics,i); if S.currentClericIndex>#S.clerics then S.currentClericIndex=1 end; S.statusMessage='Removed cleric.'; logEvent(S.statusMessage); saveSettings() end
local function remTank(i) table.remove(S.tanks,i); if S.currentTankIndex>#S.tanks then S.currentTankIndex=1 end; if S.currentTankIndex<1 then S.currentTankIndex=1 end; S.statusMessage='Removed tank.'; logEvent(S.statusMessage); saveSettings() end
local function setTank(i) if i<1 or i>#S.tanks then return end; S.currentTankIndex=i; S.statusMessage='Current tank set to: '..currentTank(); logEvent(S.statusMessage); echo(S.statusMessage); saveSettings() end

local function pauseClerics() for _,n in ipairs(S.clerics) do bc(n,S.cwtnPauseCommand) end end
local function unpauseClerics() for _,n in ipairs(S.clerics) do bc(n,S.cwtnUnpauseCommand) end end
local function endMacros() for _,n in ipairs(S.clerics) do bc(n,'/endmacro') end end
local function stopLuas() S.statusMessage='Lua stop skipped for safety.'; logEvent(S.statusMessage) end
local function memCH() for _,n in ipairs(S.clerics) do bc(n,string.format('/memspell %d "%s"',S.spellGemNumber,S.spellName)) end end

local function buildInterruptCondition()
  return string.format(
    '${Me.Casting.ID} && ${Target.ID} && ${Target.PctHPs}>=%.0f && ${Me.CastTimeLeft.TotalSeconds}<=%.2f && ${Me.Casting.Name.Equal[%s]}',
    S.lateHealInterruptPct,
    S.lateHealInterruptSecondsLeft,
    S.spellName
  )
end


local function safeSitCondition()
  return '!${Me.Casting.ID} && !${Me.Sitting} && !${Me.Moving}'
end

local function scheduledSitAfterExpectedLandCommand()
  if S.forceSitAfterCast ~= true then
    return ''
  end

  local delaySeconds = tonumber(S.sitDelayAfterHealSeconds) or 1.0
  if delaySeconds < 0 then delaySeconds = 0 end
  if delaySeconds > 10 then delaySeconds = 10 end

  -- /timed uses tenths of a second.
  -- /timed 8 starts the cast, then 10s cast, then user-configurable sit delay.
  local sitTick = 8 + math.floor((S.spellCastSeconds + delaySeconds) * 10)

  return string.format(' ; /timed %d /if (%s) /sit', sitTick, safeSitCondition())
end

local function scheduledInterruptCommands()
  local cond = buildInterruptCondition()
  local commands = {}

  -- These timings are tenths of a second from receipt of the remote command.
  -- /timed 8 starts the cast. These checks happen late in the CH cast.
  -- No echo here; it was too spammy in the cleric MQ windows.
  local checks = {70, 75, 80, 85, 90, 95, 100}

  for _, tick in ipairs(checks) do
    table.insert(commands, string.format('/timed %d /if (%s) /stopcast', tick, cond))

    if S.forceSitAfterCast == true then
      table.insert(commands, string.format('/timed %d /if (%s) /sit', tick + 5, safeSitCondition()))
    end
  end

  return table.concat(commands, ' ; ')
end

local function castCH(char)
  local tank=currentTank(); if tank=='' then S.statusMessage='No current tank.'; logError(S.statusMessage); return end
  if char==nil or trim(char)=='' then S.statusMessage='No cleric name for current chain slot.'; logError(S.statusMessage); return end
  S.lastCastMessage=string.format('%s casting %s on %s',char,S.spellName,tank); logEvent(S.lastCastMessage); addCastVisual(char,tank)

  local command=''

  if S.enableLateHealInterrupt == true then
    local interruptCommands=scheduledInterruptCommands()
    command=string.format('/multiline ; /target clear ; /timed 3 /target %s ; /timed 8 /cast %d ; %s%s',tank,S.spellGemNumber,interruptCommands,scheduledSitAfterExpectedLandCommand())
  else
    command=string.format('/multiline ; /target clear ; /timed 3 /target %s ; /timed 8 /cast %d%s',tank,S.spellGemNumber,scheduledSitAfterExpectedLandCommand())
  end

  bc(char,command)
end

local function beginPrep(startAfter)
  S.preparingChain=true; S.chainRunning=false; S.prepFinishTime=now()+S.memDelaySeconds; S.prepStartAfterPrep=startAfter==true
  S.statusMessage='Preparing clerics: pausing CWTN, ending macros, memming spell.'; logEvent(S.statusMessage)
  pauseClerics(); endMacros(); if S.stopOtherLuasOnClerics then stopLuas() end; memCH()
  if S.prepStartAfterPrep then S.statusMessage=string.format('Waiting %.1f seconds, then chain starts.',S.memDelaySeconds) else S.statusMessage=string.format('Waiting %.1f seconds for spell mem.',S.memDelaySeconds) end
end
local function startChain()
  if S.preparingChain then S.statusMessage='Preparation is already running.'; echo(S.statusMessage); return end
  local ok,msg=timingStatus(); S.statusMessage=msg; if not ok and S.stopIfChainTooFast then echo(msg); echo('Chain refused to start. Increase delay or add clerics.'); logError('Unsafe timing.'); return end
  if not validateRaid() then return end
  beginPrep(true)
end
local function stopChain() S.chainRunning=false; S.preparingChain=false; S.prepFinishTime=0; S.prepStartAfterPrep=false; S.statusMessage='Chain stopped.'; logEvent(S.statusMessage); echo(S.statusMessage) end
local function stopUnpause() stopChain(); unpauseClerics(); echo('Chain stopped and listed clerics unpaused.') end
local function swapTank()
  if #S.tanks<=0 then
    S.statusMessage='No tanks configured.'
    logError(S.statusMessage)
    echo(S.statusMessage)
    return
  end

  local old=currentTank()

  S.currentTankIndex=tonumber(S.currentTankIndex) or 1
  S.currentTankIndex=S.currentTankIndex+1

  if S.currentTankIndex>#S.tanks then
    S.currentTankIndex=1
  end

  if S.currentTankIndex<1 then
    S.currentTankIndex=1
  end

  S.statusMessage='Swapped future heals from '..tostring(old)..' to tank: '..currentTank()
  logEvent(S.statusMessage)
  echo(S.statusMessage)
  saveSettings()
end

local function pulsePrep() if not S.preparingChain then return end; if now()<S.prepFinishTime then return end; S.preparingChain=false; if S.prepStartAfterPrep then S.currentClericIndex=1; S.nextCastTime=0; S.chainRunning=true; S.statusMessage='Chain running.'; logEvent(S.statusMessage); echo('Complete Heal chain started.') else S.statusMessage='Preparation complete.'; logEvent(S.statusMessage) end; S.prepStartAfterPrep=false end
local function pulseChain()
  if not S.chainRunning or S.preparingChain then return end
  local ok,msg=timingStatus(); S.statusMessage='Chain running. '..msg; if not ok and S.stopIfChainTooFast then echo('Timing became unsafe while running. Stopping chain.'); logError('Unsafe timing while running.'); stopChain(); return end
  local t=now(); if S.nextCastTime==0 or t>=S.nextCastTime then local c=S.clerics[S.currentClericIndex]; if c and c~='' then castCH(c) end; S.currentClericIndex=S.currentClericIndex+1; if S.currentClericIndex>#S.clerics then S.currentClericIndex=1 end; S.nextCastTime=t+S.chainDelaySeconds end
end
-- Late interrupt sends ${Me...} checks to each cleric. bc() uses /noparse so these evaluate on the cleric, not the controller.
local function pulseInterrupts()
  -- Live pulse interrupts intentionally disabled.
  -- Interrupt checks are only attached to each CH cast when Late Heal Interrupt is ON.
  return
end

local function slash(...)
  local args=trim(table.concat({...},' ')); if args=='' or args=='help' then echo('/chchain start | stop | stopunpause | swap | status | summary | gui | exit | debug on | debug off'); return end
  if args=='start' then startChain(); return end; if args=='stop' then stopChain(); return end; if args=='stopunpause' then stopUnpause(); return end; if args=='swap' then swapTank(); return end
  if args=='status' then echo(S.statusMessage); echo('Current tank: '..currentTank()); echo('Last cast: '..S.lastCastMessage); return end
  if args=='summary' then local s=summary(); echo(s); writeLog(s); return end
  if args=='debug on' then S.debugMode=true; saveSettings(); echo('Debug mode ON.'); return end
  if args=='debug off' then S.debugMode=false; saveSettings(); echo('Debug mode OFF.'); return end
  echo('Unknown command: '..args)
end


local function chainControlCastNext()
  if #S.clerics>0 then
    castCH(S.clerics[S.currentClericIndex])
    S.currentClericIndex=S.currentClericIndex+1
    if S.currentClericIndex>#S.clerics then S.currentClericIndex=1 end
    S.nextCastTime=now()+S.chainDelaySeconds
  end
end

local function chainControlCastNext()
  if #S.clerics>0 then
    castCH(S.clerics[S.currentClericIndex])
    S.currentClericIndex=S.currentClericIndex+1
    if S.currentClericIndex>#S.clerics then S.currentClericIndex=1 end
    S.nextCastTime=now()+S.chainDelaySeconds
  end
end


local COLOR_CLERIC = {0.2, 1.0, 0.2, 1.0}
local COLOR_TANK = {1.0, 1.0, 0.1, 1.0}
local COLOR_GREEN = {0.2, 1.0, 0.2, 1.0}
local COLOR_RED = {1.0, 0.15, 0.15, 1.0}
local COLOR_NORMAL = {1.0, 1.0, 1.0, 1.0}

local function coloredText(text, color)
  text = tostring(text or '')
  color = color or COLOR_NORMAL

  if ImGui.TextColored ~= nil then
    local ok = pcall(function()
      ImGui.TextColored(color[1], color[2], color[3], color[4], text)
    end)

    if ok then return end

    ok = pcall(function()
      ImGui.TextColored(color, text)
    end)

    if ok then return end
  end

  ImGui.Text(text)
end

local function clericText(text)
  coloredText(text, COLOR_CLERIC)
end

local function tankText(text)
  coloredText(text, COLOR_TANK)
end

local function greenText(text)
  coloredText(text, COLOR_GREEN)
end

local function redText(text)
  coloredText(text, COLOR_RED)
end

local function normalText(text)
  coloredText(text, COLOR_NORMAL)
end

local function labelAndColoredValue(label, value, color)
  ImGui.Text(tostring(label or ''))
  sameLine()
  coloredText(tostring(value or ''), color)
end

local function drawChainControls(idSuffix, stacked, includeSwapTank)
  idSuffix = tostring(idSuffix or 'Main')
  includeSwapTank = includeSwapTank == true

  ImGui.Text('Chain Control')

  if not S.chainRunning and not S.preparingChain then
    if button('Start / Prep / Mem / Run Chain##'..idSuffix..'Start') then startChain() end

    if not stacked then sameLine() end

    if button('Prep / Mem Only##'..idSuffix..'PrepOnly') then beginPrep(false) end
  else
    if button('Stop Chain##'..idSuffix..'Stop') then stopChain() end
  end

  if includeSwapTank then
    if not stacked then sameLine() end
    if button('Swap Tank##'..idSuffix..'SwapTank') then swapTank() end
  end

  if not stacked then sameLine() end

  if button('Stop + Unpause Clerics##'..idSuffix..'StopUnpause') then stopUnpause() end

  if not stacked then sameLine() end

  if button('Cast Next Cleric Now##'..idSuffix..'CastNext') then
    chainControlCastNext()
  end
end

local function visualText(text)
  ImGui.Text(tostring(text or ''))
end

local function drawBigCastMonitor()
  local oldScaleSet=false
  pcall(function() S.activeCasts=S.activeCasts or {}; S.completedCasts=S.completedCasts or {}; if S.remoteCastStartDelaySeconds==nil then S.remoteCastStartDelaySeconds=0.8 end; if S.visualFontScale==nil then S.visualFontScale=1.8 end; if S.visualFontScale<1 then S.visualFontScale=1 end; if S.visualFontScale>3 then S.visualFontScale=3 end; ImGui.SetWindowFontScale(S.visualFontScale); oldScaleSet=true end)

  visualText('HEAL CAST MONITOR')
  ImGui.Separator()

  local t=now()
  labelAndColoredValue('Tank:', currentTank(), COLOR_TANK)

  if S.preparingChain then
    greenText('PREP/MEM: '..fmtSec(S.prepFinishTime-t)..' remaining')
  end

  ImGui.Separator()
  drawChainControls('RightMonitor', true, true)
  ImGui.Separator()

  S.activeCasts=S.activeCasts or {}; S.completedCasts=S.completedCasts or {}
  if #S.activeCasts==0 then
    if S.forceSitAfterCast then greenText('Auto-sit after cast/interruption: ON') else redText('Auto-sit after cast/interruption: OFF') end
    visualText('ACTIVE CASTS: none')
  else
    visualText('ACTIVE CASTS:')
    for i=1,#S.activeCasts do
      local c=S.activeCasts[i]
      local total=math.max(0.1,c.landTime-c.castStartTime)
      local remaining=math.max(0,c.landTime-t)
      local elapsed=total-remaining
      if t<c.castStartTime then elapsed=0; remaining=c.landTime-t end
      local pct=elapsed/total
      if pct<0 then pct=0 end
      if pct>1 then pct=1 end

      clericText(tostring(c.clericName))
      tankText('  -> '..tostring(c.tankName))
      redText(tostring(c.spellName)..' lands in '..fmtSec(remaining))

      if ImGui.ProgressBar~=nil then
        local ok=pcall(function() ImGui.ProgressBar(pct, 260, 26) end)
        if not ok then
          local bars=math.floor(pct*20); visualText('['..string.rep('#',bars)..string.rep('-',20-bars)..']')
        end
      else
        local bars=math.floor(pct*20); visualText('['..string.rep('#',bars)..string.rep('-',20-bars)..']')
      end
    end
  end

  -- Recent landed heals intentionally hidden to keep the driver monitor simple.


  pcall(function() ImGui.SetWindowFontScale(1.0) end)
end

local function drawLeftConfig()
  local ok,msg=timingStatus()
  ImGui.Text('Complete Heal Chain'); ImGui.Text('Status: '..tostring(S.statusMessage)); ImGui.Text('Timing: '..tostring(msg)); ImGui.Text('Current Tank: '..tostring(currentTank())); ImGui.Text('Last Cast: '..tostring(S.lastCastMessage))
  if S.preparingChain then greenText(string.format('Prep wait remaining: %.1f seconds',math.max(0,S.prepFinishTime-now()))) end
  ImGui.Separator(); ImGui.Text('Spell / Timing')
  S.spellName=inputText('Spell Name',S.spellName); S.spellGemNumber=inputInt('Spell Gem Number',S.spellGemNumber); S.chainDelaySeconds=inputFloat('Delay Between Clerics Seconds',S.chainDelaySeconds,0.5,1.0,'%.1f'); S.spellCastSeconds=10; S.recoveryBufferSeconds=1.5; S.memDelaySeconds=12; ImGui.Text('Spell Cast Seconds: 10.0'); ImGui.Text('Spell Mem Wait Seconds: 12.0'); ImGui.Text('Recovery Buffer Seconds: 1.5')
  if S.spellGemNumber<1 then S.spellGemNumber=1 end; if S.spellGemNumber>13 then S.spellGemNumber=13 end
  S.stopIfChainTooFast=toggle('Refuse Start If Chain Too Fast',S.stopIfChainTooFast)
  ImGui.Separator(); ImGui.Text('Late Heal Interrupt')
  S.enableLateHealInterrupt=toggle('Enable Late Heal Interrupt',S.enableLateHealInterrupt); if S.enableLateHealInterrupt == true then greenText('Late interrupt is ON') else redText('Late interrupt is OFF - no new interrupt timers will be sent') end; S.lateHealInterruptPct=inputFloat('Interrupt If Target HP >=',S.lateHealInterruptPct,1.0,5.0,'%.0f'); S.lateHealInterruptSecondsLeft=inputFloat('Interrupt Only If Cast Seconds Left <=',S.lateHealInterruptSecondsLeft,0.25,1.0,'%.2f'); S.lateHealInterruptCheckIntervalMs=inputInt('Interrupt Check Interval MS',S.lateHealInterruptCheckIntervalMs)
  ImGui.Separator(); ImGui.Text('Sit After Heal / Interrupt')
  S.forceSitAfterCast=toggle('Force Sit After Cast Or Interrupt',S.forceSitAfterCast)
  S.sitDelayAfterHealSeconds=inputFloat('Sit Delay After Heal Lands Seconds',S.sitDelayAfterHealSeconds,0.5,1.0,'%.1f')
  if S.sitDelayAfterHealSeconds<0 then S.sitDelayAfterHealSeconds=0 end
  if S.sitDelayAfterHealSeconds>10 then S.sitDelayAfterHealSeconds=10 end; ImGui.Text(string.format('Cancel gap max: %.1f seconds',cancelGap()))
  ImGui.Separator(); ImGui.Text('Tanks'); S.newTankName=inputText('New Tank Name',S.newTankName); if button('Add Tank From Raid') then addTank() end
  for i=1,#S.tanks do tankText(string.format('%d. %s',i,S.tanks[i])); sameLine(); if button('Set Tank '..i) then setTank(i) end; sameLine(); if button('Up T'..i) then moveUp(S.tanks,i) end; sameLine(); if button('Down T'..i) then moveDown(S.tanks,i) end; sameLine(); if button('Remove T'..i) then remTank(i); break end end
  ImGui.Separator(); ImGui.Text('Cleric Chain Order'); S.newClericName=inputText('New Cleric Name',S.newClericName); if button('Add Cleric From Raid') then addCleric() end
  for i=1,#S.clerics do clericText(string.format('%d. %s',i,S.clerics[i])); sameLine(); if button('Up C'..i) then moveUp(S.clerics,i) end; sameLine(); if button('Down C'..i) then moveDown(S.clerics,i) end; sameLine(); if button('Remove C'..i) then remCleric(i); break end end
  ImGui.Separator(); ImGui.Text('CWTN / MQ2Cleric'); S.cwtnPauseCommand=inputText('Pause Command',S.cwtnPauseCommand); S.cwtnUnpauseCommand=inputText('Unpause Command',S.cwtnUnpauseCommand); if button('Pause Chain Clerics Now') then pauseClerics() end; sameLine(); if button('Unpause Chain Clerics Now') then unpauseClerics() end; sameLine(); if button('End Macros On Chain Clerics') then endMacros() end
  ImGui.Separator(); ImGui.Text('Settings / Debug'); if button('Save Settings') then saveSettings() end; sameLine(); if button('Reload Settings') then loadSettings() end; sameLine(); if button('Reset Defaults') then resetDefaults() end; sameLine(); if button('Clear Debug Log') then clearLog() end
  S.debugMode=toggle('Enable Debug Mode',S.debugMode); S.debugEchoToMQ=toggle('Debug Echo To MQ Window',S.debugEchoToMQ); S.debugLogToFile=toggle('Debug Log To File',S.debugLogToFile); S.debugLogPulseChecks=toggle('Log Spammy Pulse Checks',S.debugLogPulseChecks); S.autoSaveOnExit=toggle('Auto Save On Exit',S.autoSaveOnExit); S.visualFontScale=inputFloat('Right Monitor Font Scale',S.visualFontScale or 1.8,0.1,0.5,'%.1f')
  ImGui.Text('Last Event: '..tostring(S.lastEventMessage)); ImGui.Text('Last Command: '..tostring(S.lastCommandSent)); ImGui.Text('Last Error: '..tostring(S.lastErrorMessage)); ImGui.Text(string.format('Commands Sent: %d',S.commandCounter)); ImGui.Text(string.format('Errors: %d',S.errorCounter)); ImGui.Text(string.format('GUI Errors: %d',S.guiErrorCounter))
end


local function drawBody()
  -- Two-column layout. Force the right monitor back to the top of column 2.
  -- Some MQ ImGui builds do not automatically reset Y on NextColumn(), which caused
  -- the monitor to draw at the bottom/right instead of the top/right.
  local startY = nil

  pcall(function()
    if ImGui.GetCursorPosY ~= nil then
      startY = ImGui.GetCursorPosY()
    end
  end)

  if ImGui.Columns ~= nil then
    ImGui.Columns(2, 'CHChainMainColumns', true)

    pcall(function()
      local winWidth = 1000
      if ImGui.GetWindowWidth ~= nil then
        winWidth = ImGui.GetWindowWidth()
      end

      if ImGui.SetColumnWidth ~= nil then
        ImGui.SetColumnWidth(0, winWidth * 0.58)
      end
    end)

    drawLeftConfig()

    ImGui.NextColumn()

    pcall(function()
      if startY ~= nil and ImGui.SetCursorPosY ~= nil then
        ImGui.SetCursorPosY(startY)
      end
    end)

    drawBigCastMonitor()

    ImGui.Columns(1)
  else
    drawLeftConfig()
    ImGui.Separator()
    drawBigCastMonitor()
  end
end

local function drawGUI()
  if not S.openGUI then return end

  -- Do not pass S.openGUI into ImGui.Begin.
  -- This MQ ImGui build was flipping S.openGUI false/nil from the Begin return values,
  -- which made the script keep running but the GUI disappear after one frame.
  local visible = true
  local okBegin, beginResult = pcall(function()
    return ImGui.Begin('Complete Heal Chain GUI')
  end)

  if not okBegin then
    S.guiErrorCounter = S.guiErrorCounter + 1
    S.lastErrorMessage = 'ImGui.Begin failed: '..tostring(beginResult)
    mq.cmd('/echo [CH GUI ERROR] '..tostring(S.lastErrorMessage))
    return
  end

  if type(beginResult) == 'boolean' then
    visible = beginResult
  end

  if visible then
    local ok,err=pcall(drawBody)
    if not ok then
      S.guiErrorCounter=S.guiErrorCounter+1
      S.lastErrorMessage='GUI body failed: '..tostring(err)
      mq.cmd('/echo [CH GUI ERROR] '..tostring(err))
    end
  end

  ImGui.End()
end

loadSettings()
mq.bind('/chchain',slash)
mq.imgui.init('CHChainGUI',drawGUI)
echo('Loaded. Use GUI or /chchain help.')
while S.scriptRunning do safe('pulsePrep',pulsePrep); safe('pulseChain',pulseChain); safe('pulseCastVisuals',pulseCastVisuals); mq.delay(50) end
if S.chainRunning then stopChain() end
if S.autoSaveOnExit then saveSettings() end
echo('Exited.')

Here is a Lua i created also for CH chain casting. you will want to modify to stop / pause the Lua running on clerics when you start the script. I have an option to end macro and stop CWTN, but nothign for stopping other Lua's, i don't think.

it allows you to add clerics to the chain, remove, change order, add tanks, remove, hcane order, tank swap, clerics interrupt at a threshold, sit down between casts, etc.

i went form clerics OOM before defensive was up to essentially 90 mana with this script. Turn it on, let it run for hours, it won't land any spells or use any mana until the tank needs to be healed. I'm going to tweak it as the gui is big and takes up a lot of the screen. The visuals of countdown timers and what not can all be shrunken when the chain is casting.

I did not code any of this, this is just using AI and telling it what i wanted, tweaking, testing, etc.
 
gonna lock this thread (algar/derp are welcome to open it if they weren't bothered by the off-topic). the question was about for rgmercs, so going off to talk about reacts (which is strongly recommended against due to limitations and bugs prevalent with mq2react compared to something like LEM or just a Lua), or entire other scripts is just not really all that helpful.
 
Question - Cleric CH Rotation
Status
Not open for further replies.

Users who are viewing this thread

Back
Top
Cart