-- Weapon Proficiency Module -- Implements the Weapon Proficiency system from Summer Update 2025 -- credits: cipsoft if not WeaponProficiency then WeaponProficiency = {} WeaponProficiency.__index = WeaponProficiency WeaponProficiency.window = nil WeaponProficiency.displayItemPanel = nil WeaponProficiency.perkPanel = nil WeaponProficiency.bonusDetailPanel = nil WeaponProficiency.starProgressPanel = nil WeaponProficiency.optionFilter = nil WeaponProficiency.itemListScroll = nil WeaponProficiency.vocationWarning = nil WeaponProficiency.warningWindow = nil WeaponProficiency.button = nil WeaponProficiency.itemList = {} WeaponProficiency.cacheList = {} -- [itemId] = {experience, perks} WeaponProficiency.allProficiencyRequested = false WeaponProficiency.firstItemRequested = nil WeaponProficiency.saveWeaponMissing = false WeaponProficiency.ItemCategory = { Axes = 17, Clubs = 18, DistanceWeapons = 19, Swords = 20, WandsRods = 21, FistWeapons = 27 } WeaponProficiency.perkPanelsName = {"oneBonusIconPanel", "twoBonusIconPanel", "threeBonusIconPanel"} WeaponProficiency.filters = { ["levelButton"] = false, ["vocButton"] = false, ["oneButton"] = false, ["twoButton"] = false } -- Search filter WeaponProficiency.searchFilter = nil -- Scrollable settings WeaponProficiency.listWidgetHeight = 34 WeaponProficiency.listCapacity = 0 WeaponProficiency.listMinWidgets = 0 WeaponProficiency.listMaxWidgets = 0 WeaponProficiency.offset = 0 WeaponProficiency.listPool = {} WeaponProficiency.listData = {} end WeaponProficiency = WeaponProficiency or {} WeaponProficiency.__index = WeaponProficiency WeaponProficiency.itemList = WeaponProficiency.itemList or {} WeaponProficiency.cacheList = WeaponProficiency.cacheList or {} WeaponProficiency.ItemCategory = WeaponProficiency.ItemCategory or { Axes = 17, Clubs = 18, DistanceWeapons = 19, Swords = 20, WandsRods = 21, FistWeapons = 27 } WeaponProficiency.perkPanelsName = WeaponProficiency.perkPanelsName or {"oneBonusIconPanel", "twoBonusIconPanel", "threeBonusIconPanel"} WeaponProficiency.filters = WeaponProficiency.filters or {} WeaponProficiency.filters["levelButton"] = WeaponProficiency.filters["levelButton"] or false WeaponProficiency.filters["vocButton"] = WeaponProficiency.filters["vocButton"] or false WeaponProficiency.filters["oneButton"] = WeaponProficiency.filters["oneButton"] or false WeaponProficiency.filters["twoButton"] = WeaponProficiency.filters["twoButton"] or false WeaponProficiency.listPool = WeaponProficiency.listPool or {} WeaponProficiency.listData = WeaponProficiency.listData or {} WeaponProficiency.listWidgetHeight = WeaponProficiency.listWidgetHeight or 34 WeaponProficiency.listCapacity = WeaponProficiency.listCapacity or 0 WeaponProficiency.listMinWidgets = WeaponProficiency.listMinWidgets or 0 WeaponProficiency.listMaxWidgets = WeaponProficiency.listMaxWidgets or 0 WeaponProficiency.offset = WeaponProficiency.offset or 0 local function getNumericCall(obj, methodName) if not obj or not obj[methodName] then return 0 end local ok, value = pcall(function() return obj[methodName](obj) end) if not ok then return 0 end return tonumber(value) or 0 end local function getLeftSlotItem() local player = g_game.getLocalPlayer() return player and player:getInventoryItem(InventorySlotLeft) or nil end local function getPlayerWheelVocation() local player = g_game.getLocalPlayer() if not player then return 0 end if translateWheelVocation then return translateWheelVocation(player:getVocation()) end local vocation = player:getVocation() return vocation > 10 and vocation - 10 or vocation end local function hasBit(mask, bitMask) if Bit and Bit.hasBit then return Bit.hasBit(mask, bitMask) end return math.floor(mask / bitMask) % 2 == 1 end local function vocationBit(vocation) if vocation <= 0 then return 0 end if Bit and Bit.bit then return Bit.bit(vocation) end return 2 ^ (vocation - 1) end local function vocationRestrictionMatches(restrictVocation, playerVocation) if not restrictVocation or restrictVocation == 0 then return true end if type(restrictVocation) == "table" then for _, vocationId in pairs(restrictVocation) do local normalized = vocationId > 10 and vocationId - 10 or vocationId if normalized == playerVocation then return true end end return false end local bitMask = vocationBit(playerVocation) return bitMask > 0 and hasBit(restrictVocation, bitMask) end local function categoryMatchesPlayerVocation(category) local requiredMask = WeaponCategoryVocation and WeaponCategoryVocation[category] or 0 if requiredMask == 0 then return true end local bitMask = vocationBit(getPlayerWheelVocation()) return bitMask > 0 and hasBit(requiredMask, bitMask) end local function getVocationWarningDecision(marketData, thingType) local player = g_game.getLocalPlayer() local rawVocation = player and player:getVocation() or 0 local playerLevel = player and player:getLevel() or 0 local playerVocation = getPlayerWheelVocation() local playerBit = vocationBit(playerVocation) local category = marketData and marketData.category or nil local requiredMask = WeaponCategoryVocation and WeaponCategoryVocation[category] or 0 local restrictVocation = marketData and marketData.restrictVocation or 0 local minimumLevel = getNumericCall(thingType, "getMinimumLevel") if minimumLevel == 0 and marketData and marketData.requiredLevel then minimumLevel = tonumber(marketData.requiredLevel) or 0 end local categoryMatch = categoryMatchesPlayerVocation(category) local hasRestrictVocation = restrictVocation and restrictVocation ~= 0 local restrictMatch = vocationRestrictionMatches(restrictVocation, playerVocation) local levelMatch = minimumLevel == 0 or playerLevel >= minimumLevel local vocationMatch = hasRestrictVocation and restrictMatch or categoryMatch local showWarning = not vocationMatch or not levelMatch return { rawVocation = rawVocation, playerLevel = playerLevel, playerVocation = playerVocation, playerBit = playerBit, category = category, requiredMask = requiredMask, categoryMatch = categoryMatch, restrictVocation = restrictVocation, hasRestrictVocation = hasRestrictVocation, restrictMatch = restrictMatch, vocationMatch = vocationMatch, minimumLevel = minimumLevel, levelMatch = levelMatch, showWarning = showWarning } end function init() -- Load proficiency JSON data if ProficiencyData:loadProficiencyJson() then -- Create item cache from market data WeaponProficiency:createItemCache() end -- Connect to game events connect(g_game, { onGameStart = onGameStart, onGameEnd = onGameEnd, onWeaponProficiency = onWeaponProficiency, onWeaponProficiencyExperience = onWeaponProficiencyExperience }) end function terminate() disconnect(g_game, { onGameStart = onGameStart, onGameEnd = onGameEnd, onWeaponProficiency = onWeaponProficiency, onWeaponProficiencyExperience = onWeaponProficiencyExperience }) if WeaponProficiency.window then WeaponProficiency.window:destroy() WeaponProficiency.window = nil end if WeaponProficiency.warningWindow then WeaponProficiency.warningWindow:destroy() WeaponProficiency.warningWindow = nil end end function onGameStart() if not g_game.getFeature(GameProficiency) then scheduleEvent(function() g_modules.getModule("game_proficiency"):unload() end, 100) else WeaponProficiency.allProficiencyRequested = false WeaponProficiency.saveWeaponMissing = false WeaponProficiency.firstItemRequested = nil WeaponProficiency.cacheList = {} WeaponProficiency.currentEquippedExp = 0 WeaponProficiency.currentEquippedMaxExp = 0 -- Client version can change after module init; reload before rebuilding the item cache. ProficiencyData:loadProficiencyJson(true) -- Recreate item cache on each login (may have been cleared by reset()) WeaponProficiency:createItemCache() -- Add button to main panel (only for clients that support proficiency system) -- Use addToggleButton for notification support (20x40 vertical image) WeaponProficiency.button = modules.game_mainpanel.addToggleButton('ProficiencyButton', tr('Open Weapon Proficiency'), '/images/options/button_proficiency', function() toggle() end, false, 21, -- index for ordering true -- vertical image (20x40) ) if WeaponProficiency.button then local btn = WeaponProficiency.button:getChildById('button') if btn then btn:setOn(false) end end -- Initialize topbar proficiency widget initTopBarProficiency() end end -- Initialize the proficiency widget in the top stats bar function initTopBarProficiency(attempts) attempts = attempts or 0 local maxRetries = 15 -- Delay initialization to ensure StatsBar is fully loaded scheduleEvent(function() -- Access StatsBar through modules.game_interface local StatsBarModule = modules.game_interface.StatsBar if not StatsBarModule then if attempts < maxRetries then scheduleEvent(initTopBarProficiency, 500, attempts + 1) end return end local statsBar = StatsBarModule.getCurrentStatsBarWithPosition and StatsBarModule.getCurrentStatsBarWithPosition() if statsBar then local profWidget = statsBar:recursiveGetChildById('proficiencyTopBar') if profWidget then profWidget:setVisible(true) -- Request proficiency data for equipped weapon local player = g_game.getLocalPlayer() if player then local leftSlotItem = player:getInventoryItem(InventorySlotLeft) if leftSlotItem then local itemId = leftSlotItem:getId() g_game.sendWeaponProficiencyAction(0, itemId) end end updateTopBarProficiency() end else if attempts < maxRetries then scheduleEvent(initTopBarProficiency, 500, attempts + 1) end end end, 500) -- 500ms delay end -- Update the proficiency progress bar in the top bar function updateTopBarProficiency() -- Access StatsBar through modules.game_interface local StatsBarModule = modules.game_interface.StatsBar if not StatsBarModule then return end local statsBar = StatsBarModule.getCurrentStatsBarWithPosition and StatsBarModule.getCurrentStatsBarWithPosition() if not statsBar then return end local profWidget = statsBar:recursiveGetChildById('proficiencyTopBar') if not profWidget then local topLargeProficiency = statsBar:recursiveGetChildById('proficiencyIcon') if not topLargeProficiency then return end profWidget = topLargeProficiency end -- Get equipped weapon local player = g_game.getLocalPlayer() if not player then return end local leftSlotItem = player:getInventoryItem(InventorySlotLeft) if not leftSlotItem then -- Hide the proficiency progress bar, icon tree and label on Parallel/Default bars -- when there is no weapon equipped. local statsBarName = StatsBarModule.getCurrentStatsBarWithPositionName and StatsBarModule.getCurrentStatsBarWithPositionName() if statsBarName and (statsBarName == 'LargeOnTop' or statsBarName == 'LargeOnBottom') then local highlightProficiencyButton = profWidget:getChildById('highlightProficiencyButton') or nil local proficiencyProgress = profWidget:getChildById('proficiencyProgress') or nil local proficiencyButton = profWidget:getChildById('proficiencyButton') or nil if highlightProficiencyButton then highlightProficiencyButton:hide() highlightProficiencyButton:setVisible(false) end if proficiencyProgress then proficiencyProgress:setPercent(0) proficiencyProgress:hide() proficiencyProgress:setVisible(false) end if proficiencyButton then proficiencyButton:setWidth(28) proficiencyButton:setHeight(28) end end if statsBarName and (statsBarName == 'ParallelOnTop' or statsBarName == 'ParallelOnBottom' or statsBarName == 'DefaultOnTop' or statsBarName == 'DefaultOnBottom') then local progressBar = profWidget:getChildById('proficiencyProgress') or nil local IconTree = profWidget:getChildById('proficiencyIconTree') or nil local label = profWidget:getChildById('proficiencyLabel') or nil local profBg = profWidget:getChildById('proficiencyBg') or nil if progressBar then progressBar:setPercent(0) progressBar:hide() end if IconTree then IconTree:setImageSource('/images/game/topbar/icon-proficiencytree-off') IconTree:hide() end if label then label:setText('0%') label:hide() end if profBg then profBg:hide() end local iconsPanel = statsBar:getChildById('icons') if iconsPanel then iconsPanel:setMarginLeft(0) end profWidget:setMarginRight(10) return end return end local itemId = leftSlotItem:getId() local cacheData = WeaponProficiency.cacheList[itemId] -- Restore the default margins on Parallel/Default bars when a weapon is equipped. local statsBarName = StatsBarModule.getCurrentStatsBarWithPositionName and StatsBarModule.getCurrentStatsBarWithPositionName() if statsBarName and (statsBarName == 'LargeOnTop' or statsBarName == 'LargeOnBottom') then local highlightProficiencyButton = profWidget:getChildById('highlightProficiencyButton') or nil local proficiencyProgress = profWidget:getChildById('proficiencyProgress') or nil local proficiencyButton = profWidget:getChildById('proficiencyButton') or nil if highlightProficiencyButton then highlightProficiencyButton:show() end if proficiencyProgress then proficiencyProgress:show() end if proficiencyButton then proficiencyButton:setWidth(20) proficiencyButton:setHeight(20) end end if statsBarName and (statsBarName == 'ParallelOnTop' or statsBarName == 'ParallelOnBottom' or statsBarName == 'DefaultOnTop' or statsBarName == 'DefaultOnBottom') then local iconsPanel = statsBar:getChildById('icons') if iconsPanel then iconsPanel:setMarginLeft(60) end profWidget:setMarginRight(60) end if cacheData then local exp = cacheData.exp or 0 -- Get thingType for calculations local thingType = nil if leftSlotItem.getThingType then thingType = leftSlotItem:getThingType() end -- Calculate percent for next level (not total) local percent = 0 local currentLevel = 0 local nextLevelExp = 0 local currentLevelExp = 0 if ProficiencyData and ProficiencyData.getCurrentLevelByExp and ProficiencyData.getLevelPercent then -- Get current level currentLevel = ProficiencyData:getCurrentLevelByExp(leftSlotItem, exp, false, thingType) or 0 -- Get percent progress to next level local nextLevel = currentLevel + 1 percent = ProficiencyData:getLevelPercent(exp, nextLevel, leftSlotItem, thingType) or 0 -- Get exp values for tooltip if ProficiencyData.getMaxExperienceByLevel then currentLevelExp = currentLevel > 0 and (ProficiencyData:getMaxExperienceByLevel(currentLevel, leftSlotItem, thingType) or 0) or 0 nextLevelExp = ProficiencyData:getMaxExperienceByLevel(nextLevel, leftSlotItem, thingType) or 0 end end percent = math.min(100, math.max(0, percent)) local progressBar = profWidget:getChildById('proficiencyProgress') local label = profWidget:getChildById('proficiencyLabel') or nil local bg = profWidget:getChildById('proficiencyBg') or nil if progressBar then progressBar:setPercent(percent) progressBar:show() end if label then label:setText(percent .. '%') label:show() end local iconTree = profWidget:getChildById('proficiencyIconTree') or nil if iconTree then iconTree:show() end if bg then bg:show() local expInLevel = exp - currentLevelExp local expNeeded = nextLevelExp - currentLevelExp bg:setTooltip(string.format("Proficiency Progress: %s / %s", tostring(expInLevel), tostring(expNeeded))) end -- Show/hide highlight based on unused perk local highlight = profWidget:getChildById('highlightProficiencyButton') if highlight then highlight:setVisible(WeaponProficiency.hasUnusedPerk == true) end -- Store for reference WeaponProficiency.currentEquippedExp = exp WeaponProficiency.currentEquippedMaxExp = nextLevelExp else g_game.sendWeaponProficiencyAction(0, itemId) end end function onGameEnd() if WeaponProficiency.window then WeaponProficiency.window:hide() end -- Remove button from main panel if WeaponProficiency.button then WeaponProficiency.button:destroy() WeaponProficiency.button = nil end WeaponProficiency:reset() end -- Called when server sends proficiency info (opcode 0xC4) function onWeaponProficiency(itemId, experience, perks, marketCategory) -- Ensure perks is a table if type(perks) ~= "table" then perks = {} end -- IMPORTANT: Server sends perks in 0-indexed format, convert to 1-indexed for Lua -- Also filter out invalid perks (values >= 200 are clearly invalid, likely from uninitialized data) local convertedPerks = {} for _, perk in ipairs(perks) do if type(perk) == "table" and #perk >= 2 then local level = perk[1] local perkPos = perk[2] -- Filter out invalid values (255 becomes 256 after +1, which is invalid) -- Valid levels are 0-6 (0-indexed), valid perk positions are 0-2 (0-indexed) if level >= 0 and level <= 10 and perkPos >= 0 and perkPos <= 10 then -- Convert from 0-indexed (server) to 1-indexed (Lua) table.insert(convertedPerks, {level + 1, perkPos + 1}) end end end -- Only update cache perks if server returned non-empty perks -- Otherwise, keep existing cache perks (they were just applied) local existingCache = WeaponProficiency.cacheList[itemId] if #convertedPerks > 0 then -- Server confirmed perks, use them (now in 1-indexed format) WeaponProficiency.cacheList[itemId] = { exp = experience, perks = convertedPerks } else -- Server returned empty perks, but we may have just applied some -- Keep existing perks in cache if they exist if existingCache and existingCache.perks and #existingCache.perks > 0 then WeaponProficiency.cacheList[itemId] = { exp = experience, perks = existingCache.perks } else WeaponProficiency.cacheList[itemId] = { exp = experience, perks = {} } end end local cachePerks = WeaponProficiency.cacheList[itemId].perks -- Re-sort the item list when we receive new proficiency data if marketCategory then sortWeaponProficiency(marketCategory) sortWeaponProficiency(MarketCategory.WeaponsAll) end if WeaponProficiency.window and WeaponProficiency.window:isVisible() then -- Refresh item list to update stars and order WeaponProficiency:refreshItemList() WeaponProficiency:onUpdateSelectedProficiency(itemId) -- If this is the currently selected item, update display with cached perks if WeaponProficiency.selectedItemId == itemId then WeaponProficiency:displayProficiencyData(itemId, experience, cachePerks) end end -- Update top bar proficiency display updateTopBarProficiency() end function onWeaponProficiencyExperience(itemId, experience, hasUnusedPerk) local itemCache = WeaponProficiency.cacheList[itemId] if not itemCache then WeaponProficiency.cacheList[itemId] = { exp = experience, perks = {} } else if experience > 0 then itemCache.exp = experience end end -- Re-sort all categories when experience changes sortWeaponProficiency(MarketCategory.WeaponsAll) for _, categoryId in pairs(WeaponProficiency.ItemCategory) do sortWeaponProficiency(categoryId) end -- Store the unused perk state globally WeaponProficiency.hasUnusedPerk = hasUnusedPerk -- Show/hide highlight on proficiency button based on unused perks updateProficiencyHighlight() -- Refresh item list if window is visible if WeaponProficiency.window and WeaponProficiency.window:isVisible() then WeaponProficiency:refreshItemList() end -- Update top bar proficiency display updateTopBarProficiency() end -- Update the proficiency button highlight based on unused perk state function updateProficiencyHighlight() if WeaponProficiency.button then local highlight = WeaponProficiency.button:getChildById('highlight') local bright = WeaponProficiency.button:getChildById('brightButton') local shouldShow = WeaponProficiency.hasUnusedPerk == true if highlight then highlight:setVisible(shouldShow) end if bright then bright:setVisible(shouldShow) end end end -- Public function to open the proficiency window function show() if not WeaponProficiency.window then createWindow() end -- Reset search filter and clear search text WeaponProficiency.searchFilter = nil local searchText = WeaponProficiency.window:recursiveGetChildById('searchText') if searchText then searchText:setText('') end -- Reset filter buttons visual state (but keep filter state) -- The filters persist across open/close WeaponProficiency.window:show() WeaponProficiency.window:raise() WeaponProficiency.window:focus() -- Update button state (for highlight widget, use child button) if WeaponProficiency.button then local btn = WeaponProficiency.button:getChildById('button') if btn then btn:setOn(true) -- For vertical images, set correct clip btn:setImageClip('0 20 20 20') end -- Hide highlight when window is opened local highlight = WeaponProficiency.button:getChildById('highlight') local bright = WeaponProficiency.button:getChildById('brightButton') if highlight then highlight:setVisible(false) end if bright then bright:setVisible(false) end end -- Refresh item list to show all items WeaponProficiency:refreshItemList() -- Auto-select item when window opens (equipped weapon or first in list) -- Use longer delay to ensure items are loaded, with retry WeaponProficiency.autoSelectRetries = 0 scheduleEvent(function() autoSelectItem() end, 300) end -- Auto-select an item (equipped weapon or first in list) function autoSelectItem() if not WeaponProficiency.window or not WeaponProficiency.window:isVisible() then return end -- Already has a selected item with perks displayed? Skip if WeaponProficiency.selectedMarketItem and WeaponProficiency.selectedMarketItem.displayItem then local perkPanel = WeaponProficiency.perkPanel if perkPanel and perkPanel:getChildCount() > 0 then return end end local targetItemId = nil local targetMarketItem = nil -- Get all items from all categories local allItems = WeaponProficiency.itemList[MarketCategory.WeaponsAll] or {} -- First, check if player has an equipped weapon local player = g_game.getLocalPlayer() if player then local leftSlotItem = player:getInventoryItem(InventorySlotLeft) if leftSlotItem then local equippedId = leftSlotItem:getId() -- Search for this item in our list for _, marketItem in ipairs(allItems) do local itemId = marketItem.originalId or (marketItem.displayItem and marketItem.displayItem:getId()) local displayId = marketItem.displayId or itemId if itemId == equippedId or displayId == equippedId then targetItemId = itemId targetMarketItem = marketItem break end end end end -- If no equipped weapon found, select first item from the allItems list if not targetItemId and #allItems > 0 then -- Just take the first item from the list local firstItem = allItems[1] if firstItem then targetItemId = firstItem.originalId or (firstItem.displayItem and firstItem.displayItem:getId()) targetMarketItem = firstItem end end -- Fallback: check UI item list if allItems is empty if not targetItemId then local itemList = WeaponProficiency.window:recursiveGetChildById("itemList") if itemList then local children = itemList:getChildren() for _, child in ipairs(children) do local itemWidget = child:getChildById('item') if itemWidget then local displayItem = itemWidget:getItem() if displayItem and displayItem:getId() > 0 then local displayItemId = displayItem:getId() -- Find the marketItem for this display for _, marketItem in ipairs(allItems) do local mItemId = marketItem.originalId or (marketItem.displayItem and marketItem.displayItem:getId()) local mDisplayId = marketItem.displayId or mItemId if mDisplayId == displayItemId or mItemId == displayItemId then targetItemId = mItemId targetMarketItem = marketItem break end end if targetItemId then break end end end end end end -- Select the target item if targetItemId and targetMarketItem then WeaponProficiency:selectItem(targetItemId, targetMarketItem) else -- Retry if no item found yet (cache might not be ready) WeaponProficiency.autoSelectRetries = (WeaponProficiency.autoSelectRetries or 0) + 1 if WeaponProficiency.autoSelectRetries < 5 then scheduleEvent(function() autoSelectItem() end, 200) end end end function hide() if not WeaponProficiency.window then return end -- Close window WeaponProficiency.window:hide() -- Update button state (for highlight widget, use child button) if WeaponProficiency.button then local btn = WeaponProficiency.button:getChildById('button') if btn then btn:setOn(false) -- For vertical images, restore normal clip btn:setImageClip('0 0 20 20') end end -- Re-show highlight if there are still unused perks updateProficiencyHighlight() -- Reset selected item state so auto-select works on next open WeaponProficiency.selectedItemId = nil WeaponProficiency.selectedDisplayId = nil WeaponProficiency.selectedMarketItem = nil end function WeaponProficiency:onCloseWindow() local hasPending = self.pendingSelections and next(self.pendingSelections) ~= nil if not hasPending then hide() return true end if self.warningWindow then self.warningWindow:destroy() self.warningWindow = nil end local yesButton = function() if self.warningWindow then self.warningWindow:destroy() self.warningWindow = nil end self:applyPendingSelections() hide() end local noButton = function() if self.warningWindow then self.warningWindow:destroy() self.warningWindow = nil end self.pendingSelections = {} self:updateApplyButtonState() hide() end self.warningWindow = displayGeneralBox('Save?', "You did not save the changes you have made to your perks.\n\nWould you like to save your perks?", {{ text = tr('Yes'), callback = yesButton }, { text = tr('No'), callback = noButton }}, yesButton, noButton) return false end function toggle() if WeaponProficiency.window and WeaponProficiency.window:isVisible() then WeaponProficiency:onCloseWindow() else requestOpenWindow() end end -- Request to open proficiency window with optional item redirect function requestOpenWindow(redirectItem) WeaponProficiency:ensureItemCache() local category = "Weapons: All" local targetItemId = nil local targetMarketItem = nil -- Check left hand slot for equipped weapon local leftSlotItem = getLeftSlotItem() if leftSlotItem then local weaponType = leftSlotItem:getWeaponType() or 0 if weaponType > 0 then category = getWeaponCategoryString(weaponType) targetItemId = leftSlotItem:getId() end end if redirectItem then local weaponType = redirectItem:getWeaponType() or 0 if weaponType > 0 then category = getWeaponCategoryString(weaponType) targetItemId = redirectItem:getId() end end if not targetItemId and WeaponProficiency.firstItemRequested then targetItemId = WeaponProficiency.firstItemRequested:getId() local weaponType = WeaponProficiency.firstItemRequested.getWeaponType and WeaponProficiency.firstItemRequested:getWeaponType() or 0 category = getWeaponCategoryString(weaponType) end if targetItemId then targetMarketItem = WeaponProficiency:findMarketItem(targetItemId) end -- Request all proficiencies from server if not WeaponProficiency.allProficiencyRequested then g_game.sendWeaponProficiencyAction(1) -- Request all weapons WeaponProficiency.allProficiencyRequested = true WeaponProficiency.firstItemRequested = redirectItem end show() if WeaponProficiency.optionFilter and category then WeaponProficiency.optionFilter:setCurrentOption(category, true) WeaponProficiency:refreshItemList() end if targetMarketItem then scheduleEvent(function() if WeaponProficiency.window and WeaponProficiency.window:isVisible() then local displayId = targetMarketItem.displayId or targetMarketItem.originalId or targetItemId WeaponProficiency:selectItem(displayId, targetMarketItem) end end, 50) end end -- Helper function to get weapon category string function getWeaponCategoryString(weaponType) if WeaponCategoryToString[weaponType] then return WeaponCategoryToString[weaponType] end local categoryMap = { [1] = "Weapons: Clubs", -- WEAPON_CLUB [2] = "Weapons: Axes", -- WEAPON_AXE [3] = "Weapons: Swords", -- WEAPON_SWORD [4] = "Weapons: Wands", -- WEAPON_WANDROD [7] = "Weapons: Distance", -- WEAPON_BOW [8] = "Weapons: Distance", -- WEAPON_THROW [9] = "Weapons: Distance", -- WEAPON_CROSSBOW [0] = "Weapons: Fist" -- WEAPON_FIST } return categoryMap[weaponType] or "Weapons: All" end -- Create the proficiency window function createWindow() WeaponProficiency.window = g_ui.displayUI('proficiency') WeaponProficiency.window:hide() WeaponProficiency.displayItemPanel = WeaponProficiency.window:recursiveGetChildById("itemPanel") WeaponProficiency.perkPanel = WeaponProficiency.window:recursiveGetChildById("bonusProgressBackground") WeaponProficiency.bonusDetailPanel = WeaponProficiency.window:recursiveGetChildById("bonusDetailBackground") WeaponProficiency.optionFilter = WeaponProficiency.window:recursiveGetChildById("classFilter") WeaponProficiency.starProgressPanel = WeaponProficiency.window:recursiveGetChildById("starsPanelBackground") WeaponProficiency.itemListScroll = WeaponProficiency.window:recursiveGetChildById("itemListScroll") WeaponProficiency.vocationWarning = WeaponProficiency.window:recursiveGetChildById("vocationWarning") -- Debug: verify panels are found -- Setup category dropdown options if WeaponProficiency.optionFilter then WeaponProficiency.optionFilter:clearOptions() WeaponProficiency.optionFilter:addOption("Weapons: All") WeaponProficiency.optionFilter:addOption("Weapons: Swords") WeaponProficiency.optionFilter:addOption("Weapons: Axes") WeaponProficiency.optionFilter:addOption("Weapons: Clubs") WeaponProficiency.optionFilter:addOption("Weapons: Distance") WeaponProficiency.optionFilter:addOption("Weapons: Wands") WeaponProficiency.optionFilter:addOption("Weapons: Fist") WeaponProficiency.optionFilter.onOptionChange = function(widget, option) WeaponProficiency:refreshItemList() end end -- Setup search text handler local searchText = WeaponProficiency.window:recursiveGetChildById('searchText') if searchText then searchText.onTextChange = function(widget, text) WeaponProficiency.searchFilter = text WeaponProficiency:refreshItemList() end end -- Setup clear search button local clearButton = WeaponProficiency.window:recursiveGetChildById('clearSearchButton') if clearButton then clearButton.onClick = function() local searchWidget = WeaponProficiency.window:recursiveGetChildById('searchText') if searchWidget then searchWidget:setText('') WeaponProficiency.searchFilter = nil WeaponProficiency:refreshItemList() end end end -- Initialize item list WeaponProficiency:refreshItemList() end -- Reset proficiency data function WeaponProficiency:reset() self.cacheList = {} self.allProficiencyRequested = false self.itemList = {} self._itemCacheReady = false end function WeaponProficiency:ensureItemCache() if self._itemCacheReady then return end self:createItemCache() end -- Create item cache from proficiency things function WeaponProficiency:createItemCache() self.itemList = {} self.itemList[MarketCategory.WeaponsAll] = {} self.ItemCategory = self.ItemCategory or { Axes = 17, Clubs = 18, DistanceWeapons = 19, Swords = 20, WandsRods = 21, FistWeapons = 27 } for _, v in pairs(self.ItemCategory) do self.itemList[v] = {} end -- Weapon categories that support proficiency local weaponCategories = { [MarketCategory.Axes] = true, [MarketCategory.Clubs] = true, [MarketCategory.DistanceWeapons] = true, [MarketCategory.Swords] = true, [MarketCategory.WandsRods] = true, [MarketCategory.FistWeapons] = true } local itemTypes = {} local useProficiencyThings = g_things.getProficiencyThings ~= nil if useProficiencyThings then itemTypes = g_things.getProficiencyThings() else itemTypes = g_things.getThingTypes(ThingCategoryItem) end for _, itemType in pairs(itemTypes) do local marketData = itemType.getMarketData and itemType:getMarketData() or {} local category = marketData and marketData.category or nil if not weaponCategories[category] then category = getUnknownMarketCategory(itemType) end if self.itemList[category] and (useProficiencyThings or weaponCategories[category]) then local originalId = itemType:getId() local showAs = marketData and marketData.showAs or nil if not showAs or showAs == 0 then showAs = originalId end local name = marketData and marketData.name or nil if (not name or name == "") then name = g_things.getCyclopediaItemName(originalId) end if not name or name == "" then name = itemType.getName and itemType:getName() or tostring(originalId) end local item = Item.create(originalId) item:setId(showAs) marketData = marketData or {} marketData.category = category marketData.showAs = showAs marketData.name = name local marketItem = { displayItem = item, thingType = itemType, marketData = marketData, originalId = originalId, displayId = showAs } table.insert(self.itemList[category], marketItem) table.insert(self.itemList[MarketCategory.WeaponsAll], marketItem) end end -- Sort by name initially local function sortByName(a, b) local nameA = (a.marketData.name or ""):lower() local nameB = (b.marketData.name or ""):lower() return nameA < nameB end for _, v in pairs(self.itemList) do table.sort(v, sortByName) end if not self.firstItemRequested and self.itemList[MarketCategory.WeaponsAll][1] then self.firstItemRequested = self.itemList[MarketCategory.WeaponsAll][1].displayItem end self._itemCacheReady = true end -- Sort weapons by experience (highest first), then by name function sortWeaponProficiency(marketCategory) if not WeaponProficiency.itemList then return end local itemList = WeaponProficiency.itemList[marketCategory] if not itemList then return end table.sort(itemList, function(a, b) local idA = a.originalId or a.displayId or (a.marketData and a.marketData.showAs) local idB = b.originalId or b.displayId or (b.marketData and b.marketData.showAs) local expA = WeaponProficiency.cacheList[idA] and WeaponProficiency.cacheList[idA].exp or 0 local expB = WeaponProficiency.cacheList[idB] and WeaponProficiency.cacheList[idB].exp or 0 if expA == expB then local nameA = (a.marketData.name or ""):lower() local nameB = (b.marketData.name or ""):lower() return nameA < nameB end return expA > expB end) end function WeaponProficiency:findMarketItem(itemId) local allItems = self.itemList[MarketCategory.WeaponsAll] or {} for _, marketItem in ipairs(allItems) do local originalId = marketItem.originalId local displayId = marketItem.displayId local showAs = marketItem.marketData and marketItem.marketData.showAs if itemId == originalId or itemId == displayId or itemId == showAs then return marketItem end end return nil end -- Check if mastery is achieved for an item function isMasteryAchieved(displayItem, cacheId, thingType, marketData) if not displayItem then return false end local itemId = cacheId or displayItem:getId() local weaponEntry = WeaponProficiency.cacheList[itemId] local currentExperience = weaponEntry and weaponEntry.exp or 0 -- Get proficiency data local tt = thingType or displayItem:getThingType() local proficiencyId = ProficiencyData:getProficiencyIdForItem(displayItem, tt, marketData) local perkCount = ProficiencyData:getPerkLaneCount(proficiencyId) local maxExperience = ProficiencyData:getMaxExperience(perkCount, displayItem, tt, marketData) return currentExperience >= maxExperience end -- Get unknown market category for item function getUnknownMarketCategory(itemType) local weaponType = itemType:getWeaponType() or 0 if WeaponCategoryToString[weaponType] then return weaponType end return UnknownCategories[weaponType] or MarketCategory.WeaponsAll end -- Update selected proficiency display function WeaponProficiency:onUpdateSelectedProficiency(itemId) if not self.displayItemPanel then return end -- Check if the itemId matches the currently selected item's originalId local selectedOriginalId = self.selectedMarketItem and self.selectedMarketItem.originalId if not selectedOriginalId or selectedOriginalId ~= itemId then return end -- Get display item from selected market item local displayItem = self.selectedMarketItem and self.selectedMarketItem.displayItem if not displayItem then return end local currentData = self.cacheList[itemId] or { exp = 0, perks = {} } self:updateExperienceProgress(currentData.exp, displayItem) end -- Update experience progress display function WeaponProficiency:updateExperienceProgress(currentExp, displayItem) if not self.window then return end if not displayItem then return end local experienceWidget = self.window:recursiveGetChildById("progressDescription") local experienceLeftWidget = self.window:recursiveGetChildById("nextLevelDescription") local totalProgressWidget = self.window:recursiveGetChildById("proficiencyProgress") local proficiencyIconTree = self.window:recursiveGetChildById("proficiencyIconTree") if not experienceWidget or not experienceLeftWidget then return end local thingType = self.selectedMarketItem and self.selectedMarketItem.thingType local marketData = self.selectedMarketItem and self.selectedMarketItem.marketData local proficiencyId = ProficiencyData:getProficiencyIdForItem(displayItem, thingType, marketData) local perkCount = ProficiencyData:getPerkLaneCount(proficiencyId) local currentCeilExperience = ProficiencyData:getCurrentCeilExperience(currentExp, displayItem, thingType, marketData) local maxExperience = ProficiencyData:getMaxExperience(perkCount, displayItem, thingType, marketData) local masteryAchieved = currentExp >= maxExperience experienceWidget:setText(string.format("%s / %s", comma_value(currentExp), comma_value(currentCeilExperience))) if masteryAchieved then experienceLeftWidget:setText("Mastery achieved") if proficiencyIconTree then proficiencyIconTree:setImageSource('/images/game/topbar/icon-proficiencytree-on') end else experienceLeftWidget:setText(string.format("%s XP for next level", comma_value(currentCeilExperience - currentExp))) if proficiencyIconTree then proficiencyIconTree:setImageSource('/images/game/topbar/icon-proficiencytree-off') end end if totalProgressWidget then totalProgressWidget:setPercent(ProficiencyData:getTotalPercent(currentExp, perkCount, displayItem, thingType, marketData)) end end -- Toggle filter option (Level, Voc, 1H, 2H buttons) function WeaponProficiency:toggleFilterOption(button) if not button then return end local buttonId = button:getId() if buttonId == "oneButton" and self.filters["twoButton"] then self.filters["twoButton"] = false local twoButton = self.window and self.window:recursiveGetChildById("twoButton") if twoButton then twoButton:setOn(false) end elseif buttonId == "twoButton" and self.filters["oneButton"] then self.filters["oneButton"] = false local oneButton = self.window and self.window:recursiveGetChildById("oneButton") if oneButton then oneButton:setOn(false) end end self.filters[buttonId] = not self.filters[buttonId] -- Update button visual state if self.filters[buttonId] then button:setOn(true) else button:setOn(false) end -- Refresh item list with new filters self:refreshItemList() end -- Refresh item list based on current filters and category function WeaponProficiency:refreshItemList() if not self.window then return end local itemList = self.window:recursiveGetChildById('itemList') if not itemList then return end -- Get current category from dropdown local categoryDropdown = self.window:recursiveGetChildById('classFilter') local currentCategory = MarketCategory.WeaponsAll if categoryDropdown then local selectedText = categoryDropdown:getText() currentCategory = WeaponStringToCategory[selectedText] or MarketCategory.WeaponsAll end -- Sort items by experience (highest first) sortWeaponProficiency(currentCategory) -- Get items for current category local items = self.itemList[currentCategory] or {} -- Apply filters items = self:applyLevelFilter(items) items = self:applyVocationFilter(items) -- Apply 1H/2H filters local oneActive = self.filters["oneButton"] local twoActive = self.filters["twoButton"] if oneActive and not twoActive then -- Only show one-handed weapons local filteredItems = {} for _, item in ipairs(items) do local thingType = item.thingType if thingType then local slotType = thingType.getClothSlot and thingType:getClothSlot() or 0 if slotType == 6 then table.insert(filteredItems, item) end else table.insert(filteredItems, item) end end items = filteredItems elseif twoActive and not oneActive then -- Only show two-handed weapons local filteredItems = {} for _, item in ipairs(items) do local thingType = item.thingType if thingType then local slotType = thingType.getClothSlot and thingType:getClothSlot() or 0 if slotType == 0 then table.insert(filteredItems, item) end end end items = filteredItems end -- If both active or neither, show all -- Apply search text filter if self.searchFilter and self.searchFilter ~= '' then local searchLower = string.lower(self.searchFilter) local filteredItems = {} for _, item in ipairs(items) do local itemName = item.marketData and item.marketData.name or "" if string.find(string.lower(itemName), searchLower, 1, true) then table.insert(filteredItems, item) end end items = filteredItems end itemList:destroyChildren() -- Populate items with click handlers. The grid itself owns scrolling, so do -- not cap this to the number of visible cells. local index = 1 for _, marketItem in ipairs(items) do local child = g_ui.createWidget("ItemBox", itemList, "widget_" .. index) local itemWidget = child and child:getChildById('item') if itemWidget and marketItem.displayItem then -- Use stored displayId (guaranteed non-zero) instead of displayItem:getId() local displayId = marketItem.displayId or marketItem.originalId local cacheId = marketItem.originalId or displayId itemWidget:setItemId(displayId) ItemsDatabase.setRarityItem(itemWidget, itemWidget:getItem()) -- Add tooltip with item name child:setTooltip(marketItem.marketData.name or "") -- Add stars based on proficiency level local starPanel = child:getChildById('starsBackground') if starPanel then starPanel:destroyChildren() -- Get experience and calculate level local cacheEntry = self.cacheList[cacheId] local exp = cacheEntry and cacheEntry.exp or 0 local weaponLevel = ProficiencyData:getCurrentLevelByExp(marketItem.displayItem, exp, false, marketItem.thingType, marketItem.marketData) or 0 -- Create star widgets for each level achieved if weaponLevel > 0 then local mastery = isMasteryAchieved(marketItem.displayItem, cacheId, marketItem.thingType, marketItem.marketData) for i = 1, weaponLevel do local star = g_ui.createWidget("MiniStar", starPanel) if star and mastery then star:setImageSource(proficiencyImage("icon-star-tiny-gold")) end end end end -- Add click handler child.onClick = function() WeaponProficiency:selectItem(displayId, marketItem) end end index = index + 1 end if self.itemListScroll then self.itemListScroll:setValue(0) end end -- Handle category change from dropdown function WeaponProficiency:onCategoryChange(dropdown) self:refreshItemList() end -- Select an item from the list function WeaponProficiency:selectItem(itemId, marketItem) if not self.window then return end -- Use originalId for cache lookups (server uses this ID) local cacheId = marketItem.originalId or itemId self.selectedItemId = cacheId -- Use cacheId for proficiency data lookup self.selectedDisplayId = itemId -- Keep display ID for UI self.selectedMarketItem = marketItem -- Get the item panel local itemPanel = self.window:recursiveGetChildById('itemPanel') if not itemPanel then return end -- Update item display panel local itemNameLabel = itemPanel:getChildById('itemNameTitle') local itemIconWidget = itemPanel:recursiveGetChildById('item') local displayItem = marketItem.displayItem -- Ensure displayItem has a valid ID (fix for items where showAs was 0) local actualDisplayId = marketItem.displayId or marketItem.originalId if displayItem and displayItem:getId() == 0 and actualDisplayId > 0 then displayItem:setId(actualDisplayId) end if itemNameLabel and marketItem then itemNameLabel:setText(marketItem.marketData.name or "Unknown Item") end -- Set item using setItem method if itemIconWidget and displayItem then itemIconWidget:setItem(displayItem) end -- Destroy and recreate perk panels for fresh display if self.perkPanel then self.perkPanel:destroyChildren() end if self.bonusDetailPanel then self.bonusDetailPanel:destroyChildren() end if self.starProgressPanel then self.starProgressPanel:destroyChildren() end -- Initialize cache entry if not exists if not self.cacheList[cacheId] then self.cacheList[cacheId] = { exp = 0, perks = {} } end local currentData = self.cacheList[cacheId] local thingType = marketItem.thingType local marketData = marketItem.marketData local vocationDecision = getVocationWarningDecision(marketData, thingType) if self.vocationWarning then self.vocationWarning:setVisible(vocationDecision.showWarning) end -- Get proficiency ID using wrapper function, passing thingType and marketData for proper category lookup local proficiencyId = ProficiencyData:getProficiencyIdForItem(displayItem, thingType, marketData) local profEntry = ProficiencyData:getContentById(proficiencyId) if profEntry then -- Update experience display self:updateExperienceProgress(currentData.exp, displayItem) -- Create star widgets for i = 1, #profEntry.Levels do local starWidget = g_ui.createWidget('StarWidget', self.starProgressPanel) if starWidget then starWidget:setId('starWidget' .. i) end end -- Create perk column panels for i, levelData in ipairs(profEntry.Levels) do local perkColumn = g_ui.createWidget('BonusSelectPanel', self.perkPanel) if perkColumn then perkColumn:setId('perkColumn_' .. i) local progress = perkColumn:getChildById('bonusSelectProgress') if progress then progress:setPercent(0) end end local bonusDetail = g_ui.createWidget('BonusDetailPanel', self.bonusDetailPanel) if bonusDetail then bonusDetail:setId('bonusDetail_' .. i) end end -- Load saved perks into pendingSelections for UI display self.pendingSelections = {} if currentData.perks and #currentData.perks > 0 then for i, perk in ipairs(currentData.perks) do if type(perk) == "table" and #perk >= 2 then local level = perk[1] local perkPos = perk[2] self.pendingSelections[level] = perkPos end end end -- Display perks and update UI self:displayPerks(cacheId, currentData.perks, displayItem) else -- Fallback: show basic info without perks self:updateExperienceProgress(currentData.exp, displayItem) end -- Request proficiency info from server if needed - use cacheId (originalId) g_game.sendWeaponProficiencyAction(0, cacheId) end -- Display proficiency data for selected item function WeaponProficiency:displayProficiencyData(itemId, experience, perks) if not self.window then return end if self.selectedItemId ~= itemId then return end local displayItem = self.selectedMarketItem and self.selectedMarketItem.displayItem local thingType = self.selectedMarketItem and self.selectedMarketItem.thingType if not displayItem then return end -- Update experience display self:updateExperienceProgress(experience, displayItem) -- Get proficiency content using wrapper function (with thingType and marketData) local marketData = self.selectedMarketItem and self.selectedMarketItem.marketData local proficiencyId = ProficiencyData:getProficiencyIdForItem(displayItem, thingType, marketData) local profEntry = ProficiencyData:getContentById(proficiencyId) if not profEntry then return end local levels = profEntry.Levels or {} local currentLevel = ProficiencyData:getCurrentLevelByExp(displayItem, experience, false, thingType, marketData) local maxExperience = ProficiencyData:getMaxExperience(#levels, displayItem, thingType, marketData) local masteryAchieved = experience >= maxExperience -- Update perk columns for each level for i, levelData in ipairs(levels) do local perkColumn = self.perkPanel:getChildById('perkColumn_' .. i) local starWidget = self.starProgressPanel:getChildById('starWidget' .. i) if perkColumn then self:updatePerkColumn(perkColumn, levelData, i, currentLevel, perks, experience, displayItem, masteryAchieved, thingType, marketData) end if starWidget then self:updateStarWidget(starWidget, i, currentLevel, experience, displayItem, masteryAchieved, thingType, marketData) end end -- Update bonus detail panels self:updateBonusDetails(profEntry, perks) -- Update item frame self:updateItemAddons(experience, displayItem, masteryAchieved, thingType, marketData) end -- Display perks in the perk panel function WeaponProficiency:displayPerks(itemId, perks, displayItem) if not self.window or not self.perkPanel then return end if not displayItem then return end -- Get proficiency content using wrapper function (with thingType and marketData for proper category lookup) local thingType = self.selectedMarketItem and self.selectedMarketItem.thingType local marketData = self.selectedMarketItem and self.selectedMarketItem.marketData local proficiencyId = ProficiencyData:getProficiencyIdForItem(displayItem, thingType, marketData) local proficiencyContent = ProficiencyData:getContentById(proficiencyId) if not proficiencyContent then return end -- Use cacheId (originalId) for cache lookups local cacheId = self.selectedMarketItem and self.selectedMarketItem.originalId or itemId local levels = proficiencyContent.Levels or {} local experience = self.cacheList[cacheId] and self.cacheList[cacheId].exp or 0 -- Calculate current level based on experience (starts at 0 if no experience) local currentLevel = ProficiencyData:getCurrentLevelByExp(displayItem, experience, false, thingType, marketData) -- Check if mastery is achieved local maxExperience = ProficiencyData:getMaxExperience(#levels, displayItem, thingType, marketData) local masteryAchieved = experience >= maxExperience -- Update perk columns for each level for i, levelData in ipairs(levels) do local perkColumn = self.perkPanel:getChildById('perkColumn_' .. i) local starWidget = self.starProgressPanel:getChildById('starWidget' .. i) if perkColumn and levelData then self:updatePerkColumn(perkColumn, levelData, i, currentLevel, perks, experience, displayItem, masteryAchieved, thingType, marketData) end -- Update star widget if starWidget then self:updateStarWidget(starWidget, i, currentLevel, experience, displayItem, masteryAchieved, thingType, marketData) end end -- Update bonus detail panels self:updateBonusDetails(proficiencyContent, perks) -- Update item frame/addons based on level self:updateItemAddons(experience, displayItem, masteryAchieved, thingType, marketData) end -- Update a single star widget - matching RTC implementation function WeaponProficiency:updateStarWidget(starWidget, levelIndex, currentLevel, experience, displayItem, masteryAchieved, thingType, marketData) if not starWidget then return end local starProgress = starWidget:getChildById('starProgress') local starIcon = starWidget:getChildById('star') if starProgress then -- Calculate percent for this level (same as perk column) local percent = ProficiencyData:getLevelPercent(experience or 0, levelIndex, displayItem, thingType, marketData) starProgress:setPercent(percent) -- Set tooltip with experience info local maxLevelExp = ProficiencyData:getMaxExperienceByLevel(levelIndex, displayItem, thingType, marketData) starProgress:setTooltip(string.format("%s / %s", comma_value(experience or 0), comma_value(maxLevelExp or 0))) end -- Update star icon color based on completion (100% = complete) if starIcon then local percent = ProficiencyData:getLevelPercent(experience or 0, levelIndex, displayItem, thingType, marketData) if percent >= 100 then -- Level complete - show gold or silver star local iconType = masteryAchieved and "gold" or "silver" starIcon:setImageSource(proficiencyImage('icon-star-tiny-' .. iconType)) else -- Level not complete - show dark star starIcon:setImageSource(proficiencyImage('icon-star-dark')) end end end -- Update item frame/addons based on weapon level function WeaponProficiency:updateItemAddons(currentExp, displayItem, masteryAchieved, thingType, marketData) if not self.window then return end if not displayItem then return end local weaponLevel = math.min(7, ProficiencyData:getCurrentLevelByExp(displayItem, currentExp, false, thingType, marketData) or 0) local iconLevelWidget = self.window:recursiveGetChildById("iconMasteryLevel") local weaponLevelWidget = self.window:recursiveGetChildById("itemMasteryLevel") if iconLevelWidget then iconLevelWidget:setImageSource(proficiencyImage("icon-masterylevel-" .. weaponLevel)) end if weaponLevelWidget then weaponLevelWidget:setVisible(weaponLevel > 0) if weaponLevel > 0 then local color = masteryAchieved and "gold" or "silver" weaponLevelWidget:setImageSource(proficiencyImage( string.format("icon-masterylevel-%d-%s", weaponLevel, color))) end end end -- Update a single perk column function WeaponProficiency:updatePerkColumn(perkColumn, levelData, levelIndex, currentLevel, selectedPerks, experience, displayItem, masteryAchieved, thingType, marketData) if not perkColumn or not levelData then return end local perksData = levelData.Perks or {} local perkCount = #perksData -- Show appropriate panel based on perk count local oneBonusPanel = perkColumn:getChildById('oneBonusIconPanel') local twoBonusPanel = perkColumn:getChildById('twoBonusIconPanel') local threeBonusPanel = perkColumn:getChildById('threeBonusIconPanel') if oneBonusPanel then oneBonusPanel:setVisible(perkCount == 1) end if twoBonusPanel then twoBonusPanel:setVisible(perkCount == 2) end if threeBonusPanel then threeBonusPanel:setVisible(perkCount == 3) end -- Determine which panel to use local activePanel = nil if perkCount == 1 and oneBonusPanel then activePanel = oneBonusPanel elseif perkCount == 2 and twoBonusPanel then activePanel = twoBonusPanel elseif perkCount == 3 and threeBonusPanel then activePanel = threeBonusPanel end if not activePanel then return end -- Store currentPerkPanel reference for later use perkColumn.currentPerkPanel = activePanel -- Level is unlocked if we have enough experience local isLevelUnlocked = levelIndex <= currentLevel -- Update progress bar for this column. local progressBar = perkColumn:getChildById('bonusSelectProgress') if progressBar then local percent = ProficiencyData:getLevelPercent(experience or 0, levelIndex, displayItem, thingType, marketData) progressBar:setPercent(percent) -- Unlock perks if this level is complete (100%) if percent >= 100 then for _, widget in pairs(activePanel:getChildren()) do if widget.blocked then widget.blocked = false end end end end -- Update each perk icon for perkIndex, perkData in ipairs(perksData) do local bonusIcon = activePanel:getChildById('bonusIcon' .. (perkIndex - 1)) if bonusIcon then -- Get image source and clip local imagePath, imageClip = ProficiencyData:getImageSourceAndClip(perkData) local iconWidget = bonusIcon:getChildById('icon') local iconGreyWidget = bonusIcon:getChildById('icon-grey') local lockedWidget = bonusIcon:getChildById('locked-perk') local borderWidget = bonusIcon:getChildById('border') local highlightWidget = bonusIcon:getChildById('highlight') -- Check if this perk is selected local isSelected = false if selectedPerks and type(selectedPerks) == "table" then -- Format 1: Array format from server/cache: {{level, perkPos}, ...} (1-indexed) if #selectedPerks > 0 and type(selectedPerks[1]) == "table" then for _, perk in ipairs(selectedPerks) do if type(perk) == "table" and #perk >= 2 and perk[1] == levelIndex and perk[2] == perkIndex then isSelected = true break end end -- Format 2: Indexed format from pendingSelections: {[levelIndex] = perkIndex} (1-indexed) elseif selectedPerks[levelIndex] ~= nil then isSelected = (selectedPerks[levelIndex] == perkIndex) end end -- Set icon images local clipX, clipY = imageClip:match("(%d+)%s+(%d+)") clipX = tonumber(clipX) or 0 clipY = tonumber(clipY) or 0 if iconWidget then iconWidget:setImageSource(imagePath) iconWidget:setImageClip({ x = clipX, y = clipY, width = 64, height = 64 }) end -- Handle grey icon - spell augments use separate -off image, others use Y+64 offset if iconGreyWidget then if perkData.Type == PERK_SPELL_AUGMENT then -- Spell augments have a separate -off image source iconGreyWidget:setImageSource(imagePath .. "-off") iconGreyWidget:setImageClip({ x = clipX, y = clipY, width = 64, height = 64 }) else -- Other perks use Y+64 for grey version iconGreyWidget:setImageSource(imagePath) iconGreyWidget:setImageClip({ x = clipX, y = clipY + 64, width = 64, height = 64 }) end end -- Handle augment overlay icons for spell augments local iconPerks = bonusIcon:getChildById('iconPerks') local iconPerksGrey = bonusIcon:getChildById('iconPerks-grey') if perkData.Type == PERK_SPELL_AUGMENT and perkData.AugmentType then local augmentClip = ProficiencyData:getAugmentIconClip(perkData) local augX = tonumber(augmentClip:match("(%d+)")) or 0 if iconPerks then iconPerks:setVisible(true) iconPerks:setImageClip({ x = augX, y = 0, width = 32, height = 32 }) end if iconPerksGrey then iconPerksGrey:setVisible(true) iconPerksGrey:setImageClip({ x = augX, y = 32, width = 32, height = 32 }) end else if iconPerks then iconPerks:setVisible(false) end if iconPerksGrey then iconPerksGrey:setVisible(false) end end -- Show/hide based on unlock state and selection local showColorIcon = isLevelUnlocked and isSelected local showGreyIcon = not isLevelUnlocked or (isLevelUnlocked and not isSelected) if iconWidget then iconWidget:setVisible(showColorIcon) end if iconGreyWidget then iconGreyWidget:setVisible(showGreyIcon) iconGreyWidget:setOpacity(1.0) end -- Handle augment overlay icons visibility for spell augments local iconPerks = bonusIcon:getChildById('iconPerks') local iconPerksGrey = bonusIcon:getChildById('iconPerks-grey') if perkData.Type == PERK_SPELL_AUGMENT then if iconPerks then iconPerks:setVisible(showColorIcon) end if iconPerksGrey then iconPerksGrey:setVisible(showGreyIcon) iconPerksGrey:setOpacity(1.0) end end -- Show locked icon if level not reached if lockedWidget then lockedWidget:setVisible(not isLevelUnlocked) end -- Update border based on state if borderWidget then if isSelected and isLevelUnlocked then borderWidget:setImageSource(proficiencyImage('border-weaponmasterytreeicons-active')) else borderWidget:setImageSource(proficiencyImage('border-weaponmasterytreeicons-inactive')) end end -- Highlight selected perk if highlightWidget then highlightWidget:setVisible(isSelected and isLevelUnlocked) end -- Set tooltip local bonusName, bonusTooltip = ProficiencyData:getBonusNameAndTooltip(perkData) bonusIcon:setTooltip(string.format("%s\n\n%s", bonusName, bonusTooltip)) -- Store perk data for later use bonusIcon.perkData = perkData bonusIcon.blocked = not isLevelUnlocked bonusIcon.locked = false bonusIcon.active = isSelected bonusIcon.levelIndex = levelIndex bonusIcon.perkIndex = perkIndex -- Add click handler for selecting perk bonusIcon.onClick = function(widget) if widget.blocked then return end WeaponProficiency:onPerkClick(widget) end end end end -- Handle perk icon click function WeaponProficiency:onPerkClick(bonusIcon) if not bonusIcon then return end local levelIndex = bonusIcon.levelIndex local perkIndex = bonusIcon.perkIndex -- Initialize pending selections if not exists if not self.pendingSelections then self.pendingSelections = {} end -- Get currently saved perk for this level from cache local savedPerk = nil if self.selectedItemId and self.cacheList[self.selectedItemId] then local cachedPerks = self.cacheList[self.selectedItemId].perks or {} for _, perk in ipairs(cachedPerks) do if type(perk) == "table" and perk[1] == levelIndex then savedPerk = perk[2] break end end end -- Determine if this click changes from saved state local currentSelection = self.pendingSelections[levelIndex] or savedPerk if currentSelection == perkIndex then -- Clicking on already selected perk - check if we should deselect or revert to saved if savedPerk == perkIndex then -- This is the saved perk, clicking it again does nothing (can't deselect saved perks) else -- This was a pending selection, deselect it (revert to saved or none) self.pendingSelections[levelIndex] = nil end else -- Selecting a different perk for this level if perkIndex == savedPerk then -- Reverting to saved perk - remove from pending self.pendingSelections[levelIndex] = nil else -- New selection different from saved self.pendingSelections[levelIndex] = perkIndex end end -- Update visual state for all perks in this level column self:updatePerkVisualState(levelIndex) -- Update button states self:updateApplyButtonState() end -- Update visual state for perks in a level column function WeaponProficiency:updatePerkVisualState(levelIndex) local perkColumn = self.perkPanel:getChildById('perkColumn_' .. levelIndex) if not perkColumn or not perkColumn.currentPerkPanel then return end -- Get pending selection first, then fall back to cached perk local selectedPerkIndex = self.pendingSelections and self.pendingSelections[levelIndex] -- If no pending selection, check cached perks if not selectedPerkIndex and self.selectedItemId and self.cacheList[self.selectedItemId] then local cachedPerks = self.cacheList[self.selectedItemId].perks or {} for _, perk in ipairs(cachedPerks) do if type(perk) == "table" and perk[1] == levelIndex then selectedPerkIndex = perk[2] break end end end -- Update each perk icon in this column for perkIdx = 0, 2 do local bonusIcon = perkColumn.currentPerkPanel:getChildById('bonusIcon' .. perkIdx) if bonusIcon then local isSelected = (selectedPerkIndex == (perkIdx + 1)) local isLevelUnlocked = not bonusIcon.blocked local iconWidget = bonusIcon:getChildById('icon') local iconGreyWidget = bonusIcon:getChildById('icon-grey') local borderWidget = bonusIcon:getChildById('border') local highlightWidget = bonusIcon:getChildById('highlight') -- Show color icon only if unlocked AND selected if iconWidget then iconWidget:setVisible(isLevelUnlocked and isSelected) end if iconGreyWidget then iconGreyWidget:setVisible(not isSelected or not isLevelUnlocked) end -- Update border if borderWidget then if isSelected and isLevelUnlocked then borderWidget:setImageSource(proficiencyImage('border-weaponmasterytreeicons-active')) else borderWidget:setImageSource(proficiencyImage('border-weaponmasterytreeicons-inactive')) end end -- Highlight selected if highlightWidget then highlightWidget:setVisible(isSelected and isLevelUnlocked) end bonusIcon.active = isSelected end end -- Update bonus detail panel self:updateBonusDetailForLevel(levelIndex) end -- Update bonus detail panel for a specific level function WeaponProficiency:updateBonusDetailForLevel(levelIndex) local bonusDetailPanel = self.window:recursiveGetChildById('bonusDetailBackground') if not bonusDetailPanel then return end local detailPanel = bonusDetailPanel:getChildById('bonusDetail_' .. levelIndex) if not detailPanel then return end local bonusNameWidget = detailPanel:getChildById('bonusName') if not bonusNameWidget then return end local selectedPerkIndex = self.pendingSelections and self.pendingSelections[levelIndex] if selectedPerkIndex then -- Get perk data from the perk column local perkColumn = self.perkPanel:getChildById('perkColumn_' .. levelIndex) if perkColumn and perkColumn.currentPerkPanel then local bonusIcon = perkColumn.currentPerkPanel:getChildById('bonusIcon' .. (selectedPerkIndex - 1)) if bonusIcon and bonusIcon.perkData then local _, tooltip = ProficiencyData:getBonusNameAndTooltip(bonusIcon.perkData) bonusNameWidget:setText(tooltip) bonusNameWidget:setTooltip(tooltip) bonusNameWidget:setImageSource("") return end end end -- No selection - show lock icon bonusNameWidget:setText("") bonusNameWidget:setImageSource(proficiencyImage("icon-lock-grey")) end -- Update bonus detail panels at the bottom function WeaponProficiency:updateBonusDetails(proficiencyContent, selectedPerks) local bonusDetailPanel = self.window:recursiveGetChildById('bonusDetailBackground') if not bonusDetailPanel then return end local levels = proficiencyContent.Levels or {} for i = 1, #levels do local detailPanel = bonusDetailPanel:getChildById('bonusDetail_' .. i) if detailPanel then local bonusNameWidget = detailPanel:getChildById('bonusName') local levelData = levels[i] if bonusNameWidget and levelData then -- Find selected perk for this level local selectedPerkIndex = nil if selectedPerks and type(selectedPerks) == "table" then -- Check array format first: {{level, perkPos}, ...} (from cache/server) local isArrayFormat = false if #selectedPerks > 0 and type(selectedPerks[1]) == "table" then isArrayFormat = true for _, perk in ipairs(selectedPerks) do if type(perk) == "table" and #perk >= 2 and perk[1] == i then selectedPerkIndex = perk[2] -- Already 1-indexed from cache break end end end -- Check indexed format: {[levelIndex] = perkIndex} (from pendingSelections) if not isArrayFormat and not selectedPerkIndex then local key = i -- pendingSelections uses 1-indexed level if selectedPerks[key] ~= nil then local value = selectedPerks[key] if type(value) == "number" then selectedPerkIndex = value -- Already 1-indexed from pendingSelections end end end end if selectedPerkIndex then local perksData = levelData.Perks or {} local perkData = perksData[selectedPerkIndex] if perkData then local _, tooltip = ProficiencyData:getBonusNameAndTooltip(perkData) bonusNameWidget:setText(tooltip) bonusNameWidget:setTooltip(tooltip) bonusNameWidget:setImageSource("") -- Hide lock icon else bonusNameWidget:setText("") bonusNameWidget:setImageSource(proficiencyImage("icon-lock-grey")) end else bonusNameWidget:setText("") bonusNameWidget:setImageSource(proficiencyImage("icon-lock-grey")) end elseif bonusNameWidget then bonusNameWidget:setText("") bonusNameWidget:setImageSource(proficiencyImage("icon-lock-grey")) end end end end -- Handle item box click in the list function WeaponProficiency:onItemBoxClick(widget) if not widget then return end local itemWidget = widget:getChildById('item') if not itemWidget then return end local itemId = itemWidget:getItemId() if not itemId or itemId == 0 then return end -- Find the market item data local categoryDropdown = self.window:recursiveGetChildById('classFilter') local currentCategory = MarketCategory.WeaponsAll if categoryDropdown then local selectedText = categoryDropdown:getText() currentCategory = WeaponStringToCategory[selectedText] or MarketCategory.WeaponsAll end local items = self.itemList[currentCategory] or {} local marketItem = nil for _, item in ipairs(items) do if item.displayItem and item.displayItem:getId() == itemId then marketItem = item break end end if marketItem then self:selectItem(itemId, marketItem) end end -- Apply filter for Level button function WeaponProficiency:applyLevelFilter(items) if not self.filters["levelButton"] then return items end local player = g_game.getLocalPlayer() if not player then return items end local playerLevel = player:getLevel() local filteredItems = {} for _, item in ipairs(items) do local requiredLevel = getNumericCall(item.thingType, "getMinimumLevel") if requiredLevel == 0 then requiredLevel = item.marketData.requiredLevel or 0 end if playerLevel >= requiredLevel then table.insert(filteredItems, item) end end return filteredItems end -- Apply filter for Vocation button function WeaponProficiency:applyVocationFilter(items) if not self.filters["vocButton"] then return items end local playerVocation = getPlayerWheelVocation() local filteredItems = {} for _, item in ipairs(items) do local restrictVocation = item.marketData and item.marketData.restrictVocation or 0 local category = item.marketData and item.marketData.category or nil if restrictVocation and restrictVocation ~= 0 then if vocationRestrictionMatches(restrictVocation, playerVocation) then table.insert(filteredItems, item) end elseif categoryMatchesPlayerVocation(category) then table.insert(filteredItems, item) end end return filteredItems end -- Apply filter for 1H (one-handed) weapons function WeaponProficiency:applyOneHandedFilter(items) if not self.filters["oneButton"] then return items end local filteredItems = {} for _, item in ipairs(items) do local thingType = item.thingType if thingType then local slotType = thingType:getClothSlot() or 0 if slotType == 6 then table.insert(filteredItems, item) end end end return filteredItems end -- Apply filter for 2H (two-handed) weapons function WeaponProficiency:applyTwoHandedFilter(items) if not self.filters["twoButton"] then return items end local filteredItems = {} for _, item in ipairs(items) do local thingType = item.thingType if thingType then local slotType = thingType:getClothSlot() or 0 if slotType == 0 then table.insert(filteredItems, item) end end end return filteredItems end -- Apply button click handler function WeaponProficiency:onApplyClick() local success, err = pcall(function() self:applyPendingSelections() end) if not success then -- Log error with context before clearing warn("Failed to apply pending selections: " .. tostring(err)) -- Clear pending selections on error to allow closing self.pendingSelections = {} self:updateApplyButtonState() end end -- Ok button click handler function WeaponProficiency:onOkClick() -- Apply pending selections if any if self.pendingSelections and next(self.pendingSelections) ~= nil then local success, err = pcall(function() self:applyPendingSelections() end) if not success then -- Log error with context before clearing warn("Failed to apply pending selections in onOkClick: " .. tostring(err)) -- Clear on error to allow closing self.pendingSelections = {} end end -- Always close window, even if there was an error hide() end -- Reset button click handler function WeaponProficiency:onResetClick() self.pendingSelections = {} -- Send empty perks list to server to clear all perks -- This is more reliable than using action type 2 (reset) if self.selectedItemId then -- Send empty arrays to clear all perks g_game.sendWeaponProficiencyApply(self.selectedItemId, {}, {}) end -- Clear local cache and refresh display if self.selectedItemId and self.selectedMarketItem then local displayItem = self.selectedMarketItem.displayItem local cacheData = self.cacheList[self.selectedItemId] if displayItem and cacheData then -- Clear cached perks since we reset them cacheData.perks = {} self:displayPerks(self.selectedItemId, cacheData.perks, displayItem) end end self:updateApplyButtonState() end -- Apply pending perk selections to server function WeaponProficiency:applyPendingSelections() if not self.selectedItemId then return end -- Build complete perk selection list for server -- Start with cached perks (already saved on server) local allPerks = {} -- {[levelIndex] = perkIndex} in 1-indexed format -- First, load existing cached perks if self.cacheList[self.selectedItemId] and self.cacheList[self.selectedItemId].perks then for _, perk in ipairs(self.cacheList[self.selectedItemId].perks) do if type(perk) == "table" and #perk >= 2 then allPerks[perk[1]] = perk[2] -- level -> perkIndex (1-indexed) end end end -- Then, merge with pending selections (these override cached perks) if self.pendingSelections then for levelIndex, perkIndex in pairs(self.pendingSelections) do allPerks[levelIndex] = perkIndex -- level -> perkIndex (1-indexed) end end -- Convert to array format for sending: {level (0-indexed), perkPosition (0-indexed)} local selections = {} for levelIndex, perkIndex in pairs(allPerks) do table.insert(selections, {levelIndex - 1, perkIndex - 1}) end -- Sort by level for consistency table.sort(selections, function(a, b) return a[1] < b[1] end) if #selections == 0 then return end -- Build two parallel arrays for C++ (levels and perkPositions) local levels = {} local perkPositions = {} -- Log selection details (0-indexed in Lua, will be converted to 1-indexed in C++) for i, sel in ipairs(selections) do table.insert(levels, sel[1]) table.insert(perkPositions, sel[2]) end -- Send to server using the protocol function with two parallel arrays -- g_game.sendWeaponProficiencyApply(itemId, levelsArray, perkPositionsArray) g_game.sendWeaponProficiencyApply(self.selectedItemId, levels, perkPositions) -- Update cache with ALL applied perks (convert back to server format: 1-indexed) -- This includes both cached perks and new pending selections if self.cacheList[self.selectedItemId] then local appliedPerks = {} for _, sel in ipairs(selections) do table.insert(appliedPerks, {sel[1] + 1, sel[2] + 1}) -- Convert back to 1-indexed for cache end self.cacheList[self.selectedItemId].perks = appliedPerks -- Clear pendingSelections - perks are now saved in cache, no longer "pending" self.pendingSelections = {} -- Update UI immediately with applied perks (using cache format) -- This keeps the visual selection active if self.selectedMarketItem and self.selectedMarketItem.displayItem then self:displayPerks(self.selectedItemId, appliedPerks, self.selectedMarketItem.displayItem) end -- Update button states (Apply should be disabled since we just applied) self:updateApplyButtonState() -- Clear the hasUnusedPerk flag and hide highlight after applying -- The user has now used their perks, so no notification needed self.hasUnusedPerk = false updateProficiencyHighlight() -- Request updated proficiency info from server to confirm scheduleEvent(function() if self.selectedItemId then g_game.sendWeaponProficiencyAction(0, self.selectedItemId) end end, 200) end -- Pending selections already cleared above end -- Update Apply/Ok/Reset button enabled state based on pending selections function WeaponProficiency:updateApplyButtonState() if not self.window then return end local applyBtn = self.window:getChildById('apply') local okBtn = self.window:getChildById('ok') local resetBtn = self.window:getChildById('reset') local hasPendingSelections = self.pendingSelections and next(self.pendingSelections) ~= nil -- Check if there are applied perks in cache local hasAppliedPerks = false if self.selectedItemId and self.cacheList[self.selectedItemId] then local cachedPerks = self.cacheList[self.selectedItemId].perks hasAppliedPerks = cachedPerks and #cachedPerks > 0 end -- Apply/Ok: enabled when there are pending selections (changes to apply) if applyBtn then applyBtn:setEnabled(hasPendingSelections) end if okBtn then okBtn:setEnabled(true) -- Always enabled to allow closing end -- Reset: enabled when there are applied perks if resetBtn then resetBtn:setEnabled(hasAppliedPerks) end end