-- LuaFormatter off
local DEBUG_MODE = false
local function debugPrint(...)
if DEBUG_MODE then
print("InfoBanner Debug:", ...)
end
end
local OPEN_FRAMES = {
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim0",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim1",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim2",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim3",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim4",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim5",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim6",
"/game_notifications/assets/images/infobanner/backdrop-infobanner-anim7"
}
local MAX_WIDTH = 289
local BANNER_HEIGHT = 88
local FRAME_MS = 45
local DEFAULT_HOLD_MS = 3000
local FADE_IN_MS = 400
local FADE_OUT_MS = 300
local FADE_INTERVAL_MS = 20
local ICON_SHOW_PROGRESS = 0.25
local ICON_HIDE_PROGRESS = 0.25
local BANNER_MARGIN_OFFSET = 10
local ANIM_OFFSET = 10
local TOTAL_FRAMES = #OPEN_FRAMES
local eventCategory = {
CLIENT_EVENT_TYPE_SIMPLE = 1,
CLIENT_EVENT_TYPE_ACHIEVEMENT = 2,
CLIENT_EVENT_TYPE_TITLE = 3,
CLIENT_EVENT_TYPE_LEVEL = 4,
CLIENT_EVENT_TYPE_SKILL = 5,
CLIENT_EVENT_TYPE_BESTIARY = 6,
CLIENT_EVENT_TYPE_BOSSTIARY = 7,
CLIENT_EVENT_TYPE_QUEST = 8,
CLIENT_EVENT_TYPE_COSMETIC = 9,
CLIENT_EVENT_TYPE_PROFICIENCY = 10,
CLIENT_EVENT_TYPE_BOUNTY_TASK = 11,
CLIENT_EVENT_TYPE_WEEKLY_TASK = 12,
CLIENT_EVENT_TYPE_SPELL_UNLOCKED = 13,
CLIENT_EVENT_TYPE_LAST = 14
}
local eventType = {
CLIENT_EVENT_NONE = 0,
CLIENT_EVENT_BOSSDEFEATED = 1,
CLIENT_EVENT_DEATHPVE = 2,
CLIENT_EVENT_DEATHPVP = 3,
CLIENT_EVENT_PLAYERKILLASSIST = 4,
CLIENT_EVENT_PLAYERKILL = 5,
CLIENT_EVENT_PLAYERATTACKING = 6,
CLIENT_EVENT_TREASUREFOUND = 7,
CLIENT_EVENT_GIFTOFLIFE = 8,
CLIENT_EVENT_ATTACKSTOPPED = 9,
CLIENT_EVENT_CAPACITYLIMIT = 10,
CLIENT_EVENT_OUTOFAMMO = 11,
CLIENT_EVENT_TARGETTOOCLOSE = 12,
CLIENT_EVENT_OUTOFSOULPOINTS = 13,
CLIENT_EVENT_TUTORIALCOMPLETE = 14,
CLIENT_EVENT_LAST = 15
}
local skinType = {
outfit = 0,
addon1 = 1,
addon2 = 2,
mount = 3
}
local SkillId = {
Magic = 1,
Sword = 2,
Club = 3,
Axe = 4,
Fist = 5,
Distance = 6,
Shielding = 7,
Fishing = 8
}
local skillNames = {
[SkillId.Magic] = { name = "Magic Level", icon = "magic" },
[SkillId.Sword] = { name = "Sword Fighting", icon = "sword" },
[SkillId.Club] = { name = "Club Fighting", icon = "club" },
[SkillId.Axe] = { name = "Axe Fighting", icon = "axe" },
[SkillId.Fist] = { name = "Fist Fighting", icon = "fist" },
[SkillId.Distance] = { name = "Distance Fighting", icon = "distance" },
[SkillId.Shielding] = { name = "Shielding", icon = "shielding" },
[SkillId.Fishing] = { name = "Fishing", icon = "fishing" }
}
local infoPopUp = {
[eventCategory.CLIENT_EVENT_TYPE_COSMETIC] = {
--type(int), lookType(int), skinName(string), skinType(int)
{
title = "Outfit Unlocked",
description = "You have unlocked '%s'", --skinName
hasCreatureId = true,
img = "/game_notifications/assets/images/nodo/icon-infobanner-unlock"
}
},
[eventCategory.CLIENT_EVENT_TYPE_BOSSTIARY] = {
--type(int), raceId(int), progressLevel(int)
{
title = "Bosstiary Progress",
description = "You have progressed '%s'",--progressLevel
hasRaceId = true,
img = "/game_notifications/assets/images/nodo/icon-infobanner-unlock"
}
},
[eventCategory.CLIENT_EVENT_TYPE_BESTIARY] = {
--type(int), raceId(int), progressLevel(int)
{
title = "Bestiary Progress",
description = "You have progressed '%s'", --progressLevel
hasRaceId = true,
img = "/game_notifications/assets/images/nodo/icon-infobanner-unlock"
}
},
[eventCategory.CLIENT_EVENT_TYPE_ACHIEVEMENT] = {
-- type(int), name(string)
{
title = "New Achievement",
description = "You have earned '%s'", --name
img = "/game_notifications/assets/images/nodo/icon-infobanner-achievements"
}
},
[eventCategory.CLIENT_EVENT_TYPE_TITLE] = {
-- type(int), name(string)
{
title = "Title Gained",
description = "You have earned '%s'", --name
img = "/game_notifications/assets/images/nodo/icon-infobanner-title"
}
},
[eventCategory.CLIENT_EVENT_TYPE_PROFICIENCY] = {
-- type(int), itemId(int), message(string)
{
title = "Weapon Proficiency",
description = "you have improved '%s'", -- message
hasItemId = true,
img = "/game_notifications/assets/images/nodo/icon-infobanner-unlock"
}
},
[eventCategory.CLIENT_EVENT_TYPE_QUEST] = {
-- type(int), questName(string), isCompleted(bool)
[true] = { -- isCompleted(bool)
title = "Quest completed",
description = "you have finished '%s'",
img = "/game_notifications/assets/images/nodo/icon-infobanner-quests"
},
[false] = { -- isCompleted(bool)
title = "Quest started",
description = "you have begun '%s'",
img = "/game_notifications/assets/images/nodo/icon-infobanner-quests"
}
},
[eventCategory.CLIENT_EVENT_TYPE_LEVEL] = {
{
title = "Level %d!",
description = "You gained hit points, mana, and capacity.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-levelup"
}
},
[eventCategory.CLIENT_EVENT_TYPE_SKILL] = {
-- type(int), skillId(int), level(int)
{
title = "%s",
description = "your skill has advanced to level %d",
img = "/game_notifications/assets/images/nodo/icon-infobanner-skill-%s"
}
},
[eventCategory.CLIENT_EVENT_TYPE_SIMPLE] = {
-- type(int), eventType(int)
[eventType.CLIENT_EVENT_CAPACITYLIMIT] = {
title = "Capacity Limit",
description = "Remove items before adding new ones.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-quests"
},
[eventType.CLIENT_EVENT_OUTOFAMMO] = {
title = "Out of Ammunition",
description = "You have no arrow or bolt equipped.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-hint"
},
[eventType.CLIENT_EVENT_TARGETTOOCLOSE] = {
title = "Target Too Close",
description = "You are using a ranged auto-attack at melee distance.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-hint"
},
[eventType.CLIENT_EVENT_OUTOFSOULPOINTS] = {
title = "Out of Soul Points",
description = "You don't have enough soul points to cast this spell.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-hint"
},
[eventType.CLIENT_EVENT_TUTORIALCOMPLETE] = {
title = "Off to New Shores",
description = "Leave the village and set sail to start your real adventure.",
img = "/game_notifications/assets/images/nodo/icon-infobanner-offtonewshores"
}
}
}
-- LuaFormatter on
notificationsController.event = nil
notificationsController.state = "idle"
notificationsController.queue = {}
notificationsController.widgets = {}
function notificationsController:onClientEvent(eventCat, ...)
if not modules.client_options.getOption("showInfoBanner") then
g_logger.debug("The server has sent infobaner, but the checkbox in client_options is disabled..")
return
end
local args = {...}
local popupTemplate = nil
if eventCat == eventCategory.CLIENT_EVENT_TYPE_SIMPLE then
local eventType = args[1]
popupTemplate = infoPopUp[eventCat] and infoPopUp[eventCat][eventType]
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_QUEST then
local isCompleted = args[2] == 1 or args[2] == true
popupTemplate = infoPopUp[eventCat] and infoPopUp[eventCat][isCompleted]
elseif infoPopUp[eventCat] and infoPopUp[eventCat][1] then
popupTemplate = infoPopUp[eventCat][1]
end
if not popupTemplate then
debugPrint("No infoPopUp found for eventCat:", eventCat)
return
end
local title = popupTemplate.title
local description = popupTemplate.description
local img = popupTemplate.img
local extraData = {}
if eventCat == eventCategory.CLIENT_EVENT_TYPE_QUEST then
local questName = args[1]
title = type(title) == 'string' and title:format(questName) or title
description = type(description) == 'string' and description:format(questName) or description
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_PROFICIENCY then
local itemId = args[1]
local message = args[2]
description = type(description) == 'string' and description:format(message) or description
if popupTemplate.hasItemId then
extraData.itemId = itemId
end
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_LEVEL then
local level = args[1]
title = type(title) == 'string' and title:format(level) or title
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_SKILL then
local skillId = args[1]
local level = args[2]
local data = skillNames[skillId] or {
name = "Skill",
icon = "fist"
}
title = type(title) == 'string' and title:format(data.name) or title
description = type(description) == 'string' and description:format(level) or description
img = type(img) == 'string' and img:format(data.icon) or img
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_COSMETIC then
local lookType = args[1]
local skinName = args[2]
local skinType = tonumber(args[3])
if skinType == 1 then
skinName = skinName .. " (Addon 1)"
elseif skinType == 2 then
skinName = skinName .. " (Addon 2)"
end
description = type(description) == 'string' and description:format(skinName) or description
if popupTemplate.hasCreatureId then
extraData.creatureId = lookType
extraData.skinType = skinType
end
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_BESTIARY or eventCat == eventCategory.CLIENT_EVENT_TYPE_BOSSTIARY then
local raceId = args[1]
local progressLevel = args[2]
description = type(description) == 'string' and description:format(progressLevel) or description
if popupTemplate.hasRaceId then
extraData.raceId = raceId
end
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_ACHIEVEMENT then
local name = args[1]
description = type(description) == 'string' and description:format(name) or description
elseif eventCat == eventCategory.CLIENT_EVENT_TYPE_TITLE then
local name = args[1]
description = type(description) == 'string' and description:format(name) or description
end
self:show(title, description, img, DEFAULT_HOLD_MS, extraData)
end
function infoBanner_onTerminate()
notificationsController:hideImmediate()
if notificationsController.ui then
notificationsController:unloadHtml()
end
end
function notificationsController:ensure()
if self.ui and not self.ui:isDestroyed() then
return
end
self:loadHtml("templates/infobanner.html", modules.game_interface.getMapPanel())
self.ui:hide()
self.widgets = {
paper = self:findWidget("#paper"),
anim = self:findWidget("#animation"),
icon = self:findWidget("#icon"),
title = self:findWidget("#title"),
desc = self:findWidget("#desc"),
append = self:findWidget("#append"),
fadeTexts = self.ui:querySelectorAll(".fade-text"),
fadeIcons = self.ui:querySelectorAll(".fade-icon")
}
debugPrint("HTML Widget Initialized/Ensured")
end
function notificationsController:updateBannerPosition()
if not self.ui or self.ui:isDestroyed() then
return
end
local statsBarHeight = modules.game_interface.StatsBar.getHeight()
local marginTop = statsBarHeight + BANNER_MARGIN_OFFSET
self.ui:setMarginTop(marginTop)
debugPrint("Banner margin-top set to:", marginTop)
end
function notificationsController:cancelEvent()
if self.event then
removeEvent(self.event)
self.event = nil
end
end
function notificationsController:show(title, desc, img, holdMs, extraData)
self:ensure()
debugPrint("Adding to queue ->", tostring(title))
table.insert(self.queue, {
title = title,
desc = desc,
img = img,
holdMs = holdMs or DEFAULT_HOLD_MS,
extraData = extraData or {}
})
if self.state == "idle" then
self:processNext()
end
end
function notificationsController:setWidgetsOpacity(widgets, opacity)
for _, widget in ipairs(widgets) do
widget:setOpacity(opacity)
end
end
function notificationsController:setContentOpacity(opacity)
if self.widgets.fadeTexts then
self:setWidgetsOpacity(self.widgets.fadeTexts, opacity)
end
end
function notificationsController:setLeftIconsOpacity(opacity)
if self.widgets.fadeIcons then
self:setWidgetsOpacity(self.widgets.fadeIcons, opacity)
end
end
function notificationsController:setPaperSize(width)
local paper = self.widgets.paper
paper:setWidth(width)
paper:setImageRect({
x = 0,
y = 0,
width = width,
height = BANNER_HEIGHT
})
end
function notificationsController:resetBanner()
local w = self.ui
w:setOpacity(1.0)
w:setMarginLeft(0)
w:show()
self:setContentOpacity(0)
self:setLeftIconsOpacity(0)
self:setPaperSize(0)
if self.widgets.append then
self.widgets.append:destroyChildren()
end
local anim = self.widgets.anim
anim:show()
anim:setMarginLeft(0)
anim:setImageSource(OPEN_FRAMES[1])
end
function notificationsController:processNext()
self:cancelEvent()
if #self.queue == 0 then
debugPrint("Queue empty. Unloading UI.")
self.state = "idle"
if self.ui then
self:unloadHtml()
self.widgets = {}
end
return
end
self:updateBannerPosition()
local data = table.remove(self.queue, 1)
if not self.ui or self.ui:isDestroyed() then
self.state = "idle"
return
end
self:resetBanner()
if data.img then
self.widgets.icon:setImageSource(data.img)
end
self.widgets.title:setText(data.title or "")
self.widgets.desc:setText(data.desc or "")
if data.extraData and self.widgets.append then
local appendW = self.widgets.append
appendW:destroyChildren()
if data.extraData.itemId then
local itemId = data.extraData.itemId
local html = string.format([[
]], itemId)
appendW:append(html)
elseif data.extraData.raceId then
local raceId = data.extraData.raceId
local raceData = g_things.getRaceData(raceId)
if not raceData or (raceData.raceId == 0 and (not raceData.outfit or raceData.outfit.type == 0)) then
g_logger.warning(string.format("Creature with race id %s was not found.", raceId))
else
local html = string.format([[
]])
local outfit = appendW:append(html)
outfit:setOutfit(raceData.outfit)
end
elseif data.extraData.creatureId then
local html = string.format([[
]], data.extraData.creatureId)
if data.extraData.skinType == skinType.outfit then
local outfit = appendW:append(html)
outfit:setOutfit({
type = data.extraData.creatureId
})
elseif data.extraData.skinType == skinType.addon1 or data.extraData.skinType == skinType.addon2 then
local outfit = appendW:append(html)
outfit:setOutfit({
type = data.extraData.creatureId,
addons = data.extraData.skinType
})
elseif data.extraData.skinType == skinType.mount then
local outfit = appendW:append(html)
outfit:setOutfit({
type = data.extraData.creatureId
})
end
end
end
self.state = "opening"
debugPrint("Starting Banner ->", data.title)
self:animateOpen(data.holdMs)
end
function notificationsController:animateOpen(holdMs)
local frame = 1
local iconsShown = false
local anim = self.widgets.anim
local function animate()
if not self.ui or self.ui:isDestroyed() then
return
end
frame = frame + 1
if frame > TOTAL_FRAMES then
self:finishOpening(holdMs)
return
end
local progress = (frame - 1) / (TOTAL_FRAMES - 1)
local currentWidth = MAX_WIDTH * progress
self:setPaperSize(currentWidth)
anim:setMarginLeft(currentWidth - ANIM_OFFSET)
anim:setImageSource(OPEN_FRAMES[frame])
if not iconsShown and progress >= ICON_SHOW_PROGRESS then
self:setLeftIconsOpacity(1)
iconsShown = true
end
self.event = scheduleEvent(animate, FRAME_MS)
end
self.event = scheduleEvent(animate, FRAME_MS)
end
function notificationsController:finishOpening(holdMs)
debugPrint("Opening finished. Holding.")
self:setPaperSize(MAX_WIDTH)
self.widgets.anim:hide()
self.ui:setMarginLeft(0)
self.state = "holding"
self:fadeIn(holdMs)
end
function notificationsController:fadeIn(holdMs)
local startTime = g_clock.millis()
local function fadeInText()
if not self.ui or self.ui:isDestroyed() then
self:cancelEvent()
self.state = "idle"
return
end
local elapsed = g_clock.millis() - startTime
local t = math.min(1, elapsed / FADE_IN_MS)
self:setContentOpacity(t)
if t < 1 then
self.event = scheduleEvent(fadeInText, FADE_INTERVAL_MS)
else
self.event = scheduleEvent(function()
self:close()
end, holdMs)
end
end
self.event = scheduleEvent(fadeInText, FADE_INTERVAL_MS)
end
function notificationsController:close()
if not self.ui or self.ui:isDestroyed() then
return
end
self:cancelEvent()
self.state = "closing"
debugPrint("Closing phase.")
self:fadeOut()
end
function notificationsController:fadeOut()
local startTime = g_clock.millis()
local function fadeOutText()
if not self.ui or self.ui:isDestroyed() then
self:cancelEvent()
self.state = "idle"
return
end
local elapsed = g_clock.millis() - startTime
local t = math.min(1, elapsed / FADE_OUT_MS)
self:setContentOpacity(1 - t)
if t < 1 then
self.event = scheduleEvent(fadeOutText, FADE_INTERVAL_MS)
else
self:animateClose()
end
end
self.event = scheduleEvent(fadeOutText, FADE_INTERVAL_MS)
end
function notificationsController:animateClose()
local frame = TOTAL_FRAMES
local iconsHidden = false
local anim = self.widgets.anim
anim:show()
local function retract()
if not self.ui or self.ui:isDestroyed() then
self:cancelEvent()
self.state = "idle"
return
end
frame = frame - 1
if frame < 1 then
debugPrint("Retract finished. Running Exit.")
self:setPaperSize(0)
anim:setMarginLeft(0)
anim:setImageSource(OPEN_FRAMES[1])
self:exit()
return
end
local progress = (frame - 1) / (TOTAL_FRAMES - 1)
local currentWidth = MAX_WIDTH * progress
self:setPaperSize(currentWidth)
anim:setMarginLeft(currentWidth - ANIM_OFFSET)
anim:setImageSource(OPEN_FRAMES[frame])
if not iconsHidden and progress <= ICON_HIDE_PROGRESS then
self:setLeftIconsOpacity(0)
iconsHidden = true
end
self.event = scheduleEvent(retract, FRAME_MS)
end
self.event = scheduleEvent(retract, FRAME_MS)
end
function notificationsController:exit()
if not self.ui or self.ui:isDestroyed() then
return
end
self:cancelEvent()
debugPrint("Exit finished.")
self.ui:hide()
self.ui:setOpacity(1)
self.state = "idle"
self:processNext()
end
function notificationsController:hideImmediate()
self:cancelEvent()
if self.ui then
self:unloadHtml()
self.widgets = {}
end
self.queue = {}
self.state = "idle"
debugPrint("Reset Immediate and Unloaded.")
end