From 9146c0f2c6b7a76282fcce44965b3f47b866b475 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 13 Nov 2019 13:31:40 +0300 Subject: [PATCH] Use ammo service to generate ammo switcher context menu --- gui/builtinContextMenus/moduleAmmoChange.py | 225 +++++--------------- service/ammo.py | 153 ++++++------- 2 files changed, 137 insertions(+), 241 deletions(-) diff --git a/gui/builtinContextMenus/moduleAmmoChange.py b/gui/builtinContextMenus/moduleAmmoChange.py index bdcf2dc6e..c9adc25b9 100644 --- a/gui/builtinContextMenus/moduleAmmoChange.py +++ b/gui/builtinContextMenus/moduleAmmoChange.py @@ -3,19 +3,17 @@ import wx import gui.fitCommands as cmd import gui.mainFrame -from eos.const import FittingHardpoint -from eos.saveddata.module import Module from gui.bitmap_loader import BitmapLoader from gui.contextMenu import ContextMenuCombined from gui.fitCommands.helpers import getSimilarModPositions +from service.ammo import Ammo from service.fit import Fit -from service.market import Market class ChangeModuleAmmo(ContextMenuCombined): - DAMAGE_TYPES = ("em", "explosive", "kinetic", "thermal") - MISSILE_ORDER = ("em", "thermal", "kinetic", "explosive", "mixed") + DAMAGE_TYPES = ('em', 'explosive', 'kinetic', 'thermal') + MISSILE_ORDER = ('em', 'thermal', 'kinetic', 'explosive', 'mixed') def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -23,13 +21,13 @@ class ChangeModuleAmmo(ContextMenuCombined): self.loadableCharges = {} def display(self, callingWindow, srcContext, mainItem, selection): - if srcContext not in ("fittingModule", "projectedModule"): + if srcContext not in ('fittingModule', 'projectedModule'): return False if self.mainFrame.getActiveFit() is None: return False - self.mainCharges = self.getChargesForMod(mainItem) + self.mainCharges = Ammo.getInstance().getModuleFlatAmmo(mainItem) if not self.mainCharges: return False @@ -39,186 +37,79 @@ class ChangeModuleAmmo(ContextMenuCombined): return True def getText(self, callingWindow, itmContext, mainItem, selection): - return "Charge" - - def getChargesForMod(self, mod): - sMkt = Market.getInstance() - if mod is None or mod.isEmpty: - return set() - typeID = mod.item.ID - if typeID in self.loadableCharges: - return self.loadableCharges[typeID] - chargeSet = self.loadableCharges.setdefault(typeID, set()) - # Do not try to grab it for modes which can also be passed as part of selection - if isinstance(mod, Module): - for charge in mod.getValidCharges(): - if sMkt.getPublicityByItem(charge): - chargeSet.add(charge) - return chargeSet - - def turretSorter(self, charge): - damage = 0 - range_ = (self.module.item.getAttribute("maxRange")) * \ - (charge.getAttribute("weaponRangeMultiplier") or 1) - falloff = (self.module.item.getAttribute("falloff") or 0) * \ - (charge.getAttribute("fallofMultiplier") or 1) - for type_ in self.DAMAGE_TYPES: - d = charge.getAttribute("%sDamage" % type_) - if d > 0: - damage += d - - # Take optimal and falloff as range factor - rangeFactor = range_ + falloff - - return - rangeFactor, charge.name.rsplit()[-2:], damage, charge.name - - def missileSorter(self, charge): - # Get charge damage type and total damage - chargeDamageType, totalDamage = self.damageInfo(charge) - # Find its position in sort list - position = self.MISSILE_ORDER.index(chargeDamageType) - return position, totalDamage, charge.name - - def damageInfo(self, charge): - # Set up data storage for missile damage stuff - damageMap = {} - totalDamage = 0 - # Fill them with the data about charge - for damageType in self.DAMAGE_TYPES: - currentDamage = charge.getAttribute("{0}Damage".format(damageType)) or 0 - damageMap[damageType] = currentDamage - totalDamage += currentDamage - # Detect type of ammo - chargeDamageType = None - for damageType in damageMap: - # If all damage belongs to certain type purely, set appropriate - # ammoType - if damageMap[damageType] == totalDamage: - chargeDamageType = damageType - break - # Else consider ammo as mixed damage - if chargeDamageType is None: - chargeDamageType = "mixed" - - return chargeDamageType, totalDamage - - @staticmethod - def numericConverter(string): - return int(string) if string.isdigit() else string - - def nameSorter(self, charge): - parts = charge.name.split(" ") - return list(map(self.numericConverter, parts)) + return 'Charge' def addCharge(self, menu, charge): id_ = ContextMenuCombined.nextID() - name = charge.name if charge is not None else "Empty" - self.chargeIds[id_] = charge + name = charge.name if charge is not None else 'Empty' + self.chargeEventMap[id_] = charge item = wx.MenuItem(menu, id_, name) menu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item) item.charge = charge if charge is not None and charge.iconID is not None: - bitmap = BitmapLoader.getBitmap(charge.iconID, "icons") + bitmap = BitmapLoader.getBitmap(charge.iconID, 'icons') if bitmap is not None: item.SetBitmap(bitmap) return item @staticmethod - def addSeperator(m, text): + def addSeparator(m, text): id_ = ContextMenuCombined.nextID() m.Append(id_, '─ %s ─' % text) m.Enable(id_, False) def getSubMenu(self, callingWindow, context, mainItem, selection, rootMenu, i, pitem): - msw = True if "wxMSW" in wx.PlatformInfo else False - m = wx.Menu() - self.chargeIds = {} - hardpoint = self.module.hardpoint - moduleName = self.module.item.name - # Make sure we do not consider mining turrets as combat turrets - if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None: - self.addSeperator(m, "Long Range") - items = [] - range_ = None - nameBase = None - sub = None - chargesSorted = sorted(self.mainCharges, key=self.turretSorter) - for charge in chargesSorted: - if "civilian" in charge.name.lower(): - continue - currBase = charge.name.rsplit()[-2:] - currRange = charge.getAttribute("weaponRangeMultiplier") - if nameBase is None or range_ != currRange or nameBase != currBase: - if sub is not None: - self.addSeperator(sub, "More Damage") - - sub = None - base = charge - nameBase = currBase - range_ = currRange - item = self.addCharge(rootMenu if msw else m, charge) - items.append(item) + msw = True if 'wxMSW' in wx.PlatformInfo else False + menu = wx.Menu() + self.chargeEventMap = {} + modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module) + if modType == 'ddTurret': + self.addSeparator(menu, 'Long Range') + menuItems = [] + for charges in chargeDict.values(): + if len(charges) == 1: + menuItems.append(self.addCharge(rootMenu if msw else menu, charges[0])) else: - if sub is None and item and base: - sub = wx.Menu() - sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch) - self.addSeperator(sub, "Less Damage") - item.SetSubMenu(sub) - sub.Append(self.addCharge(rootMenu if msw else sub, base)) - - sub.Append(self.addCharge(rootMenu if msw else sub, charge)) - - if sub is not None: - self.addSeperator(sub, "More Damage") - - for item in items: - m.Append(item) - - self.addSeperator(m, "Short Range") - elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher': - type_ = None - sub = None - defender = None - chargesSorted = sorted(self.mainCharges, key=self.missileSorter) - for charge in chargesSorted: - currType = self.damageInfo(charge)[0] - - if currType != type_ or type_ is None: - if sub is not None: - self.addSeperator(sub, "More Damage") - - type_ = currType - item = wx.MenuItem(m, wx.ID_ANY, type_.capitalize()) - bitmap = BitmapLoader.getBitmap("%s_small" % type_, "gui") - if bitmap is not None: - item.SetBitmap(bitmap) - - sub = wx.Menu() - sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch) - self.addSeperator(sub, "Less Damage") - item.SetSubMenu(sub) - m.Append(item) - - if charge.name not in ("Light Defender Missile I", "Heavy Defender Missile I"): - sub.Append(self.addCharge(rootMenu if msw else sub, charge)) - else: - defender = charge - - if defender is not None: - m.Append(self.addCharge(rootMenu if msw else m, defender)) - if sub is not None: - self.addSeperator(sub, "More Damage") - else: - chargesSorted = sorted(self.mainCharges, key=self.nameSorter) - for charge in chargesSorted: - m.Append(self.addCharge(rootMenu if msw else m, charge)) - - m.Append(self.addCharge(rootMenu if msw else m, None)) - return m + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + baseCharge = charges[0] + menuItem = self.addCharge(rootMenu if msw else menu, baseCharge) + menuItems.append(menuItem) + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem.SetSubMenu(subMenu) + self.addSeparator(subMenu, 'Less Damage') + for charge in charges: + subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) + self.addSeparator(subMenu, 'More Damage') + for menuItem in menuItems: + menu.Append(menuItem) + self.addSeparator(menu, 'Short Range') + elif modType == 'ddMissile': + menuItems = [] + for chargeCatName, charges in chargeDict.items(): + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem = wx.MenuItem(menu, wx.ID_ANY, chargeCatName.capitalize()) + menuItems.append(menuItem) + subMenu = wx.Menu() + subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch) + menuItem.SetSubMenu(subMenu) + self.addSeparator(subMenu, 'Less Damage') + for charge in charges: + subMenu.Append(self.addCharge(rootMenu if msw else subMenu, charge)) + self.addSeparator(subMenu, 'More Damage') + for menuItem in menuItems: + menu.Append(menuItem) + elif modType == 'general': + for charge in chargeDict['general']: + menu.Append(self.addCharge(rootMenu if msw else menu, charge)) + menu.Append(self.addCharge(rootMenu if msw else menu, None)) + return menu def handleAmmoSwitch(self, event): - charge = self.chargeIds.get(event.Id, False) + charge = self.chargeEventMap.get(event.Id, False) if charge is False: event.Skip() return @@ -254,7 +145,7 @@ class ChangeModuleAmmo(ContextMenuCombined): positions = [] for position, mod in enumerate(modContainer): if mod in self.selection: - modCharges = self.getChargesForMod(mod) + modCharges = Ammo.getInstance().getModuleFlatAmmo(mod) if modCharges.issubset(self.mainCharges): positions.append(position) self.mainFrame.command.Submit(command( diff --git a/service/ammo.py b/service/ammo.py index 6a866f2e1..cce6dba24 100644 --- a/service/ammo.py +++ b/service/ammo.py @@ -19,6 +19,7 @@ import math +from collections import OrderedDict from eos.const import FittingHardpoint from eos.saveddata.module import Module @@ -52,95 +53,99 @@ class Ammo: @classmethod def getModuleStructuredAmmo(cls, mod): - - def getChargeDamageInfo(charge): - # Set up data storage for missile damage stuff - damageMap = {} - totalDamage = 0 - # Fill them with the data about charge - for damageType in DmgTypes.names(): - currentDamage = charge.getAttribute('{}Damage'.format(damageType)) or 0 - damageMap[damageType] = currentDamage - totalDamage += currentDamage - # Detect type of ammo - chargeDamageType = None - for damageType in damageMap: - # If all damage belongs to certain type purely, set appropriate - # ammoType - if damageMap[damageType] == totalDamage: - chargeDamageType = damageType - break - # Else consider ammo as mixed damage - if chargeDamageType is None: - chargeDamageType = 'mixed' - return chargeDamageType, totalDamage - - def turretSorter(mod, charge): - damage = 0 - range_ = (mod.item.getAttribute('maxRange')) * \ - (charge.getAttribute('weaponRangeMultiplier') or 1) - falloff = (mod.item.getAttribute('falloff') or 0) * \ - (charge.getAttribute('fallofMultiplier') or 1) - for type_ in DmgTypes.names(): - d = charge.getAttribute('%sDamage' % type_) - if d > 0: - damage += d - # Take optimal and falloff as range factor - rangeFactor = range_ + falloff - return -rangeFactor, charge.name.rsplit()[-2:], damage, charge.name - - def missileSorter(mod, charge): - # Get charge damage type and total damage - chargeDamageType, totalDamage = getChargeDamageInfo(charge) - # Find its position in sort list - try: - position = DmgTypes.names().index(chargeDamageType) - # Put charges which have non-standard damage type after charges with - # standard damage type - except ValueError: - position = math.inf - return position, totalDamage, charge.name - - def nameSorter(charge): - parts = charge.name.split(" ") - return [int(p) if p.isdigit() else p for p in parts] - chargesFlat = cls.getModuleFlatAmmo(mod) # Make sure we do not consider mining turrets as combat turrets if mod.hardpoint == FittingHardpoint.TURRET and mod.getModifiedItemAttr('miningAmount', None) is None: - all = [] + + def turretSorter(charge): + damage = 0 + range_ = (mod.item.getAttribute('maxRange')) * \ + (charge.getAttribute('weaponRangeMultiplier') or 1) + falloff = (mod.item.getAttribute('falloff') or 0) * \ + (charge.getAttribute('fallofMultiplier') or 1) + for type_ in DmgTypes.names(): + d = charge.getAttribute('%sDamage' % type_) + if d > 0: + damage += d + # Take optimal and falloff as range factor + rangeFactor = range_ + falloff + return -rangeFactor, charge.name.rsplit()[-2:], damage, charge.name + + all = OrderedDict() sub = [] prevNameBase = None prevRange = None for charge in sorted(chargesFlat, key=turretSorter): if 'civilian' in charge.name.lower(): continue - currNameBase = charge.name.rsplit()[-2:] + currNameBase = ' '.join(charge.name.rsplit()[-2:]) currRange = charge.getAttribute('weaponRangeMultiplier') - if prevNameBase is None or currRange != prevRange or currNameBase != prevNameBase: - if sub: - all.append(sub) - sub = [] - sub.append(charge) - prevNameBase = currNameBase - prevRange = currRange - else: - sub.append(charge) + if sub and (currRange != prevRange or currNameBase != prevNameBase): + all[prevNameBase] = sub + sub = [] + sub.append(charge) + prevNameBase = currNameBase + prevRange = currRange + else: + if sub: + all[prevNameBase] = sub return 'ddTurret', all + elif mod.hardpoint == FittingHardpoint.MISSILE and mod.item.name != 'Festival Launcher': - all = [] + + def getChargeDamageInfo(charge): + # Set up data storage for missile damage stuff + damageMap = {} + totalDamage = 0 + # Fill them with the data about charge + for damageType in DmgTypes.names(): + currentDamage = charge.getAttribute('{}Damage'.format(damageType)) or 0 + damageMap[damageType] = currentDamage + totalDamage += currentDamage + # Detect type of ammo + chargeDamageType = None + for damageType in damageMap: + # If all damage belongs to certain type purely, set appropriate + # ammoType + if damageMap[damageType] == totalDamage: + chargeDamageType = damageType + break + # Else consider ammo as mixed damage + if chargeDamageType is None: + chargeDamageType = 'mixed' + return chargeDamageType, totalDamage + + def missileSorter(charge): + # Get charge damage type and total damage + chargeDamageType, totalDamage = getChargeDamageInfo(charge) + # Find its position in sort list + try: + position = DmgTypes.names().index(chargeDamageType) + # Put charges which have non-standard damage type after charges with + # standard damage type + except ValueError: + position = math.inf + return position, totalDamage, charge.name + + all = OrderedDict() sub = [] prevType = None for charge in sorted(chargesFlat, key=missileSorter): currType = getChargeDamageInfo(charge)[0] - if prevType is None or currType != prevType: - if sub: - all.append(sub) - sub = [] - sub.append(charge) - prevType = currType - else: - sub.append(charge) + if sub and currType != prevType: + all[prevType] = sub + sub = [] + sub.append(charge) + prevType = currType + else: + if sub: + all[prevType] = sub return 'ddMissile', all + else: - return 'general', sorted(chargesFlat, key=nameSorter) + + def nameSorter(charge): + parts = charge.name.split(" ") + return [int(p) if p.isdigit() else p for p in parts] + + return 'general', {'general': sorted(chargesFlat, key=nameSorter)}