Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions spec/System/TestGemSelectControl_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
describe("TestGemSelectControl", function()
before_each(function()
newBuild()
build.skillsTab:PasteSocketGroup("Fireball 20/0 1")
runCallback("OnFrame")
end)

local function getGemSelect()
return build.skillsTab.gemSlots[1].nameSpec
end

local function selectTemporaryGem(control, gemName)
control:OnFocusGained()
control.buf = gemName
control:BuildList(control.buf)
control.selIndex = 1
control:UpdateGem(false, false, false)
assert.are.equal(gemName, build.skillsTab.displayGroup.gemList[1].nameSpec)
end

it("queues DPS sorting with PoE2's fast calculation options", function()
local control = getGemSelect()
assert.is_false(control.dpsBuildFlag)

control:OnFocusGained()

assert.is_true(control.dpsBuildFlag)
assert.is_table(control.sortCache.pendingGems)
assert.same({
nodeAlloc = true,
requirementsItems = true,
requirementsGems = true,
skipEHP = true,
fullDPSOnly = false,
}, control.sortCache.fastCalcOptions)
end)

it("keeps the current gem selected when the DPS list is resorted", function()
local control = getGemSelect()
control:OnFocusGained()
control.buf = control.buf:lower()
control.selIndex = 0

control:SortCurrentList()

local selectedGem = control.gems[control.list[control.selIndex]]
assert.is_not_nil(selectedGem)
assert.are.equal("fireball", selectedGem.name:lower())
end)

it("waits for the hover selection to settle before calculating its tooltip", function()
local control = getGemSelect()
control.hoverSel = 2

assert.is_false(control:IsHoverSelectionReady())
assert.is_false(control:IsHoverSelectionReady())
assert.is_true(control:IsHoverSelectionReady())

control.hoverSel = 3
assert.is_false(control:IsHoverSelectionReady())
control.hoverSel = nil
assert.is_false(control:IsHoverSelectionReady())
assert.is_nil(control.lastHoverSel)
assert.are.equal(0, control.hoverFrameCount)
end)

it("restores the existing gem when selection is cancelled with Escape", function()
local control = getGemSelect()
selectTemporaryGem(control, "Spark")

control:OnKeyDown("ESCAPE")

assert.are.equal("Fireball", control.buf)
assert.are.equal("Fireball", build.skillsTab.displayGroup.gemList[1].nameSpec)
end)

it("restores the existing gem when the control loses focus", function()
local control = getGemSelect()
selectTemporaryGem(control, "Spark")

control:OnFocusLost()

assert.are.equal("Fireball", control.buf)
assert.are.equal("Fireball", build.skillsTab.displayGroup.gemList[1].nameSpec)
end)

it("restores the existing gem when clicking outside the dropdown", function()
local control = getGemSelect()
selectTemporaryGem(control, "Spark")
control.IsMouseOver = function()
return false
end

control:OnKeyDown("LEFTBUTTON")

assert.are.equal("Fireball", control.buf)
assert.are.equal("Fireball", build.skillsTab.displayGroup.gemList[1].nameSpec)
end)
end)
181 changes: 138 additions & 43 deletions src/Classes/GemSelectControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function GemSelectClass:GemSelectControl(anchor, rect, skillsTab, index, changeF
self.forceTooltip = forceTooltip
self.list = { }
self.mode = ""
self.dpsBuildFlag = false
self.changeFunc = function()
if not self.dropped then
self.dropped = true
Expand Down Expand Up @@ -149,11 +150,12 @@ function GemSelectClass:BuildList(buf)
t_remove(tagsList, 1)

-- Search for gem name using increasingly broad search patterns
local lowerSearch = searchTerm:lower()
local patternList = {
"^ " .. searchTerm:lower().."$", -- Exact match
"^" .. searchTerm:lower():gsub("%a", " %0%%l+") .. "$", -- Simple abbreviation ("CtF" -> "Cold to Fire")
"^ " .. searchTerm:lower(), -- Starts with
searchTerm:lower(), -- Contains
"^ " .. lowerSearch.."$", -- Exact match
"^" .. lowerSearch:gsub("%a", " %0%%l+") .. "$", -- Simple abbreviation ("CtF" -> "Cold to Fire")
"^ " .. lowerSearch, -- Starts with
lowerSearch, -- Contains
}
for i, pattern in ipairs(patternList) do
local matchList = { }
Expand Down Expand Up @@ -327,44 +329,106 @@ function GemSelectClass:UpdateSortCache()
-- Check for nil because some fields may not be populated, default to 0
local baseDPS = (dpsField == "FullDPS" and calcBase[dpsField] ~= nil and calcBase[dpsField]) or (calcBase.Minion and calcBase.Minion.CombinedDPS) or (calcBase[dpsField] ~= nil and calcBase[dpsField]) or 0

sortCache.calcFunc = calcFunc
sortCache.useFullDPS = useFullDPS
sortCache.fastCalcOptions = fastCalcOptions
sortCache.baseDPS = baseDPS
sortCache.dpsField = dpsField
sortCache.pendingGems = { }

for gemId, gemData in pairs(self.gems) do
sortCache.dps[gemId] = baseDPS
-- Ignore gems that don't support the active skill
-- Gems that support the active skill or have global effects need DPS calc
if sortCache.canSupport[gemId] or (gemData.grantedEffect.hasGlobalEffect and not gemData.grantedEffect.support) then
local output = self:CalcOutputWithThisGem(calcFunc, gemData, useFullDPS, fastCalcOptions)
-- Check for nil because some fields may not be populated, default to 0
sortCache.dps[gemId] = (dpsField == "FullDPS" and output[dpsField] ~= nil and output[dpsField]) or (output.Minion and output.Minion.CombinedDPS) or (output[dpsField] ~= nil and output[dpsField]) or 0
end
-- Color based on the DPS
if sortCache.dps[gemId] > baseDPS then
sortCache.dpsColor[gemId] = "^x228866"
elseif sortCache.dps[gemId] < baseDPS then
sortCache.dpsColor[gemId] = "^xFF4422"
else
sortCache.dpsColor[gemId] = "^xFFFF66"
t_insert(sortCache.pendingGems, gemId)
end
-- Neutral color until DPS is computed
sortCache.dpsColor[gemId] = ""
end

--ConPrintf("Gem Selector time: %d ms", GetTime() - start)
self.dpsBuildFlag = true
end

function GemSelectClass:SortGemList(gemList)
local sortCache = self.sortCache
local gems = self.gems
-- cache names to avoid repeated table lookups in comparator
local names = {}
for _, gemId in ipairs(gemList) do
local gem = gems[gemId]
names[gemId] = gem and gem.name or gemId
end
t_sort(gemList, function(a, b)
if sortCache.canSupport[a] == sortCache.canSupport[b] then
if self.skillsTab.sortGemsByDPS and sortCache.dps[a] ~= sortCache.dps[b] then
return sortCache.dps[a] > sortCache.dps[b]
else
local nameA = (self.gems[a] and self.gems[a].name) or a
local nameB = (self.gems[b] and self.gems[b].name) or b
return nameA < nameB
return names[a] < names[b]
end
else
return sortCache.canSupport[a]
end
end)
end

function GemSelectClass:SyncSelection()
self.selIndex = 0
for index, gemId in ipairs(self.list) do
if self.gems[gemId] and self.gems[gemId].name:lower() == self.buf:lower() then
self.selIndex = index
self:ScrollSelIntoView()
break
end
end
end

function GemSelectClass:SortCurrentList()
if #self.searchStr == 0 then
self:SortGemList(self.list)
self:SyncSelection()
end
end

function GemSelectClass:DPSBuilder()
local sortCache = self.sortCache
if not sortCache or not sortCache.pendingGems then return end

local pending = sortCache.pendingGems
local calcFunc = sortCache.calcFunc
local useFullDPS = sortCache.useFullDPS
local fastCalcOptions = sortCache.fastCalcOptions
local baseDPS = sortCache.baseDPS
local dpsField = sortCache.dpsField
local start = GetTime()

for index, gemId in ipairs(pending) do
local gemData = self.gems[gemId]
if gemData then
local output = self:CalcOutputWithThisGem(calcFunc, gemData, useFullDPS, fastCalcOptions)
sortCache.dps[gemId] = (dpsField == "FullDPS" and output[dpsField] ~= nil and output[dpsField]) or (output.Minion and output.Minion.CombinedDPS) or (output[dpsField] ~= nil and output[dpsField]) or 0
if sortCache.dps[gemId] > baseDPS then
sortCache.dpsColor[gemId] = "^x228866"
elseif sortCache.dps[gemId] < baseDPS then
sortCache.dpsColor[gemId] = "^xFF4422"
else
sortCache.dpsColor[gemId] = "^xFFFF66"
end
end
local now = GetTime()
if now - start > 50 then
self:SortCurrentList()
if self.dpsBuilderCallback then
self.dpsBuilderCallback(m_floor(index/#pending*100))
end
coroutine.yield()
start = now
end
end

self:SortCurrentList()
sortCache.pendingGems = nil
end

function GemSelectClass:UpdateGem(setText, addUndo, focusLost)
local gemId = self.list[m_max(self.selIndex, 1)]
-- don't process unless the buffer equals an actual gem, whether typed, clicked, or navigated with arrows
Expand Down Expand Up @@ -414,7 +478,40 @@ function GemSelectClass:IsMouseOver()
return mOver, mOverComp
end

function GemSelectClass:IsHoverSelectionReady()
if not self.hoverSel then
self.lastHoverSel = nil
self.hoverFrameCount = 0
return false
end
if self.hoverSel == self.lastHoverSel then
self.hoverFrameCount = (self.hoverFrameCount or 0) + 1
else
self.lastHoverSel = self.hoverSel
self.hoverFrameCount = 0
end
return self.hoverFrameCount >= 2
end

function GemSelectClass:Draw(viewPort, noTooltip)
self.sortPercentage = self.sortPercentage or ""
if self.dpsBuildFlag then
self.dpsBuildFlag = false
self.dpsBuilder = coroutine.create(self.DPSBuilder)
self.dpsBuilderCallback = function(percentage)
self.sortPercentage = ("%d%%"):format(percentage)
end
end
if self.dpsBuilder then
local res, errMsg = coroutine.resume(self.dpsBuilder, self)
if launch.devMode and not res then
error(errMsg)
end
if coroutine.status(self.dpsBuilder) == "dead" then
self.dpsBuilder = nil
end
end

self.EditControl:Draw(viewPort, noTooltip and not self.forceTooltip)
local x, y = self:GetPos()
local width, height = self:GetSize()
Expand All @@ -433,6 +530,10 @@ function GemSelectClass:Draw(viewPort, noTooltip)
end
if self.dropped then
SetDrawLayer(nil, 5)
if self.dpsBuilder then
SetDrawColor(0.75, 0.75, 0.75)
DrawString(x + width - 4, y, "RIGHT_X", height - 2, "VAR", "Sorting " .. self.sortPercentage)
end
local cursorX, cursorY = GetCursorPos()
self.hoverSel = mOverComp == "DROP" and math.floor((cursorY - y - height + scrollBar.offset) / (height - 4)) + 1
if self.hoverSel and not self.gems[self.list[self.hoverSel]] then
Expand Down Expand Up @@ -462,18 +563,18 @@ function GemSelectClass:Draw(viewPort, noTooltip)
local gemText = gemData and gemData.name or "<No matches>"
DrawString(0, y, "LEFT", height - 4, "VAR", gemText)
if gemData then
if gemData.grantedEffect.support and self.sortCache.canSupport[gemId] then
if gemData.grantedEffect.support and self.sortCache.canSupport[gemId] and self.sortCache.dpsColor[gemId] ~= "" then
SetDrawColor(self.sortCache.dpsColor[gemId])
main:DrawCheckMark(width - 4 - height / 2 - (scrollBar.enabled and 18 or 0), y + (height - 4) / 2, (height - 4) * 0.8)
elseif gemData.grantedEffect.hasGlobalEffect then
elseif gemData.grantedEffect.hasGlobalEffect and self.sortCache.dpsColor[gemId] ~= "" then
SetDrawColor(self.sortCache.dpsColor[gemId])
DrawString(width - 4 - height / 2 - (scrollBar.enabled and 18 or 0), y - 2, "CENTER_X", height, "VAR", "+")
end
end
end
SetViewport()
self:DrawControls(viewPort, (noTooltip and not self.forceTooltip) and self)
if self.hoverSel then
if self:IsHoverSelectionReady() then
local calcFunc, calcBase = self.skillsTab.build.calcsTab:GetMiscCalculator(self.build)
if calcFunc then
self.tooltip.maxWidth = 500
Expand Down Expand Up @@ -508,6 +609,8 @@ function GemSelectClass:Draw(viewPort, noTooltip)
end
SetDrawLayer(nil, 0)
else
self.lastHoverSel = nil
self.hoverFrameCount = 0
-- not dropped
local hoverControl
if self.skillsTab.selControl and self.skillsTab.selControl._className == "GemSelectControl" then
Expand Down Expand Up @@ -584,27 +687,23 @@ end
function GemSelectClass:OnFocusGained()
self.EditControl:OnFocusGained()
self.dropped = true
self.selIndex = 0
self:UpdateSortCache()
self:BuildList("")
for index, gemId in pairs(self.list) do
if self.gems[gemId].name == self.buf then
self.selIndex = index
self:ScrollSelIntoView()
break
end
end
self:SyncSelection()
self.initialBuf = self.buf
self.initialIndex = self.selIndex
end

function GemSelectClass:CancelSelection()
self.dropped = false
self.buf = self.initialBuf
self:BuildList("")
self:SyncSelection()
self:UpdateGem(false, true, true)
end

function GemSelectClass:OnFocusLost()
if self.dropped then
self.dropped = false
if self.noMatches then
self:SetText("")
end
self:UpdateGem(true, true, true)
self:CancelSelection()
end
end

Expand Down Expand Up @@ -638,6 +737,7 @@ function GemSelectClass:OnKeyDown(key, doubleClick)
end
if self.dropped then
if key:match("BUTTON") and not self:IsMouseOver() then
self:CancelSelection()
return
end
if key == "LEFTBUTTON" then
Expand All @@ -660,11 +760,7 @@ function GemSelectClass:OnKeyDown(key, doubleClick)
self:UpdateGem(true, true, true)
return
elseif key == "ESCAPE" then
self.dropped = false
self:BuildList("")
self.buf = self.initialBuf
self.selIndex = self.initialIndex
self:UpdateGem(false,true, true)
self:CancelSelection()
return
elseif self.controls.scrollBar:IsScrollUpKey(key) then
self.controls.scrollBar:Scroll(-1)
Expand Down Expand Up @@ -704,7 +800,6 @@ function GemSelectClass:OnKeyDown(key, doubleClick)
elseif key == "RETURN" or key == "RIGHTBUTTON" then
self.dropped = true
self:UpdateSortCache()
self.initialIndex = self.selIndex
self.initialBuf = self.buf
return self
end
Expand Down
Loading