diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py index 727037421..4294f4ef7 100644 --- a/eos/db/gamedata/attribute.py +++ b/eos/db/gamedata/attribute.py @@ -39,6 +39,8 @@ attributes_table = Table("dgmattribs", gamedata_meta, Column("displayName", String), Column("highIsGood", Boolean), Column("iconID", Integer), + Column("attributeCategory", Integer), + Column("tooltipDescription", Integer), Column("unitID", Integer, ForeignKey("dgmunits.unitID"))) mapper(Attribute, typeattributes_table, diff --git a/eos/gamedata.py b/eos/gamedata.py index 5ec2a1e14..1abb3f69b 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -561,6 +561,15 @@ class Unit(EqBase): self.name = None self.displayName = None + @property + def rigSizes(self): + return { + 1: "Small", + 2: "Medium", + 3: "Large", + 4: "X-Large" + } + @property def translations(self): """ This is a mapping of various tweaks that we have to do between the internal representation of an attribute @@ -593,10 +602,10 @@ class Unit(EqBase): lambda u: "m³", lambda d: d), "Sizeclass": ( - lambda v: v, - lambda v: v, - lambda u: "", - lambda d: d), + lambda v: self.rigSizes[v], + lambda v: self.rigSizes[v], + lambda d: next(i for i in self.rigSizes.keys() if self.rigSizes[i] == 'Medium'), + lambda u: ""), "Absolute Percent": ( lambda v: v * 100, lambda v: v * 100, diff --git a/eve.db b/eve.db index 417b53d1c..6556d4f06 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/bitmap_loader.py b/gui/bitmap_loader.py index 7a8d41a46..042a6674b 100644 --- a/gui/bitmap_loader.py +++ b/gui/bitmap_loader.py @@ -82,9 +82,7 @@ class BitmapLoader(object): @classmethod def loadBitmap(cls, name, location): if cls.scaling_factor is None: - import gui.mainFrame - cls.scaling_factor = int(gui.mainFrame.MainFrame.getInstance().GetContentScaleFactor()) - + cls.scaling_factor = int(wx.GetApp().GetTopWindow().GetContentScaleFactor()) scale = cls.scaling_factor filename, img = cls.loadScaledBitmap(name, location, scale) diff --git a/gui/builtinItemStatsViews/attributeGrouping.py b/gui/builtinItemStatsViews/attributeGrouping.py new file mode 100644 index 000000000..de466cf73 --- /dev/null +++ b/gui/builtinItemStatsViews/attributeGrouping.py @@ -0,0 +1,254 @@ +from enum import Enum, auto + + +# Define the various groups of attributes +class AttrGroup(Enum): + FITTING = auto() + STRUCTURE = auto() + SHIELD = auto() + ARMOR = auto() + TARGETING = auto() + EWAR_RESISTS = auto() + CAPACITOR = auto() + SHARED_FACILITIES = auto() + FIGHTER_FACILITIES = auto() + ON_DEATH = auto() + JUMP_SYSTEMS = auto() + PROPULSIONS = auto() + FIGHTERS = auto() + + +RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), []) + +#todo: maybe moved some of these basic definitions into eos proper? Can really be useful with effect writing as a lot of these are used over and over +damage_types = ["em", "thermal", "kinetic", "explosive"] +scan_types = ["radar", "magnetometric", "gravimetric", "ladar"] + +DamageAttrs = ["{}Damage".format(x) for x in damage_types] +HullResistsAttrs = ["{}DamageResonance".format(x) for x in damage_types] +ArmorResistsAttrs = ["armor{}DamageResonance".format(x.capitalize()) for x in damage_types] +ShieldResistsAttrs = ["shield{}DamageResonance".format(x.capitalize()) for x in damage_types] +ScanStrAttrs = ["scan{}Strength".format(x.capitalize()) for x in scan_types] + +# todo: convert to named tuples? +AttrGroups = [ + (DamageAttrs, "Damage"), + (HullResistsAttrs, "Resistances"), + (ArmorResistsAttrs, "Resistances"), + (ShieldResistsAttrs, "Resistances"), + (ScanStrAttrs, "Sensor Strengths") +] + +GroupedAttributes = [] +for x in AttrGroups: + GroupedAttributes += x[0] + +# Start defining all the known attribute groups +AttrGroupDict = { + AttrGroup.FITTING : { + "label" : "Fitting", + "attributes": [ + # parent-level attributes + "cpuOutput", + "powerOutput", + "upgradeCapacity", + "hiSlots", + "medSlots", + "lowSlots", + "serviceSlots", + "turretSlotsLeft", + "launcherSlotsLeft", + "upgradeSlotsLeft", + # child-level attributes + "cpu", + "power", + "rigSize", + "upgradeCost", + # "mass", + ] + }, + AttrGroup.STRUCTURE : { + "label" : "Structure", + "attributes": [ + "hp", + "capacity", + "mass", + "volume", + "agility", + "droneCapacity", + "droneBandwidth", + "specialOreHoldCapacity", + "specialGasHoldCapacity", + "specialMineralHoldCapacity", + "specialSalvageHoldCapacity", + "specialShipHoldCapacity", + "specialSmallShipHoldCapacity", + "specialMediumShipHoldCapacity", + "specialLargeShipHoldCapacity", + "specialIndustrialShipHoldCapacity", + "specialAmmoHoldCapacity", + "specialCommandCenterHoldCapacity", + "specialPlanetaryCommoditiesHoldCapacity", + "structureDamageLimit", + "specialSubsystemHoldCapacity", + "emDamageResonance", + "thermalDamageResonance", + "kineticDamageResonance", + "explosiveDamageResonance" + ] + }, + AttrGroup.ARMOR : { + "label": "Armor", + "attributes":[ + "armorHP", + "armorDamageLimit", + "armorEmDamageResonance", + "armorThermalDamageResonance", + "armorKineticDamageResonance", + "armorExplosiveDamageResonance", + ] + + }, + AttrGroup.SHIELD : { + "label": "Shield", + "attributes": [ + "shieldCapacity", + "shieldRechargeRate", + "shieldDamageLimit", + "shieldEmDamageResonance", + "shieldExplosiveDamageResonance", + "shieldKineticDamageResonance", + "shieldThermalDamageResonance", + ] + + }, + AttrGroup.EWAR_RESISTS : { + "label": "Electronic Warfare", + "attributes": [ + "ECMResistance", + "remoteAssistanceImpedance", + "remoteRepairImpedance", + "energyWarfareResistance", + "sensorDampenerResistance", + "stasisWebifierResistance", + "targetPainterResistance", + "weaponDisruptionResistance", + ] + }, + AttrGroup.CAPACITOR : { + "label": "Capacitor", + "attributes": [ + "capacitorCapacity", + "rechargeRate", + ] + }, + AttrGroup.TARGETING : { + "label": "Targeting", + "attributes": [ + "maxTargetRange", + "maxRange", + "maxLockedTargets", + "signatureRadius", + "optimalSigRadius", + "scanResolution", + "proximityRange", + "falloff", + "trackingSpeed", + "scanRadarStrength", + "scanMagnetometricStrength", + "scanGravimetricStrength", + "scanLadarStrength", + ] + }, + AttrGroup.SHARED_FACILITIES : { + "label" : "Shared Facilities", + "attributes": [ + "fleetHangarCapacity", + "shipMaintenanceBayCapacity", + "maxJumpClones", + ] + }, + AttrGroup.FIGHTER_FACILITIES: { + "label": "Fighter Squadron Facilities", + "attributes": [ + "fighterCapacity", + "fighterTubes", + "fighterLightSlots", + "fighterSupportSlots", + "fighterHeavySlots", + "fighterStandupLightSlots", + "fighterStandupSupportSlots", + "fighterStandupHeavySlots", + ] + }, + AttrGroup.ON_DEATH : { + "label": "On Death", + "attributes": [ + "onDeathDamageEM", + "onDeathDamageTherm", + "onDeathDamageKin", + "onDeathDamageExp", + "onDeathAOERadius", + "onDeathSignatureRadius", + ] + }, + AttrGroup.JUMP_SYSTEMS : { + "label": "Jump Drive Systems", + "attributes": [ + "jumpDriveCapacitorNeed", + "jumpDriveRange", + "jumpDriveConsumptionType", + "jumpDriveConsumptionAmount", + "jumpPortalCapacitorNeed", + "jumpDriveDuration", + "specialFuelBayCapacity", + "jumpPortalConsumptionMassFactor", + "jumpPortalDuration", + ] + }, + AttrGroup.PROPULSIONS : { + "label": "Propulsion", + "attributes": [ + "maxVelocity" + ] + }, + AttrGroup.FIGHTERS : { + "label": "Fighter", + "attributes": [ + "mass", + "maxVelocity", + "agility", + "volume", + "signatureRadius", + "fighterSquadronMaxSize", + "fighterRefuelingTime", + "fighterSquadronOrbitRange", + ] + }, +} + +Group1 = [ + AttrGroup.FITTING, + AttrGroup.STRUCTURE, + AttrGroup.ARMOR, + AttrGroup.SHIELD, + AttrGroup.EWAR_RESISTS, + AttrGroup.CAPACITOR, + AttrGroup.TARGETING, + AttrGroup.SHARED_FACILITIES, + AttrGroup.FIGHTER_FACILITIES, + AttrGroup.ON_DEATH, + AttrGroup.JUMP_SYSTEMS, + AttrGroup.PROPULSIONS, +] + +CategoryGroups = { + "Fighter" : [ + AttrGroup.FIGHTERS, + AttrGroup.SHIELD, + AttrGroup.TARGETING, + ], + "Ship" : Group1, + "Drone" : Group1, + "Structure": Group1 +} diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index 45eb6ec04..747f25066 100644 --- a/gui/builtinItemStatsViews/itemAttributes.py +++ b/gui/builtinItemStatsViews/itemAttributes.py @@ -3,11 +3,18 @@ import config # noinspection PyPackageRequirements import wx - -from .helpers import AutoListCtrl +import wx.lib.agw.hypertreelist +from gui.builtinItemStatsViews.helpers import AutoListCtrl from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount, roundDec +from enum import IntEnum +from gui.builtinItemStatsViews.attributeGrouping import * + + +class AttributeView(IntEnum): + NORMAL = 1 + RAW = -1 class ItemParams(wx.Panel): @@ -15,8 +22,9 @@ class ItemParams(wx.Panel): wx.Panel.__init__(self, parent) 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) + self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS) + self.paramList.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) self.SetSizer(mainSizer) @@ -27,14 +35,19 @@ class ItemParams(wx.Panel): self.attrValues = {} self._fetchValues() + self.paramList.AddColumn("Attribute") + self.paramList.AddColumn("Current Value") + if self.stuff is not None: + self.paramList.AddColumn("Base Value") + + self.paramList.SetMainColumn(0) # the one with the tree in it... + self.paramList.SetColumnWidth(0, 300) + 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, "Toggle view mode", wx.DefaultPosition, wx.DefaultSize, + self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, "Veiw Raw Data", wx.DefaultPosition, wx.DefaultSize, 0) bSizer.Add(self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL) @@ -76,10 +89,10 @@ class ItemParams(wx.Panel): def UpdateList(self): self.Freeze() - self.paramList.ClearAll() + self.paramList.DeleteRoot() self.PopulateList() self.Thaw() - self.paramList.resizeLastColumn(100) + # self.paramList.resizeLastColumn(100) def RefreshValues(self, event): self._fetchValues() @@ -151,89 +164,154 @@ class ItemParams(wx.Panel): ] ) + def AddAttribute(self, parent, attr): + if attr in self.attrValues and attr not in self.processed_attribs: + + data = self.GetData(attr) + if data is None: + return + + attrIcon, attrName, currentVal, baseVal = data + attr_item = self.paramList.AppendItem(parent, attrName) + + self.paramList.SetItemText(attr_item, currentVal, 1) + if self.stuff is not None: + self.paramList.SetItemText(attr_item, baseVal, 2) + self.paramList.SetItemImage(attr_item, attrIcon, which=wx.TreeItemIcon_Normal) + self.processed_attribs.add(attr) + + def ExpandOrDelete(self, item): + if self.paramList.GetChildrenCount(item) == 0: + self.paramList.Delete(item) + else: + self.paramList.Expand(item) + def PopulateList(self): - self.paramList.InsertColumn(0, "Attribute") - self.paramList.InsertColumn(1, "Current Value") - if self.stuff is not None: - self.paramList.InsertColumn(2, "Base Value") - self.paramList.SetColumnWidth(0, 110) - self.paramList.SetColumnWidth(1, 90) - if self.stuff is not None: - self.paramList.SetColumnWidth(2, 90) - self.paramList.setResizeColumn(0) + # self.paramList.setResizeColumn(0) self.imageList = wx.ImageList(16, 16) - self.paramList.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL) + + self.processed_attribs = set() + root = self.paramList.AddRoot("The Root Item") + misc_parent = root + + # We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group + order = CategoryGroups.get(self.item.category.categoryName, [AttrGroup.FITTING]) + # start building out the tree + for data in [AttrGroupDict[o] for o in order]: + heading = data.get("label") + + header_item = self.paramList.AppendItem(root, heading) + for attr in data.get("attributes", []): + # Attribute is a "grouped" attr (eg: damage, sensor strengths, etc). Automatically group these into a child item + if attr in GroupedAttributes: + # find which group it's in + for grouping in AttrGroups: + if attr in grouping[0]: + break + + # create a child item with the groups label + item = self.paramList.AppendItem(header_item, grouping[1]) + for attr2 in grouping[0]: + # add each attribute in the group + self.AddAttribute(item, attr2) + + self.ExpandOrDelete(item) + continue + + self.AddAttribute(header_item, attr) + + self.ExpandOrDelete(header_item) names = list(self.attrValues.keys()) names.sort() - idNameMap = {} - idCount = 0 + # this will take care of any attributes that weren't collected withe the defined grouping (or all attributes if the item ddidn't have anything defined) for name in names: - info = self.attrInfo.get(name) - att = self.attrValues[name] + if name in GroupedAttributes: + # find which group it's in + for grouping in AttrGroups: + if name in grouping[0]: + break - # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function, - # which will return the value with respect to the effective base (with mutators / overrides in place) - valDefault = getattr(info, "value", None) # Get default value from attribute - if self.stuff is not None: - # if it's a stuff, overwrite default (with fallback to current value) - valDefault = self.stuff.getBaseAttrValue(name, valDefault) - valueDefault = valDefault if valDefault is not None else att + # get all attributes in group + item = self.paramList.AppendItem(root, grouping[1]) + for attr2 in grouping[0]: + self.AddAttribute(item, attr2) - val = getattr(att, "value", None) - value = val if val is not None else att + self.ExpandOrDelete(item) + continue - if info and info.displayName and self.toggleView == 1: - attrName = info.displayName - else: - attrName = name + self.AddAttribute(root, name) - if info and config.debug: - attrName += " ({})".format(info.ID) + self.paramList.AssignImageList(self.imageList) + self.Layout() - if info: - if info.iconID is not None: - iconFile = info.iconID - icon = BitmapLoader.getBitmap(iconFile, "icons") + def GetData(self, attr): + info = self.attrInfo.get(attr) + att = self.attrValues[attr] - if icon is None: - icon = BitmapLoader.getBitmap("transparent16x16", "gui") + # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function, + # which will return the value with respect to the effective base (with mutators / overrides in place) + valDefault = getattr(info, "value", None) # Get default value from attribute + if self.stuff is not None: + # if it's a stuff, overwrite default (with fallback to current value) + valDefault = self.stuff.getBaseAttrValue(attr, valDefault) + valueDefault = valDefault if valDefault is not None else att - attrIcon = self.imageList.Add(icon) - else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + val = getattr(att, "value", None) + value = val if val is not None else att + + if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs): + return None + + if info and info.displayName and self.toggleView == 1: + attrName = info.displayName + else: + attrName = attr + + if info and config.debug: + attrName += " ({})".format(info.ID) + + if info: + if info.iconID is not None: + iconFile = info.iconID + icon = BitmapLoader.getBitmap(iconFile, "icons") + + if icon is None: + icon = BitmapLoader.getBitmap("transparent16x16", "gui") + + attrIcon = self.imageList.Add(icon) else: attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + else: + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) - index = self.paramList.InsertItem(self.paramList.GetItemCount(), attrName, attrIcon) - idNameMap[idCount] = attrName - self.paramList.SetItemData(index, idCount) - idCount += 1 + # index = self.paramList.AppendItem(root, attrName) + # idNameMap[idCount] = attrName + # self.paramList.SetPyData(index, idCount) + # idCount += 1 - if self.toggleView != 1: - valueUnit = str(value) - elif info and info.unit: - valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) - else: - valueUnit = formatAmount(value, 3, 0, 0) + if self.toggleView != 1: + valueUnit = str(value) + elif info and info.unit: + valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) + else: + valueUnit = formatAmount(value, 3, 0, 0) - if self.toggleView != 1: - valueUnitDefault = str(valueDefault) - elif info and info.unit: - valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) - else: - valueUnitDefault = formatAmount(valueDefault, 3, 0, 0) + if self.toggleView != 1: + valueUnitDefault = str(valueDefault) + elif info and info.unit: + valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) + else: + valueUnitDefault = formatAmount(valueDefault, 3, 0, 0) - self.paramList.SetItem(index, 1, valueUnit) - if self.stuff is not None: - self.paramList.SetItem(index, 2, valueUnitDefault) - # @todo: pheonix, this lamda used cmp() which no longer exists in py3. Probably a better way to do this in the - # long run, take a look - self.paramList.SortItems(lambda id1, id2: (idNameMap[id1] > idNameMap[id2]) - (idNameMap[id1] < idNameMap[id2])) - self.paramList.RefreshRows() - self.totalAttrsLabel.SetLabel("%d attributes. " % idCount) - self.Layout() + # todo: attribute that point to another item should load that item's icon. + return (attrIcon, attrName, valueUnit, valueUnitDefault) + + # self.paramList.SetItemText(index, valueUnit, 1) + # if self.stuff is not None: + # self.paramList.SetItemText(index, valueUnitDefault, 2) + # self.paramList.SetItemImage(index, attrIcon, which=wx.TreeItemIcon_Normal) @staticmethod def FormatValue(value, unit, rounding='prec', digits=3): @@ -246,3 +324,43 @@ class ItemParams(wx.Panel): else: fvalue = value return "%s %s" % (fvalue, unit) + + +if __name__ == "__main__": + + import eos.db + # need to set up some paths, since bitmap loader requires config to have things + # Should probably change that so that it's not dependant on config + import os + os.chdir('..') + import config + config.defPaths(None) + config.debug = True + class Frame(wx.Frame): + def __init__(self, ): + # item = eos.db.getItem(23773) # Ragnarok + item = eos.db.getItem(23061) # Einherji I + #item = eos.db.getItem(24483) # Nidhoggur + #item = eos.db.getItem(587) # Rifter + #item = eos.db.getItem(2486) # Warrior I + #item = eos.db.getItem(526) # Stasis Webifier I + item = eos.db.getItem(486) # 200mm AutoCannon I + #item = eos.db.getItem(200) # Phased Plasma L + super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500)) + + if 'wxMSW' in wx.PlatformInfo: + color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE) + self.SetBackgroundColour(color) + + main_sizer = wx.BoxSizer(wx.HORIZONTAL) + + panel = ItemParams(self, None, item) + + main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2) + + self.SetSizer(main_sizer) + + app = wx.App(redirect=False) # Error messages go to popup window + top = Frame() + top.Show() + app.MainLoop()