# noinspection PyPackageRequirements import wx from .helpers import AutoListCtrl from service.price import Price as ServicePrice from service.market import Market from service.attribute import Attribute from gui.utils.numberFormatter import formatAmount _t = wx.GetTranslation # Mapping of repair/transfer amount attributes to their duration attribute and display name PER_SECOND_ATTRIBUTES = { "armorDamageAmount": { "durationAttr": "duration", "displayName": "Armor Hitpoints Repaired per second", "unit": "HP/s" }, "shieldBonus": { "durationAttr": "duration", "displayName": "Shield Hitpoints Repaired per second", "unit": "HP/s" }, "powerTransferAmount": { "durationAttr": "duration", "displayName": "Capacitor Transferred per second", "unit": "GJ/s" } } class PerSecondAttributeInfo: """Helper class to store info about computed per-second attributes""" def __init__(self, displayName, unit): self.displayName = displayName self.unit = PerSecondUnit(unit) class PerSecondUnit: """Helper class to mimic the Unit class for per-second attributes""" def __init__(self, displayName): self.displayName = displayName self.name = "" class PerSecondAttributeValue: """Helper class to store computed per-second attribute values""" def __init__(self, value): self.value = value self.info = None # Will be set when adding to attrs def defaultSort(item): return (item.metaLevel or 0, item.name) class ItemCompare(wx.Panel): def __init__(self, parent, stuff, item, items, context=None): # Start dealing with Price stuff to get that thread going sPrice = ServicePrice.getInstance() sPrice.getPrices(items, self.UpdateList, fetchTimeout=90) wx.Panel.__init__(self, parent) self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) mainSizer = wx.BoxSizer(wx.VERTICAL) self.paramList = AutoListCtrl(self, wx.ID_ANY, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES | wx.NO_BORDER) mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) self.SetSizer(mainSizer) self.toggleView = 1 self.stuff = stuff self.currentSort = None self.sortReverse = False self.item = item self.items = sorted(items, key=defaultSort) self.attrs = {} self.computedAttrs = {} # Store computed per-second attributes self.HighlightOn = wx.Colour(255, 255, 0, wx.ALPHA_OPAQUE) self.highlightedNames = [] self.bangBuckColumn = None # Store the column selected for bang/buck calculation self.bangBuckColumnName = None # Store the display name of the selected column self.columnHighlightColour = wx.Colour(173, 216, 230, wx.ALPHA_OPAQUE) # Light blue for column highlight # get a dict of attrName: attrInfo of all unique attributes across all items for item in self.items: for attr in list(item.attributes.keys()): if item.attributes[attr].info.displayName: self.attrs[attr] = item.attributes[attr].info # Compute per-second attributes for items that have both the amount and duration for perSecondKey, config in PER_SECOND_ATTRIBUTES.items(): amountAttr = perSecondKey durationAttr = config["durationAttr"] perSecondAttrName = f"{perSecondKey}_per_second" # Check if any item has both attributes hasPerSecondAttr = False for item in self.items: if amountAttr in item.attributes and durationAttr in item.attributes: hasPerSecondAttr = True break if hasPerSecondAttr: # Add the per-second attribute info to attrs perSecondInfo = PerSecondAttributeInfo(config["displayName"], config["unit"]) self.attrs[perSecondAttrName] = perSecondInfo self.computedAttrs[perSecondAttrName] = { "amountAttr": amountAttr, "durationAttr": durationAttr } # Process attributes for items and find ones that differ for attr in list(self.attrs.keys()): value = None for item in self.items: # Check if this is a computed attribute if attr in self.computedAttrs: computed = self.computedAttrs[attr] amountAttr = computed["amountAttr"] durationAttr = computed["durationAttr"] # Item needs both attributes to compute per-second value if amountAttr not in item.attributes or durationAttr not in item.attributes: break # Calculate per-second value amountValue = item.attributes[amountAttr].value durationValue = item.attributes[durationAttr].value # Duration is in milliseconds, convert to seconds perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0 if value is None: value = perSecondValue continue if perSecondValue != value: break else: # Regular attribute handling if attr not in item.attributes: break if value is None: value = item.attributes[attr].value continue if item.attributes[attr].value != value: break else: # attribute values were all the same, delete del self.attrs[attr] self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.m_staticline, 0, wx.EXPAND) bSizer = wx.BoxSizer(wx.HORIZONTAL) self.totalAttrsLabel = wx.StaticText(self, wx.ID_ANY, " ", wx.DefaultPosition, wx.DefaultSize, 0) bSizer.Add(self.totalAttrsLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT) self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, _t("Toggle view mode"), wx.DefaultPosition, wx.DefaultSize, 0) bSizer.Add(self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL) self.refreshBtn = wx.Button(self, wx.ID_ANY, _t("Refresh"), wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT) bSizer.Add(self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) self.refreshBtn.Bind(wx.EVT_BUTTON, self.RefreshValues) mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT) self.PopulateList() self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode) self.Bind(wx.EVT_LIST_COL_CLICK, self.SortCompareCols) self.Bind(wx.EVT_LIST_COL_RIGHT_CLICK, self.OnColumnRightClick) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.HighlightRow) def HighlightRow(self, event): itemIdx = event.GetIndex() name = self.paramList.GetItem(itemIdx).Text if name in self.highlightedNames: self.highlightedNames.remove(name) else: self.highlightedNames.append(name) self.Freeze() self.paramList.ClearAll() self.PopulateList() self.Thaw() event.Skip() def OnColumnRightClick(self, event): column = event.GetColumn() # Column 0 is "Item", column len(self.attrs) + 1 is "Price", len(self.attrs) + 2 is "Buck/bang" # Only allow selecting attribute columns (1 to len(self.attrs)) if 1 <= column <= len(self.attrs): # If clicking the same column, deselect it if self.bangBuckColumn == column: self.bangBuckColumn = None self.bangBuckColumnName = None else: self.bangBuckColumn = column # Get the display name of the selected column attr_key = list(self.attrs.keys())[column - 1] self.bangBuckColumnName = self.attrs[attr_key].displayName if self.attrs[attr_key].displayName else attr_key self.UpdateList() event.Skip() def SortCompareCols(self, event): self.Freeze() self.paramList.ClearAll() self.PopulateList(event.Column) self.Thaw() def UpdateList(self, items=None): # We do nothing with `items`, but it gets returned by the price service thread self.Freeze() self.paramList.ClearAll() self.PopulateList() self.Thaw() self.paramList.resizeLastColumn(100) def RefreshValues(self, event): self.UpdateList() event.Skip() def ToggleViewMode(self, event): self.toggleView *= -1 self.UpdateList() event.Skip() def processPrices(self, prices): for i, price in enumerate(prices): self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(price.value, 3, 3, 9, currency=True)) def PopulateList(self, sort=None): if sort is not None and self.currentSort == sort: self.sortReverse = not self.sortReverse else: self.currentSort = sort self.sortReverse = False if sort is not None: if sort == 0: # Name sort func = lambda _val: _val.name else: try: # Remember to reduce by 1, because the attrs array # starts at 0 while the list has the item name as column 0. attr = str(list(self.attrs.keys())[sort - 1]) # Handle computed attributes for sorting if attr in self.computedAttrs: computed = self.computedAttrs[attr] amountAttr = computed["amountAttr"] durationAttr = computed["durationAttr"] func = lambda _val: (_val.attributes[amountAttr].value / (_val.attributes[durationAttr].value / 1000.0)) if (amountAttr in _val.attributes and durationAttr in _val.attributes and _val.attributes[durationAttr].value > 0) else 0.0 else: func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0 # Clicked on a column that's not part of our array (price most likely) except IndexError: # Price if sort == len(self.attrs) + 1: func = lambda i: i.price.price if i.price.price != 0 else float("Inf") # Buck/bang elif sort == len(self.attrs) + 2: if self.bangBuckColumn is not None: attr_key = list(self.attrs.keys())[self.bangBuckColumn - 1] if attr_key in self.computedAttrs: computed = self.computedAttrs[attr_key] amountAttr = computed["amountAttr"] durationAttr = computed["durationAttr"] func = lambda i: (i.price.price / (i.attributes[amountAttr].value / (i.attributes[durationAttr].value / 1000.0)) if (amountAttr in i.attributes and durationAttr in i.attributes and i.attributes[durationAttr].value > 0 and (i.attributes[amountAttr].value / (i.attributes[durationAttr].value / 1000.0)) > 0) else float("Inf")) else: func = lambda i: (i.price.price / i.attributes[attr_key].value if (attr_key in i.attributes and i.attributes[attr_key].value > 0) else float("Inf")) else: func = defaultSort # Something else else: self.sortReverse = False func = defaultSort self.items = sorted(self.items, key=func, reverse=self.sortReverse) self.paramList.InsertColumn(0, _t("Item")) self.paramList.SetColumnWidth(0, 200) for i, attr in enumerate(self.attrs.keys()): name = self.attrs[attr].displayName if self.attrs[attr].displayName else attr # Add indicator if this column is selected for bang/buck calculation if self.bangBuckColumn == i + 1: name = "► " + name self.paramList.InsertColumn(i + 1, name) self.paramList.SetColumnWidth(i + 1, 120) self.paramList.InsertColumn(len(self.attrs) + 1, _t("Price")) self.paramList.SetColumnWidth(len(self.attrs) + 1, 60) # Add Buck/bang column header buckBangHeader = _t("Buck/bang") if self.bangBuckColumnName: buckBangHeader = _t("Buck/bang ({})").format(self.bangBuckColumnName) self.paramList.InsertColumn(len(self.attrs) + 2, buckBangHeader) self.paramList.SetColumnWidth(len(self.attrs) + 2, 80) toHighlight = [] for item in self.items: i = self.paramList.InsertItem(self.paramList.GetItemCount(), item.name) for x, attr in enumerate(self.attrs.keys()): # Handle computed attributes if attr in self.computedAttrs: computed = self.computedAttrs[attr] amountAttr = computed["amountAttr"] durationAttr = computed["durationAttr"] # Item needs both attributes to display per-second value if amountAttr in item.attributes and durationAttr in item.attributes: amountValue = item.attributes[amountAttr].value durationValue = item.attributes[durationAttr].value # Duration is in milliseconds, convert to seconds perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0 info = self.attrs[attr] if self.toggleView == 1: valueUnit = formatAmount(perSecondValue, 3, 0, 0) + " " + info.unit.displayName else: valueUnit = str(perSecondValue) self.paramList.SetItem(i, x + 1, valueUnit) # else: leave cell empty elif attr in item.attributes: info = self.attrs[attr] value = item.attributes[attr].value if self.toggleView != 1: valueUnit = str(value) elif info and info.unit and self.toggleView == 1: valueUnit = self.TranslateValueUnit(value, info.unit.displayName, info.unit.name) else: valueUnit = formatAmount(value, 3, 0, 0) self.paramList.SetItem(i, x + 1, valueUnit) # Add prices self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "") # Add buck/bang values if self.bangBuckColumn is not None and item.price.price and item.price.price > 0: attr_key = list(self.attrs.keys())[self.bangBuckColumn - 1] if attr_key in self.computedAttrs: computed = self.computedAttrs[attr_key] amountAttr = computed["amountAttr"] durationAttr = computed["durationAttr"] if amountAttr in item.attributes and durationAttr in item.attributes: amountValue = item.attributes[amountAttr].value durationValue = item.attributes[durationAttr].value perSecondValue = amountValue / (durationValue / 1000.0) if durationValue > 0 else 0 if perSecondValue > 0: buckBangValue = item.price.price / perSecondValue self.paramList.SetItem(i, len(self.attrs) + 2, formatAmount(buckBangValue, 3, 3, 9, currency=True)) elif attr_key in item.attributes: attrValue = item.attributes[attr_key].value if attrValue > 0: buckBangValue = item.price.price / attrValue self.paramList.SetItem(i, len(self.attrs) + 2, formatAmount(buckBangValue, 3, 3, 9, currency=True)) if item.name in self.highlightedNames: toHighlight.append(i) self.paramList.RefreshRows() self.Layout() # Highlight after layout, otherwise colors are getting overwritten for itemIdx in toHighlight: listItem = self.paramList.GetItem(itemIdx) listItem.SetBackgroundColour(self.HighlightOn) listItem.SetFont(listItem.GetFont().MakeBold()) self.paramList.SetItem(listItem) @staticmethod def TranslateValueUnit(value, unitName, unitDisplayName): def itemIDCallback(): item = Market.getInstance().getItem(value) return "%s (%d)" % (item.name, value) if item is not None else str(value) def groupIDCallback(): group = Market.getInstance().getGroup(value) return "%s (%d)" % (group.name, value) if group is not None else str(value) def attributeIDCallback(): attribute = Attribute.getInstance().getAttributeInfo(value) return "%s (%d)" % (attribute.name.capitalize(), value) trans = { "Inverse Absolute Percent": (lambda: (1 - value) * 100, unitName), "Inversed Modifier Percent": (lambda: (1 - value) * 100, unitName), "Modifier Percent": (lambda: ("%+.2f" if ((value - 1) * 100) % 1 else "%+d") % ((value - 1) * 100), unitName), "Volume": (lambda: value, "m\u00B3"), "Sizeclass": (lambda: value, ""), "Absolute Percent": (lambda: (value * 100), unitName), "Milliseconds": (lambda: value / 1000.0, unitName), "typeID": (itemIDCallback, ""), "groupID": (groupIDCallback, ""), "attributeID": (attributeIDCallback, "") } override = trans.get(unitDisplayName) if override is not None: v = override[0]() if isinstance(v, str): fvalue = v elif isinstance(v, (int, float)): fvalue = formatAmount(v, 3, 0, 0) else: fvalue = v return "%s %s" % (fvalue, override[1]) else: return "%s %s" % (formatAmount(value, 3, 0), unitName)