diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index f1e9954c3..956ef054e 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -391,6 +391,10 @@ def directAttributeRequest(itemIDs, attrIDs): return result +def getAbyssalTypes(): + return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()]) + + def getRequiredFor(itemID, attrMapping): Attribute1 = aliased(Attribute) Attribute2 = aliased(Attribute) diff --git a/eos/gamedata.py b/eos/gamedata.py index 3af302e81..342a4caf4 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -208,6 +208,8 @@ class Item(EqBase): MOVE_ATTR_INFO = None + ABYSSAL_TYPES = None + @classmethod def getMoveAttrInfo(cls): info = getattr(cls, "MOVE_ATTR_INFO", None) @@ -463,6 +465,17 @@ class Item(EqBase): return self.__price + @property + def isAbyssal(self): + if Item.ABYSSAL_TYPES is None: + Item.getAbyssalYypes() + + return self.ID in Item.ABYSSAL_TYPES + + @classmethod + def getAbyssalYypes(cls): + cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes() + def __repr__(self): return "Item(ID={}, name={}) at {}".format( self.ID, self.name, hex(id(self)) @@ -539,7 +552,101 @@ class MetaType(EqBase): class Unit(EqBase): - pass + + def __init__(self): + self.name = None + self.displayName = None + + @property + def translations(self): + """ This is a mapping of various tweaks that we have to do between the internal representation of an attribute + value and the display (for example, 'Millisecond' units have the display name of 's', so we have to convert value + from ms to s) """ + return { + "Inverse Absolute Percent": ( + lambda v: (1 - v) * 100, + lambda d: -1 * (d / 100) + 1, + lambda u: u), + "Inversed Modifier Percent": ( + lambda v: (1 - v) * 100, + lambda d: -1 * (d / 100) + 1, + lambda u: u), + "Modifier Percent": ( + lambda v: ("%+.2f" if ((v - 1) * 100) % 1 else "%+d") % ((v - 1) * 100), + lambda d: (d / 100) + 1, + lambda u: u), + "Volume": ( + lambda v: v, + lambda d: d, + lambda u: "m³"), + "Sizeclass": ( + lambda v: v, + lambda d: d, + lambda u: ""), + "Absolute Percent": ( + lambda v: (v * 100), + lambda d: d / 100, + lambda u: u), + "Milliseconds": ( + lambda v: v / 1000.0, + lambda d: d * 1000.0, + lambda u: u), + "Boolean": ( + lambda v, u: "Yes" if v == 1 else "No", + lambda d: 1.0 if d == "Yes" else 0.0, + lambda u: ""), + "typeID": ( + self.itemIDCallback, + None, # we could probably convert these back if we really tried hard enough + lambda u: ""), + "groupID": ( + self.groupIDCallback, + None, + lambda u: ""), + "attributeID": ( + self.attributeIDCallback, + None, + lambda u: ""), + } + + @staticmethod + def itemIDCallback(v): + v = int(v) + item = eos.db.getItem(int(v)) + return "%s (%d)" % (item.name, v) if item is not None else str(v) + + @staticmethod + def groupIDCallback(v): + v = int(v) + group = eos.db.getGroup(v) + return "%s (%d)" % (group.name, v) if group is not None else str(v) + + @staticmethod + def attributeIDCallback(v): + v = int(v) + if not v: # some attributes come through with a value of 0? See #1387 + return "%d" % (v) + attribute = eos.db.getAttributeInfo(v, eager=("unit")) + return "%s (%d)" % (attribute.name.capitalize(), v) + + def TranslateValue(self, value): + """Attributes have to be translated certain ways based on their unit (ex: decimals converting to percentages). + This allows us to get an easy representation of how the attribute should be printed """ + + override = self.translations.get(self.name) + if override is not None: + return override[0](value), override[2](self.displayName) + + return value, self.displayName + + def ComplicateValue(self, value): + """Takes the display value and turns it back into the internal representation of it""" + + override = self.translations.get(self.name) + if override is not None: + return override[1](value) + + return value class Traits(EqBase): diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index c16f7969c..8146561c5 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -18,6 +18,7 @@ # =============================================================================== from logbook import Logger +from copy import deepcopy from sqlalchemy.orm import validates, reconstructor from math import floor @@ -110,6 +111,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__item = None self.__baseItem = None self.__charge = None + self.__mutaplasmid = None # we need this early if module is invalid and returns early self.__slot = self.dummySlot @@ -206,7 +208,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return False return self.__item is None or \ (self.__item.category.name not in ("Module", "Subsystem", "Structure Module") and - self.__item.group.name not in self.SYSTEM_GROUPS) + self.__item.group.name not in self.SYSTEM_GROUPS) or \ + (self.item.isAbyssal and (not self.baseItemID or not self.mutaplasmidID) ) @property def isMutated(self): @@ -352,11 +355,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def baseItem(self): - return self.__baseItem if self.__baseItem != 0 else None # what? + return self.__baseItem @property def mutaplasmid(self): - return self.__mutaplasmid if self.__mutaplasmid != 0 else None + return self.__mutaplasmid @property def charge(self): @@ -835,9 +838,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if item is None: copy = Module.buildEmpty(self.slot) else: - copy = Module(self.item) + copy = Module(self.item, self.baseItem, self.mutaplasmid) copy.charge = self.charge copy.state = self.state + + for x in self.mutators.values(): + Mutator(copy, x.attribute, x.value) + return copy def __repr__(self): diff --git a/eos/saveddata/mutator.py b/eos/saveddata/mutator.py index 84e3ddbc4..ff1473005 100644 --- a/eos/saveddata/mutator.py +++ b/eos/saveddata/mutator.py @@ -81,12 +81,15 @@ class Mutator(EqBase): """ Validates values as properly falling within the range of the modules' Mutaplasmid """ mod = val/self.baseValue - if self.minMod < mod < self.maxMod: + if self.minMod <= mod <= self.maxMod: # sweet, all good returnVal = val else: # need to fudge the numbers a bit. Go with the value closest to base - returnVal = min(self.maxValue, max(self.minValue, val)) + if val >= 0: + returnVal = min(self.maxValue, max(self.minValue, val)) + else: + returnVal = max(self.maxValue, min(self.minValue, val)) return returnVal @@ -105,11 +108,11 @@ class Mutator(EqBase): @property def minMod(self): - return self.dynamicAttribute.min + return round(self.dynamicAttribute.min, 3) @property def maxMod(self): - return self.dynamicAttribute.max + return round(self.dynamicAttribute.max, 3) @property def baseValue(self): diff --git a/eve.db b/eve.db index 5d1793cbf..79cc01ecf 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/builtinAdditionPanes/cargoView.py b/gui/builtinAdditionPanes/cargoView.py index ed5687cfc..a5b94f31b 100644 --- a/gui/builtinAdditionPanes/cargoView.py +++ b/gui/builtinAdditionPanes/cargoView.py @@ -118,13 +118,22 @@ class CargoView(d.Display): # Gather module information to get position module = fit.modules[modIdx] + if module.item.isAbyssal: + dlg = wx.MessageDialog(self, + "Moving this Abyssal module to the cargo will convert it to the base module. Do you wish to proceed?", + "Confirm", wx.YES_NO | wx.ICON_QUESTION) + result = dlg.ShowModal() == wx.ID_YES + + if not result: + return + if dstRow != -1: # we're swapping with cargo if mstate.cmdDown: # if copying, append to cargo - sFit.addCargo(self.mainFrame.getActiveFit(), module.item.ID) + sFit.addCargo(self.mainFrame.getActiveFit(), module.item.ID if not module.item.isAbyssal else module.baseItemID) else: # else, move / swap sFit.moveCargoToModule(self.mainFrame.getActiveFit(), module.position, dstRow) else: # dragging to blank spot, append - sFit.addCargo(self.mainFrame.getActiveFit(), module.item.ID) + sFit.addCargo(self.mainFrame.getActiveFit(), module.item.ID if not module.item.isAbyssal else module.baseItemID) if not mstate.cmdDown: # if not copying, remove module sFit.removeModule(self.mainFrame.getActiveFit(), module.position) diff --git a/gui/builtinAdditionPanes/projectedView.py b/gui/builtinAdditionPanes/projectedView.py index 6857adb78..24ca11f3a 100644 --- a/gui/builtinAdditionPanes/projectedView.py +++ b/gui/builtinAdditionPanes/projectedView.py @@ -109,7 +109,7 @@ class ProjectedView(d.Display): dstRow, _ = self.HitTest((x, y)) # Gather module information to get position module = fit.modules[int(data[1])] - sFit.project(fit.ID, module.item.ID) + sFit.project(fit.ID, module) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fit.ID)) elif data[0] == "market": sFit = Fit.getInstance() diff --git a/gui/builtinContextMenus/itemStats.py b/gui/builtinContextMenus/itemStats.py index 6221fa1e0..46b87226f 100644 --- a/gui/builtinContextMenus/itemStats.py +++ b/gui/builtinContextMenus/itemStats.py @@ -63,7 +63,7 @@ class ItemStats(ContextMenu): size = wx.DefaultSize pos = wx.DefaultPosition ItemStatsDialog(stuff, fullContext, pos, size, maximized) - lastWnd.closeEvent(None) + lastWnd.Close() else: ItemStatsDialog(stuff, fullContext) diff --git a/gui/builtinItemStatsViews/attributeSlider.py b/gui/builtinItemStatsViews/attributeSlider.py index f26be8492..49cb776fa 100644 --- a/gui/builtinItemStatsViews/attributeSlider.py +++ b/gui/builtinItemStatsViews/attributeSlider.py @@ -104,7 +104,7 @@ class AttributeSlider(wx.Panel): elif mod > 1: modEnd = self.UserMaxValue slider_percentage = ((mod-1)/(modEnd-1)) * 100 - print(slider_percentage) + # print(slider_percentage) if self.inverse: slider_percentage *= -1 self.slider.SetValue(slider_percentage) diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index bc7cde36b..6081b3f24 100644 --- a/gui/builtinItemStatsViews/itemAttributes.py +++ b/gui/builtinItemStatsViews/itemAttributes.py @@ -7,8 +7,6 @@ import wx from .helpers import AutoListCtrl from gui.bitmap_loader import BitmapLoader -from service.market import Market -from service.attribute import Attribute from gui.utils.numberFormatter import formatAmount @@ -86,7 +84,8 @@ class ItemParams(wx.Panel): def RefreshValues(self, event): self._fetchValues() self.UpdateList() - event.Skip() + if event: + event.Skip() def ToggleViewMode(self, event): self.toggleView *= -1 @@ -215,14 +214,14 @@ class ItemParams(wx.Panel): if self.toggleView != 1: valueUnit = str(value) elif info and info.unit: - valueUnit = self.TranslateValueUnit(value, info.unit.displayName, info.unit.name) + valueUnit = self.FormatValue(*info.unit.TranslateValue(value)) else: valueUnit = formatAmount(value, 3, 0, 0) if self.toggleView != 1: valueUnitDefault = str(valueDefault) elif info and info.unit: - valueUnitDefault = self.TranslateValueUnit(valueDefault, info.unit.displayName, info.unit.name) + valueUnitDefault = self.FormatValue(*info.unit.TranslateValue(valueDefault)) else: valueUnitDefault = formatAmount(valueDefault, 3, 0, 0) @@ -237,44 +236,11 @@ class ItemParams(wx.Panel): self.Layout() @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(): - if not value: # some attributes come through with a value of 0? See #1387 - return "%d" % (value) - 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]) + def FormatValue(value, unit): + """Formats a value / unit combination into a string + @todo: move this to a more central location, since this is also used in the item mutator panel""" + if isinstance(value, (int, float)): + fvalue = formatAmount(value, 3, 0, 0) else: - return "%s %s" % (formatAmount(value, 3, 0), unitName) + fvalue = value + return "%s %s" % (fvalue, unit) diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py index 35d3b3401..ac76955b1 100644 --- a/gui/builtinItemStatsViews/itemMutator.py +++ b/gui/builtinItemStatsViews/itemMutator.py @@ -6,9 +6,15 @@ from .attributeSlider import AttributeSlider, EVT_VALUE_CHANGED import gui.mainFrame from gui.contextMenu import ContextMenu +from .itemAttributes import ItemParams from gui.bitmap_loader import BitmapLoader import gui.globalEvents as GE import gui.mainFrame +import random + +from logbook import Logger + +pyfalog = Logger(__name__) class ItemMutator(wx.Panel): @@ -26,8 +32,10 @@ class ItemMutator(wx.Panel): self.event_mapping = {} for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): - slider = AttributeSlider(self, m.baseValue, m.minMod, m.maxMod, not m.highIsGood) - slider.SetValue(m.value, False) + baseValueFormated = m.attribute.unit.TranslateValue(m.baseValue)[0] + valueFormated = m.attribute.unit.TranslateValue(m.value)[0] + slider = AttributeSlider(self, baseValueFormated, m.minMod, m.maxMod, not m.highIsGood) + slider.SetValue(valueFormated, False) slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue) self.event_mapping[slider] = m headingSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -50,14 +58,14 @@ class ItemMutator(wx.Panel): worse_range = max_t else: worse_range = min_t - - print("{}: \nHigh is good: {}".format(m.attribute.displayName, m.attribute.highIsGood)) - print("Value {}".format(m.baseValue)) - - print(min_t) - print(max_t) - print(better_range) - print(worse_range) + # + # print("{}: \nHigh is good: {}".format(m.attribute.displayName, m.attribute.highIsGood)) + # print("Value {}".format(m.baseValue)) + # + # print(min_t) + # print(max_t) + # print(better_range) + # print(worse_range) font = parent.GetFont() font.SetWeight(wx.BOLD) @@ -69,14 +77,15 @@ class ItemMutator(wx.Panel): headingSizer.Add(displayName, 3, wx.ALL | wx.EXPAND, 0) - range_low = wx.StaticText(self, wx.ID_ANY, "{} {}".format(worse_range[0], m.attribute.unit.displayName)) + + range_low = wx.StaticText(self, wx.ID_ANY, ItemParams.FormatValue(*m.attribute.unit.TranslateValue(worse_range[0]))) range_low.SetForegroundColour(self.goodColor if worse_range[2] else self.badColor) - range_high = wx.StaticText(self, wx.ID_ANY, "{} {}".format(better_range[0], m.attribute.unit.displayName)) + range_high = wx.StaticText(self, wx.ID_ANY, ItemParams.FormatValue(*m.attribute.unit.TranslateValue(better_range[0]))) range_high.SetForegroundColour(self.goodColor if better_range[2] else self.badColor) headingSizer.Add(range_low, 0, wx.ALL | wx.EXPAND, 0) - headingSizer.Add(wx.StaticText(self, wx.ID_ANY, " ── "), 0, wx.RIGHT | wx.LEFT | wx.EXPAND, 5) + headingSizer.Add(wx.StaticText(self, wx.ID_ANY, " ─ "), 0, wx.RIGHT | wx.LEFT | wx.EXPAND, 5) headingSizer.Add(range_high, 0, wx.RIGHT | wx.EXPAND, 10) mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5) @@ -91,8 +100,13 @@ class ItemMutator(wx.Panel): bSizer = wx.BoxSizer(wx.HORIZONTAL) - self.saveBtn = wx.Button(self, wx.ID_ANY, "Save Attributes", wx.DefaultPosition, wx.DefaultSize, 0) - bSizer.Add(self.saveBtn, 0, wx.ALIGN_CENTER_VERTICAL) + self.refreshBtn = wx.Button(self, wx.ID_ANY, "Reset defaults", wx.DefaultPosition, wx.DefaultSize, 0) + bSizer.Add(self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) + self.refreshBtn.Bind(wx.EVT_BUTTON, self.resetMutatedValues) + + self.randomBtn = wx.Button(self, wx.ID_ANY, "Random stats", wx.DefaultPosition, wx.DefaultSize, 0) + bSizer.Add(self.randomBtn, 0, wx.ALIGN_CENTER_VERTICAL) + self.randomBtn.Bind(wx.EVT_BUTTON, self.randomMutatedValues) mainSizer.Add(bSizer, 0, wx.RIGHT | wx.LEFT | wx.EXPAND, 0) @@ -102,19 +116,56 @@ class ItemMutator(wx.Panel): def changeMutatedValue(self, evt): m = self.event_mapping[evt.Object] value = evt.Value + value = m.attribute.unit.ComplicateValue(value) sFit = Fit.getInstance() sFit.changeMutatedValue(m, value) if self.timer: self.timer.Stop() self.timer = None + + for x in self.Parent.Children: + if isinstance(x, ItemParams): + x.RefreshValues(None) + break self.timer = wx.CallLater(1000, self.callLater) + def resetMutatedValues(self, evt): + sFit = Fit.getInstance() + + for slider, m in self.event_mapping.items(): + value = sFit.changeMutatedValue(m, m.baseValue) + value = m.attribute.unit.TranslateValue(value)[0] + slider.SetValue(value) + + evt.Skip() + + def randomMutatedValues(self, evt): + sFit = Fit.getInstance() + + for slider, m in self.event_mapping.items(): + value = random.uniform(m.minValue, m.maxValue) + value = sFit.changeMutatedValue(m, value) + value = m.attribute.unit.TranslateValue(value)[0] + slider.SetValue(value) + + evt.Skip() + def callLater(self): self.timer = None - print("recalc fit") sFit = Fit.getInstance() - sFit.refreshFit(self.activeFit) - # todo BUG: if fit is not currently active, this causes the changed fit to show...? - wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitID=self.activeFit)) + + # recalc the fit that this module affects. This is not necessarily the currently active fit + sFit.refreshFit(self.activeFit) + + mainFrame = gui.mainFrame.MainFrame.getInstance() + activeFit = mainFrame.getActiveFit() + + if activeFit != self.activeFit: + # if we're no longer on the fit this module is affecting, simulate a "switch fit" so that the active fit + # can be recalculated (if needed) + sFit.switchFit(activeFit) + + # Send signal to GUI to update stats with current active fit + wx.PostEvent(mainFrame, GE.FitChanged(fitID=activeFit)) diff --git a/gui/itemStats.py b/gui/itemStats.py index d25c670e7..6a75b543d 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -147,7 +147,7 @@ class ItemStatsDialog(wx.Dialog): ItemStatsDialog.counter -= 1 self.parentWnd.UnregisterStatsWindow(self) - self.Destroy() + event.Skip() class ItemStatsContainer(wx.Panel): @@ -166,7 +166,7 @@ class ItemStatsContainer(wx.Panel): if isinstance(stuff, Module) and stuff.isMutated: self.mutator = ItemMutator(self.nbContainer, stuff, item) - self.nbContainer.AddPage(self.mutator, "Multiplasmid") + self.nbContainer.AddPage(self.mutator, "Mutations") self.desc = ItemDescription(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.desc, "Description") diff --git a/service/fit.py b/service/fit.py index f09808f9c..dccb58e5e 100644 --- a/service/fit.py +++ b/service/fit.py @@ -379,7 +379,10 @@ class Fit(object): thing = eos.db.getItem(thing, eager=("attributes", "group.category")) - if isinstance(thing, FitType): + if isinstance(thing, es_Module): + thing = copy.deepcopy(thing) + fit.projectedModules.append(thing) + elif isinstance(thing, FitType): if thing in fit.projectedFits: return @@ -522,7 +525,7 @@ class Fit(object): mutator.value = value eos.db.commit() - #self.recalc(fit) + return mutator.value def appendModule(self, fitID, itemID): pyfalog.debug("Appending module for fit ({0}) using item: {1}", fitID, itemID) @@ -705,11 +708,12 @@ class Fit(object): cargo.amount -= 1 if not module.isEmpty: # if module is placeholder, we don't want to convert/add it - for x in fit.cargo.find(module.item): + moduleItem = module.item if not module.item.isAbyssal else module.baseItem + for x in fit.cargo.find(moduleItem ): x.amount += 1 break else: - moduleP = es_Cargo(module.item) + moduleP = es_Cargo(moduleItem ) moduleP.amount = 1 fit.cargo.insert(cargoIdx, moduleP)