diff --git a/config.py b/config.py index 0b8e4128d..495bc7fe6 100644 --- a/config.py +++ b/config.py @@ -21,7 +21,7 @@ evemonMinVersion = "4081" # Database version (int ONLY) # Increment every time we need to flag for user database upgrade/modification -dbversion = 2 +dbversion = 3 pyfaPath = None savePath = None diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 9113c9121..bedb917d6 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -40,6 +40,15 @@ gamedata_meta = MetaData() gamedata_meta.bind = gamedata_engine gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)() +# This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new +# game db because we haven't reached gamedata_meta.create_all() +try: + config.gamedata_version = gamedata_session.execute( + "SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'" + ).fetchone()[0] +except: + config.gamedata_version = None + saveddata_connectionstring = config.saveddata_connectionstring if saveddata_connectionstring is not None: if callable(saveddata_connectionstring): @@ -75,3 +84,4 @@ if config.saveddata_connectionstring == "sqlite:///:memory:": def rollback(): with sd_lock: saveddata_session.rollback() + diff --git a/eos/db/migrations/upgrade3.py b/eos/db/migrations/upgrade3.py new file mode 100644 index 000000000..5467fdf2d --- /dev/null +++ b/eos/db/migrations/upgrade3.py @@ -0,0 +1,13 @@ +""" +Migration 3 + +- Adds mode column for fits (t3 dessy) +""" + +import sqlalchemy + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT mode FROM fits LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE fits ADD COLUMN modeID INTEGER") diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index b06b8f924..6dfea588b 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -40,7 +40,9 @@ fits_table = Table("fits", saveddata_meta, Column("characterID", ForeignKey("characters.ID"), nullable = True), Column("damagePatternID", ForeignKey("damagePatterns.ID"), nullable=True), Column("booster", Boolean, nullable = False, index = True, default = 0), - Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True)) + Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True), + Column("modeID", Integer, nullable=True), +) projectedFits_table = Table("projectedFits", saveddata_meta, Column("sourceID", ForeignKey("fits.ID"), primary_key = True), diff --git a/eos/effects/freighteragilitybonus2o2.py b/eos/effects/freighteragilitybonus2o2.py new file mode 100644 index 000000000..7364d2015 --- /dev/null +++ b/eos/effects/freighteragilitybonus2o2.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("ORE Freighter").level + fit.ship.boostItemAttr("shipMaintenanceBayCapacity", ship.getModifiedItemAttr("freighterBonusO1")*level) diff --git a/eos/effects/freightersmacapacitybonuso1.py b/eos/effects/freightersmacapacitybonuso1.py new file mode 100644 index 000000000..4b1e591e8 --- /dev/null +++ b/eos/effects/freightersmacapacitybonuso1.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("ORE Freighter").level + fit.ship.boostItemAttr("agility", ship.getModifiedItemAttr("freighterBonusO2")*level, + stackingPenalties = True) diff --git a/eos/effects/modeagilitypostdiv.py b/eos/effects/modeagilitypostdiv.py new file mode 100644 index 000000000..8251e2852 --- /dev/null +++ b/eos/effects/modeagilitypostdiv.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, module, context): + fit.ship.multiplyItemAttr("agility", 1/module.getModifiedItemAttr("modeAgilityPostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/modearmorresonancepostdiv.py b/eos/effects/modearmorresonancepostdiv.py new file mode 100644 index 000000000..56e4bef1e --- /dev/null +++ b/eos/effects/modearmorresonancepostdiv.py @@ -0,0 +1,11 @@ +type = "passive" +def handler(fit, module, context): + for resType in ("Em", "Explosive", "Kinetic"): + fit.ship.multiplyItemAttr("armor{0}DamageResonance".format(resType), + 1/module.getModifiedItemAttr("mode{0}ResistancePostDiv".format(resType)), + stackingPenalties = True, penaltyGroup="postDiv") + + # Thermal != Thermic + fit.ship.multiplyItemAttr("armorThermalDamageResonance", + 1/module.getModifiedItemAttr("modeThermicResistancePostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/modesigradiuspostdiv.py b/eos/effects/modesigradiuspostdiv.py new file mode 100644 index 000000000..20e530328 --- /dev/null +++ b/eos/effects/modesigradiuspostdiv.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, module, context): + level = fit.character.getSkill("Minmatar Destroyer").level + fit.ship.multiplyItemAttr("signatureRadius", 1/module.getModifiedItemAttr("modeSignatureRadiusPostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/modevelocitypostdiv.py b/eos/effects/modevelocitypostdiv.py new file mode 100644 index 000000000..91dc341cd --- /dev/null +++ b/eos/effects/modevelocitypostdiv.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, module, context): + fit.ship.multiplyItemAttr("maxVelocity", 1/module.getModifiedItemAttr("modeVelocityPostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py b/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py new file mode 100644 index 000000000..026fb0818 --- /dev/null +++ b/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, ship, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Astrometrics"), + "cpu", ship.getModifiedItemAttr("roleBonusTacticalDestroyer1")) diff --git a/eos/effects/shipheatdamageamarrtacticaldestroyer3.py b/eos/effects/shipheatdamageamarrtacticaldestroyer3.py new file mode 100644 index 000000000..9585d71c7 --- /dev/null +++ b/eos/effects/shipheatdamageamarrtacticaldestroyer3.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Amarr Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: True, "heatDamage", + ship.getModifiedItemAttr("shipBonusTacticalDestroyerAmarr3") * level) diff --git a/eos/effects/shipmodemaxtargetrangepostdiv.py b/eos/effects/shipmodemaxtargetrangepostdiv.py new file mode 100644 index 000000000..0a7d0304a --- /dev/null +++ b/eos/effects/shipmodemaxtargetrangepostdiv.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, module, context): + fit.ship.multiplyItemAttr("maxTargetRange", 1/module.getModifiedItemAttr("modeMaxTargetRangePostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/shipmodescanrespostdiv.py b/eos/effects/shipmodescanrespostdiv.py new file mode 100644 index 000000000..2df22198c --- /dev/null +++ b/eos/effects/shipmodescanrespostdiv.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, module, context): + fit.ship.multiplyItemAttr("scanResolution", 1/module.getModifiedItemAttr("modeScanResPostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/shipmodescanstrengthpostdiv.py b/eos/effects/shipmodescanstrengthpostdiv.py new file mode 100644 index 000000000..3dbc07954 --- /dev/null +++ b/eos/effects/shipmodescanstrengthpostdiv.py @@ -0,0 +1,4 @@ +type = "passive" +def handler(fit, module, context): + fit.ship.multiplyItemAttr("scanRadarStrength", 1/module.getModifiedItemAttr("modeRadarStrengthPostDiv"), + stackingPenalties = True, penaltyGroup="postDiv") diff --git a/eos/effects/shipmodesetoptimalrangepostdiv.py b/eos/effects/shipmodesetoptimalrangepostdiv.py new file mode 100644 index 000000000..baf3f6864 --- /dev/null +++ b/eos/effects/shipmodesetoptimalrangepostdiv.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, module, context): + fit.modules.filteredItemMultiply(lambda mod: mod.item.requiresSkill("Small Energy Turret"), + "maxRange", 1/module.getModifiedItemAttr("modeMaxRangePostDiv"), + stackingPenalties=True, penaltyGroup="postDiv") diff --git a/eos/effects/shipsetcapneedamarrtacticaldestroyer2.py b/eos/effects/shipsetcapneedamarrtacticaldestroyer2.py new file mode 100644 index 000000000..048c17a26 --- /dev/null +++ b/eos/effects/shipsetcapneedamarrtacticaldestroyer2.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Amarr Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Energy Turret"), + "capacitorNeed", ship.getModifiedItemAttr("shipBonusTacticalDestroyerAmarr2") * level) diff --git a/eos/effects/shipsetdamageamarrtacticaldestroyer1.py b/eos/effects/shipsetdamageamarrtacticaldestroyer1.py new file mode 100644 index 000000000..3bca59b8e --- /dev/null +++ b/eos/effects/shipsetdamageamarrtacticaldestroyer1.py @@ -0,0 +1,5 @@ +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Amarr Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Energy Turret"), + "damageMultiplier", ship.getModifiedItemAttr("shipBonusTacticalDestroyerAmarr1") * level) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 8be6266d0..e16dc9d8b 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -27,6 +27,7 @@ from copy import deepcopy from math import sqrt, log, asinh from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill from eos.saveddata.module import State +from eos.saveddata.mode import Mode import time try: @@ -66,6 +67,7 @@ class Fit(object): self.gangBoosts = None self.timestamp = time.time() self.ecmProjectedStr = 1 + self.modeID = None self.build() @reconstructor @@ -99,6 +101,10 @@ class Fit(object): self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes.original = self.EXTRA_ATTRIBUTES self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None + if self.ship is not None: + self.mode = self.ship.checkModeItem(db.getItem(self.modeID) if self.modeID else None) + else: + self.mode = None @property def targetResists(self): @@ -122,6 +128,15 @@ class Fit(object): self.__ehp = None self.__effectiveTank = None + @property + def mode(self): + return self._mode + + @mode.setter + def mode(self, mode): + self._mode = mode + self.modeID = mode.item.ID if mode is not None else None + @property def character(self): return self.__character if self.__character is not None else Character.getAll0() @@ -369,9 +384,9 @@ class Fit(object): # Avoid adding projected drones and modules when fit is projected onto self # TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented if forceProjected is True: - c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules) + c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules) else: - c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules, + c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules, self.projectedDrones, self.projectedModules) if self.gangBoosts is not None: @@ -494,6 +509,10 @@ class Fit(object): Slot.RIG: "rigSlots", Slot.SUBSYSTEM: "maxSubSystems"} + if type == Slot.MODE: + # Mode slot doesn't really exist, return default 0 + return 0 + slotsUsed = self.getSlotsUsed(type, countDummies) totalSlots = self.ship.getModifiedItemAttr(slots[type]) or 0 return int(totalSlots - slotsUsed) diff --git a/eos/saveddata/mode.py b/eos/saveddata/mode.py new file mode 100644 index 000000000..83d0c0bc2 --- /dev/null +++ b/eos/saveddata/mode.py @@ -0,0 +1,65 @@ +#=============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +#=============================================================================== + +from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut +from eos.effectHandlerHelpers import HandledItem + +class Mode(ItemAttrShortcut, HandledItem): + + def __init__(self, item): + self.__item = item + self.__itemModifiedAttributes = ModifiedAttributeDict() + + if not isinstance(item, int): + self.__buildOriginal() + + def __fetchItemInfo(self): + import eos.db + self.__item = eos.db.getItem(self.__item) + self.__buildOriginal() + + def __buildOriginal(self): + self.__itemModifiedAttributes.original = self.item.attributes + + @property + def item(self): + if isinstance(self.__item, int): + self.__fetchItemInfo() + + return self.__item + + @property + def itemModifiedAttributes(self): + if isinstance(self.__item, int): + self.__fetchItemInfo() + + return self.__itemModifiedAttributes + + # @todo: rework to fit only on t3 dessy + def fits(self, fit): + raise NotImplementedError() + + def clear(self): + self.itemModifiedAttributes.clear() + + def calculateModifiedAttributes(self, fit, runTime, forceProjected = False): + if self.item: + for effect in self.item.effects.itervalues(): + if effect.runTime == runTime: + effect.handler(fit, self, context = ("module",)) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index db44d9f16..4688e53c4 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -36,6 +36,7 @@ class Slot(Enum): HIGH = 3 RIG = 4 SUBSYSTEM = 5 + MODE = 6 # not a real slot, need for pyfa display rack separation class Hardpoint(Enum): NONE = 0 diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 05cd99c02..49c3a3f58 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -19,6 +19,7 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem +from eos.saveddata.mode import Mode class Ship(ItemAttrShortcut, HandledItem): def __init__(self, item): @@ -65,6 +66,52 @@ class Ship(ItemAttrShortcut, HandledItem): if effect.runTime == runTime and effect.isType("passive"): effect.handler(fit, self, ("ship",)) + def checkModeItem(self, item): + """ + Checks if provided item is a valid mode. + + If ship has modes, and current item is not valid, return forced mode + else if mode is valid, return Mode + else if ship does not have modes, return None + + @todo: rename this + """ + items = self.getModeItems() + + if items != None: + if item == None or item not in items: + # We have a tact dessy, but mode is None or not valid. Force new mode + return Mode(items[0]) + elif item in items: + # We have a valid mode + return Mode(item) + return None + + def getModes(self): + items = self.getModeItems() + return [Mode(item) for item in items] if items else None + + def getModeItems(self): + """ + Returns a list of valid mode items for ship. Note that this returns the + valid Item objects, not the Mode objects. Returns None if not a + t3 dessy + """ + # @todo: is there a better way to determine this that isn't hardcoded groupIDs? + if self.item.groupID != 1305: + return None + + modeGroupID = 1306 + import eos.db + + items = [] + g = eos.db.getGroup(modeGroupID, eager=("items.icon", "items.attributes")) + for item in g.items: + if item.raceID == self.item.raceID: + items.append(item) + + return items + def __deepcopy__(self, memo): copy = Ship(self.item) return copy diff --git a/eos/types.py b/eos/types.py index 3e5223f4b..6e98749d2 100644 --- a/eos/types.py +++ b/eos/types.py @@ -32,6 +32,7 @@ from eos.saveddata.booster import SideEffect from eos.saveddata.booster import Booster from eos.saveddata.ship import Ship from eos.saveddata.fit import Fit +from eos.saveddata.mode import Mode from eos.saveddata.fleet import Fleet, Wing, Squad from eos.saveddata.miscData import MiscData import eos.db diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index e8655edbc..7ce9ed894 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -15,6 +15,7 @@ __all__ = [ "cargo", "shipJump", #"changeAffectingSkills", + "tacticalMode", "targetResists", "priceClear" ] diff --git a/gui/builtinContextMenus/changeAffectingSkills.py b/gui/builtinContextMenus/changeAffectingSkills.py index e8248a960..cfffddaff 100644 --- a/gui/builtinContextMenus/changeAffectingSkills.py +++ b/gui/builtinContextMenus/changeAffectingSkills.py @@ -65,6 +65,7 @@ class ChangeAffectingSkills(ContextMenu): return menuItem def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False self.skillIds = {} sub = wx.Menu() @@ -78,7 +79,7 @@ class ChangeAffectingSkills(ContextMenu): skillItem.SetBitmap(bitmap) for i in xrange(-1, 6): - levelItem = self.addSkill(rootMenu, skill, i) + levelItem = self.addSkill(rootMenu if msw else grandSub, skill, i) grandSub.AppendItem(levelItem) #@ todo: add check to current level. Need to fix #109 first sub.AppendItem(skillItem) diff --git a/gui/builtinContextMenus/damagePattern.py b/gui/builtinContextMenus/damagePattern.py index 724e7ac57..352340204 100644 --- a/gui/builtinContextMenus/damagePattern.py +++ b/gui/builtinContextMenus/damagePattern.py @@ -70,6 +70,7 @@ class DamagePattern(ContextMenu): return menuItem def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False rootMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch) # this bit is required for some reason if self.m[i] not in self.subMenus: @@ -87,7 +88,7 @@ class DamagePattern(ContextMenu): # Items that have a parent for pattern in self.subMenus[self.m[i]]: - sub.AppendItem(self.addPattern(rootMenu, pattern)) + sub.AppendItem(self.addPattern(rootMenu if msw else sub, pattern)) return sub diff --git a/gui/builtinContextMenus/moduleAmmoPicker.py b/gui/builtinContextMenus/moduleAmmoPicker.py index ddb3dff44..d64ec5321 100644 --- a/gui/builtinContextMenus/moduleAmmoPicker.py +++ b/gui/builtinContextMenus/moduleAmmoPicker.py @@ -97,12 +97,12 @@ class ModuleAmmoPicker(ContextMenu): parts = charge.name.split(" ") return map(self.numericConverter, parts) - def addCharge(self, rootMenu, charge): + def addCharge(self, menu, charge): id = wx.NewId() name = charge.name if charge is not None else "Empty" self.chargeIds[id] = charge - item = wx.MenuItem(rootMenu, id, name) - rootMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item) + item = wx.MenuItem(menu, id, name) + menu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item) item.charge = charge if charge is not None and charge.icon is not None: bitmap = bitmapLoader.getBitmap(charge.icon.iconFile, "pack") @@ -117,6 +117,7 @@ class ModuleAmmoPicker(ContextMenu): m.Enable(id, False) def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False m = wx.Menu() self.chargeIds = {} hardpoint = self.module.hardpoint @@ -147,7 +148,7 @@ class ModuleAmmoPicker(ContextMenu): base = charge nameBase = currBase range = currRange - item = self.addCharge(rootMenu, charge) + item = self.addCharge(rootMenu if msw else m, charge) items.append(item) else: if sub is None: @@ -155,9 +156,9 @@ class ModuleAmmoPicker(ContextMenu): sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch) self.addSeperator(sub, "Less Damage") item.SetSubMenu(sub) - sub.AppendItem(self.addCharge(rootMenu, base)) + sub.AppendItem(self.addCharge(rootMenu if msw else sub, base)) - sub.AppendItem(self.addCharge(rootMenu, charge)) + sub.AppendItem(self.addCharge(rootMenu if msw else sub, charge)) if sub is not None: self.addSeperator(sub, "More Damage") @@ -191,20 +192,20 @@ class ModuleAmmoPicker(ContextMenu): m.AppendItem(item) if charge.name not in ("Light Defender Missile I", "Heavy Defender Missile I"): - sub.AppendItem(self.addCharge(rootMenu, charge)) + sub.AppendItem(self.addCharge(rootMenu if msw else sub, charge)) else: defender = charge if defender is not None: - m.AppendItem(self.addCharge(rootMenu, defender)) + m.AppendItem(self.addCharge(rootMenu if msw else m, defender)) if sub is not None: self.addSeperator(sub, "More Damage") else: self.charges.sort(key=self.nameSorter) for charge in self.charges: - m.AppendItem(self.addCharge(rootMenu, charge)) + m.AppendItem(self.addCharge(rootMenu if msw else m, charge)) - m.AppendItem(self.addCharge(rootMenu, None)) + m.AppendItem(self.addCharge(rootMenu if msw else m, None)) return m def handleAmmoSwitch(self, event): diff --git a/gui/builtinContextMenus/tacticalMode.py b/gui/builtinContextMenus/tacticalMode.py new file mode 100644 index 000000000..31025b397 --- /dev/null +++ b/gui/builtinContextMenus/tacticalMode.py @@ -0,0 +1,60 @@ +import wx +from gui.contextMenu import ContextMenu +import gui.mainFrame +import service +import gui.globalEvents as GE + +class TacticalMode(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, srcContext, selection): + if self.mainFrame.getActiveFit() is None or srcContext != "fittingShip": + return False + + sFit = service.Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + fit = sFit.getFit(fitID) + + self.modes = fit.ship.getModes() + self.currMode = fit.mode + + return srcContext == "fittingShip" and self.modes is not None + + def getText(self, itmContext, selection): + return "Tactical Mode" + + def addMode(self, menu, mode): + label = mode.item.name.rsplit()[-2] + id = wx.NewId() + self.modeIds[id] = mode + menuItem = wx.MenuItem(menu, id, label, kind=wx.ITEM_RADIO) + menu.Bind(wx.EVT_MENU, self.handleMode, menuItem) + return menuItem + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False + self.context = context + self.modeIds = {} + + sub = wx.Menu() + + for mode in self.modes: + menuItem = self.addMode(rootMenu if msw else sub, mode) + sub.AppendItem(menuItem) + menuItem.Check(self.currMode.item == mode.item) + + return sub + + def handleMode(self, event): + item = self.modeIds[event.Id] + if item is False or item not in self.modes: + event.Skip() + return + + sFit = service.Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + sFit.setMode(fitID, self.modeIds[event.Id]) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + +TacticalMode.register() diff --git a/gui/builtinContextMenus/targetResists.py b/gui/builtinContextMenus/targetResists.py index 6f725fe4a..27e6c5153 100644 --- a/gui/builtinContextMenus/targetResists.py +++ b/gui/builtinContextMenus/targetResists.py @@ -61,6 +61,7 @@ class TargetResists(ContextMenu): return item def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False self.patternIds = {} self.subMenus = OrderedDict() self.singles = [] @@ -78,12 +79,12 @@ class TargetResists(ContextMenu): else: self.singles.append(pattern) - sub.AppendItem(self.addPattern(rootMenu, None)) # Add reset + sub.AppendItem(self.addPattern(rootMenu if msw else sub, None)) # Add reset sub.AppendSeparator() # Single items, no parent for pattern in self.singles: - sub.AppendItem(self.addPattern(rootMenu, pattern)) + sub.AppendItem(self.addPattern(rootMenu if msw else sub, pattern)) # Items that have a parent for menuName, patterns in self.subMenus.items(): @@ -99,7 +100,7 @@ class TargetResists(ContextMenu): # Append child items to child menu for pattern in patterns: - grandSub.AppendItem(self.addPattern(rootMenu, pattern)) + grandSub.AppendItem(self.addPattern(rootMenu if msw else grandSub, pattern)) sub.AppendItem(item) #finally, append parent item to root menu return sub diff --git a/gui/builtinContextMenus/whProjector.py b/gui/builtinContextMenus/whProjector.py index 5b4c10e38..ac1bb66cb 100644 --- a/gui/builtinContextMenus/whProjector.py +++ b/gui/builtinContextMenus/whProjector.py @@ -15,6 +15,7 @@ class WhProjector(ContextMenu): return "Add System Effects" def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False sMkt = service.Market.getInstance() effdata = sMkt.getSystemWideEffects() @@ -32,7 +33,10 @@ class WhProjector(ContextMenu): swObj, swName, swClass = swData self.idmap[wxid] = (swObj, swName) grandSubItem = wx.MenuItem(grandSub, wxid, swClass) - rootMenu.Bind(wx.EVT_MENU, self.handleSelection, grandSubItem) + if msw: + rootMenu.Bind(wx.EVT_MENU, self.handleSelection, grandSubItem) + else: + grandSub.Bind(wx.EVT_MENU, self.handleSelection, grandSubItem) grandSub.AppendItem(grandSubItem) return sub diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index 5b2a65d58..0966c9c7d 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -42,7 +42,10 @@ class BaseName(ViewColumn): return "%s (%s)" % (stuff.name, stuff.ship.item.name) elif isinstance(stuff, Rack): if service.Fit.getInstance().serviceFittingOptions["rackLabels"]: - return u'─ {} Slots ─'.format(Slot.getName(stuff.slot).capitalize()) + if stuff.slot == Slot.MODE: + return u'─ Tactical Mode ─' + else: + return u'─ {} Slots ─'.format(Slot.getName(stuff.slot).capitalize()) else: return "" elif isinstance(stuff, Module): diff --git a/gui/builtinViewColumns/capacitorUse.py b/gui/builtinViewColumns/capacitorUse.py index 206429af0..9e4916f2b 100644 --- a/gui/builtinViewColumns/capacitorUse.py +++ b/gui/builtinViewColumns/capacitorUse.py @@ -23,6 +23,7 @@ import service from gui.utils.numberFormatter import formatAmount from gui.viewColumn import ViewColumn from gui import bitmapLoader +from eos.types import Mode class CapacitorUse(ViewColumn): name = "Capacitor Usage" @@ -38,6 +39,9 @@ class CapacitorUse(ViewColumn): def getText(self, mod): + if isinstance(mod, Mode): + return "" + capUse = mod.capUse if capUse: return "%s%s" % ("+" if capUse < 0 else "", (formatAmount(-capUse, 3, 0, 3))) diff --git a/gui/builtinViewColumns/maxRange.py b/gui/builtinViewColumns/maxRange.py index db5b19929..be6be2a86 100644 --- a/gui/builtinViewColumns/maxRange.py +++ b/gui/builtinViewColumns/maxRange.py @@ -23,6 +23,7 @@ from gui import bitmapLoader import service from gui.utils.numberFormatter import formatAmount import wx +from eos.types import Mode class MaxRange(ViewColumn): name = "Max Range" @@ -51,6 +52,9 @@ class MaxRange(ViewColumn): self.mask |= wx.LIST_MASK_TEXT def getText(self, stuff): + if isinstance(stuff, Mode): + return "" + maxRange = stuff.maxRange if hasattr(stuff, "maxRange") else stuff.getModifiedItemAttr("maxRange") falloff = stuff.falloff if falloff: diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index 6610b3111..dd83ea22c 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -19,7 +19,6 @@ import gui.mainFrame -from gui import builtinViewColumns from gui.viewColumn import ViewColumn from gui import bitmapLoader from gui.utils.numberFormatter import formatAmount @@ -69,7 +68,9 @@ class Miscellanea(ViewColumn): itemGroup = item.group.name itemCategory = item.category.name - if itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"): + if itemGroup == "Ship Modifiers": + return "", None + elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"): trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed") if not trackingSpeed: return "", None diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 186bb8232..c889f36cb 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -403,6 +403,10 @@ class FittingView(d.Display): self.mods = fit.modules[:] self.mods.sort(key=lambda mod: (slotOrder.index(mod.slot), mod.position)) + # Blanks is a list of indexes that mark non-module positions (such + # as Racks and tactical Modes. This allows us to skip over common + # module operations such as swapping, removing, copying, etc. that + # would otherwise cause complications self.blanks = [] # preliminary markers where blanks will be inserted if sFit.serviceFittingOptions["rackSlots"]: @@ -419,6 +423,15 @@ class FittingView(d.Display): for i, (x, slot) in enumerate(self.blanks): self.blanks[i] = x+i # modify blanks with actual index self.mods.insert(x+i, Rack.buildRack(slot)) + + if fit.mode: + # Modes are special snowflakes and need a little manual loving + # We basically append the Mode rack and Mode to the modules + # while also marking their positions in the Blanks list + self.blanks.append(len(self.mods)) + self.mods.append(Rack.buildRack(Slot.MODE)) + self.blanks.append(len(self.mods)) + self.mods.append(fit.mode) else: self.mods = None @@ -457,7 +470,7 @@ class FittingView(d.Display): sel = self.GetFirstSelected() contexts = [] - while sel != -1: + while sel != -1 and sel not in self.blanks: mod = self.mods[self.GetItemData(sel)] if not mod.isEmpty: srcContext = "fittingModule" @@ -544,14 +557,17 @@ class FittingView(d.Display): font = (self.GetClassDefaultAttributes()).font for i, mod in enumerate(self.mods): - if slotMap[mod.slot]: + if hasattr(mod,"slot") and slotMap[mod.slot]: self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51)) elif sFit.serviceFittingOptions["colorFitBySlot"] and not isinstance(mod, Rack): self.SetItemBackgroundColour(i, self.slotColour(mod.slot)) else: self.SetItemBackgroundColour(i, self.GetBackgroundColour()) - if i in self.blanks and sFit.serviceFittingOptions["rackSlots"] and sFit.serviceFittingOptions["rackLabels"]: + # Set rack face to bold + if isinstance(mod, Rack) and \ + sFit.serviceFittingOptions["rackSlots"] and \ + sFit.serviceFittingOptions["rackLabels"]: font.SetWeight(wx.FONTWEIGHT_BOLD) self.SetItemFont(i, font) else: diff --git a/gui/itemStats.py b/gui/itemStats.py index d225abe65..c727c170b 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -22,9 +22,9 @@ import re import gui.mainFrame import bitmapLoader import sys -import wx.lib.mixins.listctrl as listmix +import wx.lib.mixins.listctrl as listmix import wx.html -from eos.types import Ship, Module, Skill, Booster, Implant, Drone +from eos.types import Ship, Module, Skill, Booster, Implant, Drone, Mode from gui.utils.numberFormatter import formatAmount import service import config @@ -535,7 +535,7 @@ class ItemEffects (wx.Panel): class ItemAffectedBy (wx.Panel): - ORDER = [Ship, Module, Drone, Implant, Booster, Skill] + ORDER = [Ship, Mode, Module, Drone, Implant, Booster, Skill] def __init__(self, parent, stuff, item): wx.Panel.__init__ (self, parent) self.stuff = stuff diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 0f5760b86..2d71da48a 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -290,6 +290,7 @@ class MainFrame(wx.Frame): event.Skip() def ShowAboutBox(self, evt): + import eos.config info = wx.AboutDialogInfo() info.Name = "pyfa" info.Version = gui.aboutData.versionString @@ -299,7 +300,8 @@ class MainFrame(wx.Frame): "\n\t".join(gui.aboutData.credits) + "\n\nLicenses:\n\t" + "\n\t".join(gui.aboutData.licenses) + - "\n\nPython: \t" + sys.version + + "\n\nEVE Data: \t" + eos.config.gamedata_version + + "\nPython: \t" + sys.version + "\nwxPython: \t" + wx.__version__ + "\nSQLAlchemy: \t" + sqlalchemy.__version__, 700, wx.ClientDC(self)) diff --git a/scripts/itemDiff.py b/scripts/itemDiff.py index b1877897c..6f63a5cf1 100755 --- a/scripts/itemDiff.py +++ b/scripts/itemDiff.py @@ -42,7 +42,10 @@ def main(old, new, groups=True, effects=True, attributes=True, renames=True): new_cursor = new_db.cursor() # Force some of the items to make them published - FORCEPUB_TYPES = ("Ibis", "Impairor", "Velator", "Reaper") + FORCEPUB_TYPES = ("Ibis", "Impairor", "Velator", "Reaper", + "Amarr Tactical Destroyer Propulsion Mode", + "Amarr Tactical Destroyer Sharpshooter Mode", + "Amarr Tactical Destroyer Defense Mode") OVERRIDES_TYPEPUB = 'UPDATE invtypes SET published = 1 WHERE typeName = ?' for typename in FORCEPUB_TYPES: old_cursor.execute(OVERRIDES_TYPEPUB, (typename,)) @@ -211,7 +214,7 @@ def main(old, new, groups=True, effects=True, attributes=True, renames=True): # Initialize container for the data for each item with empty stuff besides groupID dictionary[itemid] = [groupID, set(), {}] # Add items filtered by group - query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID WHERE it.published = 1 AND ig.groupName IN ("Effect Beacon")' + query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID WHERE it.published = 1 AND ig.groupName IN ("Effect Beacon", "Ship Modifiers")' cursor.execute(query) for row in cursor: itemid = row[0] diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 5c861e522..eb1678fd2 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -110,7 +110,7 @@ def main(db, json_path): sectionLines.append(headerText) for bonusData in sectionData["bonuses"]: prefix = u"{} ".format(bonusData["number"]) if "number" in bonusData else "" - bonusText = u"{}{}".format(prefix, bonusData["text"]) + bonusText = u"{}{}".format(prefix, bonusData["text"].replace(u"\u00B7", u"\u2022 ")) sectionLines.append(bonusText) sectionLine = u"
\n".join(sectionLines) return sectionLine @@ -143,15 +143,17 @@ def main(db, json_path): tableData = convertTraits(tableData) data[jsonName] = tableData + # 1306 - group Ship Modifiers, for items like tactical t3 ship modes + # Do some preprocessing to make our job easier invTypes = set() for row in data["invtypes"]: - if row["published"]: + if (row["published"] or row['groupID'] == 1306): invTypes.add(row["typeID"]) # ignore checker def isIgnored(file, row): - if file == "invtypes" and not row["published"]: + if file == "invtypes" and not (row["published"] or row['groupID'] == 1306): return True elif file == "dgmtypeeffects" and not row["typeID"] in invTypes: return True @@ -180,7 +182,7 @@ def main(db, json_path): eos.db.gamedata_session.commit() - print("done") + print("done") if __name__ == "__main__": parser = argparse.ArgumentParser(description="This scripts dumps effects from an sqlite cache dump to mongo") diff --git a/service/conversions/skinnedShips.py b/service/conversions/skinnedShips.py index 9690e61ec..2b170dcc7 100644 --- a/service/conversions/skinnedShips.py +++ b/service/conversions/skinnedShips.py @@ -48,6 +48,8 @@ CONVERSIONS = { "Merlin Wiyrkomi Edition": "Merlin", "Miasmos Amastris Edition": "Miasmos Quafe Ultra Edition", "Miasmos Quafe Ultramarine Edition": "Miasmos Quafe Ultra Edition", + "Moros Interbus Edition": "Moros", + "Naglfar Justice Edition": "Naglfar", "Nefantar Thrasher": "Thrasher", "Omen Kador Edition": "Omen", "Omen Tash-Murkon Edition": "Omen", @@ -55,6 +57,7 @@ CONVERSIONS = { "Paladin Blood Raider Edition": "Paladin", "Paladin Kador Edition": "Paladin", "Paladin Tash-Murkon Edition": "Paladin", + "Phoenix Wiyrkomi Edition": "Phoenix", "Police Pursuit Comet": "Federation Navy Comet", "Prophecy Blood Raiders Edition": "Prophecy", "Punisher Kador Edition": "Punisher", @@ -64,6 +67,7 @@ CONVERSIONS = { "Raven Kaalakiota Edition": "Raven", "Raven Nugoeihuvi Edition": "Raven", "Rattlesnake Victory Edition": "Rattlesnake", + "Revelation Sarum Edition": "Revelation", "Rifter Krusual Edition": "Rifter", "Rifter Nefantar Edition": "Rifter", "Rokh Nugoeihuvi Edition": "Rokh", diff --git a/service/fit.py b/service/fit.py index 60e899faf..52c0e48cb 100644 --- a/service/fit.py +++ b/service/fit.py @@ -147,6 +147,7 @@ class Fit(object): def newFit(self, shipID, name=None): fit = eos.types.Fit() fit.ship = eos.types.Ship(eos.db.getItem(shipID)) + fit.mode = fit.ship.checkModeItem(None) fit.name = name if name is not None else "New %s" % fit.ship.item.name fit.damagePattern = self.pattern fit.targetResists = self.targetResists @@ -719,6 +720,16 @@ class Fit(object): self.recalc(fit) + def setMode(self, fitID, mode): + if fitID is None: + return + + fit = eos.db.getFit(fitID) + fit.mode = mode + eos.db.commit() + + self.recalc(fit) + def setAsPattern(self, fitID, ammo): if fitID is None: return diff --git a/service/market.py b/service/market.py index dd811282a..fdde3c598 100644 --- a/service/market.py +++ b/service/market.py @@ -214,7 +214,8 @@ class Market(): "Goru's Shuttle": False, "Guristas Shuttle": False, "Mobile Decoy Unit": False, # Seems to be left over test mod for deployables - "Tournament Micro Jump Unit": False } # Normally seen only on tournament arenas + "Tournament Micro Jump Unit": False} # Normally seen only on tournament arenas + # do not publish ships that we convert for name in conversions.packs['skinnedShips']: diff --git a/staticdata/eve.db b/staticdata/eve.db index c4c2a5283..0fc75bf9b 100644 Binary files a/staticdata/eve.db and b/staticdata/eve.db differ diff --git a/staticdata/icons/ships/34317.png b/staticdata/icons/ships/34317.png new file mode 100644 index 000000000..4f0052a32 Binary files /dev/null and b/staticdata/icons/ships/34317.png differ diff --git a/staticdata/icons/ships/34328.png b/staticdata/icons/ships/34328.png new file mode 100644 index 000000000..72ee8c17e Binary files /dev/null and b/staticdata/icons/ships/34328.png differ diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 000000000..e69de29bb