-- Smart Pickup — fanerne Lagre, vareoversigten (v1.52) -- Én række pr. vare med samlet beholdning, fyldningsbjælke, værdi og gårdens netto; klik folder varen ud -- til bygningerne den ligger i. Øverst nøgletal og "kræver handling". Tallene regnes i ADStorageStats; -- her tegnes kun. Indlæses efter SmartPickupStorageTab.lua (funktionerne lægges på SmartPickupFrame). SmartPickupFrame.STORAGE_VIEW_ITEMS, SmartPickupFrame.STORAGE_VIEW_BUILDINGS = 1, 2 SmartPickupFrame.MAX_ATTENTION = 4 -- nøgletalsrækken er højere end de andre rækker (tal over label) SmartPickupFrame.KPI_ROW_SCALE = 1.6 -- ændret struktur (ny vare, flere alarmer) genopbygges højst hvert N. sekund, så fokus ikke hopper SmartPickupFrame.ITEM_REBUILD_TICKS = 10 SmartPickupFrame.LEVEL_COLORS = { red = {0.90, 0.25, 0.20, 1}, yellow = {0.95, 0.72, 0.15, 1}, green = {0.30, 0.75, 0.35, 1}, } SmartPickupFrame.NEUTRAL_BAR_COLOR = {0.35, 0.60, 0.85, 1} SmartPickupFrame.TEXT_COLOR = {1, 1, 1, 1} local unpackFn = table.unpack or unpack -- ---- små tegne-hjælpere ---- local function setTextColor(element, color) if element ~= nil and element.setTextColor ~= nil then element:setTextColor(unpackFn(color)) end end -- Fyld bjælken til brøken af baggrundens bredde; tom bjælke skjules (bredde 0 tegnes som en prik). function SmartPickupFrame.setBar(bar, background, fraction, color) if bar == nil or background == nil or background.size == nil then return end if fraction <= 0 then bar:setVisible(false) return end bar:setVisible(true) bar:setSize(background.size[1] * fraction, background.size[2]) if bar.setImageColor ~= nil then bar:setImageColor(nil, unpackFn(color)) end end -- Bjælkens farve: rød/gul ved problem, ellers grøn for varer i omløb og blå for rent lager. function SmartPickupFrame.barColor(level, isActive) if level ~= nil then return SmartPickupFrame.LEVEL_COLORS[level] end return isActive and SmartPickupFrame.LEVEL_COLORS.green or SmartPickupFrame.NEUTRAL_BAR_COLOR end function SmartPickupFrame:formatDays(days) return string.format(self.i18n:getText("spu_stat_days"), days) end -- "−14.4k/døgn · 3.2 døgn", "+3.6k/døgn · fuld om 2.0 døgn", "+1.2k/døgn · stiger" eller "-" function SmartPickupFrame:itemRateText(stat) local fc = SmartPickupFrame.formatCompact if (stat.net or 0) == 0 then return (stat.consumption or 0) > 0 and self.i18n:getText("spu_item_balanced") or "-" end local rate = string.format(self.i18n:getText("spu_item_perDay"), (stat.net < 0 and "-" or "+") .. fc(math.abs(stat.net))) if stat.days ~= nil then return rate .. " · " .. self:formatDays(stat.days) elseif stat.daysToFull ~= nil then return rate .. " · " .. string.format(self.i18n:getText("spu_item_fullIn"), self:formatDays(stat.daysToFull)) end return rate .. " · " .. self.i18n:getText("spu_stat_rising") end -- "1.2M/1.5M · 80 %" eller "24 paller · 1 %" function SmartPickupFrame:itemAmountText(entry) local fc = SmartPickupFrame.formatCompact local amount = entry.count ~= nil and string.format(self.i18n:getText("spu_storage_pallets"), entry.count) or string.format("%s/%s", fc(entry.level or 0), fc(entry.capacity or 0)) if entry.percent ~= nil then amount = amount .. string.format(" · %d %%", entry.percent) end return amount end -- ---- data ---- -- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården. function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory) self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen) local farmId = g_currentMission:getFarmId() local totals = ADStorageStats.farmTotals(fullInventory) local capacities = ADStorageStats.farmCapacities(fullInventory) local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {} local stats = {} local titleOf = function(fillType) return self:fillTypeTitle(fillType) end for _, group in ipairs(ADStorageStats.groupByFillType(inventory, titleOf)) do local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(group.fillType) or nil local farm = {level = totals[group.fillType], capacity = capacities[group.fillType]} table.insert(stats, ADStorageStats.groupStats(group, farm, rates[group.fillType], quote)) end return stats end -- Nøgle der ændrer sig når rækkerne skal være anderledes (varer, udfoldede bygninger, alarmer). function SmartPickupFrame:itemStructureKey(stats, attention) local parts = {} for _, stat in ipairs(stats) do table.insert(parts, tostring(stat.fillType)) if self:isItemExpanded(stat.fillType) then -- sorteret: at to bygninger bytter plads i mængde må ikke udløse en genopbygning local placeables = {} for _, building in ipairs(stat.buildings) do table.insert(placeables, tostring(building.placeable)) end table.sort(placeables) table.insert(parts, table.concat(placeables, ";")) end end table.insert(parts, "a" .. #attention) return table.concat(parts, ",") end function SmartPickupFrame:isItemExpanded(fillType) local expanded = self.expandedItems ~= nil and self.expandedItems[self.currentTab] or nil return expanded ~= nil and expanded[fillType] == true end function SmartPickupFrame:toggleItem(fillType) self.expandedItems = self.expandedItems or {} self.expandedItems[self.currentTab] = self.expandedItems[self.currentTab] or {} local expanded = self.expandedItems[self.currentTab] expanded[fillType] = not expanded[fillType] or nil -- listen hopper ellers til toppen ved genopbygningen local layout = self.boxLayout local scrollY = layout ~= nil and layout.firstVisibleY or nil self:rebuild() if scrollY ~= nil and layout.scrollTo ~= nil then pcall(layout.scrollTo, layout, scrollY, true) end end -- ---- visningsvælger (Varer | Bygninger) ---- function SmartPickupFrame:getStorageView() local views = self.storageViewByTab or {} return views[self.currentTab] or SmartPickupFrame.STORAGE_VIEW_ITEMS end function SmartPickupFrame:addStorageViewRow() local row = self:addRow(self.productPrefab, {kind = "storageView"}) local option = row:getDescendantByName("option") option:setTexts({self.i18n:getText("spu_view_items"), self.i18n:getText("spu_view_buildings")}) option:setState(self:getStorageView(), false) row:getDescendantByName("title"):setText(self.i18n:getText("spu_view_title")) local tooltip = option:getDescendantByName("ignore") if tooltip ~= nil then tooltip:setText(self.i18n:getText("spu_view_tooltip")) end option.toolTipText = self.i18n:getText("spu_view_tooltip") end function SmartPickupFrame:onClickStorageView(state) self.storageViewByTab = self.storageViewByTab or {} self.storageViewByTab[self.currentTab] = state self:requestRebuild() end -- ---- opbygning ---- function SmartPickupFrame:kpiTexts(stats) local kpi = ADStorageStats.kpis(stats) local i18n = self.i18n return { {SmartPickupFrame.formatMoney(kpi.valueNow), i18n:getText("spu_kpi_value")}, {"+" .. SmartPickupFrame.formatMoney(kpi.gain), i18n:getText("spu_kpi_gain")}, {tostring(kpi.emptyCount), i18n:getText("spu_kpi_empty")}, {tostring(kpi.fullCount), i18n:getText("spu_kpi_full")}, }, kpi end function SmartPickupFrame:setKpiRow(row, stats) local texts, kpi = self:kpiTexts(stats) for index, pair in ipairs(texts) do local value = row:getDescendantByName("v" .. index) if value ~= nil then value:setText(pair[1]) end local label = row:getDescendantByName("l" .. index) if label ~= nil then label:setText(pair[2]) end end local colors = SmartPickupFrame.LEVEL_COLORS setTextColor(row:getDescendantByName("v2"), kpi.gain > 0 and colors.green or SmartPickupFrame.TEXT_COLOR) setTextColor(row:getDescendantByName("v3"), kpi.emptyCount > 0 and colors.red or SmartPickupFrame.TEXT_COLOR) setTextColor(row:getDescendantByName("v4"), kpi.fullCount > 0 and colors.yellow or SmartPickupFrame.TEXT_COLOR) end function SmartPickupFrame:addKpiRow(stats) local row = self:addRow(self.kpiPrefab, {kind = "text"}) if row.size ~= nil then row:setSize(row.size[1], row.size[2] * SmartPickupFrame.KPI_ROW_SCALE) end self:setKpiRow(row, stats) return row end -- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet -- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS, -- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks. function SmartPickupFrame:attentionFor(stats) local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION) if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then local okEnv, adEnv = pcall(self.getAdEnv) if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end end if ADChain == nil or ADChain.last == nil then return attention end return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION) end function SmartPickupFrame:alertText(entry) local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n if entry.kind == "chainSuggest" then return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0) end if entry.kind == "chainProblem" then return string.format(i18n:getText("spu_attention_chainProblems"), entry.count or 0) end if entry.kind == "dependencyOff" then if (entry.more or 0) > 0 then return string.format(i18n:getText("spu_attention_depOffMore"), tostring(entry.buildingName), entry.more) end return string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName)) end if entry.kind == "empty" then return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0))) elseif entry.kind == "full" then if stat.daysToFull ~= nil then return string.format(i18n:getText("spu_alert_fullIn"), stat.title, stat.percent or 0, self:formatDays(stat.daysToFull)) end return string.format(i18n:getText("spu_alert_full"), stat.title, stat.percent or 0) end return string.format(i18n:getText("spu_alert_sell"), stat.title, math.floor((stat.priceRatio or 0) * 100 + 0.5), SmartPickupFrame.formatMoney(stat.valueNow)) end function SmartPickupFrame:setAlertRow(row, entry) row.entry = entry local hint = "" if SmartPickupFrame.attentionAdvice ~= nil then local ok, advice, info = pcall(self.attentionAdvice, self, entry) if ok then hint = self:attentionHint(advice, info) end end row.c1:setText(self:alertText(entry) .. hint) if row.dot ~= nil and row.dot.setImageColor ~= nil then row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green)) end end function SmartPickupFrame:itemCells(stat) local marker = self:isItemExpanded(stat.fillType) and "– " or "+ " local value = stat.valueNow ~= nil and SmartPickupFrame.formatMoney(stat.valueNow) or self.i18n:getText("spu_stat_noSell") return {marker .. stat.title, self:itemAmountText(stat), value, self:itemRateText(stat)} end function SmartPickupFrame:setItemRow(row, stat) SmartPickupFrame.setCells(row.cells, self:itemCells(stat)) local isActive = (stat.consumption or 0) > 0 or (stat.production or 0) > 0 SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(stat.percent), SmartPickupFrame.barColor(stat.severity, isActive)) local rateColor = SmartPickupFrame.TEXT_COLOR if stat.emptyLevel ~= nil then rateColor = SmartPickupFrame.LEVEL_COLORS[stat.emptyLevel] elseif stat.fullLevel ~= nil then rateColor = SmartPickupFrame.LEVEL_COLORS[stat.fullLevel] end setTextColor(row.cells[4], rateColor) end -- Bygningens andel af varen: værdi efter samme pris som varen, bjælke efter bygningens egen fyldning. function SmartPickupFrame:setSubRow(row, stat, building) local kind = SmartPickupFrame.getTabKind(self.currentTab) local isStableOutput = kind == "husbandry" and (stat.production or 0) > 0 and (stat.consumption or 0) == 0 local value = "-" if stat.valueNow ~= nil and stat.level > 0 then value = SmartPickupFrame.formatMoney(stat.valueNow * building.level / stat.level) end local share = stat.level > 0 and math.floor(100 * building.level / stat.level + 0.5) or 0 SmartPickupFrame.setCells(row.cells, {tostring(building.name), self:itemAmountText(building), value, string.format(self.i18n:getText("spu_item_share"), share)}) local level = ADStorageStats.buildingLevel(building, kind, isStableOutput) SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(building.percent), SmartPickupFrame.barColor(level, false)) end -- Klon af item-/sub-prefab med referencer til cellerne og bjælken. function SmartPickupFrame:addBarRow(prefab) local row = self:addRow(prefab, {kind = "text"}) local cells = {} for index = 1, 4 do cells[index] = row:getDescendantByName("c" .. index) end return {element = row, cells = cells, bar = row:getDescendantByName("bar"), barBg = row:getDescendantByName("barBg")} end function SmartPickupFrame:addItemHeader() local i18n = self.i18n local header = self:addBarRow(self.itemPrefab) SmartPickupFrame.setCells(header.cells, {i18n:getText("spu_col_item"), i18n:getText("spu_icol_amount"), i18n:getText("spu_icol_value"), i18n:getText("spu_icol_rate")}) if header.bar ~= nil then header.bar:setVisible(false) end if header.barBg ~= nil then header.barBg:setVisible(false) end end function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory) local i18n = self.i18n local stats = self:computeItemStats(adEnv, inventory, fullInventory) local attention = self:attentionFor(stats) self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)} self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS self:addTextRow(i18n:getText("spu_attention")) if #attention == 0 then self:addTextRow(i18n:getText("spu_attention_none")) end for _, entry in ipairs(attention) do local element = self:addRow(self.alertPrefab, {kind = "text"}) local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")} self:setAlertRow(row, entry) -- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua) self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end) table.insert(self.itemView.alertRows, row) end self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint")) if #stats == 0 then self:addTextRow(i18n:getText("spu_storage_empty")) return end self:addItemHeader() for _, stat in ipairs(stats) do local row = self:addBarRow(self.itemPrefab) local fillType = stat.fillType self:makeRowClickable(row.element, function(frame) frame:toggleItem(fillType) end) self:setItemRow(row, stat) row.subs = {} if self:isItemExpanded(fillType) then for _, building in ipairs(stat.buildings) do local sub = self:addBarRow(self.subPrefab) sub.placeable = building.placeable self:setSubRow(sub, stat, building) table.insert(row.subs, sub) end end table.insert(self.itemView.itemRows, row) end end -- Hvert sekund: tal og bjælker opdateres på stedet; ny struktur -> genopbygning (højst hvert 10. s). function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory) local view = self.itemView if view == nil then return end local stats = self:computeItemStats(adEnv, inventory, fullInventory) local attention = self:attentionFor(stats) if self:itemStructureKey(stats, attention) ~= view.key then self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1 if self.itemRebuildTicks <= 0 then self:requestRebuild() end return end self:setKpiRow(view.kpiRow, stats) for index, entry in ipairs(attention) do self:setAlertRow(view.alertRows[index], entry) end for index, stat in ipairs(stats) do local row = view.itemRows[index] self:setItemRow(row, stat) -- underrækkerne beholder rækkefølgen fra opbygningen; tallene findes via bygningen local byPlaceable = {} for _, building in ipairs(stat.buildings) do byPlaceable[building.placeable] = building end for _, sub in ipairs(row.subs) do local building = byPlaceable[sub.placeable] if building ~= nil then self:setSubRow(sub, stat, building) end end end end