skillController = Controller:new() skillsWindow = nil skillsButton = nil skillsSettings = nil local ExpRating = {} local smallSkillsCache = {} -- Cache for stats data when UI elements are hidden local statsCache = { flatDamageHealing = 0, attackValue = 0, attackElement = 0, convertedDamage = 0, convertedElement = 0, lifeLeech = 0, manaLeech = 0, critChance = 0, critDamage = 0, onslaught = 0, defense = 0, armor = 0, mantra = 0, mitigation = 0, dodge = 0, damageReflection = 0, combatAbsorbValues = { [0] = 0, -- physicalResist [1] = 0, -- fireResist [2] = 0, -- earthResist [3] = 0, -- energyResist [4] = 0, -- IceResist [5] = 0, -- HolyResist [6] = 0, -- deathResist [7] = 0, -- HealingResist [8] = 0, -- drowResist [9] = 0, -- lifedrainResist [10] = 0 -- manadRainResist }, momentum = 0, transcendence = 0, amplification = 0 } local function setupUIButtons() local toggleFilterButton = skillsWindow:recursiveGetChildById('toggleFilterButton') if toggleFilterButton then toggleFilterButton:setVisible(false) toggleFilterButton:setOn(false) end local contextMenuButton = skillsWindow:recursiveGetChildById('contextMenuButton') local minimizeButton = skillsWindow:recursiveGetChildById('minimizeButton') if contextMenuButton and minimizeButton then contextMenuButton:addAnchor(AnchorTop, minimizeButton:getId(), AnchorTop) contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft) contextMenuButton:setMarginRight(7) contextMenuButton.onClick = function(widget, mousePos, mouseButton) return showSkillsContextMenu(widget, mousePos, mouseButton) end end local newWindowButton = skillsWindow:recursiveGetChildById('newWindowButton') if newWindowButton then newWindowButton.onClick = function() if modules.game_cyclopedia then modules.game_cyclopedia.show("character") end end end end function skillController:onInit() skillController:registerEvents(LocalPlayer, { onExperienceChange = onExperienceChange, onLevelChange = onLevelChange, onHealthChange = onHealthChange, onManaChange = onManaChange, onSoulChange = onSoulChange, onFreeCapacityChange = onFreeCapacityChange, onTotalCapacityChange = onTotalCapacityChange, onBaseCapacityChange = onBaseCapacityChange, onStaminaChange = onStaminaChange, onOfflineTrainingChange = onOfflineTrainingChange, onRegenerationChange = onRegenerationChange, onSpeedChange = onSpeedChange, onBaseSpeedChange = onBaseSpeedChange, onMagicLevelChange = onMagicLevelChange, onBaseMagicLevelChange = onBaseMagicLevelChange, onSkillChange = onSkillChange, onBaseSkillChange = onBaseSkillChange, -- 14.12 onFlatDamageHealingChange = onFlatDamageHealingChange, onAttackInfoChange = onAttackInfoChange, onConvertedDamageChange = onConvertedDamageChange, onImbuementsChange = onImbuementsChange, onDefenseInfoChange = onDefenseInfoChange, onCombatAbsorbValuesChange = onCombatAbsorbValuesChange, onForgeBonusesChange = onForgeBonusesChange, onExperienceRateChange = onExperienceRateChange, -- 15.24 onMultiOfflineTrainingDialog = onMultiOfflineTrainingDialog }) skillsButton = modules.game_mainpanel.addToggleButton('skillsButton', tr('Skills') .. ' (Alt+S)', '/images/options/button_skills', toggle, false, 1) skillsButton:setOn(true) skillsWindow = g_ui.loadUI('skills') skillsWindow:setContentMinimumHeight(80) Keybind.new("Windows", "Show/hide skills windows", "Alt+S", "") Keybind.bind("Windows", "Show/hide skills windows", { { type = KEY_DOWN, callback = toggle, } }) skillSettings = g_settings.getNode('skills-hide') if not skillSettings then skillSettings = {} end setupUIButtons() skillsWindow:setup() end function skillController:onTerminate() Keybind.delete("Windows", "Show/hide skills windows") skillsWindow:destroy() skillsButton:destroy() skillsWindow = nil skillsButton = nil end local SKILL_GROUPS = { offence = { 'damageHealing', 'attackValue', 'convertedDamage', 'convertedElement', 'lifeLeech', 'manaLeech', 'criticalChance', 'criticalExtraDamage', 'onslaught' }, defence = { 'physicalResist', 'fireResist', 'earthResist', 'energyResist', 'IceResist', 'HolyResist', 'deathResist', 'HealingResist', 'drowResist', 'lifedrainResist', 'manadRainResist', 'defenceValue', 'armorValue', 'mantraValue', 'mitigation', 'dodge', 'damageReflection' }, misc = { 'momentum', 'transcendence', 'amplification' }, individual = { 'level', 'stamina', 'offlineTraining', 'magiclevel', 'skillId0', 'skillId1', 'skillId2', 'skillId3', 'skillId4', 'skillId5', 'skillId6' }, GameAdditionalSkills = { 'skillId7', 'skillId8', 'skillId9', 'skillId10', 'skillId11', 'skillId12' }, GameForgeSkillStats = { 'skillId13', 'skillId14', 'skillId15' }, GameForgeSkillStats1332 = { 'skillId16' } } local function setSkillGroupVisibility(groupName, visible) local skills = SKILL_GROUPS[groupName] if not skills then return end for _, skillId in pairs(skills) do local skill = skillsWindow:recursiveGetChildById(skillId) if skill then if visible then local valueWidget = skill:getChildById('value') local text = valueWidget and valueWidget:getText() or "" if g_game.getClientVersion() >= 1410 then skill:setVisible(text ~= "" and text ~= "0" and text ~= "0%" and text ~= "+ 0%" and text ~= "0.0%" and text ~= "+ 0.0%") else skill:setVisible(true) end else skill:setVisible(false) end end end if groupName == 'offence' then local criticalHitWidget = skillsWindow:recursiveGetChildById('criticalHit') if criticalHitWidget then local hasCritValue = math.abs(statsCache.critChance or 0) > 0.0001 or math.abs(statsCache.critDamage or 0) > 0.0001 criticalHitWidget:setVisible(visible and hasCritValue) end end end local function areStatsVisible(groupName) if g_game.getClientVersion() < 1412 and groupName ~= 'individual' then return false end local skills = SKILL_GROUPS[groupName] if not skills then return false end for _, skillId in pairs(skills) do local skill = skillsWindow:recursiveGetChildById(skillId) if skill and skill:isVisible() then return true end end return false end local function refreshGroupData(groupName) -- When stats are made visible, apply cached data to the UI elements local player = g_game.getLocalPlayer() if not player then return end scheduleEvent(function() if groupName == 'offence' then -- Apply cached offence data onFlatDamageHealingChange(player, statsCache.flatDamageHealing) onAttackInfoChange(player, statsCache.attackValue, statsCache.attackElement) onConvertedDamageChange(player, statsCache.convertedDamage, statsCache.convertedElement) onImbuementsChange(player, statsCache.lifeLeech, statsCache.manaLeech, statsCache.critChance, statsCache.critDamage, statsCache.onslaught) elseif groupName == 'defence' then -- Apply cached defence data onDefenseInfoChange(player, statsCache.defense, statsCache.armor, statsCache.mantra, statsCache.mitigation, statsCache.dodge, statsCache.damageReflection) onCombatAbsorbValuesChange(player, statsCache.combatAbsorbValues) elseif groupName == 'misc' then -- Apply cached misc data onForgeBonusesChange(player, statsCache.momentum, statsCache.transcendence, statsCache.amplification) end end, 50) end local function toggleGroupVisibility(groupName) if g_game.getClientVersion() < 1412 and groupName ~= 'individual' then return end local shouldShow = not areStatsVisible(groupName) setSkillGroupVisibility(groupName, shouldShow) -- If we're showing stats, refresh the data if shouldShow then refreshGroupData(groupName) end local char = g_game.getCharacterName() if not skillSettings[char] then skillSettings[char] = {} end skillSettings[char][groupName .. 'Stats_visible'] = shouldShow g_settings.setNode('skills-hide', skillSettings) end local function hideOldClientStats() local features = { newAppearance = g_game.getFeature(GameEnterGameShowAppearance), charSkills = g_game.getFeature(GameCharacterSkillStats), additionalSkills = g_game.getFeature(GameAdditionalSkills), forgeSkills = g_game.getFeature(GameForgeSkillStats) } if features.newAppearance then skillsWindow:recursiveGetChildById('regenerationTime'):getChildByIndex(1):setText('Food') skillsWindow:recursiveGetChildById('experience'):getChildByIndex(1):setText('XP') end local version = g_game.getClientVersion() setSkillGroupVisibility('offence', features.charSkills) setSkillGroupVisibility('defence', features.charSkills) setSkillGroupVisibility('misc', features.charSkills) setSkillGroupVisibility('GameAdditionalSkills', features.additionalSkills) setSkillGroupVisibility('GameForgeSkillStats1332', features.forgeSkills and version >= 1332) setSkillGroupVisibility('GameForgeSkillStats', features.forgeSkills) end local function hideMenuOptionsForOldClients(menu) if not g_game.getFeature(GameOfflineTrainingTime) then local offlineTrainingOption = menu:getChildById('showOfflineTraining') if offlineTrainingOption then offlineTrainingOption:setVisible(false) end end if g_game.getClientVersion() < 1412 then local statsOptions = {'showOffenceStats', 'showDefenceStats', 'showMiscStats'} for _, optionId in pairs(statsOptions) do local option = menu:getChildById(optionId) if option then option:setVisible(false) end end end end function showSkillsContextMenu(widget, mousePos, mouseButton) local menu = g_ui.createWidget('SkillsListSubMenu') menu:setGameMenu(true) hideMenuOptionsForOldClients(menu) for _, choice in ipairs(menu:getChildren()) do local choiceId = choice:getId() if choiceId and choiceId ~= 'HorizontalSeparator' then if choiceId == 'resetExperienceCounter' then choice.onClick = function() onSkillsMenuAction(choiceId) menu:destroy() end else local currentState = getSkillVisibilityState(choiceId) choice:setChecked(currentState) choice.onCheckChange = function() onSkillsMenuAction(choiceId) menu:destroy() end end end end local buttonPos = widget:getPosition() local buttonSize = widget:getSize() local menuWidth = menu:getWidth() local buttonCenterX = buttonPos.x + buttonSize.width / 2 local buttonCenterY = buttonPos.y + buttonSize.height / 2 local menuX = buttonCenterX - menuWidth local menuY = buttonCenterY menu:display({x = menuX, y = menuY}) return true end function onSkillsMenuAction(actionId) if actionId == 'resetExperienceCounter' then resetExperienceCounter() elseif actionId == 'showOffenceStats' then toggleGroupVisibility('offence') elseif actionId == 'showDefenceStats' then toggleGroupVisibility('defence') elseif actionId == 'showMiscStats' then toggleGroupVisibility('misc') elseif actionId == 'showAllSkillBars' then toggleAllSkillBars() else local skillMap = { showLevel = 'level', showStamina = 'stamina', showOfflineTraining = 'offlineTraining', showMagic = 'magiclevel', showFist = 'skillId0', showClub = 'skillId1', showSword = 'skillId2', showAxe = 'skillId3', showDistance = 'skillId4', showShielding = 'skillId5', showFishing = 'skillId6' } local skillId = skillMap[actionId] if skillId then toggleSkillProgressBar(skillId) end end end function getSkillVisibilityState(actionId) local skillMap = { showLevel = 'level', showStamina = 'stamina', showOfflineTraining = 'offlineTraining', showMagic = 'magiclevel', showFist = 'skillId0', showClub = 'skillId1', showSword = 'skillId2', showAxe = 'skillId3', showDistance = 'skillId4', showShielding = 'skillId5', showFishing = 'skillId6' } local groupMap = { showOffenceStats = 'offence', showDefenceStats = 'defence', showMiscStats = 'misc' } local skillId = skillMap[actionId] if skillId then return isSkillPercentBarVisible(skillId) end local groupName = groupMap[actionId] if groupName then return areStatsVisible(groupName) end if actionId == 'showAllSkillBars' then return areAllSkillBarsVisible() end return false end function isSkillVisible(skillId) local skill = skillsWindow:recursiveGetChildById(skillId) return skill and skill:isVisible() end function isSkillPercentBarVisible(skillId) local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') return percentBar and percentBar:isVisible() end return false end function toggleSkillProgressBar(skillId) local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') local skillIcon = skill:getChildById('icon') if percentBar then local isVisible = percentBar:isVisible() percentBar:setVisible(not isVisible) -- Also toggle skill icon if it exists if skillIcon then skillIcon:setVisible(not isVisible) end -- Adjust skill button height if not isVisible then skill:setHeight(21) -- Show progress bar else skill:setHeight(15) -- Hide progress bar end -- Save the setting local char = g_game.getCharacterName() if not skillSettings[char] then skillSettings[char] = {} end skillSettings[char][skillId] = isVisible and 1 or 0 -- 1 = hidden, 0 = visible g_settings.setNode('skills-hide', skillSettings) end end end function toggleSkillVisibility(skillId) local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') local skillIcon = skill:getChildById('icon') if percentBar then local isVisible = percentBar:isVisible() percentBar:setVisible(not isVisible) -- Also toggle skill icon if it exists if skillIcon then skillIcon:setVisible(not isVisible) end -- Adjust skill button height if not isVisible then skill:setHeight(21) -- Show progress bar else skill:setHeight(15) -- Hide progress bar end -- Save the setting local char = g_game.getCharacterName() if not skillSettings[char] then skillSettings[char] = {} end skillSettings[char][skillId] = isVisible and 1 or 0 -- 1 = hidden, 0 = visible g_settings.setNode('skills-hide', skillSettings) end end end function resetExperienceCounter() local player = g_game.getLocalPlayer() if player then modules.game_textmessage.displayGameMessage('Experience counter has been reset.') end end function areOffenceStatsVisible() return areStatsVisible('offence') end function toggleOffenceStatsVisibility() toggleGroupVisibility('offence') end function areDefenceStatsVisible() return areStatsVisible('defence') end function toggleDefenceStatsVisibility() toggleGroupVisibility('defence') end function areMiscStatsVisible() return areStatsVisible('misc') end function toggleMiscStatsVisibility() toggleGroupVisibility('misc') end function areAllSkillBarsVisible() for _, skillId in pairs(SKILL_GROUPS.individual) do local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') if percentBar and not percentBar:isVisible() then return false end end end return true end function toggleAllSkillBars() local shouldShow = not areAllSkillBarsVisible() for _, skillId in pairs(SKILL_GROUPS.individual) do local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') local skillIcon = skill:getChildById('icon') if percentBar then percentBar:setVisible(shouldShow) if skillIcon then skillIcon:setVisible(shouldShow) end skill:setHeight(shouldShow and 21 or 15) local char = g_game.getCharacterName() if not skillSettings[char] then skillSettings[char] = {} end skillSettings[char][skillId] = shouldShow and 0 or 1 end end end g_settings.setNode('skills-hide', skillSettings) end function expForLevel(level) return math.floor((50 * level * level * level) / 3 - 100 * level * level + (850 * level) / 3 - 200) end function expToAdvance(currentLevel, currentExp) return expForLevel(currentLevel + 1) - currentExp end function resetSkillColor(id) local skill = skillsWindow:recursiveGetChildById(id) local widget = skill:getChildById('value') widget:setColor('#bbbbbb') end function toggleSkill(id, state) local skill = skillsWindow:recursiveGetChildById(id) skill:setVisible(state) end function setSkillBase(id, value, baseValue) if baseValue <= 0 or value < 0 then return end local skill = skillsWindow:recursiveGetChildById(id) local widget = skill:getChildById('value') if value > baseValue then widget:setColor('#008b00') -- green skill:setTooltip(baseValue .. ' +' .. (value - baseValue)) elseif value < baseValue then widget:setColor('#b22222') -- red skill:setTooltip(baseValue .. ' ' .. (value - baseValue)) else widget:setColor('#bbbbbb') -- default skill:removeTooltip() end end function setSkillValue(id, value) local skill = skillsWindow:recursiveGetChildById(id) if not skill then return end local widget = skill:getChildById('value') local GameAdditionalSkills = isSkillInGroups(id, {'GameAdditionalSkills'}) if GameAdditionalSkills then local usePercentage = g_game.getFeature(GameEnterGameShowAppearance) if usePercentage then local needsDecimals = (id == 'skillId10' or id == 'skillId12') local displayValue = needsDecimals and (value / 100) or value local text = needsDecimals and string.format("%.2f%%", displayValue) or (displayValue .. "%") widget:setText(text) local color = (displayValue > 0 and 'green') or (displayValue == 0 and '#C0C0C0') or 'red' widget:setColor(color) else widget:setText(((id == 'skillId8') and "+" or "") .. value .. "%") end else widget:setText(value) widget:setColor('#C0C0C0') end if id == 'capacity' then local player = g_game.getLocalPlayer() if player then local totalCap = player:getTotalCapacity() local baseCap = player:getBaseCapacity() local freeCap = player:getFreeCapacity() if freeCap == 0 then widget:setColor('#b22222') elseif totalCap > 0 and baseCap > 0 and baseCap ~= 4294967295 and totalCap ~= baseCap then widget:setColor('#44ad25') else local ratio = totalCap > 0 and (freeCap / totalCap * 100) or 100 widget:setColor(ratio <= 20 and '#b22222' or '#C0C0C0') end if totalCap > 0 then skill:setTooltip(tr('You have %s of %s capacity left', comma_value(freeCap), comma_value(totalCap))) end end end end function isSkillInGroups(skillId, groupNames) if smallSkillsCache[skillId] ~= nil then return smallSkillsCache[skillId] end for _, groupName in ipairs(groupNames) do local skills = SKILL_GROUPS[groupName] if skills then for _, id in ipairs(skills) do if id == skillId then smallSkillsCache[skillId] = true return true end end end end smallSkillsCache[skillId] = false return false end function setSkillColor(id, value) local skill = skillsWindow:recursiveGetChildById(id) if skill then local widget = skill:getChildById('value') widget:setColor(value) end end function setSkillTooltip(id, value) local skill = skillsWindow:recursiveGetChildById(id) if skill then if value then skill:setTooltip(value) else skill:removeTooltip() end end end function setSkillPercent(id, percent, tooltip, color) local skill = skillsWindow:recursiveGetChildById(id) if skill then local widget = skill:getChildById('percent') if widget then widget:setPercent(math.floor(percent)) if tooltip then widget:setTooltip(tooltip) end if color then widget:setBackgroundColor(color) end end end end function checkAlert(id, value, maxValue, threshold, greaterThan) if greaterThan == nil then greaterThan = false end local alert = false -- maxValue can be set to false to check value and threshold -- used for regeneration checking if type(maxValue) == 'boolean' then if maxValue then return end if greaterThan then if value > threshold then alert = true end else if value < threshold then alert = true end end elseif type(maxValue) == 'number' then if maxValue < 0 then return end local percent = math.floor((value / maxValue) * 100) if greaterThan then if percent > threshold then alert = true end else if percent < threshold then alert = true end end end if alert then setSkillColor(id, '#b22222') -- red else resetSkillColor(id) end end function update() local offlineTraining = skillsWindow:recursiveGetChildById('offlineTraining') if not g_game.getFeature(GameOfflineTrainingTime) then offlineTraining:setVisible(false) else offlineTraining:show() end local regenerationTime = skillsWindow:recursiveGetChildById('regenerationTime') if not g_game.getFeature(GamePlayerRegenerationTime) then regenerationTime:setVisible(false) else regenerationTime:show() end local xpBoostButton = skillsWindow:recursiveGetChildById('xpBoostButton') local xpGainRate = skillsWindow:recursiveGetChildById('xpGainRate') if g_game.getFeature(GameExperienceBonus) then xpGainRate:show() else xpBoostButton:setVisible(false) xpGainRate:setVisible(false) end end function skillController:onGameStart() skillsWindow:setupOnStart() skillsWindow:close(true) -- OT-MMO: empieza cerrada siempre (antes reaparecía al morir y volver) refresh() local newWindowButton = skillsWindow:recursiveGetChildById('newWindowButton') if g_game.getClientVersion() < 1310 and newWindowButton then newWindowButton:setVisible(false) local sepForge = skillsWindow:recursiveGetChildById('separadorOnForgeBonusesChange') if sepForge and sepForge:isVisible() then sepForge:setVisible(false) end local sepDefense = skillsWindow:recursiveGetChildById('separadorOnDefenseInfoChange') if sepDefense and sepDefense:isVisible() then sepDefense:setVisible(false) end local lockButton = skillsWindow:recursiveGetChildById('lockButton') local contextMenuButton = skillsWindow:recursiveGetChildById('contextMenuButton') if lockButton and contextMenuButton then lockButton:breakAnchors() lockButton:addAnchor(AnchorTop, contextMenuButton:getId(), AnchorTop) lockButton:addAnchor(AnchorRight, contextMenuButton:getId(), AnchorLeft) lockButton:setMarginRight(2) lockButton:setMarginTop(0) end end scheduleEvent(function() loadSkillsVisibilitySettings() end, 100) hideOldClientStats() updateHeight() end function refresh() local player = g_game.getLocalPlayer() if not player then return end if expSpeedEvent then expSpeedEvent:cancel() expSpeedEvent = nil end expSpeedEvent = cycleEvent(checkExpSpeed, 30 * 1000) onExperienceChange(player, player:getExperience()) onLevelChange(player, player:getLevel(), player:getLevelPercent()) onHealthChange(player, player:getHealth(), player:getMaxHealth()) onManaChange(player, player:getMana(), player:getMaxMana()) onSoulChange(player, player:getSoul()) onFreeCapacityChange(player, player:getFreeCapacity()) onStaminaChange(player, player:getStamina()) onMagicLevelChange(player, player:getMagicLevel(), player:getMagicLevelPercent()) onOfflineTrainingChange(player, player:getOfflineTrainingTime()) onRegenerationChange(player, player:getRegenerationTime()) onSpeedChange(player, player:getSpeed()) for i = Skill.Fist, Skill.Transcendence do onSkillChange(player, i, player:getSkillLevel(i), player:getSkillLevelPercent(i)) end update() updateHeight() if g_game.getClientVersion() >= 1410 then onFlatDamageHealingChange(player, statsCache.flatDamageHealing) onAttackInfoChange(player, statsCache.attackValue, statsCache.attackElement) onConvertedDamageChange(player, statsCache.convertedDamage, statsCache.convertedElement) onImbuementsChange(player, statsCache.lifeLeech, statsCache.manaLeech, statsCache.critChance, statsCache.critDamage, statsCache.onslaught) onDefenseInfoChange(player, statsCache.defense, statsCache.armor, statsCache.mantra, statsCache.mitigation, statsCache.dodge, statsCache.damageReflection) onCombatAbsorbValuesChange(player, statsCache.combatAbsorbValues) onForgeBonusesChange(player, statsCache.momentum, statsCache.transcendence, statsCache.amplification) end end function loadSkillsVisibilitySettings() local char = g_game.getCharacterName() if not char or not skillSettings[char] then return end local settings = skillSettings[char] for _, skillId in pairs(SKILL_GROUPS.individual) do if settings[skillId] ~= nil then local skill = skillsWindow:recursiveGetChildById(skillId) if skill then local percentBar = skill:getChildById('percent') local skillIcon = skill:getChildById('icon') if percentBar then local shouldShow = settings[skillId] ~= 1 percentBar:setVisible(shouldShow) if skillIcon then skillIcon:setVisible(shouldShow) end skill:setHeight(shouldShow and 21 or 15) end end end end if g_game.getClientVersion() >= 1412 then local groupSettings = { {name = 'offence', key = 'offenceStats_visible'}, {name = 'defence', key = 'defenceStats_visible'}, {name = 'misc', key = 'miscStats_visible'} } for _, group in pairs(groupSettings) do if settings[group.key] == nil then settings[group.key] = true g_settings.setNode('skills-hide', skillSettings) end if settings[group.key] ~= nil then setSkillGroupVisibility(group.name, settings[group.key]) end end end end function updateHeight() local maximumHeight = 8 -- margin top and bottom local minimumHeight = 80 -- ensure minimum height is maintained if g_game.isOnline() then local char = g_game.getCharacterName() if not skillSettings[char] then skillSettings[char] = {} end local skillsButtons = skillsWindow:recursiveGetChildById('experience'):getParent():getChildren() for _, skillButton in pairs(skillsButtons) do local percentBar = skillButton:getChildById('percent') if skillButton:isVisible() then if percentBar then showPercentBar(skillButton, skillSettings[char][skillButton:getId()] ~= 1) end maximumHeight = maximumHeight + skillButton:getHeight() + skillButton:getMarginBottom() end end else maximumHeight = 390 end local contentsPanel = skillsWindow:getChildById('contentsPanel') skillsWindow:setContentMinimumHeight(math.max(minimumHeight, 44)) skillsWindow:setContentMaximumHeight(maximumHeight) end local function resetTable(t) for k, v in pairs(t) do if type(v) == "table" then resetTable(v) else t[k] = 0 end end end function skillController:onGameEnd() skillsWindow:setParent(nil, true) if expSpeedEvent then expSpeedEvent:cancel() expSpeedEvent = nil end local allGroups = {'offence', 'defence', 'misc', 'GameAdditionalSkills', 'GameForgeSkillStats', 'GameForgeSkillStats1332'} for _, groupName in pairs(allGroups) do local skills = SKILL_GROUPS[groupName] if skills then for _, skillId in pairs(skills) do local skill = skillsWindow:recursiveGetChildById(skillId) if skill then if not string.find(skillId, "separador") then local valueWidget = skill:getChildById('value') if valueWidget then valueWidget:setText("0") end skill:setVisible(false) end end end end end resetTable(statsCache) g_settings.setNode('skills-hide', skillSettings) end function toggle() if skillsButton:isOn() then skillsWindow:close() skillsButton:setOn(false) else if not skillsWindow:getParent() then local panel = modules.game_interface.findContentPanelAvailable(skillsWindow, skillsWindow:getMinimumHeight()) if not panel then return end panel:addChild(skillsWindow) end skillsWindow:open() skillsButton:setOn(true) updateHeight() end end function checkExpSpeed() local player = g_game.getLocalPlayer() if not player then return end local currentExp = player:getExperience() local currentTime = g_clock.seconds() if player.lastExps == nil then player.lastExps = {} end table.insert(player.lastExps, {currentExp, currentTime}) if #player.lastExps > 30 then table.remove(player.lastExps, 1) end if #player.lastExps >= 2 then local oldestEntry = player.lastExps[1] local expGained = currentExp - oldestEntry[1] local timeElapsed = currentTime - oldestEntry[2] player.expSpeed = timeElapsed > 0 and (expGained / timeElapsed) or 0 onLevelChange(player, player:getLevel(), player:getLevelPercent()) onExperienceChange(player, player:getExperience()) end end function onMiniWindowOpen() if skillsButton then skillsButton:setOn(true) end end function onMiniWindowClose() if skillsButton then skillsButton:setOn(false) end end function onSkillButtonClick(button) local percentBar = button:getChildById('percent') local skillIcon = button:getChildById('icon') if percentBar and skillIcon then showPercentBar(button, not percentBar:isVisible()) skillIcon:setVisible(skillIcon:isVisible()) local char = g_game.getCharacterName() if percentBar:isVisible() then skillsWindow:modifyMaximumHeight(6) skillSettings[char][button:getId()] = 0 else skillsWindow:modifyMaximumHeight(-6) skillSettings[char][button:getId()] = 1 end end end function showPercentBar(button, show) local percentBar = button:getChildById('percent') local skillIcon = button:getChildById('icon') if percentBar and skillIcon then percentBar:setVisible(show) skillIcon:setVisible(show) if show then button:setHeight(21) else button:setHeight(21 - 6) end end end local function getExperienceTooltip(localPlayer) local currentLevel = localPlayer:getLevel() local currentExp = localPlayer:getExperience() local nextLevelExp = expForLevel(currentLevel + 1) local expNeeded = nextLevelExp - currentExp if expNeeded <= 0 then return nil end if localPlayer.expSpeed and localPlayer.expSpeed > 0 then local expPerHour = math.floor(localPlayer.expSpeed * 3600) local hoursLeft = expNeeded / expPerHour local minutesLeft = math.floor((hoursLeft - math.floor(hoursLeft)) * 60) hoursLeft = math.floor(hoursLeft) return tr('%s of experience per hour', comma_value(expPerHour)) .. '\n' .. tr('Next level in %d hours and %d minutes', hoursLeft, minutesLeft) end local states = localPlayer:getStates() local isInBattle = Player.isStateActive(states, PlayerStates.Swords) or Player.isStateActive(states, PlayerStates.RedSwords) if not isInBattle then return tr('%s XP for next level', comma_value(expNeeded)) end return nil end function onExperienceChange(localPlayer, value) if not localPlayer then return end setSkillValue('experience', comma_value(value)) setSkillTooltip('experience', getExperienceTooltip(localPlayer)) onLevelChange(localPlayer, localPlayer:getLevel(), localPlayer:getLevelPercent()) end function onLevelChange(localPlayer, value, percent) if not localPlayer then return end -- The C++ onLevelChange callback passes m_levelPercent raw, which is centipercent -- (0-10000) once GameLevelPercentU16 is enabled (protocol >= 1520), not the 0-100 the -- UI expects. getLevelPercent() normalizes it, so always use the getter: otherwise the -- bar renders full and the tooltip reads "-5500 percent to go" at high levels. percent = localPlayer:getLevelPercent() setSkillValue('level', comma_value(value)) local text = tr('You have %s percent to go', 100 - percent) setSkillPercent('level', percent, text) end function onHealthChange(localPlayer, health, maxHealth) setSkillValue('health', comma_value(health)) checkAlert('health', health, maxHealth, 30) end function onManaChange(localPlayer, mana, maxMana) setSkillValue('mana', comma_value(mana)) checkAlert('mana', mana, maxMana, 30) end function onSoulChange(localPlayer, soul) setSkillValue('soul', soul) end function onFreeCapacityChange(localPlayer, freeCapacity) setSkillValue('capacity', comma_value(freeCapacity)) end function onTotalCapacityChange(localPlayer, totalCapacity) local player = g_game.getLocalPlayer() if player then setSkillValue('capacity', comma_value(player:getFreeCapacity())) end end function onBaseCapacityChange(localPlayer, baseCapacity) local player = g_game.getLocalPlayer() if player then setSkillValue('capacity', comma_value(player:getFreeCapacity())) end end local function formatTime(minutes) local hours = math.floor(minutes / 60) local mins = minutes % 60 if mins < 10 then mins = '0' .. mins end return hours .. ':' .. mins end local function getStaminaTooltip(hours, minutes, stamina) local timeText = tr('You have %s hours and %s minutes left', hours, minutes) if stamina > 2400 and g_game.getClientVersion() >= 1038 then local player = g_game.getLocalPlayer() if player:isPremium() then return timeText .. '\n' .. tr('Now you will gain 50%% more experience') else return timeText .. '\n' .. tr('You will not gain 50%% more experience because you aren\'t premium player, now you receive only 1x experience points') end elseif stamina >= 2400 and g_game.getClientVersion() < 1038 then return timeText .. '\n' .. tr('If you are premium player, you will gain 50%% more experience') elseif stamina < 2400 and stamina > 840 then return timeText elseif stamina <= 840 and stamina > 0 then return timeText .. '\n' .. tr('You gain only 50%% experience and you don\'t may gain loot from monsters') elseif stamina == 0 then return timeText .. '\n' .. tr('You don\'t may receive experience and loot from monsters') end return timeText end function onStaminaChange(localPlayer, stamina) local hours = math.floor(stamina / 60) local minutes = stamina % 60 local percent = math.floor(100 * stamina / (42 * 60)) setSkillValue('stamina', formatTime(stamina)) local tooltip = getStaminaTooltip(hours, minutes, stamina) local color = 'orange' if stamina > 2400 then color = g_game.getClientVersion() >= 1038 and localPlayer:isPremium() and 'green' or '#89F013' elseif stamina <= 840 and stamina > 0 then color = 'red' elseif stamina == 0 then color = 'black' end setSkillPercent('stamina', percent, tooltip, color) end function onOfflineTrainingChange(localPlayer, offlineTrainingTime) if not g_game.getFeature(GameOfflineTrainingTime) then return end local percent = 100 * offlineTrainingTime / (12 * 60) setSkillValue('offlineTraining', formatTime(offlineTrainingTime)) setSkillPercent('offlineTraining', percent, tr('You have %s percent', percent)) end function onRegenerationChange(localPlayer, regenerationTime) if not g_game.getFeature(GamePlayerRegenerationTime) or regenerationTime < 0 then return end local hours = math.floor(regenerationTime / 3600) local minutes = math.floor(regenerationTime / 60) local seconds = regenerationTime % 60 if seconds < 10 then seconds = '0' .. seconds end if minutes < 10 then minutes = '0' .. minutes end if hours < 10 then hours = '0' .. hours end local fmt = "" local alert = 300 if g_game.getFeature(GameEnterGameShowAppearance) then fmt = string.format("%02d:%02d:%02d", hours, minutes, seconds) alert = 0 else fmt = string.format("%02d:%02d", minutes, seconds) end setSkillValue('regenerationTime', fmt) checkAlert('regenerationTime', regenerationTime, false, alert) if g_game.getFeature(GameEnterGameShowAppearance) then modules.game_interface.StatsBar.onHungryChange(regenerationTime, alert) end end function onSpeedChange(localPlayer, speed) setSkillValue('speed', comma_value(speed)) onBaseSpeedChange(localPlayer, localPlayer:getBaseSpeed()) end function onBaseSpeedChange(localPlayer, baseSpeed) setSkillBase('speed', localPlayer:getSpeed(), baseSpeed) end function onMagicLevelChange(localPlayer, magiclevel, percent) setSkillValue('magiclevel', magiclevel) setSkillPercent('magiclevel', percent, tr('You have %s percent to go', 100 - percent)) onBaseMagicLevelChange(localPlayer, localPlayer:getBaseMagicLevel()) end function onBaseMagicLevelChange(localPlayer, baseMagicLevel) setSkillBase('magiclevel', localPlayer:getMagicLevel(), baseMagicLevel) end function onSkillChange(localPlayer, id, level, percent) setSkillValue('skillId' .. id, level) setSkillPercent('skillId' .. id, percent, tr('You have %s percent to go', 100 - percent)) onBaseSkillChange(localPlayer, id, localPlayer:getSkillBaseLevel(id)) if id > Skill.ManaLeechAmount then toggleSkill('skillId' .. id, level > 0) end end function onBaseSkillChange(localPlayer, id, baseLevel) setSkillBase('skillId' .. id, localPlayer:getSkillLevel(id), baseLevel) end local function updateExperienceRate(localPlayer) local baseRate = ExpRating[ExperienceRate.BASE] or 100 local expRateTotal = baseRate for type, value in pairs(ExpRating) do if type ~= ExperienceRate.BASE and type ~= ExperienceRate.STAMINA_MULTIPLIER then expRateTotal = expRateTotal + (value or 0) end end local staminaMultiplier = ExpRating[ExperienceRate.STAMINA_MULTIPLIER] or 100 expRateTotal = expRateTotal * staminaMultiplier / 100 local xpgainrate = skillsWindow:recursiveGetChildById("xpGainRate") if not xpgainrate then return end local widget = xpgainrate:getChildById("value") if not widget then return end widget:setText(math.floor(expRateTotal) .. "%") local tooltip = string.format("Your current XP gain rate amounts to %d%%.", math.floor(expRateTotal)) tooltip = tooltip .. string.format("\nYour XP gain rate is calculated as follows:\n- Base XP gain rate %d%%", baseRate) if (ExpRating[ExperienceRate.VOUCHER] or 0) > 0 then tooltip = tooltip .. string.format("\n- Voucher: %d%%", ExpRating[ExperienceRate.VOUCHER]) end if (ExpRating[ExperienceRate.XP_BOOST] or 0) > 0 then tooltip = tooltip .. string.format("\n- XP Boost: %d%% (%s h remaining)", ExpRating[ExperienceRate.XP_BOOST], formatTimeBySeconds(localPlayer:getStoreExpBoostTime())) end if (ExpRating[ExperienceRate.LOW_LEVEL] or 0) > 0 then tooltip = tooltip .. string.format("\n- Low Level Bonus: %d%%", ExpRating[ExperienceRate.LOW_LEVEL]) end tooltip = tooltip .. string.format("\n- Stamina multiplier: x%.1f (%s h remaining)", staminaMultiplier / 100, formatTimeByMinutes(localPlayer:getStamina() - 2340)) xpgainrate:setTooltip(tooltip) local colors = { [0] = "#ff4a4a", ["greater"] = "#00cc00", ["less"] = "#ff9429", ["equal"] = "#ffffff" } local colorKey = expRateTotal == 0 and 0 or (expRateTotal > 100 and "greater" or (expRateTotal < 100 and "less" or "equal")) widget:setColor(colors[colorKey]) end function onExperienceRateChange(localPlayer, type, value) ExpRating[type] = value updateExperienceRate(localPlayer) end local function setSkillValueWithTooltips(id, value, tooltip, showPercentage, color) local skill = skillsWindow:recursiveGetChildById(id) if not skill then return end if g_game.getClientVersion() < 1412 then local oldClientStats = { 'skillId7', 'skillId8', 'skillId9', 'skillId10', 'skillId11', 'skillId12', 'skillId13', 'skillId14', 'skillId15', 'skillId16', 'damageHealing', 'attackValue', 'convertedDamage', 'convertedElement', 'criticalHit', 'lifeLeech', 'manaLeech', 'criticalChance', 'criticalExtraDamage', 'onslaught', 'physicalResist', 'fireResist', 'earthResist', 'energyResist', 'IceResist', 'HolyResist', 'deathResist', 'HealingResist', 'drowResist', 'lifedrainResist', 'manadRainResist', 'defenceValue', 'armorValue', 'mantraValue', 'mitigation', 'dodge', 'damageReflection', 'momentum', 'transcendence', 'amplification' } for _, statId in pairs(oldClientStats) do if id == statId then skill:setVisible(false) return end end end if g_game.getClientVersion() >= 1412 then local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] then local settings = skillSettings[char] for groupName, groupStats in pairs({offence = SKILL_GROUPS.offence, defence = SKILL_GROUPS.defence, misc = SKILL_GROUPS.misc}) do for _, statId in pairs(groupStats) do if id == statId and settings[groupName .. 'Stats_visible'] == false then skill:setVisible(false) return end end end end end if value ~= nil then local shouldHide = false if value == 0 or (type(value) == "number" and math.abs(value) < 0.0001) then shouldHide = true elseif showPercentage then local percentValue = math.floor(value * 10000) / 100 shouldHide = (percentValue == 0 or math.abs(percentValue) < 0.01) end if shouldHide then skill:setVisible(false) return end skill:show() local widget = skill:getChildById('value') if not widget then return end if color then widget:setColor(color) end if showPercentage then local percentValue = math.floor(value * 10000) / 100 local sign = percentValue > 0 and "+ " or "" widget:setText(sign .. percentValue .. "%") if percentValue < 0 then widget:setColor("#FF9854") end else widget:setText(tostring(value)) end if tooltip then skill:setTooltip(tooltip) end elseif string.find(id, "separador") then -- Separators should be shown when the group is visible, regardless of value skill:show() else skill:setVisible(false) end end function onFlatDamageHealingChange(localPlayer, flatBonus) -- Cache the data regardless of visibility statsCache.flatDamageHealing = flatBonus or 0 local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] and skillSettings[char]['offenceStats_visible'] ~= false then skillsWindow:recursiveGetChildById("separadorOnOffenceInfoChange"):setVisible(true) end local tooltips = "This flat bonus is the main source of your character's power, added \nto most of the damage and healing values you cause." setSkillValueWithTooltips('damageHealing', flatBonus, tooltips, false) end function onAttackInfoChange(localPlayer, attackValue, attackElement) -- Cache the data regardless of visibility statsCache.attackValue = attackValue or 0 statsCache.attackElement = attackElement or 0 local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] and skillSettings[char]['offenceStats_visible'] ~= false then skillsWindow:recursiveGetChildById("separadorOnOffenceInfoChange"):setVisible(true) end local tooltips = "This is your character's basic attack power whenever you enter a \nfight with a weapon or your fists. It does not apply to any spells \nyou cast. The attack value is calculated from the weapon's attack\n value, the corresponding weapon skill, combat tactics, the bonus \nreceived from the Revelation Perks and the player's level. The \nvalue represents the average damage you would inflict on a\ncreature which had no kind of defence or protection." setSkillValueWithTooltips('attackValue', attackValue, tooltips, false) local skill = skillsWindow:recursiveGetChildById("attackValue") if skill then local element = clientCombat[attackElement] if element then skill:getChildById('icon'):setImageSource(element.path) skill:getChildById('icon'):setImageSize({width = 9, height = 9}) end end end function onConvertedDamageChange(localPlayer, convertedDamage, convertedElement) -- Cache the data regardless of visibility statsCache.convertedDamage = convertedDamage or 0 statsCache.convertedElement = convertedElement or 0 setSkillValueWithTooltips('convertedDamage', convertedDamage, false, true) setSkillValueWithTooltips('convertedElement', convertedElement, false, true) end function onImbuementsChange(localPlayer, lifeLeech, manaLeech, critChance, critDamage, onslaught) -- Cache the data regardless of visibility statsCache.lifeLeech = lifeLeech or 0 statsCache.manaLeech = manaLeech or 0 statsCache.critChance = critChance or 0 statsCache.critDamage = critDamage or 0 statsCache.onslaught = onslaught or 0 local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] and skillSettings[char]['offenceStats_visible'] ~= false then skillsWindow:recursiveGetChildById("separadorOnOffenceInfoChange"):setVisible(true) end local tooltips = { lifeLeech = "You have a +11.4% chance to trigger Onslaught, granting you 60% increased damage for all attacks.", manaLeech = "You have a +1% chance to cause +1% extra damage.", critChance = "Critical Hits deal more damage than normal attacks. They have a chance to be \ntriggered during combat, inflicting additional damage beyond the standard amount.", critDamage = "You get +1% of the damage dealt as mana", onslaught = "You get +1% of the damage dealt as hit points" } setSkillValueWithTooltips('lifeLeech', lifeLeech, tooltips.lifeLeech, true) setSkillValueWithTooltips('manaLeech', manaLeech, tooltips.manaLeech, true) setSkillValueWithTooltips('criticalChance', critChance, tooltips.critChance, true) setSkillValueWithTooltips('criticalExtraDamage', critDamage, tooltips.critDamage, true) setSkillValueWithTooltips('onslaught', onslaught, tooltips.onslaught, true) local criticalHitWidget = skillsWindow:recursiveGetChildById("criticalHit") if criticalHitWidget then local hasCritValue = math.abs(critChance or 0) > 0.0001 or math.abs(critDamage or 0) > 0.0001 local groupHidden = (function() local char = g_game.getCharacterName() return char and skillSettings and skillSettings[char] and skillSettings[char]['offenceStats_visible'] == false end)() criticalHitWidget:setVisible(hasCritValue and not groupHidden) end end local combatIdToWidgetId = { [0] = "physicalResist", [1] = "fireResist", [2] = "earthResist", [3] = "energyResist", [4] = "IceResist", [5] = "HolyResist", [6] = "deathResist", [7] = "HealingResist", [8] = "drowResist", [9] = "lifedrainResist", [10] = "manadRainResist" } function onCombatAbsorbValuesChange(localPlayer, absorbValues) -- Cache the data regardless of visibility statsCache.combatAbsorbValues = absorbValues or {} for id, widgetId in pairs(combatIdToWidgetId) do local skill = skillsWindow:recursiveGetChildById(widgetId) if skill then local value = absorbValues[id] if value ~= nil then setSkillValueWithTooltips(widgetId, value, false, true, "#44AD25") else skill:setVisible(false) end end end end function onDefenseInfoChange(localPlayer, defense, armor, mantra, mitigation, dodge, damageReflection) -- Cache the data regardless of visibility statsCache.defense = defense or 0 statsCache.armor = armor or 0 statsCache.mantra = mantra or 0 statsCache.mitigation = mitigation or 0 statsCache.dodge = dodge or 0 statsCache.damageReflection = damageReflection or 0 -- Show separator if defense stats are visible local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] and skillSettings[char]['defenceStats_visible'] ~= false then local separator = skillsWindow:recursiveGetChildById("separadorOnDefenseInfoChange") if separator then separator:setVisible(true) end end local tooltips = { defense = "When attacked, you have a +9.6% chance to trigger Dodge, which \nwill fully mitigate the damage.", armor = "Mitigation reduces most of the damage you take and varies based\non your shielding skill, equipped weapon, chosen combat tactics \nand any mitigation multipliers acquired in your Wheel of Destiny.", mantra = "Mantra is a special bonus stat granted by certain monk equipment.\nIt improves your monk-specific abilities and combat effectiveness.", mitigation = "This shows how well your armor protects you from all physical\nattacks.", dodge = "This is your protection against all physical attacks in close combat \nas well as all distance physical attacks. The higher the defence value, the less damage you will take from melee physical hits. The defence\n value is calculated from your shield and/or weapon\n defence and the corresponding skill. Careful! \nYour defence value protects you only from hits of two creatures in a single round." } setSkillValueWithTooltips('defenceValue', defense, tooltips.defense, false) setSkillValueWithTooltips('armorValue', armor, tooltips.armor, false) setSkillValueWithTooltips('mantraValue', mantra, tooltips.mantra, false) setSkillValueWithTooltips('mitigation', mitigation, tooltips.mitigation, true) setSkillValueWithTooltips('dodge', dodge, tooltips.dodge, true) setSkillValueWithTooltips('damageReflection', damageReflection, false, true) end function onForgeBonusesChange(localPlayer, momentum, transcendence, amplification) -- Cache the data regardless of visibility statsCache.momentum = momentum or 0 statsCache.transcendence = transcendence or 0 statsCache.amplification = amplification or 0 local char = g_game.getCharacterName() if char and skillSettings and skillSettings[char] and skillSettings[char]['miscStats_visible'] ~= false then skillsWindow:recursiveGetChildById("separadorOnForgeBonusesChange"):setVisible(true) end local tooltips = { momentum = "During combat, you have a +" .. math.floor(momentum * 10000) / 100 .. "% chance to trigger Momentum\n, which reduces all spell cooldowns by 2 seconds.", transcendence = "During combat, you have a +" .. math.floor(transcendence * 10000) / 100 .. "% chance to trigger\nTranscendence, which transforms your character into a vocation-\nspecific avatar for 7 seconds. While in this form, you will benefit\nfrom a 15% damage reduction and guaranteed critical hits that \ndeal an additional 15% damage.", amplification = "Effects of tiered items are amplified by +" .. math.floor(amplification * 10000) / 100 .. "%." } setSkillValueWithTooltips('momentum', momentum, tooltips.momentum, true) setSkillValueWithTooltips('transcendence', transcendence, tooltips.transcendence, true) setSkillValueWithTooltips('amplification', amplification, tooltips.amplification, true) end -- Function to get experience rate values for other modules function getExpRating(type) if type then return ExpRating[type] or 0 else return ExpRating end end -- Function to calculate the total experience rate multiplier (excluding base rate) function getTotalExpRateMultiplier() local baseRate = ExpRating[ExperienceRate.BASE] or 100 local expRateTotal = baseRate for type, value in pairs(ExpRating) do if type ~= ExperienceRate.BASE and type ~= ExperienceRate.STAMINA_MULTIPLIER then expRateTotal = expRateTotal + (value or 0) end end local staminaMultiplier = ExpRating[ExperienceRate.STAMINA_MULTIPLIER] or 100 expRateTotal = expRateTotal * staminaMultiplier / 100 return expRateTotal / 100 -- Return as a decimal multiplier end -- Function to get just the base experience rate function getBaseExpRate() return ExpRating[ExperienceRate.BASE] or 100 end