Merge branch 'ammo_graph' into singularity

# Conflicts:
#	gui/targetProfileEditor.py
This commit is contained in:
DarkPhoenix
2020-06-09 19:04:20 +03:00
28 changed files with 582 additions and 232 deletions

View File

@@ -26,6 +26,8 @@ from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, re
class BaseEffect:
dealsDamage = False
@staticmethod
def handler(fit, module, context, projectionRange, **kwargs):
pass
@@ -62,6 +64,7 @@ class Effect10(BaseEffect):
Modules from group: Energy Weapon (212 of 214)
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -169,6 +172,7 @@ class Effect34(BaseEffect):
Modules from group: Projectile Weapon (165 of 165)
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -189,6 +193,7 @@ class Effect38(BaseEffect):
Modules from group: Smart Bomb (118 of 118)
"""
dealsDamage = True
type = 'active'
@@ -555,6 +560,7 @@ class Effect101(BaseEffect):
Structure Modules named like: Standup Launcher (7 of 7)
"""
dealsDamage = True
type = 'active', 'projected'
@staticmethod
@@ -15124,6 +15130,7 @@ class Effect4489(BaseEffect):
Module: 'Judgment' Electromagnetic Doomsday
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -15140,6 +15147,7 @@ class Effect4490(BaseEffect):
Module: 'Oblivion' Kinetic Doomsday
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -15156,6 +15164,7 @@ class Effect4491(BaseEffect):
Module: 'Aurora Ominae' Thermal Doomsday
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -15172,6 +15181,7 @@ class Effect4492(BaseEffect):
Module: 'Gjallarhorn' Explosive Doomsday
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -27364,6 +27374,7 @@ class Effect6431(BaseEffect):
Fighters from group: Light Fighter (32 of 32)
"""
dealsDamage = True
displayName = 'Missile Attack'
hasCharges = True
prefix = 'fighterAbilityMissiles'
@@ -27644,6 +27655,7 @@ class Effect6465(BaseEffect):
Fighters from group: Heavy Fighter (34 of 34)
"""
dealsDamage = True
displayName = 'Turret Attack'
prefix = 'fighterAbilityAttackMissile'
type = 'active'
@@ -27686,6 +27698,7 @@ class Effect6472(BaseEffect):
Modules named like: Lance (4 of 4)
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -27702,6 +27715,7 @@ class Effect6473(BaseEffect):
Module: Bosonic Field Generator
"""
dealsDamage = True
type = 'active'
@staticmethod
@@ -27912,6 +27926,7 @@ class Effect6485(BaseEffect):
Fighters from group: Heavy Fighter (16 of 34)
"""
dealsDamage = True
displayName = 'Bomb'
hasCharges = True
prefix = 'fighterAbilityLaunchBomb'
@@ -33964,6 +33979,7 @@ class Effect6995(BaseEffect):
Modules from group: Precursor Weapon (19 of 19)
"""
dealsDamage = True
type = 'active'
@staticmethod

View File

@@ -146,6 +146,12 @@ class Effect(EqBase):
return self.__effectDef is not None
@property
def dealsDamage(self):
if not self.__generated:
self.__generateHandler()
return self.__dealsDamage
def isType(self, type):
"""
Check if this effect is of the passed type
@@ -167,6 +173,7 @@ class Effect(EqBase):
self.__handler = getattr(effectDef, "handler", eos.effects.BaseEffect.handler)
self.__runTime = getattr(effectDef, "runTime", "normal")
self.__activeByDefault = getattr(effectDef, "activeByDefault", True)
self.__dealsDamage = effectDef.dealsDamage
effectType = getattr(effectDef, "type", None)
effectType = effectType if isinstance(effectType, tuple) or effectType is None else (effectType,)
self.__type = effectType
@@ -175,6 +182,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.debug("ImportError generating handler: {0}", e)
except AttributeError as e:
@@ -182,6 +190,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.error("AttributeError generating handler: {0}", e)
except (KeyboardInterrupt, SystemExit):
@@ -190,6 +199,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.critical("Exception generating handler:")
pyfalog.critical(e)

View File

@@ -364,3 +364,11 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.item.groupID in fitDroneGroupLimits:
return True
return False
def canDealDamage(self, ignoreState=False):
if self.item is None:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (ignoreState or self.amountActive > 0):
return True
return False

View File

@@ -441,3 +441,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False
return True
def canDealDamage(self, ignoreState=False, ignoreAbilityState=False):
if self.item is None:
return False
if not self.active and not ignoreState:
return False
for ability in self.abilities:
if not ability.active and not ignoreAbilityState:
continue
if ability.effect.dealsDamage:
return True
return False

View File

@@ -461,6 +461,20 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return True
return False
def canDealDamage(self, ignoreState=False):
if self.isEmpty:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (
ignoreState or
effect.isType('offline') or
(effect.isType('passive') and self.state >= FittingModuleState.ONLINE) or
(effect.isType('active') and self.state >= FittingModuleState.ACTIVE) or
(effect.isType('overheat') and self.state >= FittingModuleState.OVERHEATED)
):
return True
return False
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return {0: DmgTypes(0, 0, 0, 0)}

View File

@@ -27,7 +27,7 @@ import gui.globalEvents as GE
import gui.mainFrame
from graphs.data.base import FitGraph
from graphs.events import RESIST_MODE_CHANGED
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings

View File

@@ -22,7 +22,7 @@
import wx
class AuxiliaryFrame(wx.Frame):
class AuxiliaryMixin:
_instance = None
@@ -55,14 +55,26 @@ class AuxiliaryFrame(wx.Frame):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
@classmethod
def openOne(cls, parent, *args, **kwargs):
def openOne(cls, parent, *args, forceReopen=False, **kwargs):
"""If window is open and alive - raise it, open otherwise"""
if not cls._instance:
if not cls._instance or forceReopen:
if cls._instance:
cls._instance.Close()
frame = cls(parent, *args, **kwargs)
cls._instance = frame
frame.Show()
else:
cls._instance.Raise()
return cls._instance
def OnSuppressedAction(self, event):
return
class AuxiliaryFrame(AuxiliaryMixin, wx.Frame):
pass
class AuxiliaryDialog(AuxiliaryMixin, wx.Dialog):
pass

View File

@@ -36,6 +36,11 @@ import gui.fitCommands as cmd
from gui.fitCommands.helpers import droneStackLimit
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
class DroneViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(DroneViewDrop, self).__init__(*args, **kwargs)
@@ -186,17 +191,13 @@ class DroneView(Display):
self.mainFrame.command.Submit(cmd.GuiMergeLocalDroneStacksCommand(
fitID=fitID, srcPosition=srcPosition, dstPosition=dstPosition))
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
def droneKey(self, drone):
@staticmethod
def droneKey(drone):
sMkt = Market.getInstance()
groupName = sMkt.getMarketGroupByItem(drone.item).name
return (self.DRONE_ORDER.index(groupName),
drone.item.name)
return (DRONE_ORDER.index(groupName), drone.item.name)
def fitChanged(self, event):
event.Skip()

View File

@@ -34,6 +34,9 @@ from service.fit import Fit
from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
class FighterViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(FighterViewDrop, self).__init__(*args, **kwargs)
@@ -250,11 +253,10 @@ class FighterDisplay(d.Display):
def _merge(src, dst):
return
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
def fighterKey(self, fighter):
@staticmethod
def fighterKey(fighter):
groupName = Market.getInstance().getGroupByItem(fighter.item).name
orderPos = self.FIGHTER_ORDER.index(groupName)
orderPos = FIGHTER_ORDER.index(groupName)
# Sort support fighters by name, ignore their abilities
if groupName == 'Support Fighter':
abilityEffectIDs = ()

View File

@@ -6,6 +6,7 @@ from gui.builtinContextMenus import fitAddCurrentlyOpen
from gui.builtinContextMenus import envEffectAdd
from gui.builtinContextMenus import commandFitAdd
from gui.builtinContextMenus.targetProfile import adder
from gui.builtinContextMenus import graphFitAmmoPicker
# Often-used item manipulations
from gui.builtinContextMenus import shipModeChange
from gui.builtinContextMenus import moduleAmmoChange

View File

@@ -0,0 +1,241 @@
# noinspection PyPackageRequirements
import wx
import gui.mainFrame
from gui.auxWindow import AuxiliaryDialog
from gui.contextMenu import ContextMenuSingle
from service.ammo import Ammo
from service.market import Market
class GraphFitAmmoPicker(ContextMenuSingle):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if srcContext != 'graphFitList':
return False
if mainItem is None or not mainItem.isFit:
return False
if callingWindow.graphFrame.getView().internalName != 'dmgStatsGraph':
return False
return True
def getText(self, callingWindow, itmContext, mainItem):
return 'Plot with Different Ammo...'
def activate(self, callingWindow, fullContext, mainItem, i):
AmmoPickerFrame.openOne(callingWindow, mainItem.item, forceReopen=True)
GraphFitAmmoPicker.register()
class AmmoPickerFrame(AuxiliaryDialog):
def __init__(self, parent, fit):
super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True)
padding = 5
mainSizer = wx.BoxSizer(wx.VERTICAL)
contents = AmmoPickerContents(self, fit)
mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, padding)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer:
mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, padding)
self.SetSizer(mainSizer)
self.Layout()
contW, contH = contents.GetVirtualSize()
bestW = contW + padding * 2
bestH = contH + padding * 2
if buttonSizer:
# Yeah right... whatever
buttW, buttH = buttonSizer.GetSize()
bestW = max(bestW, buttW + padding * 2)
bestH += buttH + padding * 2
bestW = min(1000, bestW)
bestH = min(700, bestH)
self.SetSize(bestW, bestH)
self.SetMinSize(wx.Size(int(bestW * 0.7), int(bestH * 0.7)))
self.CenterOnParent()
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
def kbEvent(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE:
self.Close()
return
event.Skip()
class AmmoPickerContents(wx.ScrolledCanvas):
indent = 15
def __init__(self, parent, fit):
wx.ScrolledCanvas.__init__(self, parent)
self.SetScrollRate(0, 15)
mods = self.getMods(fit)
drones = self.getDrones(fit)
fighters = self.getFighters(fit)
self.rbLabelMap = {}
self.rbCheckboxMap = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
moduleSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(moduleSizer, 0, wx.ALL, 0)
self.droneSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.droneSizer, 0, wx.ALL, 0)
fighterSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(fighterSizer, 0, wx.ALL, 0)
firstRadio = True
for modInfo, modAmmo in mods:
text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo)
modRb = self.addRadioButton(moduleSizer, text, firstRadio)
firstRadio = False
# Get actual module, as ammo getters need it
mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None)
_, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod)
if len(ammoTree) == 1:
for ammoCatName, ammos in ammoTree.items():
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
for ammoCatName, ammos in ammoTree.items():
if len(ammos) == 1:
ammo = next(iter(ammos))
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
self.addLabel(moduleSizer, '{}:'.format(ammoCatName), modRb, indentLvl=1)
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=2)
if drones:
droneRb = self.addRadioButton(self.droneSizer, 'Drones', firstRadio)
from gui.builtinAdditionPanes.droneView import DroneView
for drone in sorted(drones, key=DroneView.droneKey):
self.addCheckbox(self.droneSizer, '{}x {}'.format(drone.amount, drone.item.name), droneRb, indentLvl=1)
addBtn = wx.Button(self, wx.ID_ANY, '+', style=wx.BU_EXACTFIT)
addBtn.Bind(wx.EVT_BUTTON, self.OnDroneGroupAdd)
mainSizer.Add(addBtn, 0, wx.LEFT, self.indent)
if fighters:
fighterRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio)
from gui.builtinAdditionPanes.fighterView import FighterDisplay
for fighter in sorted(fighters, key=FighterDisplay.fighterKey):
self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), fighterRb, indentLvl=1)
self.SetSizer(mainSizer)
self.refreshStatus()
def addRadioButton(self, sizer, text, firstRadio=False):
if firstRadio:
rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP)
rb.SetValue(True)
else:
rb = wx.RadioButton(self, wx.ID_ANY, text)
rb.SetValue(False)
rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected)
sizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0)
return rb
def addCheckbox(self, sizer, text, currentRb, indentLvl=0):
cb = wx.CheckBox(self, -1, text)
sizer.Add(cb, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbCheckboxMap.setdefault(currentRb, []).append(cb)
def addLabel(self, sizer, text, currentRb, indentLvl=0):
text = text[0].capitalize() + text[1:]
label = wx.StaticText(self, wx.ID_ANY, text)
sizer.Add(label, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbLabelMap.setdefault(currentRb, []).append(label)
def getMods(self, fit):
sMkt = Market.getInstance()
sAmmo = Ammo.getInstance()
loadableChargesCache = {}
# Modules, format: {frozenset(ammo): {item: count}}
modsPrelim = {}
if fit is not None:
for mod in fit.modules:
if not mod.canDealDamage():
continue
typeID = mod.item.ID
if typeID not in loadableChargesCache:
loadableChargesCache[typeID] = sAmmo.getModuleFlatAmmo(mod)
charges = loadableChargesCache[typeID]
# We're not interested in modules which contain no charges
if charges:
data = modsPrelim.setdefault(frozenset(charges), {})
if mod.item not in data:
data[mod.item] = 0
data[mod.item] += 1
# Format: [([(item, count), ...], frozenset(ammo)), ...]
modsFinal = []
for charges, itemCounts in modsPrelim.items():
modsFinal.append((
# Sort items within group
sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0], reverseMktGrp=True), reverse=True),
charges))
# Sort item groups
modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0], reverseMktGrp=True), reverse=True)
return modsFinal
def getDrones(self, fit):
drones = []
if fit is not None:
for drone in fit.drones:
if drone.item is None:
continue
# Drones are our "ammo", so we want to pick even those which are inactive
if drone.canDealDamage(ignoreState=True):
drones.append(drone)
continue
if {'remoteWebifierEntity', 'remoteTargetPaintEntity'}.intersection(drone.item.effects):
drones.append(drone)
continue
return drones
def getFighters(self, fit):
fighters = []
if fit is not None:
for fighter in fit.fighters:
if fighter.item is None:
continue
# Fighters are our "ammo" as well
if fighter.canDealDamage(ignoreState=True):
fighters.append(fighter)
continue
for ability in fighter.abilities:
if not ability.active:
continue
if ability.effect.name == 'fighterAbilityStasisWebifier':
fighters.append(fighter)
break
return fighters
def OnDroneGroupAdd(self, event):
event.Skip()
sizer = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText()
self.droneSizer.Add(sizer, 0, wx.EXPAND | wx.LEFT, self.indent)
def refreshStatus(self):
for map in (self.rbLabelMap, self.rbCheckboxMap):
for rb, items in map.items():
for item in items:
item.Enable(rb.GetValue())
def rbSelected(self, event):
event.Skip()
self.refreshStatus()

View File

@@ -3,33 +3,28 @@ 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")
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
# Format: {type ID: set(loadable, charges)}
self.loadableCharges = {}
self.loadableChargesCache = {}
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 = self._getAmmo(mainItem)
if not self.mainCharges:
return False
@@ -39,186 +34,81 @@ class ChangeModuleAmmo(ContextMenuCombined):
return True
def getText(self, callingWindow, itmContext, mainItem, selection):
return "Charge"
return 'Charge'
def getChargesForMod(self, mod):
sMkt = Market.getInstance()
if mod is None or mod.isEmpty:
def _getAmmo(self, mod):
if mod.itemID is None:
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
if mod.itemID not in self.loadableChargesCache:
self.loadableChargesCache[mod.itemID] = Ammo.getInstance().getModuleFlatAmmo(mod)
return self.loadableChargesCache[mod.itemID]
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))
def addCharge(self, menu, 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, ammo=self.mainCharges)
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
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():
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 +144,7 @@ class ChangeModuleAmmo(ContextMenuCombined):
positions = []
for position, mod in enumerate(modContainer):
if mod in self.selection:
modCharges = self.getChargesForMod(mod)
modCharges = self._getAmmo(mod)
if modCharges.issubset(self.mainCharges):
positions.append(position)
self.mainFrame.command.Submit(command(

View File

@@ -35,7 +35,7 @@ class ItemDescription(wx.Panel):
self.Layout()
self.description.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.description.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.description.Bind(wx.EVT_KEY_UP, self.onKeyUp)
self.popupMenu = wx.Menu()
copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy')
@@ -50,7 +50,7 @@ class ItemDescription(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -14,7 +14,7 @@ class ItemTraits(wx.Panel):
self.traits.SetPage(item.traits.traitText)
self.traits.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.traits.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.traits.Bind(wx.EVT_KEY_UP, self.onKeyUp)
mainSizer.Add(self.traits, 1, wx.ALL | wx.EXPAND, 0)
self.Layout()
@@ -32,7 +32,7 @@ class ItemTraits(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -203,22 +203,6 @@ class ItemView(Display):
self.setToggles()
self.filterItemStore()
def itemSort(self, item):
sMkt = self.sMkt
catname = sMkt.getCategoryByItem(item).name
try:
mktgrpid = sMkt.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
parentname = sMkt.getParentItemByItem(item).name
# Get position of market group
metagrpid = sMkt.getMetaGroupIdByItem(item)
metatab = sMkt.META_MAP_REVERSE_INDICES.get(metagrpid)
metalvl = item.metaLevel or 0
return catname, mktgrpid, parentname, metatab, metalvl, item.name
def contextMenu(self, event):
clickedPos = self.getRowByAbs(event.Position)
self.ensureSelection(clickedPos)
@@ -241,7 +225,7 @@ class ItemView(Display):
self.unselectAll()
# Perform sorting, using item's meta levels besides other stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
# Mark current item list as active
self.active = items
# Show them
@@ -251,12 +235,10 @@ class ItemView(Display):
if len(items) > 1:
# Re-sort stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
for i, item in enumerate(items[:9]):
# set shortcut info for first 9 modules
item.marketShortcut = i + 1
Display.refresh(self, items)
def columnBackground(self, colItem, item):

View File

@@ -86,7 +86,7 @@ class PFSearchBox(wx.Window):
def OnKeyPress(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.EditBox)
HandleCtrlBackspace(self.EditBox)
else:
event.Skip()

View File

@@ -166,7 +166,7 @@ class FittingView(d.Display):
self.hoveredRow = None
self.hoveredColumn = None
self.Bind(wx.EVT_KEY_DOWN, self.kbEvent)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
self.Bind(wx.EVT_LEFT_DOWN, self.click)
self.Bind(wx.EVT_RIGHT_DOWN, self.click)
self.Bind(wx.EVT_MIDDLE_DOWN, self.click)

View File

@@ -34,7 +34,7 @@ from wx.lib.agw.floatspin import FloatSpin
import config
import gui.globalEvents as GE
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor, TextEntryValidatedDialog
from gui.builtinViews.implantEditor import BaseImplantEditorView

View File

@@ -26,7 +26,7 @@ import wx
from logbook import Logger
import eos.db
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.builtinShipBrowser.events import FitSelected

View File

@@ -26,7 +26,7 @@ import wx
from logbook import Logger
import config
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from service.prereqsCheck import version_block

View File

@@ -9,7 +9,7 @@ import config
import gui.globalEvents as GE
from eos.db import getItem
from eos.saveddata.cargo import Cargo
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.display import Display
from gui.characterEditor import APIView
from service.character import Character

View File

@@ -23,7 +23,7 @@ import wx
import config
import gui.mainFrame
from eos.saveddata.module import Module
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
from gui.builtinItemStatsViews.itemAttributes import ItemParams

View File

@@ -21,7 +21,7 @@
import wx
from logbook import Logger
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor
from gui.utils.clipboard import fromClipboard, toClipboard

View File

@@ -10,7 +10,7 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.display as d
import gui.globalEvents as GE
from eos.db.gamedata.queries import getAttributeInfo, getItem
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.marketBrowser import SearchBox
from service.fit import Fit
@@ -213,19 +213,7 @@ class ItemView(d.Display):
def itemSort(self, item):
sMkt = Market.getInstance()
isFittable = item.group.name in sMkt.FIT_GROUPS or item.category.name in sMkt.FIT_CATEGORIES
catname = sMkt.getCategoryByItem(item).name
try:
mktgrpid = sMkt.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
parentname = sMkt.getParentItemByItem(item).name
# Get position of market group
metagrpid = sMkt.getMetaGroupIdByItem(item)
metatab = sMkt.META_MAP_REVERSE_INDICES.get(metagrpid)
metalvl = item.metaLevel or 0
return not isFittable, catname, mktgrpid, parentname, metatab, metalvl, item.name
return (not isFittable, *sMkt.itemSort(item))
def populateSearch(self, itemIDs):
items = Market.getItems(itemIDs)

View File

@@ -21,7 +21,7 @@
import wx
from logbook import Logger
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.builtinViews.entityEditor import BaseValidator, EntityEditor
from gui.builtinViews.implantEditor import BaseImplantEditorView
from gui.utils.clipboard import fromClipboard, toClipboard

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# ==============================================================================
# Copyright (C) 2010 Diego Duclos
#

151
service/ammo.py Normal file
View File

@@ -0,0 +1,151 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import math
from collections import OrderedDict
from eos.const import FittingHardpoint
from eos.saveddata.module import Module
from eos.utils.stats import DmgTypes
from service.market import Market
class Ammo:
instance = None
@classmethod
def getInstance(cls):
if cls.instance is None:
cls.instance = Ammo()
return cls.instance
@staticmethod
def getModuleFlatAmmo(mod):
sMkt = Market.getInstance()
if mod is None or mod.isEmpty:
return set()
chargeSet = set()
# Do not try to grab it for t3d 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
@classmethod
def getModuleStructuredAmmo(cls, mod, ammo=None):
chargesFlat = cls.getModuleFlatAmmo(mod) if ammo is None else ammo
# Make sure we do not consider mining turrets as combat turrets
if mod.hardpoint == FittingHardpoint.TURRET and mod.getModifiedItemAttr('miningAmount', None) is None:
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 = ' '.join(charge.name.rsplit()[-2:])
currRange = charge.getAttribute('weaponRangeMultiplier')
if sub and (currRange != prevRange or currNameBase != prevNameBase):
all[sub[0].name] = sub
sub = []
sub.append(charge)
prevNameBase = currNameBase
prevRange = currRange
else:
if sub:
all[sub[0].name] = sub
return 'ddTurret', all
elif mod.hardpoint == FittingHardpoint.MISSILE and mod.item.name != 'Festival Launcher':
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 sub and currType != prevType:
all[prevType] = sub
sub = []
sub.append(charge)
prevType = currType
else:
if sub:
all[prevType] = sub
return 'ddMissile', all
else:
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)}

View File

@@ -407,6 +407,12 @@ class Market:
self.META_MAP["normal"] = frozenset((0, *(mg.ID for mg in eos.db.getMetaGroups() if mg.ID not in nonNormalMetas)))
self.META_MAP.move_to_end("normal", last=False)
self.META_MAP_REVERSE = {sv: k for k, v in self.META_MAP.items() for sv in v}
self.META_MAP_REVERSE_GROUPED = {}
i = 0
for mgids in self.META_MAP.values():
for mgid in mgids:
self.META_MAP_REVERSE_GROUPED[mgid] = i
i += 1
self.META_MAP_REVERSE_INDICES = self.__makeReverseMetaMapIndices()
self.SEARCH_CATEGORIES = (
"Drone",
@@ -939,3 +945,19 @@ class Market:
while len(recentlyUsedModules) >= 20:
recentlyUsedModules.pop(-1)
recentlyUsedModules.insert(0, itemID)
def itemSort(self, item, reverseMktGrp=False):
catname = self.getCategoryByItem(item).name
try:
mktgrpid = self.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
if reverseMktGrp:
mktgrpid = -mktgrpid
parentname = self.getParentItemByItem(item).name
# Get position of market group
metagrpid = self.getMetaGroupIdByItem(item)
metatab = self.META_MAP_REVERSE_GROUPED.get(metagrpid)
metalvl = item.metaLevel or 0
return catname, mktgrpid, parentname, metatab, metalvl, item.name