Quantcast
if select(6, GetAddOnInfo("PitBull4_" .. (debugstack():match("[o%.][d%.][u%.]les\\(.-)\\") or ""))) ~= "MISSING" then return end

local PitBull4 = _G.PitBull4
if not PitBull4 then
	error("PitBull4_VisualHeal requires PitBull4")
end

-- CONSTANTS ----------------------------------------------------------------

local EPSILON = 1e-5

-----------------------------------------------------------------------------

local L = PitBull4.L
local LibHealComm

local PitBull4_VisualHeal = PitBull4:NewModule("VisualHeal", "AceEvent-3.0")

PitBull4_VisualHeal:SetModuleType("custom")
PitBull4_VisualHeal:SetName(L["Visual heal"])
PitBull4_VisualHeal:SetDescription(L["Visualises healing done by you and your group members before it happens."])
PitBull4_VisualHeal:SetDefaults({
	show_overheal = true,
	}, {
	incoming_color = { 0.4, 0.6, 0.4, 0.75 },
	outgoing_color = { 0, 1, 0, 1 },
	outgoing_color_overheal = { 1, 0, 0, 0.65 },
	auto_luminance = true,
})

function PitBull4_VisualHeal:OnEnable()
	if not LibHealComm then
		LoadAddOn("LibHealComm-4.0")
		LibHealComm = LibStub("LibHealComm-4.0", true)
	end
	if not LibHealComm then
		error(L["PitBull4_VisualHeal requires the library LibHealComm-4.0 to be available."])
	end

	LibHealComm.RegisterCallback(self, "HealComm_HealStarted")
	LibHealComm.RegisterCallback(self, "HealComm_HealUpdated")
	LibHealComm.RegisterCallback(self, "HealComm_HealStopped")
	LibHealComm.RegisterCallback(self, "HealComm_HealDelayed")
	LibHealComm.RegisterCallback(self, "HealComm_ModifierChanged")
	LibHealComm.RegisterCallback(self, "HealComm_GUIDDisappeared")

	self:RegisterEvent("UNIT_HEALTH")
	self:RegisterEvent("UNIT_MAXHEALTH", "UNIT_HEALTH")
end

function PitBull4_VisualHeal:OnDisable()
	LibHealComm.UnregisterAllCallbacks(self)
end

local function clamp(value, min, max)
	if value < min then
		return min
	elseif value > max then
		return max
	else
		return value
	end
end

local REVERSE_POINT = {
	LEFT = "RIGHT",
	RIGHT = "LEFT",
	TOP = "BOTTOM",
	BOTTOM = "TOP",
}

local player_guid = UnitGUID("player")
local player_is_casting = false
local player_healing_guids = {}
local player_end_time = nil

function PitBull4_VisualHeal:UpdateFrame(frame)
	local health_bar = frame.HealthBar
	local unit = frame.unit
	local guid = frame.guid
	if not health_bar or not LibHealComm or not unit or not guid then
		return self:ClearFrame(frame)
	end

	local current_time = GetTime()
	local time_band = 4 -- default to a 4 second window
	if player_is_casting and player_healing_guids[guid] then
		time_band = math.min(player_end_time - current_time, 4)
	end

	local player_healing = LibHealComm:GetHealAmount(guid, LibHealComm.ALL_HEALS, current_time + time_band, player_guid)
	local others_healing = LibHealComm:GetOthersHealAmount(guid, LibHealComm.ALL_HEALS, current_time + time_band)

	-- Bail out early if nothing going on for this unit
	if not player_healing and not others_healing then
		return self:ClearFrame(frame)
	end

	local heal_modifier = LibHealComm:GetHealModifier(guid)

	local unit_health_max = UnitHealthMax(unit)
	local current_percent = UnitHealth(unit) / unit_health_max

	local others_percent = others_healing and heal_modifier * others_healing / unit_health_max or 0
	local player_percent = player_healing and heal_modifier * player_healing / unit_health_max or 0
	if others_percent <= 0 and player_percent <= 0 then
		return self:ClearFrame(frame)
	end

	local bar = frame.VisualHeal
	if not bar then
		bar = PitBull4.Controls.MakeBetterStatusBar(health_bar)
		frame.VisualHeal = bar
		bar:SetBackgroundAlpha(0)
	end

	local show_overheal = self:GetLayoutDB(frame).show_overheal

	-- If the user has selected to not show overheal we make sure to not set a value that goes beyond 100%.
	if not show_overheal and ((others_percent+current_percent) > 1) then
		others_percent = 1 - current_percent
	end

	bar:SetValue(math.min(others_percent, 1))

	if not show_overheal and ((player_percent+others_percent+current_percent) > 1) then
		player_percent = 1 - (others_percent+current_percent)
	end

	bar:SetExtraValue(player_percent)
	bar:SetTexture(health_bar:GetTexture())

	local deficit = health_bar.deficit
	local orientation = health_bar.orientation
	local reverse = health_bar.reverse
	bar:SetOrientation(orientation)
	bar:SetReverse(deficit ~= reverse)

	bar:ClearAllPoints()
	local point, attach, attach_frame
	if orientation == "HORIZONTAL" then
		point, attach = "LEFT", "RIGHT"
		bar:SetWidth(health_bar:GetWidth())
		bar:SetHeight(0)
		bar:SetPoint("TOP", health_bar, "TOP")
		bar:SetPoint("BOTTOM", health_bar, "BOTTOM")
	else
		point, attach = "BOTTOM", "TOP"
		bar:SetHeight(health_bar:GetHeight())
		bar:SetWidth(0)
		bar:SetPoint("LEFT", health_bar, "LEFT")
		bar:SetPoint("RIGHT", health_bar, "RIGHT")
	end

	if deficit then
		point, attach = attach, point
		attach_frame = health_bar.bg
	else
		attach_frame = health_bar.fg
	end

	if reverse then
		point, attach = REVERSE_POINT[point], REVERSE_POINT[attach]
	end

	bar:SetPoint(point, attach_frame, attach)

	local db = self.db.profile.global

	if others_percent > 0 then
		local r, g, b, a = unpack(db.incoming_color)
		bar:SetColor(r, g, b)
		bar:SetNormalAlpha(a)
	end

	if player_percent > 0 then
		local waste = clamp((current_percent + others_percent + player_percent - 1) / player_percent, 0, 1)

		local r, g, b, a = unpack(db.outgoing_color)
		if waste > 0 then
			local r2, g2, b2, a2 = unpack(db.outgoing_color_overheal)

			local inverse_waste = 1 - waste
			r = r * inverse_waste + r2 * waste
			g = g * inverse_waste + g2 * waste
			b = b * inverse_waste + b2 * waste
			a = a * inverse_waste + a2 * waste
		end

		if db.auto_luminance then
			local high = math.max(r, g, b, EPSILON)
			r, g, b = r / high, g / high, b / high
		end

		bar:SetExtraColor(r, g, b)
		bar:SetExtraAlpha(a)
	end

	return true
end

function PitBull4_VisualHeal:UNIT_HEALTH(event, unit)
	self:UpdateForUnitID(unit)
end

function PitBull4_VisualHeal:ClearFrame(frame)
	if not frame.VisualHeal then
		return false
	end

	frame.VisualHeal = frame.VisualHeal:Delete()
	return true
end

PitBull4_VisualHeal.OnHide = PitBull4_VisualHeal.ClearFrame

function PitBull4_VisualHeal:HealComm_HealStarted(event, caster_guid, spell_id, heal_type, end_time, ...)
	if caster_guid == player_guid and bit.band(heal_type,LibHealComm.DIRECT_HEALS) ~= 0 then
		player_is_casting = true
		wipe(player_healing_guids)
		for i=1,select('#',...) do
			player_healing_guids[select(i,...)] = true
		end
		player_end_time = end_time
	end

	for frame in PitBull4:IterateFramesForGUIDs(...) do
		self:Update(frame)
	end
end

function PitBull4_VisualHeal:HealComm_HealUpdated(event, caster_guid, spell_id, heal_type, end_time, ...)
	for frame in PitBull4:IterateFramesForGUIDs(...) do
		self:Update(frame)
	end
end

function PitBull4_VisualHeal:HealComm_HealDelayed(event, caster_guid, spell_id, heal_type, end_time, ...)
	if caster_guid == player_guid then
		player_end_time = end_time
	end

	for frame in PitBull4:IterateFramesForGUIDs(...) do
		self:Update(frame)
	end
end

function PitBull4_VisualHeal:HealComm_HealStopped(event, caster_guid, spell_id, heal_type, interrupted, ...)
	if caster_guid == player_guid and bit.band(heal_type,LibHealComm.DIRECT_HEALS) ~= 0 then
		player_is_casting = false
	end

	for frame in PitBull4:IterateFramesForGUIDs(...) do
		self:Update(frame)
	end
end

function PitBull4_VisualHeal:HealComm_ModifierChanged(event, guid)
	self:UpdateForGUID(guid)
end

function PitBull4_VisualHeal:HealComm_GUIDDisappeared(event, guid)
	self:UpdateForGUID(guid)
end

PitBull4_VisualHeal:SetColorOptionsFunction(function(self)
	local function get(info)
		return unpack(self.db.profile.global[info[#info]])
	end
	local function set(info, r, g, b, a)
		local color = self.db.profile.global[info[#info]]
		color[1], color[2], color[3], color[4] = r, g, b, a
		self:UpdateAll()
	end
	return 'incoming_color', {
		type = 'color',
		name = L['Incoming color'],
		desc = L['The color of the bar that shows incoming heals from other players.'],
		get = get,
		set = set,
		hasAlpha = true,
		width = 'double',
	},
	'outgoing_color', {
		type = 'color',
		name = L['Outgoing color (no overheal)'],
		desc = L['The color of the bar that shows your own heals, when no overhealing is due.'],
		get = get,
		set = set,
		hasAlpha = true,
		width = 'double',
	},
	'outgoing_color_overheal', {
		type = 'color',
		name = L['Outgoing color (overheal)'],
		desc = L['The color of the bar that shows your own heals, when full overhealing is due.'],
		get = get,
		set = set,
		hasAlpha = true,
		width = 'double',
	},
	'auto_luminance', {
		type = 'toggle',
		name = L["Auto-luminance"],
		desc = L["Automatically adjust the luminance of the color of the outgoing heal bar to max."],
		get = function(info)
			return self.db.profile.global.auto_luminance
		end,
		set = function(info, value)
			self.db.profile.global.auto_luminance = value
			self:UpdateAll()
		end,
		width = 'double',
	},
	function(info)
		self.db.profile.global.incoming_color = { 0.4, 0.6, 0.4, 0.75 }
		self.db.profile.global.outgoing_color = { 0, 1, 0, 1 }
		self.db.profile.global.outgoing_color_overheal = { 1, 0, 0, 0.65 }
		self.db.profile.global.auto_luminance = true
	end
end)

PitBull4_VisualHeal:SetLayoutOptionsFunction(function(self)
	local function disabled(info)
		return not PitBull4.Options.GetLayoutDB(self).enabled
	end

	return 'show_overheal', {
		type = 'toggle',
		name = L['Show overheals'],
		desc = L['Show overheals past the end of the health bar.'],
		get = function(info)
			return PitBull4.Options.GetLayoutDB(self).show_overheal
		end,
		set = function(info, value)
			PitBull4.Options.GetLayoutDB(self).show_overheal = value
		end,
		disabled = disabled,
	}
end)