local re = re local sdk = sdk local d2d = d2d local imgui = imgui local log = log local json = json local draw = draw local require = require local tostring = tostring local pairs = pairs local ipairs = ipairs local math = math local string = string local table = table local type = type local _ = _ local Core = require("_CatLib") local Debug = require("_CatLib.debug") local Draw = require("_CatLib.draw") local CONST = require("_CatLib.const") local mod = require("mhwilds_overlay.mod") local OverlayData = require("mhwilds_overlay.data") local _M = {} local ElementImages = {} local CrownImages = {} local EnemyIdNameMap = Core.GetEnumMap("app.EnemyDef.ID") local EnemyImages = {} local EnemyImageUnknown ---@param ctx EnemyContext local function TryLoadEnemyImage(ctx) local emId = ctx:get_EmID() local name = EnemyIdNameMap[emId] if not name then return end if EnemyImages[name] == false then return EnemyImageUnknown end if EnemyImages[name] ~= nil then return EnemyImages[name] end local iconPath = string.format("enemy/small/tex000201_1_IMLM4.tex.241106027_%s.png", string.upper(name)) local image = mod.LoadImage(iconPath) if image then EnemyImages[name] = image return image end EnemyImages[name] = false -- log.error(string.format("Enemy Icon: %s missing", iconPath)) return EnemyImageUnknown end -- Red HP part ---@class RedHpCtxCache ---@field lastRatio number ---@field lastHitTimestamp number ---@field redRatio number ---@field initialRedRatio number ---@field fadeStartTimestampe number ---@class RedHpCache ---@field simulatedTime number ---@field lastTime number ---@field lastIsSegmented number ---@field lastSegmentedMode number ---@field lastSegmentedValue number ---@field lastSegmentedPercentage number ---@field lastSegmentedInBar boolean ---@field Cache table ---@return RedHpCtxCache local function NewCtxRedHpCache() return { lastRatio = 1, lastHitTimestamp = -1, redRatio = 0, initialRedRatio = 0, -- 消逝过程中,初始的 red ratio fadeStartTimestampe = nil, -- 消逝起始时间 } end ---@return RedHpCache local function NewRedHpCache() return { simulatedTime = 0, -- 任务停止后 QuestTime 停了,模拟时间 lastTime = 0, -- 检测任务时间是否停止 lastIsSegmented = false, -- 切换 seg mode 时清空 lastSegmentedMode = false, -- 切换 seg mode 时清空 lastSegmentedValue = 0, -- 切换 seg value 时清空 lastSegmentedPercentage = 0, -- 切换 seg value 时清空 lastSegmentedInBar = false, Cache = {}, } end ---@type table _M.RedHpCache = {} _M.QuestElapsedTime = -1 _M.QuestTimeLimit = 1 mod.OnDebugFrame(function () if _M.QuestElapsedTime <= 0 then return end for _, cache in pairs(_M.RedHpCache) do imgui.text(string.format("QuestTime %0.1f, SimTime: %0.1f", _M.QuestElapsedTime, cache.simulatedTime)) for _, data in pairs(cache.Cache) do imgui.text(string.format("Last: ratio: %0.1f%%, hit timestamp: %0.1f", data.lastRatio*100, data.lastHitTimestamp)) imgui.text(string.format("Now timestamp: %0.1f, delta: %0.1f", _M.QuestElapsedTime, _M.QuestElapsedTime - data.lastHitTimestamp)) if data.redRatio then imgui.text(string.format("Red Ratio: %0.1f%%, fadeTime: %0.1f", data.redRatio*100, data.fadeStartTimestampe or -1)) end end imgui.text("") end end) function _M.ClearData() _M.RedHpCache = {} _M.QuestElapsedTime = -1 _M.QuestTimeLimit = 1 end local EnemySmallIconRect = { Enable = true, Width = 32, Height = 32, VerticalCenter = true, MarginX = 0, } local EnemyCrownRect = { Enable = true, Width = 32, Height = 32, VerticalCenter = true, MarginX = 0, } local ElementIconRect = { Enable = true, Width = 32, Height = 32, VerticalCenter = true, MarginX = 0, } local HpHeaderCanvas = Draw.NewDivCanvas({ Enable = true, OffsetX = 0, OffsetY = 0, Width = 1000, Height = 200, Absolute = true, UseBackground = false, }) local HpBarsCanvas = Draw.NewDivCanvas({ Enable = true, OffsetX = 0, OffsetY = 0, Width = 1000, Height = 200, Absolute = true, UseBackground = false, }) local HpSegementIndicatorsCanvas = Draw.NewDivCanvas({ Enable = true, Width = 32, Height = 32, UseBackground = false, }) local EnemyIdNameMap = Core.GetEnumMap("app.EnemyDef.ID") ---@param conf HpWidgetHeader local function DrawHeaderText(conf, ctx, canvas, id, hpCurrent, hpMax, showQuest) if not conf.Enable then return end canvas.Debug(mod.Config.Debug) canvas.Init() local maxH = 0 if conf.AutoCrownSize or conf.AutoWeaknessIconSize or conf.ShowSmallIcon then if conf.Name.Enable then local _, h = Draw.LoadD2dFontWithConfig(conf.Name):measure("4000") maxH = math.max(maxH, h) end if conf.HP.Enable then local _, h = Draw.LoadD2dFontWithConfig(conf.HP):measure("4000") maxH = math.max(maxH, h) end if conf.Action and conf.Action.Enable then local _, h = Draw.LoadD2dFontWithConfig(conf.Action):measure("4000") maxH = math.max(maxH, h) end if conf.QuestTime and conf.QuestTime.Enable then local _, h = Draw.LoadD2dFontWithConfig(conf.QuestTime):measure("4000") maxH = math.max(maxH, h) end end if conf.ShowSmallIcon then local img = TryLoadEnemyImage(ctx) if img then EnemyCrownRect.Width = maxH EnemyCrownRect.Height = maxH canvas.Image(EnemySmallIconRect, img) end end if conf.Name.Enable then local msg = Core.GetEnemyName(id) if mod.Config.Debug then local emId = ctx:get_EmID() local name = EnemyIdNameMap[emId] msg = name .. " " .. msg end canvas.Text(conf.Name, msg) end if conf.ShowCrownIcon then local crown = OverlayData.EnemyCrown[ctx] if crown and crown > CONST.CrownType.None then local img = CrownImages[crown] if img then local h = maxH if h == 0 or not conf.AutoCrownSize then h = conf.CrownIconSize end EnemyCrownRect.Width = h EnemyCrownRect.Height = h canvas.Image(EnemyCrownRect, img) else canvas.Text(conf.HP, CONST.CrownTypeNames[crown]) end end if mod.Config.Debug then canvas.Text(conf.HP, tostring(OverlayData.EnemyScale[ctx])) end end if conf.HP.Enable and (conf.ShowCurrentHpValues or conf.ShowMaxHpValues) then local s = {} if conf.ShowCurrentHpValues then s[#s+1] = tostring(hpCurrent) end if conf.ShowMaxHpValues then if conf.ShowCurrentHpValues then s[#s+1] = "/" end s[#s+1] = tostring(hpMax) end canvas.Text(conf.HP, table.concat(s,"")) end local idKey = tostring(id) if conf.ShowWeaknessIcon and OverlayData.EnemyElementMeat[idKey] then for i, data in pairs(OverlayData.EnemyElementMeat[idKey]) do if not mod.Config.Debug then if data.Meat < 19.9 then break end end local img = ElementImages[data.Type] if img then local h = maxH if h == 0 or not conf.AutoWeaknessIconSize then h = conf.WeaknessIconSize end ElementIconRect.Width = h ElementIconRect.Height = h canvas.Image(ElementIconRect, img) if conf.ShowWeaknessDetail then canvas.Text(conf.HP, string.format("%d", math.ceil(data.Meat))) end else canvas.Text(conf.HP, string.format("%s: %d", data.Type, math.ceil(data.Meat))) end if mod.Config.Debug then canvas.Text(conf.HP, tostring(data.Meat)) end end end if conf.Action and conf.Action.Enable then local actionController = OverlayData.EnemyInfo[ctx].ActionController if actionController then local current = actionController._CurrentActionID local curCate = current._Category local curIdx = current._Index local enemy = OverlayData.EnemyInfo[ctx].Character ---@type app.CharacterParamHolder local charaParamHolder = enemy:get_CharaParamHolder() -- TODO: FIXME:有时候任务结束了但是居然没清空??为什么 if charaParamHolder then local actionDict = charaParamHolder:get_ActionDictionary()._ActionIDHolder if actionDict:getActionCategory() == curCate then local ids = actionDict._ActionID._ActionIDArray:get_DataArray() local data = ids:get_Item(curIdx) ---@type string local name = data._Class if name:sub(1, 1) == "c" then name = name:sub(2) end canvas.Text(conf.Action, name) end end if mod.Config.Debug then local next = actionController._NextActionID local nextCate = next._Category local nextIdx = next._Index canvas.Text(conf.Action, string.format(" %d/%d -> %d/%d", curCate, curIdx, nextCate, nextIdx)) end end end if showQuest and conf.QuestTime and conf.QuestTime.Enable then local elapsed = _M.QuestElapsedTime local elapsedMin = math.floor(elapsed / 60.0) local elapsedSecs = elapsed % 60.0 local limit = math.floor(_M.QuestTimeLimit) local msg = string.format("%02d:%02.0f/%d:00", elapsedMin, elapsedSecs, limit) canvas.Text(conf.QuestTime, msg) end canvas.End() end ---@param conf HpWidgetHpSegement ---@param canvas DivCanvas local function DrawHpSegment(conf, canvas, hpCurrent, hpMax, hpRatio, segmentHpCurrent, segmentHpMax, totalLives, remainLives) if not conf.Enable or not conf.Indicator.Enable or hpCurrent <= 0 then return canvas.NextPos() end local needDisplay = (not conf.Indicator.HideDrainIndicator) or (remainLives > 0) local display = needDisplay or mod.Config.Debug if not display then return canvas.NextPos() end canvas.Debug(mod.Config.Debug) canvas.Init() if mod.Config.Debug then canvas.Text({ BlockRenderX = true, BlockRenderY = true, }, string.format("RemainLives: %d/%d - %d/%d", remainLives, totalLives, segmentHpCurrent, segmentHpMax)) end local indiConf = conf.Indicator for i = 0, totalLives-1, 1 do if i >= remainLives and conf.Indicator.HideDrainIndicator then goto continue end local x, y = canvas.NextPos() local ratio = 1 if i == remainLives-1 then ratio = hpRatio elseif i >= remainLives then ratio = 0 end if indiConf.UseCircleShape then canvas.Circle(indiConf.Circle, ratio) else canvas.Rect(indiConf.Rect, ratio) end if mod.Config.Debug then Draw.Text(x, y, 0xFFFFFFFF, tostring(i)) end ::continue:: end local endX, endY = canvas.NextPos() canvas.End() if indiConf.UseCircleShape then endY = endY + indiConf.Circle.Radius*2 else endY = endY + indiConf.Rect.Height end return endX, endY end ---@param theme HpWidgetTheme ---@param ctx EnemyContext function _M.DrawHpBar(theme, ctx, x, y, width, height, showQuest) if not theme.Enable then return x, y end local data = OverlayData.EnemyInfo[ctx] local startX = x local startY = y -- Health local hpCurrent = math.ceil(data.HP) local hpMax = math.ceil(data.MaxHP) local hpRatio = hpCurrent/hpMax local segmentHpCurrent = hpCurrent local segmentHpMax = hpMax local segmentRatio = segmentHpCurrent/segmentHpMax local remainLives = 1 local totalLives = 1 -- HP Segement if theme.Hp.Enable and theme.HpSegement and theme.HpSegement.Enable then local SEGMENT_MAX = theme.HpSegement.SegmentValue -- 血条长度 if theme.HpSegement.UsePercentageSegment then SEGMENT_MAX = math.ceil(theme.HpSegement.SegmentPercentage / 100 * hpMax) end local lives = math.ceil(hpMax / SEGMENT_MAX) -- 总血条数量 5000 -> 2000+2000+1000 if lives <= 0 then lives = 1 end totalLives = lives local firstLifeHp = hpMax % SEGMENT_MAX local normalizedHpMax = hpMax if firstLifeHp > 1 and hpMax - hpCurrent > firstLifeHp then normalizedHpMax = hpMax + (SEGMENT_MAX - firstLifeHp) end local usedLives = math.floor((normalizedHpMax - hpCurrent) / SEGMENT_MAX) -- 已消耗的血条数量 remainLives = lives - usedLives -- 4000 -> 2, 4500 -> 2 if remainLives == 0 then remainLives = 1 end local currentLifeMaxHp = SEGMENT_MAX local currentLifeHp = hpCurrent - ((remainLives-1)*SEGMENT_MAX) currentLifeMaxHp = math.min(currentLifeMaxHp, hpMax) if currentLifeHp <= 0 then remainLives = 0 end segmentHpCurrent = currentLifeHp segmentHpMax = currentLifeMaxHp segmentRatio = currentLifeHp/currentLifeMaxHp if theme.HpSegement.UseSegHpValueInBar then hpRatio = segmentRatio end end segmentHpCurrent = math.ceil(segmentHpCurrent) segmentHpMax = math.ceil(segmentHpMax) local nextX, nextY = startX, startY -- Text local TextX, TextY = nextX, nextY local topBarWidth = 0 if theme.Hp.Enable and topBarWidth == 0 then topBarWidth = theme.Hp.Width + theme.Hp.OffsetX elseif theme.Stamina and theme.Stamina.Enable and topBarWidth == 0 then topBarWidth = theme.Stamina.Width + theme.Stamina.OffsetX elseif theme.Angry and theme.Angry.Enable and topBarWidth == 0 then topBarWidth = theme.Angry.Width + theme.Angry.OffsetX else topBarWidth = width end if theme.Header.Enable then local posX = TextX + theme.Header.OffsetX local posY = TextY + theme.Header.OffsetY HpHeaderCanvas.RePos(posX, posY) local h = theme.Header.Height if h <= 0 then h = 1 end HpHeaderCanvas.ReSize(topBarWidth-5, h) HpHeaderCanvas.PaddingX = theme.Header.PaddingX local id = ctx:get_EmID() if theme.HpSegement and theme.HpSegement.UseSegHpValueInText then DrawHeaderText(theme.Header, ctx, HpHeaderCanvas, id, segmentHpCurrent, segmentHpMax, showQuest) else DrawHeaderText(theme.Header, ctx, HpHeaderCanvas, id, hpCurrent, hpMax, showQuest) end HpHeaderCanvas.End() nextX, nextY = startX, posY + h end -- TODO: Text -- Draw.Text(HealthXUpL, HealthYUpL, 0xFFFFFFFF, string.format("%0.1f/%0.0f (%0.1f%%)", hpCurrent, hpMax, hpRatio*100)) -- Draw.Text(StaminaXUpL, StaminaYUpL, 0xFFFFFFFF, string.format("%0.1f/%0.0f (%0.1f%%)", spCurrent, spMax, spRatio*100)) if theme.Hp.Enable and theme.RedHp and theme.RedHp.Enable then local QuestElapsedTime = _M.QuestElapsedTime if _M.RedHpCache[theme] == nil then _M.RedHpCache[theme] = NewRedHpCache() end if theme.HpSegement then if _M.RedHpCache[theme].lastIsSegmented ~= theme.HpSegement.Enable or _M.RedHpCache[theme].lastSegmentedMode ~= theme.HpSegement.UsePercentageSegment or _M.RedHpCache[theme].lastSegmentedValue ~= theme.HpSegement.SegmentValue or _M.RedHpCache[theme].lastSegmentedPercentage ~= theme.HpSegement.SegmentPercentage or _M.RedHpCache[theme].lastSegmentedInBar ~= theme.HpSegement.UseSegHpValueInBar then _M.RedHpCache[theme].Cache = {} _M.RedHpCache[theme].Cache[ctx] = NewCtxRedHpCache() _M.RedHpCache[theme].Cache[ctx].lastRatio = hpRatio end _M.RedHpCache[theme].lastIsSegmented = theme.HpSegement.Enable _M.RedHpCache[theme].lastSegmentedMode = theme.HpSegement.UsePercentageSegment _M.RedHpCache[theme].lastSegmentedValue = theme.HpSegement.SegmentValue _M.RedHpCache[theme].lastSegmentedPercentage = theme.HpSegement.SegmentPercentage _M.RedHpCache[theme].lastSegmentedInBar = theme.HpSegement.UseSegHpValueInBar end local simTime = OverlayData.SimulatedTime if _M.RedHpCache[theme].Cache[ctx] == nil then _M.RedHpCache[theme].Cache[ctx] = NewCtxRedHpCache() end if _M.RedHpCache[theme].Cache[ctx].lastHitTimestamp <= 0 then _M.RedHpCache[theme].Cache[ctx].lastRatio = hpRatio _M.RedHpCache[theme].Cache[ctx].lastHitTimestamp = simTime end local RedCtxCache = _M.RedHpCache[theme].Cache[ctx] -- 百分比变多了 if hpRatio > RedCtxCache.lastRatio then RedCtxCache.redRatio = 1 - hpRatio RedCtxCache.initialRedRatio = RedCtxCache.redRatio end _M.RedHpCache[theme].lastTime = simTime if hpRatio < RedCtxCache.lastRatio then if RedCtxCache.fadeStartTimestampe == nil then RedCtxCache.redRatio = RedCtxCache.lastRatio - hpRatio + RedCtxCache.redRatio else -- 衰减中,则直接清空 RedCtxCache.redRatio = RedCtxCache.lastRatio - hpRatio end RedCtxCache.initialRedRatio = RedCtxCache.redRatio RedCtxCache.lastHitTimestamp = simTime RedCtxCache.fadeStartTimestampe = nil elseif simTime - RedCtxCache.lastHitTimestamp > theme.RedHp.DelayTime or (hpCurrent <= 0 and simTime - RedCtxCache.lastHitTimestamp > theme.RedHp.DelayTime) then if RedCtxCache.fadeStartTimestampe == nil then RedCtxCache.fadeStartTimestampe = simTime RedCtxCache.initialRedRatio = RedCtxCache.redRatio else local fadeTime = theme.RedHp.FadeTime local elapsed = math.min(simTime - RedCtxCache.fadeStartTimestampe, fadeTime) local progress = math.min(elapsed / fadeTime, 1) -- Ease out RedCtxCache.redRatio = RedCtxCache.initialRedRatio * (1 - progress)^2 end end RedCtxCache.lastRatio = hpRatio _M.RedHpCache[theme].Cache[ctx] = RedCtxCache end HpBarsCanvas.RePos(nextX, nextY) HpBarsCanvas.ReSize(width, height) HpBarsCanvas.Debug(mod.Config.Debug) HpBarsCanvas.Init() if theme.Hp.Enable then if theme.RedHp and theme.RedHp.Enable then HpBarsCanvas.Rect(theme.Hp, hpRatio, _M.RedHpCache[theme].Cache[ctx].redRatio, theme.RedHp.Color) else HpBarsCanvas.Rect(theme.Hp, hpRatio) end end -- Stamina & Angry if (theme.Stamina and theme.Stamina.Enable) or (theme.Angry and theme.Angry.Enable) then local conds = ctx.Conditions._Conditions -- Stamina if theme.Stamina and theme.Stamina.Enable then local spCurrent local spMax local tired = conds:get_Item(1) if tired:get_IsActive() then spMax = tired:get_ActivateTime() spCurrent = spMax - tired:get_CurrentTimer() else spCurrent = tired:get_Stamina() spMax = tired:get_DefaultStamina() end local spRatio = spCurrent/spMax HpBarsCanvas.Rect(theme.Stamina, spRatio) end -- Angry if theme.Angry and theme.Angry.Enable then local angry = conds:get_Item(0) local agCurrent local agMax if angry:get_IsActive() then agMax = angry:get_ActivateTime() agCurrent = agMax - angry:get_CurrentTimer() else agCurrent = angry:get_Value() agMax = angry:get_LimitValue() end local agRatio = agCurrent/agMax HpBarsCanvas.Rect(theme.Angry, agRatio) end end _, nextY = HpBarsCanvas.NextPos() -- Segment HP Indicators if theme.HpSegement and theme.HpSegement.Enable then local posY = nextY + theme.HpSegement.Indicator.OffsetY HpSegementIndicatorsCanvas.RePos(startX + theme.HpSegement.Indicator.OffsetX, posY) HpSegementIndicatorsCanvas.ReSize(topBarWidth, 48) local _, segEndY = DrawHpSegment(theme.HpSegement, HpSegementIndicatorsCanvas, hpCurrent, hpMax, segmentRatio, segmentHpCurrent, segmentHpMax, totalLives, remainLives) nextY = math.max(nextY, segEndY) end HpBarsCanvas.End() return x, nextY end mod.D2dRegister(function () ElementImages["Fire"] = mod.LoadImage("icons/fire.png") ElementImages["Water"] = mod.LoadImage("icons/water.png") ElementImages["Ice"] = mod.LoadImage("icons/ice.png") ElementImages["Thunder"] = mod.LoadImage("icons/thunder.png") ElementImages["Dragon"] = mod.LoadImage("icons/dragon.png") CrownImages[CONST.CrownType.Small] = mod.LoadImage("icons/crown_small.png") CrownImages[CONST.CrownType.Big] = mod.LoadImage("icons/crown_silver.png") CrownImages[CONST.CrownType.King] = mod.LoadImage("icons/crown_king.png") EnemyImageUnknown = mod.LoadImage("enemy/small/tex000201_1_IMLM4.tex.241106027_EM0000_00_0.png") end, function () end) return _M