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