<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!--

Vitals display and notification plugin for the Discworld MUD.
=============================================================

Also requires my:
	* RGBToInt module.
	* window module.
	* GMCP Interface plugin.

-->
<muclient>
<plugin
	name="RossVitalsDisplay"
	author="Ross Grams"
	id="416394bc6414e779f4b9e389"
	language="Lua"
	purpose="Vitals bars and notifications"
	date_written="2019-02-14"
	save_state="y"
	requires="5.05"
	version="0.1"
	>
</plugin>
<script>
	<![CDATA[

-- Example GMCP Packet for reference:
-- char.vitals {"alignment":"quite evil","maxhp":1900,"hp":1900,"xp":83490,"maxgp":264,"burden":13,"gp":264}

require "json"

local SELF_ID = GetPluginID()
local GMCP_INTERFACE_ID = "c190b5fc9e83b05d8de996c3"

-- Get require path for modules - relative path from mushclient directory.
local mushClientPath, pluginPath = GetInfo(66), GetPluginInfo(SELF_ID, 20)
local requirePath = pluginPath:gsub(mushClientPath, ""):gsub("\\", ".")

local RGBToInt = require (requirePath .. "RGBToInt")
local window = require (requirePath .. "window")

-- Utility functions.
--------------------------------------------------
local function shallowCopy(d)
	t = {}
	for k,v in pairs(d) do  t[k] = v  end
	return t
end

local function toboolean(v) -- Convert a value to a boolean. Always returns a boolean.
	if v == nil then  return false
	elseif v == false or v == "false" then  return false  end
	return true -- Anything other than nil, false, or "false" ==> true.
end

-- Hidden User Settings:
--------------------------------------------------
--		I didn't think these needed to be in the menu options, or they just
-- 	aren't there yet, but feel free to change them.

local labelFontSizes = { 6, 8, 10, 12, 14, 16, 18 } -- Font sizes to load. Labels are scaled in increments.
local labelFontName = ""
local labelAllowedFraction = 0.35 -- Fraction of the total available width that the label is allowed to take.
local labelSizeDownFraction = 0.9 -- If the label is larger than this * allowed space, size it down.
local labelSizeUpFraction = 0.7 -- If the label is smaller than this * allowed space, size it up.
local labelColor = RGBToInt(255)
local labelStatStrings = { -- To convert stat keys into text for display labels.
	hp = "HP", gp = "GP", xp = "XP", burden = "Bu", alignment = "Al"
}
local labelHorizPadding = 2

local statColors = { -- Bar fill colors.
	hp = RGBToInt(200, 0, 0), gp = RGBToInt(90, 180, 0),
	xp = RGBToInt(20, 50, 120), burden = RGBToInt(0, 140, 255),
	alignment = RGBToInt(100),
}
local statNotifColors = {
	gain = {
		hp = RGBToInt(120, 190, 0), maxhp = RGBToInt(120, 255, 0),
		gp = RGBToInt(130, 144, 255), maxgp = RGBToInt(130, 160, 255),
		xp = RGBToInt(0, 255, 255),
		burden = RGBToInt(90, 180, 0),
		alignment = RGBToInt(220, 255, 255),
	},
	loss = {
		hp = RGBToInt(200, 0, 0), maxhp = RGBToInt(255, 0, 0),
		gp = RGBToInt(30, 144, 255), maxgp = RGBToInt(50, 160, 255),
		xp = RGBToInt(0, 255, 255),
		burden = RGBToInt(90, 180, 0),
		alignment = RGBToInt(255, 255, 200),
	},
}
local notifBracketColor = RGBToInt(190)

local winPadding = 2 -- Padding between bars and edge of window
local winBackgroundColor = RGBToInt(0, 0, 0)
local winBorderColor = 12632256
local barBackgroundColor = RGBToInt(35)
local barFillColor = RGBToInt(225, 100, 25)
local barBorderColor = RGBToInt(120)

-- Constants:
--------------------------------------------------
-- 	MUSHclient or Discworld stuff that shouldn't change.

local WIN_RECT_INFO_CODES = {x = 10, y = 11, width = 3, height = 4, z = 22}

local ALL_STATS = {
	"hp", "maxhp", "gp", "maxgp", "xp", "burden", "alignment"
}
local ALL_INDIV_STATS = { "hp", "gp", "xp", "burden", "alignment" }

local IS_STAT = {} -- Dicts with keys matching the above for identifying things.
local IS_INDIV_STAT = {} -- Dicts with keys matching the above for identifying things.
for i,v in ipairs(ALL_STATS) do  IS_STAT[v] = true  end
for i,v in ipairs(ALL_INDIV_STATS) do  IS_INDIV_STAT[v] = true  end

local NOTIF_SETTING_KEYS = {}
for i,v in ipairs(ALL_STATS) do
	table.insert(NOTIF_SETTING_KEYS, "notify" .. v .. "Gain")
	table.insert(NOTIF_SETTING_KEYS, "notify" .. v .. "Loss")
end

local MAX_HP_REGEN = 6
local MAX_GP_REGEN = 5

local ALIGNMENT_NUMBERS = {
	["extremely good"] = 50,
	["very good"] = 25,
	["quite good"] = 12.5,
	["good"] = 6,
	["barely good"] = 3,
	["neutral"] = 0,
	["barely evil"] = -3,
	["evil"] = -6,
	["quite evil"] = -12.5,
	["very evil"] = -25,
	["extremely evil"] = -50,
}

-- Current Working Data & Settings Vars:
--------------------------------------------------
-- 	If applicable, the initial defaults are set here, but they will be
--		saved and loaded from disk after the plugin is run the first time.

local vitals = {isEmptyVitals=true,alignment=0,maxhp=100,hp=0,xp=0,maxgp=100,burden=0,gp=0}
local diffs = {}
local gpRegen = 3
local hpRegen = 4
local alignStr = "neutral"
local winRect = {x = 500, y = 0, width = 350, height = 70, z = 0}
local showSettings = { -- Which stats to display.
	hp = true, gp = true, xp = true, burden = true, alignment = true
}
local notifSettings = {
	hp = {gainOn = true, gainT = 20, lossOn = false, lossT = 5},
	maxhp = {gainOn = false, gainT = 0, lossOn = true, lossT = 0},
	gp = {gainOn = true, gainT = 150, lossOn = true, lossT = 4},
	maxgp = {gainOn = false, gainT = 0, lossOn = false, lossT = 0},
	xp = {gainOn = true, gainT = 100, lossOn = false, lossT = 0},
	burden = {gainOn = false, gainT = 0, lossOn = false, lossT = 0},
	alignment = true,
}

-- Other Plugin Vars:
--------------------------------------------------
-- 	Random other stuff used by the plugin.

local showCount = 0 -- Save number of stats shown so we don't have to count keys every draw.
local winID = SELF_ID .. "VitalsDisplay"
local labelFonts = {}
local lastFont
local longestLabelString = "HP"

local HEARTBEAT_TIMER_NAME = "heartbeat"
local HEARTBEAT_TIMER_FLAGS = 1 + 1024 + 16384 -- enabled, replace (just in case), and temporary? ("don't save to world file"?)

-- Load saved settings.
--------------------------------------------------
local function updateShowCount()
	showCount = 0
	for k,v in pairs(showSettings) do
		showCount = showCount + (v and 1 or 0)
	end
end

-- Load window settings.
for k,v in pairs(winRect) do
	winRect[k] = tonumber(GetVariable("window_" .. k)) or v
end
local winLocked = toboolean(GetVariable("windowLocked"))
local stackVertically = toboolean(GetVariable("stackVertically"))

for i,stat in ipairs(ALL_STATS) do
	-- Load show settings.
	if stat ~= "maxhp" and stat ~= "maxgp" then
		local v = toboolean(GetVariable("show" .. stat))
		if v ~= nil then  showSettings[stat] = v  end
	end

	if stat == "alignment" then
		local v = toboolean(GetVariable("notifyalignment"))
	else
		local g = GetVariable("notify" .. stat .. "GainEnabled")
		if g ~= nil then  notifSettings[stat].gainOn = toboolean(g)  end
		local gt = GetVariable("notify" .. stat .. "GainThreshold")
		if gt ~= nil then  notifSettings[stat].gainT = tonumber(gt)  end

		local l = GetVariable("notify" .. stat .. "LossEnabled")
		if l ~= nil then  notifSettings[stat].lossOn = toboolean(l)  end
		local lt = GetVariable("notify" .. stat .. "LossThreshold")
		if lt ~= nil then  notifSettings[stat].lossT = tonumber(lt)  end
	end
end

updateShowCount()

-- Load regen rates.
gpRegen = tonumber(GetVariable("gpRegen")) or gpRegen
hpRegen = tonumber(GetVariable("hpRegen")) or hpRegen

-- Save persistent settings.
--------------------------------------------------

-- Need to make sure to do this before the window is deleted.
-- Apparently OnPluginSaveState() happens AFTER OnPluginClose().
local function updateWinRect()
	for k,v in pairs(winRect) do
		winRect[k] = WindowInfo(winID, WIN_RECT_INFO_CODES[k])
	end
	winLocked = window.getLocked(winID)
end

local function saveSetting(name, val, nilVal)
	if val == nil then  val = nilVal or false  end
	SetVariable(name, tostring(val))
end

function OnPluginSaveState()
	-- Save window settings.
	for k,v in pairs(winRect) do
		saveSetting("window_" .. k, v)
	end
	saveSetting("windowLocked", winLocked)

	for i,stat in ipairs(ALL_STATS) do
		if stat ~= "maxhp" and stat ~= "maxgp" then
			local varName = "show" .. stat
			saveSetting(varName, showSettings[stat])
		end

		if stat == "alignment" then
			saveSetting("notifyalignment", notifSettings[stat])
		else
			local gainOnVar = "notify" .. stat .. "GainEnabled"
			local gainTVar = "notify" .. stat .. "GainThreshold"
			local lossOnVar = "notify" .. stat .. "LossEnabled"
			local lossTVar = "notify" .. stat .. "LossThreshold"
			saveSetting(gainOnVar, notifSettings[stat].gainOn)
			saveSetting(gainTVar, notifSettings[stat].gainT)
			saveSetting(lossOnVar, notifSettings[stat].lossOn)
			saveSetting(lossTVar, notifSettings[stat].lossT)
		end
	end
	saveSetting("stackVertically", stackVertically)
	saveSetting("gpRegen", gpRegen)
end

-- Drawing & Output:
--------------------------------------------------
local function sendNotification(stat, gain_loss, diff)
	-- Use gain_loss to figure the color.
	local c = statNotifColors[gain_loss][stat]
	ColourNote(
		RGBColourToName(notifBracketColor), "", "{",
		RGBColourToName(c), "", string.format("%+i %s", diff, stat),
		RGBColourToName(notifBracketColor), "", "}"
	)
end

local function calcFontSize(text, curFont, w, h)
	local count = 0

	-- Find font index so we can increment it up or down.
	local fontIndex = 1
	for i,f in ipairs(labelFonts) do
		if f == curFont then
			fontIndex = i
			break
		end
	end

	-- Check height.
	local font = labelFonts[fontIndex]
	font = font or labelFonts[1] -- If nil, get smallest font.
	local fh = font.height
	while fh < labelSizeUpFraction * h do -- Font height is below threshold, size up.
		count = count + 1;  if count > 50 then  break  end
		if fontIndex >= #labelFonts then -- Max size font.
			break
		end
		fontIndex = math.min(fontIndex + 1, #labelFonts)
		font = labelFonts[fontIndex]
		fh = font.height
	end
	while fh > labelSizeDownFraction * h do -- Font height is above threshold, size down.
		count = count + 1;  if count > 50 then  break  end
		fontIndex = math.max(fontIndex - 1, 0)
		if fontIndex > 0 then
			font = labelFonts[fontIndex]
			fh = font.height
		else
			font, fh = nil, -math.huge
		end
	end

	-- Check width.
	-- 	Font is already as big as it can be by height, so only size down.
	local fw = -math.huge
	if font then
		fw = WindowTextWidth(winID, font.id, text)
		while fw > (labelSizeDownFraction * w - labelHorizPadding*2) do
			count = count + 1;  if count > 50 then  break  end
			fontIndex = math.max(fontIndex - 1, 0)
			if fontIndex > 0 then
				font = labelFonts[fontIndex]
				fw = WindowTextWidth(winID, font.id, text)
			else
				font, fw = nil, -math.huge
			end
		end
	end
	return font, fw
end

local function draw()
	local width, height = window.getSize(winID)

	WindowRectOp(winID, 2, 0, 0, width, height, winBackgroundColor)
	WindowRectOp(winID, 1, 0, 0, width, height, winBorderColor)

	width, height = width - winPadding * 2, height - winPadding * 2

	-- Calculate a uniform label font size.
	local font = lastFont or labelFonts[math.ceil(#labelFonts/2)]
	local textH = stackVertically and height/showCount or height
	local textW = stackVertically and width or width/showCount
	textW = textW * labelAllowedFraction
	local maxLabelW = 0
	font, maxLabelW = calcFontSize(longestLabelString, font, textW, textH)
	lastFont = font

	local i = 0
	local p = winPadding
	for _,stat in ipairs(ALL_INDIV_STATS) do
		if showSettings[stat] then
			-- Figure rect.
			local x, y, w, h
			if stackVertically then
				x, y = p, i * height/showCount + p
				w, h = width, height/showCount
			else
				x, y = i * width/showCount + p, p
				w, h = width/showCount, height
			end

			-- Draw Label Text.
			if font then
				local text = labelStatStrings[stat]
				local extraY = (h - font.height)/2
				local ty = y + extraY -- Center label vertically.
				local fw = maxLabelW
				if not stackVertically then -- If stacking vertically, align the bars.
					fw = WindowTextWidth(winID, font.id, text)
				end
				local xpad = x + labelHorizPadding
				WindowText(winID, font.id, text, xpad, ty, xpad+fw, ty+h, labelColor)
				fw = fw + labelHorizPadding * 2
				w = w - fw;  x = x + fw
			end

			-- Draw background, border, and fill.
			local curVal = vitals[stat]
			local maxVal = curVal
			if stat == "burden" then
				maxVal = 100
			elseif stat == "hp" or stat == "gp" then
				maxVal = vitals["max" .. stat]
			end
			local fract = math.max(0, math.min(1, curVal / maxVal))
			WindowRectOp(winID, 2, x, y, x+w, y+h, barBackgroundColor)
			local col = statColors[stat]
			if fract > 0 then -- Seems to be issues with drawing 0-width rects (...but only when `font == nil`???)
				WindowRectOp(winID, 2, x, y, x+w*fract, y+h, col)
			end
			WindowRectOp(winID, 1, x, y, x+w, y+h, barBorderColor)

			-- Draw Number Text.
			if font then
				local str = ""
				if stat == "alignment" then
					str = alignStr
				elseif stat == "burden" then
					str = tostring(curVal) .. " %"
				elseif stat == "xp" then
					str = tostring(curVal)
				elseif stat == "hp" or stat == "gp" then
					str = string.format("%i / %i", curVal, maxVal)
				end
				local fw = WindowTextWidth(winID, font.id, str)
				local tx = x + (w - fw)/2
				local ty = y + (h - font.height)/2
				WindowText(winID, font.id, str, tx, ty, x+w, y+h, labelColor)
			end

			i = i + 1
		end
	end
end

-- Update:
--------------------------------------------------

-- Update with new vitals data.
-- Data table can have any number of valid stat keys.
local function updateVitals(newVitals, forceUpdate)
	if type(newVitals.alignment) == "string" then
		alignStr = newVitals.alignment
		newVitals.alignment = ALIGNMENT_NUMBERS[newVitals.alignment]
	end
	local needRedraw = forceUpdate
	for stat,val in pairs(newVitals) do
		if IS_STAT[stat] then
			-- Get diff and store.
			local diff = val - vitals[stat]
			diffs[stat] = diff

			if diff ~= 0 then  needRedraw = true  end

			-- Handle notifications.
			if stat == "alignment" then
				if diff ~= 0 then -- Alignment is a special case.
					sendNotification(stat, diff > 0 and "gain" or "loss", diff)
				end
			else -- Normal stats have separate gain/loss settings.
				-- If either notification is enabled, send it. (if the diff meets the threshold)
				local notifStat = notifSettings[stat]
				local gain = notifStat.gainOn and notifStat.gainT or nil
				local loss = notifStat.lossOn and notifStat.lossT or nil
				if gain and diff > gain then
					sendNotification(stat, "gain", diff)
				elseif loss and diff < -loss then
					sendNotification(stat, "loss", diff)
				end
			end

			-- Store new value.
			vitals[stat] = val
		end
	end
	-- Redraw display if anything has changed.
	if needRedraw then  window.draw(winID)  end
end

-- GMCP Stuff:
--------------------------------------------------
function onGMCPReceived(message, params)
	local data = json.decode(params)
	if message == "char.vitals" then
		if vitals.isEmptyVitals then -- First packet.
			vitals = shallowCopy(data) -- So we won't show diff notifications.
			vitals.alignment = ALIGNMENT_NUMBERS[vitals.alignment]
		end
		updateVitals(data, true) -- We're making all diffs zero, so need to force update.
	end
end

-- Heartbeat Timer:
--------------------------------------------------
function heartbeatTick(timerName)
	if not vitals.isEmptyVitals then
		local needUpdate = false
		local newVitals
		if vitals.gp < vitals.maxgp and gpRegen > 0 then
			needUpdate = true
			newVitals = {gp = math.min(vitals.maxgp, vitals.gp + gpRegen)}
		end
		if vitals.hp < vitals.maxhp and hpRegen > 0 then
			needUpdate = true
			newVitals = newVitals or {}
			newVitals.hp = math.min(vitals.maxhp, vitals.hp + hpRegen)
		end
		if needUpdate then  updateVitals(newVitals)  end
	end
end

-- Input Box Stuff:
--------------------------------------------------
local INPUT_BOX_SETTINGS = {
	box_width = 400,
	box_height = 200,
	prompt_width = 350,
	prompt_height = 50,
	reply_width = 350,
	reply_height = 50,
}
local INPUT_BOX_MSG_PREFIX = "\n	 "
local INPUT_BOX_TITLE_PREFIX = "  "

local function inputBox(msg, title, defaultText)
	local result = utils.inputbox(
		INPUT_BOX_MSG_PREFIX .. msg, INPUT_BOX_TITLE_PREFIX .. title,
		defaultText or "", "Arial", 15, INPUT_BOX_SETTINGS
	)
	if result == "" then  result = nil  end -- Just so we can check `if result then`...
	return result
end

-- Menu Functions:
--------------------------------------------------
 -- These will always get the local full-menu index as the first arg.

local function setStackVert(i)
	stackVertically = not stackVertically
	window.checkMenuItem(winID, i)
	window.draw(winID)
end

local function toggleShowStat(i, stat)
	showSettings[stat] = not showSettings[stat]
	window.checkMenuItem(winID, i)
	updateShowCount()
	window.draw(winID)
end

local function toggleNotifEnabled(i, stat, isGain)
	local notifStat = notifSettings[stat]
	if stat == "alignment" then
		notifStat = not notifStat
	elseif isGain then
		notifStat.gainOn = not notifStat.gainOn
	else
		notifStat.lossOn = not notifStat.lossOn
	end
	window.checkMenuItem(winID, i)
end

local function setNotifThreshold(i, stat, isGain)
	local gstr = isGain and "gain" or "loss"
	local result = inputBox(
		string.format("Enter your desired %s %s notification threshold.", stat, gstr),
		"Set Notification Threshold", tostring(notifSettings[stat][gstr .. "T"])
	)
	if tonumber(result) then
		result = math.floor(tonumber(result))
		notifSettings[stat][gstr .. "T"] = result
		local newMenuStr = string.format("Set %s threshold...(cur: %i)", gstr, result)
		window.setMenuItem(winID, i, newMenuStr)
		ColourNote("#00FF00", "", "Notification threshold set to: " .. tostring(result))
	else
		ColourNote("red", "", "Set Notification Threshold Failed: Input must be a number.")
	end
end

local function setRegenRate(i, stat, rate)
	local lastRate = 3
	if stat == "HP" then
		lastRate = hpRegen
		hpRegen = rate
	elseif stat == "GP" then
		lastRate = gpRegen
		gpRegen = rate
	end
	window.checkMenuItem(winID, i, true) -- Check chose menu item.

	-- Un-check last item - find it based on the difference in rates.
	local diff = rate - lastRate
	i = i - diff
	window.checkMenuItem(winID, i, false)
end

-- Menu Handling:
--------------------------------------------------
local menu = {}
local menuResponses = {}

local statCount = #ALL_INDIV_STATS
local SHOW_RANGE = {2, 2 + statCount}

local function menuAdd(str, func, arg1, arg2, arg3)
	table.insert(menu, str)
	if func then
		menuResponses[#menu] = {func, arg1, arg2, arg3}
	end
end

local function generateMenu()
	menuAdd((stackVertically and "+" or "") .. "Stack Bars Vertically",
				setStackVert)

	-- Show stats submenu.
	menuAdd(">Stats to Show:")
	for i,stat in ipairs(ALL_INDIV_STATS) do
		menuAdd((showSettings[stat] and "+" or "") .. stat,
					toggleShowStat, stat)
	end
	menuAdd("<")

	-- Notifications submenus.
	menuAdd(">Notifications:")
	for i,stat in ipairs(ALL_STATS) do
		menuAdd(">" .. stat)
		if stat == "alignment" then
			local enabled = notifSettings[stat]
			menuAdd((enabled and "+" or "") .. "Show",
						toggleNotifEnabled, stat)
		else
			local gEnabled = notifSettings[stat].gainOn
			local gThresh = notifSettings[stat].gainT
			menuAdd((gEnabled and "+" or "") .. "Show on gain",
						toggleNotifEnabled, stat, true)
			menuAdd("Set gain threshold...(cur: " .. gThresh .. ")",
						setNotifThreshold, stat, true)
			menuAdd("-")
			local lEnabled = notifSettings[stat].lossOn
			local lThresh = notifSettings[stat].lossT
			menuAdd((lEnabled and "+" or "") .. "Show on loss",
						toggleNotifEnabled, stat, false)
			menuAdd("Set loss threshold...(cur: " .. lThresh .. ")",
						setNotifThreshold, stat, false)
		end
		menuAdd("<") -- close stat
	end
	menuAdd("<") -- close notifications

	-- Regen rate submenus.
	for i=1, 2 do
		local stat = i == 1 and "HP" or "GP"
		menuAdd(">" .. stat .. " Regen Rate:")
		local curRate = i == 1 and hpRegen or gpRegen
		local maxRate = i == 1 and MAX_HP_REGEN or MAX_GP_REGEN
		for rate=0, maxRate do
			local str = tostring(rate)
			if rate == curRate then  str = "+" .. str  end
			menuAdd(str, setRegenRate, stat, rate)
		end
		menuAdd("<") -- close submenu
	end
end

local function menuItemClicked(winID, i, prefix, item)
	--Note("menuItemClicked - i = " .. i .. ", " .. prefix .. ", " .. item)
	local resp = menuResponses[i]
	if resp then
		resp[1](i, resp[2], resp[3], resp[4]) -- Always sends local full-menu index as first arg.
	end
end

-- Setup - OnPluginInstall and OnPLuginEnable.
--------------------------------------------------
local function init()
	-- Subscribe to GMCP.
	CallPlugin(GMCP_INTERFACE_ID, "subscribe", SELF_ID, "onGMCPReceived",
		"char.vitals")

	-- Create Window.
	window.new(
		winID, winRect.x, winRect.y, winRect.width, winRect.height, winRect.z,
		nil, nil, winBackgroundColor, -- align, flags, bgColor
		true, winLocked, menuItemClicked, draw -- visible, locked, menuCb, drawCb
	)

	-- Add menu items.
	generateMenu()
	window.addMenuItems(winID, nil, menu)

	-- Load Fonts for Window.
	for i,size in ipairs(labelFontSizes) do
		local fontID = tostring(size)
		WindowFont(winID, fontID, labelFontName, size)
		local fh = WindowFontInfo(winID, fontID, 1)
		local avgCharW = WindowFontInfo(winID, fontID, 6)
		table.insert(labelFonts, {id = fontID, height = fh, avgCharW = avgCharW})
	end
	-- Get the longest label string for calculating uniform label font size.
	local fontID = labelFonts[1].id -- Just use first size to test. (shouldn't matter)
	local maxW = 0
	for stat,labelString in pairs(labelStatStrings) do
		local fw = WindowTextWidth(winID, fontID, labelString)
		if fw > maxW then
			maxW = fw
			longestLabelString = labelString
		end
	end

	-- Draw Window.
	draw()

	-- Start Heartbeat Timer.
	AddTimer(HEARTBEAT_TIMER_NAME, 0, 0, 2, "", HEARTBEAT_TIMER_FLAGS, "heartbeatTick")
end

-- Teardown - OnPluginClose (uninstall) and OnPluginDisable.
--------------------------------------------------
local function final()
	CallPlugin(GMCP_INTERFACE_ID, "unsubscribe", SELF_ID, "char.vitals")
	updateWinRect()
	WindowDelete(winID)
	DeleteTimer(HEARTBEAT_TIMER_NAME)
end

function OnPluginInstall()  init()  end
function OnPluginEnable()  init()  end
function OnPluginClose()  final()  end
function OnPluginDisable()  final() end

	]]>
</script>

</muclient>