otclient-redemption/modules/game_tutorial/controller/tutorial_controller.lua
kokekanon 4a87cebb11
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.

Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.

UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.

Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 11:26:11 -03:00

342 lines
9.1 KiB
Lua

TutorialController = Controller:new()
local INFO_HEIGHT = 227
local ANIM_DURATION = 600
local START_BUTTON_IMAGE = "/game_tutorial/assets/images/button_startplaying_idle"
TutorialController.vocations = TutorialVocations or {
--[[
{
{
id: string,
vocationId: number,
name: string,
role: string,
specialties: string,
spells: number[],
gear: number[]
},
...
}
]]
}
TutorialController.selectedVocation = nil
--[[
{
id: string,
vocationId: number,
name: string,
...
}
]]
TutorialController.vocationWidgets = {
--[["VocationName": {
"card": [UIWidget],
"info": [UIWidget],
"startButton": [UIWidget],
"confirmPanel": [UIWidget],
"highlight": [UIWidget]
]]
}
TutorialController.animTokens = {
--[[
{
"WidgetId": table (unique_token)
}
]]
}
TutorialController.tutorialStep = 2 -- default step
-- /*=============================================
-- = Local Functions =
-- =============================================*/
local function findVocation(vocationId)
for _, vocation in ipairs(TutorialController.vocations) do
if vocation.id == vocationId then
return vocation
end
end
return nil
end
local function resetCard(w)
if w.startButton then
w.startButton:show()
w.startButton:setImageSource(START_BUTTON_IMAGE)
end
if w.selectButton then
w.selectButton:setChecked(false)
end
if w.confirmPanel then
w.confirmPanel:hide()
end
if w.highlight then
w.highlight:hide()
end
if w.info then
w.info:setMarginTop(INFO_HEIGHT)
end
end
local function resetAllCards(self)
self.selectedVocation = nil
for _, widgets in pairs(self.vocationWidgets) do
resetCard(widgets)
end
end
local function animateInfo(self, info, toMargin)
if not info or info:isDestroyed() then
return
end
local id = info:getId()
local token = {}
self.animTokens[id] = token
local fromMargin = info:getMarginTop()
if fromMargin == toMargin then
return
end
local startTime = g_clock.millis()
local function step()
if self.animTokens[id] ~= token or not info or info:isDestroyed() then
return
end
local t = math.min((g_clock.millis() - startTime) / ANIM_DURATION, 1)
local eased = 1 - math.pow(1 - t, 3)
info:setMarginTop(math.floor(fromMargin + (toMargin - fromMargin) * eased))
if t < 1 then
self:scheduleEvent(step, 16)
end
end
step()
end
local function bindCardHover(self, vocationId, w)
if not w.card or not w.info then
return
end
local function onHover()
self:scheduleEvent(function()
if not self.ui then
return
end
local hovered = w.card:isHovered() or w.card:isChildHovered()
animateInfo(self, w.info, hovered and 0 or INFO_HEIGHT)
if w.highlight then
if hovered then
w.highlight:show()
else
w.highlight:hide()
end
end
end, 30, "hover_" .. vocationId)
end
w.card.onHoverChange = onHover
if w.startButton then
w.startButton.onHoverChange = onHover
end
if w.selectButton then
w.selectButton.onHoverChange = onHover
end
if w.confirmPanel then
w.confirmPanel.onHoverChange = onHover
end
if w.confirmButton then
w.confirmButton.onHoverChange = onHover
end
end
local function setupCards(self)
if not self.ui then
return false
end
local cards = self:findWidgets(".vocationSelectionCard") or {}
if #cards == 0 then
return false
end
self.vocationWidgets = {}
for index, card in ipairs(cards) do
local vocation = card.vocation or self.vocations[index]
if vocation and vocation.id then
local startButton = card:querySelector(".selectionActionButtonContainer")
local confirmPanel = card:querySelector(".confirmationButtonContainer")
local widgets = {
card = card,
info = card:querySelector(".vocationDetailsContent"),
startButton = startButton,
selectButton = startButton and startButton:querySelector(".actionButton"),
confirmPanel = confirmPanel,
confirmButton = confirmPanel and confirmPanel:querySelector(".actionButton"),
highlight = card:querySelector(".vocationSelectionHighlight")
}
self.vocationWidgets[vocation.id] = widgets
bindCardHover(self, vocation.id, widgets)
end
end
return true
end
local function applyVocationKeybinds(vocationName)
if not Keybind or not Keybind.selectPreset then
return
end
Keybind.selectPreset(vocationName)
local actionbar = modules.game_actionbar
if actionbar and actionbar.selectHotkeySet then
if not actionbar.selectHotkeySet(vocationName) then
g_logger.warning(string.format("[game_tutorial] Failed to sync action bar hotkey set '%s'.", vocationName))
end
end
end
local function applyVocationUI(self)
self:scheduleEvent(function()
local healthCircle = modules.game_healthcircle
if healthCircle and healthCircle.checkMonkVocation then
healthCircle.checkMonkVocation()
end
end, 500)
end
-- /*=============================================
-- = Controller Methods =
-- =============================================*/
function TutorialController:onGameStart()
if g_game.getClientVersion() > 1520 then
self:registerEvents(g_game, {
onTutorialHint = function(...)
self:onTutorialHint(...)
end
})
else
self:scheduleEvent(function()
g_modules.getModule("game_tutorial"):unload()
end, 100)
end
end
function TutorialController:onTerminate()
self:hide()
end
function TutorialController:onGameEnd()
self:hide()
end
-- /*=============================================
-- = Packet Handlers =
-- =============================================*/
function TutorialController:onTutorialHint(id)
if id and id == tutorialStep.chooseVocation then
self:setTutorialStep(id)
self:show()
else
print("[game_tutorial] unexpected tutorial hint: " .. tostring(id))
end
end
-- /*=============================================
-- = UI Methods =
-- =============================================*/
function TutorialController:show()
if self.ui then
self.ui:raise()
self.ui:focus()
return
end
self:loadHtml("template/html/tutorial.html")
if not self.ui then
g_logger.error("[game_tutorial] failed to load tutorial.html")
return
end
end
function TutorialController:hide()
self.selectedVocation = nil
self.animTokens = {}
self.vocationWidgets = {}
if self.ui then
self:unloadHtml()
end
end
-- /*=============================================
-- = HTML-bound methods Callbacks =
-- =============================================*/
function TutorialController:onVocationCardsRendered()
if not setupCards(self) then
return
end
resetAllCards(self)
self.ui:show()
self.ui:raise()
self.ui:focus()
end
function TutorialController:getSpellIconSource()
local settings = SpelllistSettings and SpelllistSettings["Default"]
return settings and settings.iconFile or ""
end
function TutorialController:getSpellImageClip(clientId)
if not Spells or not Spells.getImageClip then
return ""
end
return Spells.getImageClip(clientId, "Default")
end
function TutorialController:selectVocation(vocationId)
local vocation = findVocation(vocationId)
if not vocation then
g_logger.error("[game_tutorial] unknown vocation: " .. tostring(vocationId))
return
end
if self.selectedVocation and self.selectedVocation.id ~= vocation.id then
local prev = self.vocationWidgets[self.selectedVocation.id]
if prev then
resetCard(prev)
end
end
self.selectedVocation = vocation
local w = self.vocationWidgets[vocation.id]
if not w then
return
end
if w.startButton then
w.startButton:setImageSource("")
end
if w.selectButton then
w.selectButton:setChecked(true)
end
if w.confirmPanel then
w.confirmPanel:show()
w.confirmPanel:raise()
end
end
function TutorialController:confirmVocation()
local vocation = self.selectedVocation
if not vocation then
return
end
g_game.sendTutorialChangeVocation(vocation.vocationId)
applyVocationKeybinds(vocation.name)
applyVocationUI(self)
self:hide()
end
function TutorialController:setTutorialStep(step)
self.tutorialStep = step
end