diff --git a/.gitignore b/.gitignore index c5efd554c..a9eb5e25c 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ gitversion /.version *.swp +*.fsdbinary diff --git a/config.py b/config.py index f01494850..fa073ad9d 100644 --- a/config.py +++ b/config.py @@ -24,10 +24,10 @@ saveInRoot = False # Version data -version = "2.1.0" +version = "2.3.1" tag = "Stable" -expansionName = "Into the Abyss" -expansionVersion = "1.1" +expansionName = "YC120.7" +expansionVersion = "1.2" evemonMinVersion = "4081" minItemSearchLength = 3 diff --git a/dist_assets/mac/pyfa.spec b/dist_assets/mac/pyfa.spec index da72e4681..cf27cfe87 100644 --- a/dist_assets/mac/pyfa.spec +++ b/dist_assets/mac/pyfa.spec @@ -73,4 +73,7 @@ exe = EXE(pyz, app = BUNDLE(exe, name='pyfa.app', icon=icon, - bundle_identifier=None) \ No newline at end of file + bundle_identifier=None, + info_plist={ + 'NSHighResolutionCapable': 'True' + }) diff --git a/dist_assets/win/Microsoft.VC90.CRT.manifest b/dist_assets/win/Microsoft.VC90.CRT.manifest new file mode 100644 index 000000000..627da999b --- /dev/null +++ b/dist_assets/win/Microsoft.VC90.CRT.manifest @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/dist_assets/win/pyfa-setup.iss b/dist_assets/win/pyfa-setup.iss index 3d3e3df2d..7984da087 100644 --- a/dist_assets/win/pyfa-setup.iss +++ b/dist_assets/win/pyfa-setup.iss @@ -5,7 +5,7 @@ ; we do some #ifdef conditionals because automated compilation passes these as arguments #ifndef MyAppVersion - #define MyAppVersion "1.15.0" + #define MyAppVersion "2.1.0" #endif #ifndef MyAppExpansion #define MyAppExpansion "Vanguard 1.0" @@ -64,7 +64,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 [Files] -Source: "{#MyAppDir}\pyfa.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#MyAppDir}\pyfa.exe"; DestDir: "{app}"; Flags: ignoreversion; AfterInstall: RemoveFromVirtualStore Source: "{#MyAppDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files @@ -104,6 +104,22 @@ begin FSWbemLocator := Unassigned; end; +procedure RemoveFromVirtualStore; +var + VirtualStore,FileName,FilePath:String; + DriveChars:Integer; +begin + VirtualStore:=AddBackslash(ExpandConstant('{localappdata}'))+'VirtualStore'; + FileName:=ExpandConstant(CurrentFileName); + DriveChars:=Length(ExtractFileDrive(FileName)); + if DriveChars>0 then begin + Delete(FileName,1,DriveChars); + FileName:=VirtualStore+FileName; + FilePath:=ExtractFilePath(FileName); + DelTree(FilePath, True, True, True); + end; +end; + function PrepareToInstall(var NeedsRestart: Boolean): String; begin if(IsAppRunning( 'pyfa.exe' )) then diff --git a/dist_assets/win/pyfa.exe.manifest b/dist_assets/win/pyfa.exe.manifest new file mode 100644 index 000000000..1086d2201 --- /dev/null +++ b/dist_assets/win/pyfa.exe.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist_assets/win/pyfa.spec b/dist_assets/win/pyfa.spec index 445f94824..d181a5eb9 100644 --- a/dist_assets/win/pyfa.spec +++ b/dist_assets/win/pyfa.spec @@ -20,6 +20,8 @@ added_files = [ ('../../imgs/renders/*.png', 'imgs/renders'), ('../../service/jargon/*.yaml', 'service/jargon'), ('../../dist_assets/win/pyfa.ico', '.'), + ('../../dist_assets/win/pyfa.exe.manifest', '.'), + ('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'), (requests.certs.where(), '.'), # is this needed anymore? ('../../eve.db', '.'), ('../../README.md', '.'), diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 5341e84ae..f02e9d944 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -78,10 +78,10 @@ sd_lock = threading.RLock() # Import all the definitions for all our database stuff # noinspection PyPep8 -from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit +from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes # noinspection PyPep8 from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ - miscData, module, override, price, queries, skill, targetResists, user + miscData, mutator, module, override, price, queries, skill, targetResists, user # Import queries # noinspection PyPep8 diff --git a/eos/db/gamedata/__init__.py b/eos/db/gamedata/__init__.py index eabfd7f1b..465433a26 100644 --- a/eos/db/gamedata/__init__.py +++ b/eos/db/gamedata/__init__.py @@ -1,2 +1,2 @@ -__all__ = ["attribute", "category", "effect", "group", "metaData", - "icon", "item", "marketGroup", "metaGroup", "unit", "alphaClones"] +__all__ = ["attribute", "category", "effect", "group", "metaData", "dynamicAttributes", + "item", "marketGroup", "metaGroup", "unit", "alphaClones"] diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py index 20de4d1a9..727037421 100644 --- a/eos/db/gamedata/attribute.py +++ b/eos/db/gamedata/attribute.py @@ -22,7 +22,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import relation, mapper, synonym, deferred from eos.db import gamedata_meta -from eos.gamedata import Attribute, AttributeInfo, Unit, Icon +from eos.gamedata import Attribute, AttributeInfo, Unit typeattributes_table = Table("dgmtypeattribs", gamedata_meta, Column("value", Float), @@ -38,7 +38,7 @@ attributes_table = Table("dgmattribs", gamedata_meta, Column("published", Boolean), Column("displayName", String), Column("highIsGood", Boolean), - Column("iconID", Integer, ForeignKey("icons.iconID")), + Column("iconID", Integer), Column("unitID", Integer, ForeignKey("dgmunits.unitID"))) mapper(Attribute, typeattributes_table, @@ -46,7 +46,6 @@ mapper(Attribute, typeattributes_table, mapper(AttributeInfo, attributes_table, properties={ - "icon" : relation(Icon), "unit" : relation(Unit), "ID" : synonym("attributeID"), "name" : synonym("attributeName"), diff --git a/eos/db/gamedata/category.py b/eos/db/gamedata/category.py index 0fd84da79..c167cf1df 100644 --- a/eos/db/gamedata/category.py +++ b/eos/db/gamedata/category.py @@ -21,18 +21,17 @@ from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table from sqlalchemy.orm import relation, mapper, synonym, deferred from eos.db import gamedata_meta -from eos.gamedata import Category, Icon +from eos.gamedata import Category categories_table = Table("invcategories", gamedata_meta, Column("categoryID", Integer, primary_key=True), Column("categoryName", String), Column("description", String), Column("published", Boolean), - Column("iconID", Integer, ForeignKey("icons.iconID"))) + Column("iconID", Integer)) mapper(Category, categories_table, properties={ - "icon" : relation(Icon), "ID" : synonym("categoryID"), "name" : synonym("categoryName"), "description": deferred(categories_table.c.description) diff --git a/eos/db/gamedata/dynamicAttributes.py b/eos/db/gamedata/dynamicAttributes.py new file mode 100644 index 000000000..26d25be0e --- /dev/null +++ b/eos/db/gamedata/dynamicAttributes.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 sqlalchemy import Column, Float, Integer, Table, ForeignKey +from sqlalchemy.orm import mapper, relation, synonym +from sqlalchemy.ext.associationproxy import association_proxy + +from eos.db import gamedata_meta +from eos.gamedata import DynamicItem, DynamicItemAttribute, DynamicItemItem, Item + +from eos.gamedata import AttributeInfo + +dynamic_table = Table("mutaplasmids", gamedata_meta, + Column("typeID", ForeignKey("invtypes.typeID"), primary_key=True, index=True), + Column("resultingTypeID", ForeignKey("invtypes.typeID"), primary_key=True)) + +dynamicAttributes_table = Table("mutaplasmidAttributes", gamedata_meta, + Column("typeID", Integer, ForeignKey("mutaplasmids.typeID"), primary_key=True), + Column("attributeID", ForeignKey("dgmattribs.attributeID"), primary_key=True), + Column("min", Float), + Column("max", Float)) + +dynamicApplicable_table = Table("mutaplasmidItems", gamedata_meta, + Column("typeID", ForeignKey("mutaplasmids.typeID"), primary_key=True), + Column("applicableTypeID", ForeignKey("invtypes.typeID"), primary_key=True),) + +mapper(DynamicItem, dynamic_table, properties={ + "attributes": relation(DynamicItemAttribute), + "item": relation(Item, foreign_keys=[dynamic_table.c.typeID]), + "resultingItem": relation(Item, foreign_keys=[dynamic_table.c.resultingTypeID]), + "ID": synonym("typeID"), +}) + +mapper(DynamicItemAttribute, dynamicAttributes_table, + properties={"info": relation(AttributeInfo, lazy=False)}) + +mapper(DynamicItemItem, dynamicApplicable_table, properties={ + "mutaplasmid": relation(DynamicItem), + }) + +DynamicItemAttribute.ID = association_proxy("info", "attributeID") +DynamicItemAttribute.name = association_proxy("info", "attributeName") +DynamicItemAttribute.description = association_proxy("info", "description") +DynamicItemAttribute.published = association_proxy("info", "published") +DynamicItemAttribute.displayName = association_proxy("info", "displayName") +DynamicItemAttribute.highIsGood = association_proxy("info", "highIsGood") +DynamicItemAttribute.iconID = association_proxy("info", "iconID") +DynamicItemAttribute.icon = association_proxy("info", "icon") +DynamicItemAttribute.unit = association_proxy("info", "unit") diff --git a/eos/db/gamedata/group.py b/eos/db/gamedata/group.py index 4373b2ca5..a9a66a8ef 100644 --- a/eos/db/gamedata/group.py +++ b/eos/db/gamedata/group.py @@ -18,10 +18,10 @@ # =============================================================================== from sqlalchemy import Column, String, Integer, Boolean, ForeignKey, Table -from sqlalchemy.orm import relation, mapper, synonym, deferred +from sqlalchemy.orm import relation, mapper, synonym, deferred, backref from eos.db import gamedata_meta -from eos.gamedata import Category, Group, Icon +from eos.gamedata import Category, Group groups_table = Table("invgroups", gamedata_meta, Column("groupID", Integer, primary_key=True), @@ -29,12 +29,11 @@ groups_table = Table("invgroups", gamedata_meta, Column("description", String), Column("published", Boolean), Column("categoryID", Integer, ForeignKey("invcategories.categoryID")), - Column("iconID", Integer, ForeignKey("icons.iconID"))) + Column("iconID", Integer)) mapper(Group, groups_table, properties={ - "category" : relation(Category, backref="groups"), - "icon" : relation(Icon), + "category" : relation(Category, backref=backref("groups", cascade="all,delete")), "ID" : synonym("groupID"), "name" : synonym("groupName"), "description": deferred(groups_table.c.description) diff --git a/eos/db/gamedata/item.py b/eos/db/gamedata/item.py index 1760f9157..fba3e9820 100644 --- a/eos/db/gamedata/item.py +++ b/eos/db/gamedata/item.py @@ -19,12 +19,13 @@ from sqlalchemy import Column, String, Integer, Boolean, ForeignKey, Table, Float from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.orm import relation, mapper, synonym, deferred +from sqlalchemy.orm import relation, mapper, synonym, deferred, backref from sqlalchemy.orm.collections import attribute_mapped_collection from eos.db.gamedata.effect import typeeffects_table from eos.db import gamedata_meta -from eos.gamedata import Attribute, Effect, Group, Icon, Item, MetaType, Traits +from eos.gamedata import Attribute, Effect, Group, Item, MetaType, Traits, DynamicItemItem, DynamicItem +from eos.db.gamedata.dynamicAttributes import dynamicApplicable_table, dynamic_table items_table = Table("invtypes", gamedata_meta, Column("typeID", Integer, primary_key=True), @@ -37,7 +38,8 @@ items_table = Table("invtypes", gamedata_meta, Column("capacity", Float), Column("published", Boolean), Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")), - Column("iconID", Integer, ForeignKey("icons.iconID")), + Column("iconID", Integer), + Column("graphicID", Integer), Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True)) from .metaGroup import metatypes_table # noqa @@ -45,8 +47,7 @@ from .traits import traits_table # noqa mapper(Item, items_table, properties={ - "group" : relation(Group, backref="items"), - "icon" : relation(Icon), + "group" : relation(Group, backref=backref("items", cascade="all,delete")), "_Item__attributes": relation(Attribute, cascade='all, delete, delete-orphan', collection_class=attribute_mapped_collection('name')), "effects": relation(Effect, secondary=typeeffects_table, collection_class=attribute_mapped_collection('name')), "metaGroup" : relation(MetaType, @@ -57,7 +58,12 @@ mapper(Item, items_table, "description" : deferred(items_table.c.description), "traits" : relation(Traits, primaryjoin=traits_table.c.typeID == items_table.c.typeID, - uselist=False) + uselist=False), + "mutaplasmids": relation(DynamicItem, + primaryjoin=dynamicApplicable_table.c.applicableTypeID == items_table.c.typeID, + secondaryjoin=dynamicApplicable_table.c.typeID == DynamicItem.typeID, + secondary=dynamicApplicable_table, + backref="applicableItems") }) Item.category = association_proxy("group", "category") diff --git a/eos/db/gamedata/marketGroup.py b/eos/db/gamedata/marketGroup.py index faf88780b..8bd04f401 100644 --- a/eos/db/gamedata/marketGroup.py +++ b/eos/db/gamedata/marketGroup.py @@ -21,7 +21,7 @@ from sqlalchemy import Column, String, Integer, Boolean, ForeignKey, Table from sqlalchemy.orm import relation, mapper, synonym, deferred from eos.db import gamedata_meta -from eos.gamedata import Icon, Item, MarketGroup +from eos.gamedata import Item, MarketGroup marketgroups_table = Table("invmarketgroups", gamedata_meta, Column("marketGroupID", Integer, primary_key=True), @@ -30,14 +30,13 @@ marketgroups_table = Table("invmarketgroups", gamedata_meta, Column("hasTypes", Boolean), Column("parentGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID", initially="DEFERRED", deferrable=True)), - Column("iconID", Integer, ForeignKey("icons.iconID"))) + Column("iconID", Integer)) mapper(MarketGroup, marketgroups_table, properties={ "items" : relation(Item, backref="marketGroup"), "parent" : relation(MarketGroup, backref="children", remote_side=[marketgroups_table.c.marketGroupID]), - "icon" : relation(Icon), "ID" : synonym("marketGroupID"), "name" : synonym("marketGroupName"), "description": deferred(marketgroups_table.c.description) diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 737169897..90e757788 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -17,15 +17,16 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy.orm import join, exc, aliased +from sqlalchemy.orm import join, exc, aliased, joinedload, subqueryload from sqlalchemy.sql import and_, or_, select +from sqlalchemy.inspection import inspect import eos.config from eos.db import gamedata_session from eos.db.gamedata.metaGroup import metatypes_table, items_table from eos.db.gamedata.group import groups_table from eos.db.util import processEager, processWhere -from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, MetaGroup, AttributeInfo, MetaData +from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, MetaGroup, AttributeInfo, MetaData, DynamicItem cache = {} configVal = getattr(eos.config, "gamedataCache", None) @@ -97,6 +98,36 @@ def getItem(lookfor, eager=None): return item +def getMutaplasmid(lookfor, eager=None): + if isinstance(lookfor, int): + item = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == lookfor).first() + else: + raise TypeError("Need integer as argument") + return item + + +def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None): + # A lot of this is described in more detail in #1597 + item = gamedata_session.query(Item).get(lookfor) + base = getItem(baseItemID) + + # we have to load all attributes for this object, otherwise we'll lose access to them when we expunge. + # todo: figure out a way to eagerly load all these via the query... + for x in [*inspect(Item).relationships.keys(), 'description']: + getattr(item, x) + + # Copy over the attributes from the base, but ise the items attributes when there's an overlap + # WARNING: the attribute object still has the old typeID. I don't believe we access this typeID anywhere in the code, + # but should keep this in mind for now. + item._Item__attributes = {**base.attributes, **item.attributes} + + # Expunge the item form the session. This is required to have different Abyssal / Base combinations loaded in memory. + # Without expunging it, once one Abyssal Web is created, SQLAlchmey will use it for all others. We don't want this, + # we want to generate a completely new object to work with + gamedata_session.expunge(item) + return item + + @cachedQuery(1, "lookfor") def getItems(lookfor, eager=None): """ @@ -361,6 +392,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/db/migrations/upgrade22.py b/eos/db/migrations/upgrade22.py index 568351a25..5a9b01e34 100644 --- a/eos/db/migrations/upgrade22.py +++ b/eos/db/migrations/upgrade22.py @@ -14,7 +14,7 @@ def upgrade(saveddata_engine): "boosters": 2, "cargo": 2, "characters": 2, - "crest": 1, + # "crest": 1, "damagePatterns": 2, "drones": 2, "fighters": 2, diff --git a/eos/db/migrations/upgrade28.py b/eos/db/migrations/upgrade28.py new file mode 100644 index 000000000..2ca735414 --- /dev/null +++ b/eos/db/migrations/upgrade28.py @@ -0,0 +1,18 @@ +""" +Migration 28 + +- adds baseItemID and mutaplasmidID to modules table +""" +import sqlalchemy + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT baseItemID FROM modules LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE modules ADD COLUMN baseItemID INT;") + + try: + saveddata_engine.execute("SELECT mutaplasmidID FROM modules LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE modules ADD COLUMN mutaplasmidID INT;") diff --git a/eos/db/saveddata/__init__.py b/eos/db/saveddata/__init__.py index ba1ddad73..c36517623 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -1,6 +1,7 @@ __all__ = [ "character", "fit", + "mutator", "module", "user", "skill", diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 07375dcaf..0ae4509e2 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -56,7 +56,7 @@ fits_table = Table("fits", saveddata_meta, Column("booster", Boolean, nullable=False, index=True, default=0), Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True), Column("modeID", Integer, nullable=True), - Column("implantLocation", Integer, nullable=False, default=ImplantLocation.FIT), + Column("implantLocation", Integer, nullable=False), Column("notes", String, nullable=True), Column("ignoreRestrictions", Boolean, default=0), Column("created", DateTime, nullable=True, default=datetime.datetime.now), diff --git a/eos/db/saveddata/module.py b/eos/db/saveddata/module.py index 149f4f73c..220b208db 100644 --- a/eos/db/saveddata/module.py +++ b/eos/db/saveddata/module.py @@ -18,17 +18,21 @@ # =============================================================================== from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime +from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm import relation, mapper import datetime from eos.db import saveddata_meta from eos.saveddata.module import Module +from eos.saveddata.mutator import Mutator from eos.saveddata.fit import Fit modules_table = Table("modules", saveddata_meta, Column("ID", Integer, primary_key=True), Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False, index=True), Column("itemID", Integer, nullable=True), + Column("baseItemID", Integer, nullable=True), + Column("mutaplasmidID", Integer, nullable=True), Column("dummySlot", Integer, nullable=True, default=None), Column("chargeID", Integer), Column("state", Integer, CheckConstraint("state >= -1"), CheckConstraint("state <= 2")), @@ -39,4 +43,12 @@ modules_table = Table("modules", saveddata_meta, CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"')) mapper(Module, modules_table, - properties={"owner": relation(Fit)}) + properties={ + "owner": relation(Fit), + "mutators": relation( + Mutator, + backref="module", + cascade="all,delete-orphan", + collection_class=attribute_mapped_collection('attrID') + ) + }) diff --git a/eos/db/gamedata/icon.py b/eos/db/saveddata/mutator.py similarity index 53% rename from eos/db/gamedata/icon.py rename to eos/db/saveddata/mutator.py index 9fd41605a..11b595025 100644 --- a/eos/db/gamedata/icon.py +++ b/eos/db/saveddata/mutator.py @@ -17,19 +17,18 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Column, String, Integer, Table -from sqlalchemy.orm import mapper, synonym, deferred +from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime, Float +from sqlalchemy.orm import mapper +import datetime -from eos.db import gamedata_meta -from eos.gamedata import Icon +from eos.db import saveddata_meta +from eos.saveddata.mutator import Mutator -icons_table = Table("icons", gamedata_meta, - Column("iconID", Integer, primary_key=True), - Column("description", String), - Column("iconFile", String)) +mutator_table = Table("mutators", saveddata_meta, + Column("moduleID", Integer, ForeignKey("modules.ID"), primary_key=True, index=True), + Column("attrID", Integer, primary_key=True, index=True), + Column("value", Float, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) -mapper(Icon, icons_table, - properties={ - "ID" : synonym("iconID"), - "description": deferred(icons_table.c.description) - }) +mapper(Mutator, mutator_table) diff --git a/eos/effects/adaptivearmorhardener.py b/eos/effects/adaptivearmorhardener.py index 246c67fcf..25a4335a6 100644 --- a/eos/effects/adaptivearmorhardener.py +++ b/eos/effects/adaptivearmorhardener.py @@ -13,11 +13,14 @@ type = "active" def handler(fit, module, context): damagePattern = fit.damagePattern + # pyfalog.debug("==============================") static_adaptive_behavior = eos.config.settings['useStaticAdaptiveArmorHardener'] if (damagePattern.emAmount == damagePattern.thermalAmount == damagePattern.kineticAmount == damagePattern.explosiveAmount) and static_adaptive_behavior: - pyfalog.debug("Setting adaptivearmorhardener resists to uniform profile.") + # pyfalog.debug("Setting adaptivearmorhardener resists to uniform profile.") + for attr in ("armorEmDamageResonance", "armorThermalDamageResonance", "armorKineticDamageResonance", "armorExplosiveDamageResonance"): + fit.ship.multiplyItemAttr(attr, module.getModifiedItemAttr(attr), stackingPenalties=True, penaltyGroup="preMul") return # Skip if there is no damage pattern. Example: projected ships or fleet boosters @@ -30,7 +33,7 @@ def handler(fit, module, context): damagePattern.kineticAmount * fit.ship.getModifiedItemAttr('armorKineticDamageResonance'), damagePattern.explosiveAmount * fit.ship.getModifiedItemAttr('armorExplosiveDamageResonance'), ) - # pyfalog.debug("Damage Adjusted for Armor Resists: %f/%f/%f/%f", baseDamageTaken[0], baseDamageTaken[1], baseDamageTaken[2], baseDamageTaken[3]) + # pyfalog.debug("Damage Adjusted for Armor Resists: %f/%f/%f/%f" % (baseDamageTaken[0], baseDamageTaken[1], baseDamageTaken[2], baseDamageTaken[3])) resistanceShiftAmount = module.getModifiedItemAttr( 'resistanceShiftAmount') / 100 # The attribute is in percent and we want a fraction @@ -46,7 +49,7 @@ def handler(fit, module, context): cycleList = [] loopStart = -20 for num in range(50): - # pyfalog.debug("Starting cycle %d.", num) + # pyfalog.debug("Starting cycle %d." % num) # The strange order is to emulate the ingame sorting when different types have taken the same amount of damage. # This doesn't take into account stacking penalties. In a few cases fitting a Damage Control causes an inaccurate result. damagePattern_tuples = [ @@ -84,7 +87,7 @@ def handler(fit, module, context): RAHResistance[sortedDamagePattern_tuples[1][0]] = sortedDamagePattern_tuples[1][2] + change1 RAHResistance[sortedDamagePattern_tuples[2][0]] = sortedDamagePattern_tuples[2][2] + change2 RAHResistance[sortedDamagePattern_tuples[3][0]] = sortedDamagePattern_tuples[3][2] + change3 - # pyfalog.debug("Resistances shifted to %f/%f/%f/%f", RAHResistance[0], RAHResistance[1], RAHResistance[2], RAHResistance[3]) + # pyfalog.debug("Resistances shifted to %f/%f/%f/%f" % ( RAHResistance[0], RAHResistance[1], RAHResistance[2], RAHResistance[3])) # See if the current RAH profile has been encountered before, indicating a loop. for i, val in enumerate(cycleList): @@ -94,16 +97,16 @@ def handler(fit, module, context): abs(RAHResistance[2] - val[2]) <= tolerance and \ abs(RAHResistance[3] - val[3]) <= tolerance: loopStart = i - # pyfalog.debug("Loop found: %d-%d", loopStart, num) + # pyfalog.debug("Loop found: %d-%d" % (loopStart, num)) break if loopStart >= 0: break cycleList.append(list(RAHResistance)) - if loopStart < 0: - pyfalog.error("Reactive Armor Hardener failed to find equilibrium. Damage profile after armor: {0}/{1}/{2}/{3}", - baseDamageTaken[0], baseDamageTaken[1], baseDamageTaken[2], baseDamageTaken[3]) + # if loopStart < 0: + # pyfalog.error("Reactive Armor Hardener failed to find equilibrium. Damage profile after armor: {0}/{1}/{2}/{3}".format( + # baseDamageTaken[0], baseDamageTaken[1], baseDamageTaken[2], baseDamageTaken[3])) # Average the profiles in the RAH loop, or the last 20 if it didn't find a loop. loopCycles = cycleList[loopStart:] @@ -117,7 +120,7 @@ def handler(fit, module, context): average[i] = round(average[i] / numCycles, 3) # Set the new resistances - # pyfalog.debug("Setting new resist profile: %f/%f/%f/%f", average[0], average[1], average[2],average[3]) + # pyfalog.debug("Setting new resist profile: %f/%f/%f/%f" % ( average[0], average[1], average[2],average[3])) for i, attr in enumerate(( 'armorEmDamageResonance', 'armorThermalDamageResonance', 'armorKineticDamageResonance', 'armorExplosiveDamageResonance')): diff --git a/eos/effects/agilitymultipliereffectpassive.py b/eos/effects/agilitymultipliereffectpassive.py index affa839e8..d185a5002 100644 --- a/eos/effects/agilitymultipliereffectpassive.py +++ b/eos/effects/agilitymultipliereffectpassive.py @@ -6,4 +6,4 @@ type = "passive" def handler(fit, module, context): - fit.ship.boostItemAttr("agility", module.getModifiedItemAttr("agilityMultiplier"), stackingPenalties=True) + fit.ship.boostItemAttr("agility", module.getModifiedItemAttr("agilityBonus"), stackingPenalties=True) diff --git a/eos/effects/armorallrepairsystemsamountbonuspassive.py b/eos/effects/armorallrepairsystemsamountbonuspassive.py index c2eb9e2c4..8307ae2ee 100644 --- a/eos/effects/armorallrepairsystemsamountbonuspassive.py +++ b/eos/effects/armorallrepairsystemsamountbonuspassive.py @@ -1,7 +1,7 @@ # armorAllRepairSystemsAmountBonusPassive # # Used by: -# Implants named like: Agency 'Hardshell' TB Dose (3 of 4) +# Implants named like: Agency 'Hardshell' TB Dose (4 of 4) # Implants named like: Exile Booster (4 of 4) # Implant: Antipharmakon Kosybo type = "passive" diff --git a/eos/effects/boostermaxvelocitypenalty.py b/eos/effects/boostermaxvelocitypenalty.py index d42479397..83856e018 100644 --- a/eos/effects/boostermaxvelocitypenalty.py +++ b/eos/effects/boostermaxvelocitypenalty.py @@ -1,7 +1,8 @@ # boosterMaxVelocityPenalty # # Used by: -# Implants named like: Booster (12 of 33) +# Implants named like: Crash Booster (3 of 4) +# Items from market group: Implants & Boosters > Booster > Booster Slot 02 (9 of 13) type = "boosterSideEffect" # User-friendly name for the side effect diff --git a/eos/effects/boostershieldcapacitypenalty.py b/eos/effects/boostershieldcapacitypenalty.py index 6e0b11eb7..ade1336d0 100644 --- a/eos/effects/boostershieldcapacitypenalty.py +++ b/eos/effects/boostershieldcapacitypenalty.py @@ -1,7 +1,7 @@ # boosterShieldCapacityPenalty # # Used by: -# Implants named like: Booster (12 of 33) +# Implants from group: Booster (12 of 66) type = "boosterSideEffect" # User-friendly name for the side effect diff --git a/eos/effects/capacitorcapacitybonus.py b/eos/effects/capacitorcapacitybonus.py index f2c0632da..c998bcaa0 100644 --- a/eos/effects/capacitorcapacitybonus.py +++ b/eos/effects/capacitorcapacitybonus.py @@ -1,7 +1,7 @@ # capacitorCapacityBonus # # Used by: -# Modules from group: Capacitor Battery (27 of 27) +# Modules from group: Capacitor Battery (30 of 30) type = "passive" diff --git a/eos/effects/dronemaxvelocitybonus.py b/eos/effects/dronemaxvelocitybonus.py index 938b34fa0..e3fc1ba6e 100644 --- a/eos/effects/dronemaxvelocitybonus.py +++ b/eos/effects/dronemaxvelocitybonus.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, container, context): level = container.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Drones"), - "maxVelocity", container.getModifiedItemAttr("droneMaxVelocityBonus") * level) + "maxVelocity", container.getModifiedItemAttr("droneMaxVelocityBonus") * level, stackingPenalties=True) diff --git a/eos/effects/energynosferatufalloff.py b/eos/effects/energynosferatufalloff.py index 0dc4fd81b..2d6414e61 100644 --- a/eos/effects/energynosferatufalloff.py +++ b/eos/effects/energynosferatufalloff.py @@ -1,7 +1,7 @@ # energyNosferatuFalloff # # Used by: -# Modules from group: Energy Nosferatu (51 of 51) +# Modules from group: Energy Nosferatu (54 of 54) from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" diff --git a/eos/effects/missileskillwarheadupgradesemdamagebonus.py b/eos/effects/missileskillwarheadupgradesemdamagebonus.py index 30d0605d5..63e95f67f 100644 --- a/eos/effects/missileskillwarheadupgradesemdamagebonus.py +++ b/eos/effects/missileskillwarheadupgradesemdamagebonus.py @@ -1,7 +1,7 @@ # missileSkillWarheadUpgradesEmDamageBonus # # Used by: -# Implants named like: Agency 'Pyrolancea' DB Dose (3 of 4) +# Implants named like: Agency 'Pyrolancea' DB Dose (4 of 4) # Skill: Warhead Upgrades type = "passive" diff --git a/eos/effects/missileskillwarheadupgradesexplosivedamagebonus.py b/eos/effects/missileskillwarheadupgradesexplosivedamagebonus.py index f0f3fcf3b..6a6228e8e 100644 --- a/eos/effects/missileskillwarheadupgradesexplosivedamagebonus.py +++ b/eos/effects/missileskillwarheadupgradesexplosivedamagebonus.py @@ -1,7 +1,7 @@ # missileSkillWarheadUpgradesExplosiveDamageBonus # # Used by: -# Implants named like: Agency 'Pyrolancea' DB Dose (3 of 4) +# Implants named like: Agency 'Pyrolancea' DB Dose (4 of 4) # Skill: Warhead Upgrades type = "passive" diff --git a/eos/effects/missileskillwarheadupgradeskineticdamagebonus.py b/eos/effects/missileskillwarheadupgradeskineticdamagebonus.py index a9eb7b86e..8b25142a8 100644 --- a/eos/effects/missileskillwarheadupgradeskineticdamagebonus.py +++ b/eos/effects/missileskillwarheadupgradeskineticdamagebonus.py @@ -1,7 +1,7 @@ # missileSkillWarheadUpgradesKineticDamageBonus # # Used by: -# Implants named like: Agency 'Pyrolancea' DB Dose (3 of 4) +# Implants named like: Agency 'Pyrolancea' DB Dose (4 of 4) # Skill: Warhead Upgrades type = "passive" diff --git a/eos/effects/missileskillwarheadupgradesthermaldamagebonus.py b/eos/effects/missileskillwarheadupgradesthermaldamagebonus.py index 8fc3ffc5c..737e5d20d 100644 --- a/eos/effects/missileskillwarheadupgradesthermaldamagebonus.py +++ b/eos/effects/missileskillwarheadupgradesthermaldamagebonus.py @@ -1,7 +1,7 @@ # missileSkillWarheadUpgradesThermalDamageBonus # # Used by: -# Implants named like: Agency 'Pyrolancea' DB Dose (3 of 4) +# Implants named like: Agency 'Pyrolancea' DB Dose (4 of 4) # Skill: Warhead Upgrades type = "passive" diff --git a/eos/effects/modifyenergywarfareresistance.py b/eos/effects/modifyenergywarfareresistance.py index 8cffe1386..874cbf0d7 100644 --- a/eos/effects/modifyenergywarfareresistance.py +++ b/eos/effects/modifyenergywarfareresistance.py @@ -1,7 +1,7 @@ # modifyEnergyWarfareResistance # # Used by: -# Modules from group: Capacitor Battery (27 of 27) +# Modules from group: Capacitor Battery (30 of 30) type = "passive" diff --git a/eos/effects/navigationvelocitybonuspostpercentmaxvelocityship.py b/eos/effects/navigationvelocitybonuspostpercentmaxvelocityship.py index 1ffbda714..6f68c7292 100644 --- a/eos/effects/navigationvelocitybonuspostpercentmaxvelocityship.py +++ b/eos/effects/navigationvelocitybonuspostpercentmaxvelocityship.py @@ -2,7 +2,7 @@ # # Used by: # Modules from group: Rig Anchor (4 of 4) -# Implants named like: Agency 'Overclocker' SB Dose (3 of 4) +# Implants named like: Agency 'Overclocker' SB Dose (4 of 4) # Implants named like: grade Snake (16 of 18) # Modules named like: Auxiliary Thrusters (8 of 8) # Implant: Quafe Zero diff --git a/eos/effects/overloadselfdurationbonus.py b/eos/effects/overloadselfdurationbonus.py index 498e3a527..90c275da8 100644 --- a/eos/effects/overloadselfdurationbonus.py +++ b/eos/effects/overloadselfdurationbonus.py @@ -3,7 +3,7 @@ # Used by: # Modules from group: Capacitor Booster (59 of 59) # Modules from group: Energy Neutralizer (54 of 54) -# Modules from group: Energy Nosferatu (51 of 51) +# Modules from group: Energy Nosferatu (54 of 54) # Modules from group: Hull Repair Unit (25 of 25) # Modules from group: Remote Armor Repairer (39 of 39) # Modules from group: Remote Capacitor Transmitter (41 of 41) diff --git a/eos/effects/remotewebifiermaxrangebonus.py b/eos/effects/remotewebifiermaxrangebonus.py index 19498882a..46e5887dd 100644 --- a/eos/effects/remotewebifiermaxrangebonus.py +++ b/eos/effects/remotewebifiermaxrangebonus.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, src, context): fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Stasis Web", "maxRange", - src.getModifiedItemAttr("stasisWebRangeBonus"), stackingPenalties=True) + src.getModifiedItemAttr("stasisWebRangeBonus"), stackingPenalties=False) diff --git a/eos/effects/rolebonuswdrange.py b/eos/effects/rolebonuswdrange.py index 3d6cbdf24..02e61cf78 100644 --- a/eos/effects/rolebonuswdrange.py +++ b/eos/effects/rolebonuswdrange.py @@ -6,7 +6,7 @@ type = "passive" def handler(fit, src, context): - fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Weapon Disruption"), "falloff", + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Weapon Disruption"), "falloffEffectiveness", src.getModifiedItemAttr("roleBonus")) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Weapon Disruption"), "maxRange", src.getModifiedItemAttr("roleBonus")) diff --git a/eos/effects/shieldboostamplifierpassivebooster.py b/eos/effects/shieldboostamplifierpassivebooster.py index 3dbd40327..30432474d 100644 --- a/eos/effects/shieldboostamplifierpassivebooster.py +++ b/eos/effects/shieldboostamplifierpassivebooster.py @@ -1,7 +1,7 @@ # shieldBoostAmplifierPassiveBooster # # Used by: -# Implants named like: Agency 'Hardshell' TB Dose (3 of 4) +# Implants named like: Agency 'Hardshell' TB Dose (4 of 4) # Implants named like: Blue Pill Booster (5 of 5) # Implant: Antipharmakon Thureo type = "passive" diff --git a/eos/effects/shipdronescoutthermaldamagegf2.py b/eos/effects/shipdronescoutthermaldamagegf2.py index afe18cb9d..54f7716e0 100644 --- a/eos/effects/shipdronescoutthermaldamagegf2.py +++ b/eos/effects/shipdronescoutthermaldamagegf2.py @@ -6,5 +6,5 @@ type = "passive" def handler(fit, ship, context): - fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drone Avionics"), + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Light Drone Operation"), "thermalDamage", ship.getModifiedItemAttr("shipBonusGF2"), skill="Gallente Frigate") diff --git a/eos/effects/shippdmgbonusmf.py b/eos/effects/shippdmgbonusmf.py index 0a359917e..c7ad6a186 100644 --- a/eos/effects/shippdmgbonusmf.py +++ b/eos/effects/shippdmgbonusmf.py @@ -1,11 +1,12 @@ # shipPDmgBonusMF # # Used by: -# Variations of ship: Slasher (3 of 3) # Ship: Cheetah # Ship: Freki # Ship: Republic Fleet Firetail # Ship: Rifter +# Ship: Slasher +# Ship: Stiletto # Ship: Wolf type = "passive" diff --git a/eos/effects/shipprojectilerofmf.py b/eos/effects/shipprojectilerofmf.py new file mode 100644 index 000000000..d4eb0ef4a --- /dev/null +++ b/eos/effects/shipprojectilerofmf.py @@ -0,0 +1,10 @@ +# shipProjectileRofMF +# +# Used by: +# Ship: Claw +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Projectile Turret"), "speed", + src.getModifiedItemAttr("shipBonusMF"), stackingPenalties=True, skill="Minmatar Frigate") diff --git a/eos/effects/skillbonusdronedurability.py b/eos/effects/skillbonusdronedurability.py index 1023c8412..97b3c34bf 100644 --- a/eos/effects/skillbonusdronedurability.py +++ b/eos/effects/skillbonusdronedurability.py @@ -1,12 +1,13 @@ # skillBonusDroneDurability # # Used by: +# Implants from group: Cyber Drones (2 of 2) # Skill: Drone Durability type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "hp", src.getModifiedItemAttr("hullHpBonus") * lvl) fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "armorHP", diff --git a/eos/effects/skillbonusdroneinterfacing.py b/eos/effects/skillbonusdroneinterfacing.py index 4aec63e83..0fbac4beb 100644 --- a/eos/effects/skillbonusdroneinterfacing.py +++ b/eos/effects/skillbonusdroneinterfacing.py @@ -1,12 +1,13 @@ # skillBonusDroneInterfacing # # Used by: +# Implants from group: Cyber Drones (2 of 2) # Skill: Drone Interfacing type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "damageMultiplier", src.getModifiedItemAttr("damageMultiplierBonus") * lvl) fit.fighters.filteredItemBoost(lambda mod: mod.item.requiresSkill("Fighters"), diff --git a/eos/effects/skillbonusdronenavigation.py b/eos/effects/skillbonusdronenavigation.py index 01453e891..4f5dd8bc5 100644 --- a/eos/effects/skillbonusdronenavigation.py +++ b/eos/effects/skillbonusdronenavigation.py @@ -6,7 +6,7 @@ type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "maxVelocity", src.getModifiedItemAttr("maxVelocityBonus") * lvl) fit.fighters.filteredItemBoost(lambda mod: mod.item.requiresSkill("Fighters"), "maxVelocity", diff --git a/eos/effects/skillbonusdronesharpshooting.py b/eos/effects/skillbonusdronesharpshooting.py index 29654615c..ea0738dc1 100644 --- a/eos/effects/skillbonusdronesharpshooting.py +++ b/eos/effects/skillbonusdronesharpshooting.py @@ -6,7 +6,7 @@ type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "maxRange", src.getModifiedItemAttr("rangeSkillBonus") * lvl) fit.fighters.filteredItemBoost(lambda mod: mod.item.requiresSkill("Fighters"), "fighterAbilityMissilesRange", diff --git a/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py b/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py index 5a801aab4..481ef2604 100644 --- a/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py +++ b/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py @@ -1,7 +1,7 @@ # surgicalStrikeDamageMultiplierBonusPostPercentDamageMultiplierLocationShipModulesRequiringGunnery # # Used by: -# Implants named like: Agency 'Pyrolancea' DB Dose (3 of 4) +# Implants named like: Agency 'Pyrolancea' DB Dose (4 of 4) # Implants named like: Eifyr and Co. 'Gunslinger' Surgical Strike SS (6 of 6) # Implant: Standard Cerebral Accelerator type = "passive" diff --git a/eos/effects/thermodynamicsskilldamagebonus.py b/eos/effects/thermodynamicsskilldamagebonus.py index 54569b06a..0386ad6e6 100644 --- a/eos/effects/thermodynamicsskilldamagebonus.py +++ b/eos/effects/thermodynamicsskilldamagebonus.py @@ -6,5 +6,5 @@ type = "passive" def handler(fit, skill, context): - fit.modules.filteredItemBoost(lambda mod: True, "heatDamage", + fit.modules.filteredItemBoost(lambda mod: "heatDamage" in mod.item.attributes, "heatDamage", skill.getModifiedItemAttr("thermodynamicsHeatDamage") * skill.level) diff --git a/eos/gamedata.py b/eos/gamedata.py index 7949361b9..7c7f85b8f 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)) @@ -512,7 +525,15 @@ class Group(EqBase): pass -class Icon(EqBase): +class DynamicItem(EqBase): + pass + + +class DynamicItemAttribute(EqBase): + pass + + +class DynamicItemItem(EqBase): pass @@ -532,7 +553,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: "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/graph/fitDps.py b/eos/graph/fitDps.py index 6aead3583..71a95d26e 100644 --- a/eos/graph/fitDps.py +++ b/eos/graph/fitDps.py @@ -141,14 +141,14 @@ class FitDpsGraph(Graph): targetVelocity = data["velocity"] explosionRadius = ability.fighter.getModifiedItemAttr("{}ExplosionRadius".format(prefix)) explosionVelocity = ability.fighter.getModifiedItemAttr("{}ExplosionVelocity".format(prefix)) - damageReductionFactor = ability.fighter.getModifiedItemAttr("{}ReductionFactor".format(prefix)) + damageReductionFactor = ability.fighter.getModifiedItemAttr("{}ReductionFactor".format(prefix), None) # the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation # wasn't already fucked. if damageReductionFactor is None: damageReductionFactor = ability.fighter.getModifiedItemAttr("{}DamageReductionFactor".format(prefix)) - damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix)) + damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix), None) if damageReductionSensitivity is None: damageReductionSensitivity = ability.fighter.getModifiedItemAttr( "{}DamageReductionSensitivity".format(prefix)) diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index b4c73c958..06d348b81 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -33,6 +33,15 @@ class ItemAttrShortcut(object): return return_value or default + def getBaseAttrValue(self, key, default=0): + ''' + Gets base value in this order: + Mutated value > override value > attribute value + ''' + return_value = self.itemModifiedAttributes.getOriginal(key) + + return return_value or default + class ChargeAttrShortcut(object): def getModifiedChargeAttr(self, key, default=0): @@ -59,8 +68,10 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__modified = {} # Affected by entities self.__affectedBy = {} - # Overrides + # Overrides (per item) self.__overrides = {} + # Mutators (per module) + self.__mutators = {} # Dictionaries for various value modification types self.__forced = {} self.__preAssigns = {} @@ -100,6 +111,14 @@ class ModifiedAttributeDict(collections.MutableMapping): def overrides(self, val): self.__overrides = val + @property + def mutators(self): + return {x.attribute.name: x for x in self.__mutators.values()} + + @mutators.setter + def mutators(self, val): + self.__mutators = val + def __getitem__(self, key): # Check if we have final calculated value key_value = self.__modified.get(key) @@ -128,14 +147,16 @@ class ModifiedAttributeDict(collections.MutableMapping): del self.__intermediary[key] def getOriginal(self, key, default=None): + val = None if self.overrides_enabled and self.overrides: - val = self.overrides.get(key, None) - else: - val = None + val = self.overrides.get(key, val) + + # mutators are overriden by overrides. x_x + val = self.mutators.get(key, val) if val is None: if self.original: - val = self.original.get(key, None) + val = self.original.get(key, val) if val is None and val != default: val = default diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index 655a3d3f1..4c6bf23db 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -142,14 +142,8 @@ class Booster(HandledItem, ItemAttrShortcut): copy = Booster(self.item) copy.active = self.active - # Legacy booster side effect code, disabling as not currently implemented - ''' - origSideEffects = list(self.iterSideEffects()) - copySideEffects = list(copy.iterSideEffects()) - i = 0 - while i < len(origSideEffects): - copySideEffects[i].active = origSideEffects[i].active - i += 1 - ''' + for sideEffect in self.sideEffects: + copyEffect = next(filter(lambda eff: eff.effectID == sideEffect.effectID, copy.sideEffects)) + copyEffect.active = sideEffect.active return copy diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 9edb866e8..7fc112d71 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -57,8 +57,15 @@ class Character(object): def init(self): self.__skillIdMap = {} + for skill in self.__skills: self.__skillIdMap[skill.itemID] = skill + + # get a list of skills that the character does no have, and add them (removal of old skills happens in the + # Skill loading) + for skillID in set(self.getSkillIDMap().keys()).difference(set(self.__skillIdMap.keys())): + self.addSkill(Skill(self, skillID, self.defaultLevel)) + self.dirtySkills = set() self.alphaClone = None @@ -118,9 +125,16 @@ class Character(object): return all0 + def apiUpdateCharSheet(self, skills, secStatus=0.00): + self.clearSkills() + for skillRow in skills: + self.addSkill(Skill(self, skillRow["typeID"], skillRow["level"])) + self.secStatus = float(secStatus) + def clearSkills(self): del self.__skills[:] self.__skillIdMap.clear() + self.dirtySkills.clear() @property def ro(self): diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 180838e31..694970343 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -291,6 +291,10 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __deepcopy__(self, memo): copy = Fighter(self.item) copy.amount = self.amount + copy.active = self.active + for ability in self.abilities: + copyAbility = next(filter(lambda a: a.effectID == ability.effectID, copy.abilities)) + copyAbility.active = ability.active return copy def fits(self, fit): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index e768bf216..8f5cc2b36 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1016,6 +1016,16 @@ class Fit(object): def getNumSlots(self, type): return self.ship.getModifiedItemAttr(self.slots[type]) or 0 + def getHardpointsFree(self, type): + if type == Hardpoint.NONE: + return 1 + elif type == Hardpoint.TURRET: + return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(Hardpoint.TURRET) + elif type == Hardpoint.MISSILE: + return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(Hardpoint.MISSILE) + else: + raise ValueError("%d is not a valid value for Hardpoint Enum", type) + @property def calibrationUsed(self): return self.getItemAttrOnlineSum(self.modules, 'upgradeCost') @@ -1570,6 +1580,7 @@ class Fit(object): copy_ship.name = "%s copy" % self.name copy_ship.damagePattern = self.damagePattern copy_ship.targetResists = self.targetResists + copy_ship.implantLocation = self.implantLocation copy_ship.notes = self.notes toCopy = ( @@ -1588,12 +1599,27 @@ class Fit(object): for i in orig: c.append(deepcopy(i)) - for fit in self.projectedFits: - copy_ship.__projectedFits[fit.ID] = fit - # this bit is required -- see GH issue # 83 + # this bit is required -- see GH issue # 83 + def forceUpdateSavedata(fit): eos.db.saveddata_session.flush() eos.db.saveddata_session.refresh(fit) + for fit in self.commandFits: + copy_ship.__commandFits[fit.ID] = fit + forceUpdateSavedata(fit) + copyCommandInfo = fit.getCommandInfo(copy_ship.ID) + originalCommandInfo = fit.getCommandInfo(self.ID) + copyCommandInfo.active = originalCommandInfo.active + forceUpdateSavedata(fit) + + for fit in self.projectedFits: + copy_ship.__projectedFits[fit.ID] = fit + forceUpdateSavedata(fit) + copyProjectionInfo = fit.getProjectionInfo(copy_ship.ID) + originalProjectionInfo = fit.getProjectionInfo(self.ID) + copyProjectionInfo.active = originalProjectionInfo.active + forceUpdateSavedata(fit) + return copy_ship def __repr__(self): diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 41bed8c50..d95afc5c6 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 @@ -27,6 +28,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.enum import Enum from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.saveddata.citadel import Citadel +from eos.saveddata.mutator import Mutator pyfalog = Logger(__name__) @@ -72,17 +74,33 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): """An instance of this class represents a module together with its charge and modified attributes""" DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive") MINING_ATTRIBUTES = ("miningAmount",) - SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Uninteractable Localized Effect Beacon", "Non-Interactable Object") + SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object") - def __init__(self, item): + def __init__(self, item, baseItem=None, mutaplasmid=None): """Initialize a module from the program""" - self.__item = item + + self.itemID = item.ID if item is not None else None + self.baseItemID = baseItem.ID if baseItem is not None else None + self.mutaplasmidID = mutaplasmid.ID if mutaplasmid is not None else None + + if baseItem is not None: + # we're working with a mutated module, need to get abyssal module loaded with the base attributes + # Note: there may be a better way of doing this, such as a metho on this classe to convert(mutaplamid). This + # will require a bit more research though, considering there has never been a need to "swap" out the item of a Module + # before, and there may be assumptions taken with regards to the item never changing (pre-calculated / cached results, for example) + self.__item = eos.db.getItemWithBaseItemAttribute(self.itemID, self.baseItemID) + self.__baseItem = baseItem + self.__mutaplasmid = mutaplasmid + else: + self.__item = item + self.__baseItem = baseItem + self.__mutaplasmid = mutaplasmid if item is not None and self.isInvalid: raise ValueError("Passed item is not a Module") self.__charge = None - self.itemID = item.ID if item is not None else None + self.projected = False self.state = State.ONLINE self.build() @@ -91,7 +109,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def init(self): """Initialize a module from the database and validate""" 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 @@ -102,6 +122,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): pyfalog.error("Item (id: {0}) does not exist", self.itemID) return + if self.baseItemID: + self.__item = eos.db.getItemWithBaseItemAttribute(self.itemID, self.baseItemID) + self.__baseItem = eos.db.getItem(self.baseItemID) + self.__mutaplasmid = eos.db.getMutaplasmid(self.mutaplasmidID) + if self.__baseItem is None: + pyfalog.error("Base Item (id: {0}) does not exist", self.itemID) + return + if self.isInvalid: pyfalog.error("Item (id: {0}) is not a Module", self.itemID) return @@ -133,6 +161,18 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__itemModifiedAttributes.overrides = self.__item.overrides self.__hardpoint = self.__calculateHardpoint(self.__item) self.__slot = self.__calculateSlot(self.__item) + + # Instantiate / remove mutators if this is a mutated module + if self.__baseItem: + for x in self.mutaplasmid.attributes: + attr = self.item.attributes[x.name] + id = attr.ID + if id not in self.mutators: # create the mutator + Mutator(self, attr, attr.value) + # @todo: remove attributes that are no longer part of the mutaplasmid. + + self.__itemModifiedAttributes.mutators = self.mutators + if self.__charge: self.__chargeModifiedAttributes.original = self.__charge.attributes self.__chargeModifiedAttributes.overrides = self.__charge.overrides @@ -162,11 +202,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def isInvalid(self): + # todo: validate baseItem as well if it's set. if self.isEmpty: 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): + return self.baseItemID or self.mutaplasmidID @property def numCharges(self): @@ -306,6 +352,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def item(self): return self.__item if self.__item != 0 else None + @property + def baseItem(self): + return self.__baseItem + + @property + def mutaplasmid(self): + return self.__mutaplasmid + @property def charge(self): return self.__charge if self.__charge != 0 else None @@ -472,7 +526,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if max is not None: current = 0 # if self.owner != fit else -1 # Disabled, see #1278 for mod in fit.modules: - if mod.item and mod.item.groupID == self.item.groupID: + if (mod.item and mod.item.groupID == self.item.groupID and + self.modPosition != mod.modPosition): current += 1 if current >= max: @@ -480,12 +535,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): # Check this only if we're told to do so if hardpointLimit: - if self.hardpoint == Hardpoint.TURRET: - if fit.ship.getModifiedItemAttr('turretSlotsLeft') - fit.getHardpointsUsed(Hardpoint.TURRET) < 1: - return False - elif self.hardpoint == Hardpoint.MISSILE: - if fit.ship.getModifiedItemAttr('launcherSlotsLeft') - fit.getHardpointsUsed(Hardpoint.MISSILE) < 1: - return False + if fit.getHardpointsFree(self.hardpoint) < 1: + return False return True @@ -575,7 +626,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): for i in range(5): itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i), None) if itemChargeGroup is not None: - g = eos.db.getGroup(int(itemChargeGroup), eager=("items.icon", "items.attributes")) + g = eos.db.getGroup(int(itemChargeGroup), eager=("items.attributes")) if g is None: continue for singleItem in g.items: @@ -786,9 +837,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 new file mode 100644 index 000000000..07af6876f --- /dev/null +++ b/eos/saveddata/mutator.py @@ -0,0 +1,131 @@ +# =============================================================================== +# Copyright (C) 2015 Ryan Holmes +# +# 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 logbook import Logger + +from sqlalchemy.orm import validates, reconstructor + +import eos.db +from eos.eqBase import EqBase + +pyfalog = Logger(__name__) + + +class Mutator(EqBase): + """ Mutators are the object that represent an attribute override on the module level, in conjunction with + mutaplasmids. Each mutated module, when created, is instantiated with a list of these objects, dictated by the + mutaplasmid that is used on the base module. + + A note on the different attributes on this object: + * attribute: points to the definition of the attribute from dgmattribs. + * baseAttribute: points to the attribute defined for the base item (contains the base value with with to mutate) + * dynamicAttribute: points to the Mutaplasmid definition of the attribute, including min/max + + This could probably be cleaned up with smarter relationships, but whatever + """ + + def __init__(self, module, attr, value): + # this needs to be above module assignment, as assigning the module will add it to the list and it via + # relationship and needs this set 4correctly + self.attrID = attr.ID + + self.module = module + self.moduleID = module.ID + + self.__attr = attr + self.build() + self.value = value # must run after the build(), because the validator requires build() to run first + + @reconstructor + def init(self): + self.__attr = None + + if self.attrID: + self.__attr = eos.db.getAttributeInfo(self.attrID) + if self.__attr is None: + pyfalog.error("Attribute (id: {0}) does not exist", self.attrID) + return + + self.build() + self.value = self.value # run the validator (to ensure we catch any changed min/max values might CCP release) + + def build(self): + # try...except here to catch orphaned mutators. Pretty rare, only happens so far if hacking the database + # But put it here to remove the module link if it happens, until a better solution can be developed + try: + # dynamic attribute links to the Mutaplasmids attribute definition for this mutated definition + self.dynamicAttribute = next(a for a in self.module.mutaplasmid.attributes if a.attributeID == self.attrID) + # base attribute links to the base ite's attribute for this mutated definition (contains original, base value) + self.baseAttribute = self.module.item.attributes[self.dynamicAttribute.name] + except: + self.module = None + + @validates("value") + def validator(self, key, val): + """ Validates values as properly falling within the range of the modules' Mutaplasmid """ + mod = val / self.baseValue + + 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 + if val >= 0: + returnVal = min(self.maxValue, max(self.minValue, val)) + else: + returnVal = max(self.maxValue, min(self.minValue, val)) + + return returnVal + + @property + def isInvalid(self): + # @todo: need to test what happens: + # 1) if an attribute is removed from the EVE database + # 2) if a mutaplasmid does not have the attribute anymore + # 3) if a mutaplasmid does not exist (in eve or on the module's item) + # Can remove invalid ones in a SQLAlchemy collection class... eventually + return self.__attr is None + + @property + def highIsGood(self): + return self.attribute.highIsGood + + @property + def minMod(self): + return round(self.dynamicAttribute.min, 3) + + @property + def maxMod(self): + return round(self.dynamicAttribute.max, 3) + + @property + def baseValue(self): + return self.baseAttribute.value + + @property + def minValue(self): + return self.minMod * self.baseAttribute.value + + @property + def maxValue(self): + return self.maxMod * self.baseAttribute.value + + @property + def attribute(self): + return self.__attr diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 56b3f7c0d..1e1dd30d6 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -131,7 +131,7 @@ class Ship(ItemAttrShortcut, HandledItem): return None items = [] - g = eos.db.getGroup("Ship Modifiers", eager=("items.icon", "items.attributes")) + g = eos.db.getGroup("Ship Modifiers", eager=("items.attributes")) for item in g.items: # Rely on name detection because race is not reliable if item.name.lower().startswith(self.item.name.lower()): diff --git a/eve.db b/eve.db index 8a0b29576..ecab30628 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/attribute_gauge.py b/gui/attribute_gauge.py new file mode 100644 index 000000000..1057775f7 --- /dev/null +++ b/gui/attribute_gauge.py @@ -0,0 +1,503 @@ +import copy +import wx +import math + +from gui.utils import color as color_utils +from gui.utils import draw, anim_effects +from service.fit import Fit + +# todo: clean class up. Took from pyfa gauge, has a bunch of extra shit we don't need + + +class AttributeGauge(wx.Window): + def __init__(self, parent, max_range=100, animate=True, leading_edge=True, edge_on_neutral=True, guide_lines=False, size=(-1, 30), *args, + **kargs): + + super().__init__(parent, size=size, *args, **kargs) + + self._size = size + + self.guide_lines = guide_lines + + self._border_colour = wx.BLACK + self._bar_colour = None + self._bar_gradient = None + + self.leading_edge = leading_edge + self.edge_on_neutral = edge_on_neutral + + self._border_padding = 0 + self._max_range = max_range + self._value = 0 + + self._fraction_digits = 0 + + self._timer_id = wx.NewId() + self._timer = None + + self._oldValue = 0 + + self._animate = animate + self._anim_duration = 500 + self._anim_step = 0 + self._period = 20 + self._anim_value = 0 + self._anim_direction = 0 + self.anim_effect = anim_effects.OUT_QUAD + + # transition colors used based on how full (or overfilled) the gauge is. + self.transition_colors = [ + (wx.Colour(191, 191, 191), wx.Colour(96, 191, 0)), # < 0-100% + (wx.Colour(191, 167, 96), wx.Colour(255, 191, 0)), # < 100-101% + (wx.Colour(255, 191, 0), wx.Colour(255, 128, 0)), # < 101-103% + (wx.Colour(255, 128, 0), wx.Colour(255, 0, 0)) # < 103-105% + ] + + self.goodColor = wx.Colour(96, 191, 0) + self.badColor = wx.Colour(255, 64, 0) + + self.gradient_effect = -35 + + self._percentage = 0 + self._old_percentage = 0 + self._show_remaining = False + + self.SetBackgroundColour(wx.Colour(51, 51, 51)) + + self._tooltip = wx.ToolTip("0.00/100.00") + self.SetToolTip(self._tooltip) + + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_TIMER, self.OnTimer) + self.Bind(wx.EVT_ENTER_WINDOW, self.OnWindowEnter) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) + self.SetBackgroundStyle(wx.BG_STYLE_PAINT) + + def OnEraseBackground(self, event): + pass + + def OnWindowEnter(self, event): + self._show_remaining = True + self.Refresh() + + def OnWindowLeave(self, event): + self._show_remaining = False + self.Refresh() + + def GetBorderColour(self): + return self._border_colour + + def SetBorderColour(self, colour): + self._border_colour = colour + + def GetBarColour(self): + return self._bar_colour + + def SetBarColour(self, colour): + self._bar_colour = colour + + def SetFractionDigits(self, digits): + self._fraction_digits = digits + + def GetBarGradient(self): + if self._bar_gradient is None: + return None + + return self._bar_gradient[0] + + def SetBarGradient(self, gradient=None): + if gradient is None: + self._bar_gradient = None + else: + if not isinstance(gradient, list): + self._bar_gradient = [gradient] + else: + self._bar_gradient = list(gradient) + + def GetBorderPadding(self): + return self._border_padding + + def SetBorderPadding(self, padding): + self._border_padding = padding + + def GetRange(self): + """ Returns the maximum value of the gauge. """ + return self._max_range + + def Animate(self): + if self._animate: + if not self._timer: + self._timer = wx.Timer(self, self._timer_id) + + self._anim_step = 0 + self._timer.Start(self._period) + else: + self._anim_value = self._percentage + self.Refresh() + + def SetRange(self, range, reinit=False, animate=True): + """ + Sets the range of the gauge. The gauge length is its + value as a proportion of the range. + """ + + if self._max_range == range: + return + + # we cannot have a range of zero (laws of physics, etc), so we set it + if range <= 0: + self._max_range = 0.01 + else: + self._max_range = range + + if reinit is False: + self._old_percentage = self._percentage + self._percentage = (self._value / self._max_range) * 100 + else: + self._old_percentage = self._percentage + self._percentage = 0 + self._value = 0 + + if animate: + self.Animate() + + self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range if self._max_range > 0.01 else 0)) + + def GetValue(self): + return self._value + + def SetValue(self, value, animate=True): + """ Sets the current position of the gauge. """ + + print("=" * 20, self._percentage) + if self._value == value: + return + + self._old_percentage = self._percentage + self._value = value + + self._percentage = (self._value / self._max_range) * 100 + + if animate: + self.Animate() + + self._tooltip.SetTip("%.2f/%.2f" % (self._value, self._max_range)) + + def SetValueRange(self, value, range, reinit=False): + """ Set both value and range of the gauge. """ + range_ = float(range) + + if range_ <= 0: + self._max_range = 0.01 + else: + self._max_range = range_ + + value = float(value) + + self._value = value + + if reinit is False: + self._old_percentage = self._percentage + self._percentage = (self._value / self._max_range) * 100 + + else: + self._old_percentage = self._percentage + self._percentage = 0 + + self.Animate() + self._tooltip.SetTip("%.2f/%.2f" % + (self._value, self._max_range if float(self._max_range) > 0.01 else 0)) + + def OnPaint(self, event): + dc = wx.AutoBufferedPaintDC(self) + rect = self.GetClientRect() + + dc.SetBackground(wx.Brush(self.GetBackgroundColour())) + dc.Clear() + + colour = self.GetBackgroundColour() + + dc.SetBrush(wx.Brush(colour)) + dc.SetPen(wx.Pen(colour)) + + dc.DrawRectangle(rect) + + value = self._percentage + + if self._timer: + if self._timer.IsRunning(): + value = self._anim_value + + if self._border_colour: + dc.SetPen(wx.Pen(self.GetBorderColour())) + dc.DrawRectangle(rect) + pad = 1 + self.GetBorderPadding() + rect.Deflate(pad, pad) + + if True: + # if we have a bar color set, then we will use this + colour = self.goodColor if value >= 0 else self.badColor + + is_even = rect.width % 2 == 0 + + # the size of half our available drawing area (since we're only working in halves) + half = (rect.width / 2) + + # calculate width of bar as a percentage of half the space + w = abs(half * (value / 100)) + w = min(w, half) # Ensure that we don't overshoot our drawing area + w = math.ceil(w) # round up to nearest pixel, this ensures that we don't lose representation for sub pixels + + # print("Percentage: {}\t\t\t\t\tValue: {}\t\t\t\t\tWidth: {}\t\t\t\t\tHalf: {}\t\t\t\t\tRect Width: {}".format( + # round(self._percentage, 3), round(value,3), w, half, rect.width)) + + # set guide_lines every 10 pixels of the main gauge (not including borders) + if self.guide_lines: + for x in range(1, 20): + dc.SetBrush(wx.Brush(wx.LIGHT_GREY)) + dc.SetPen(wx.Pen(wx.LIGHT_GREY)) + dc.DrawRectangle(x * 10, 1, 1, rect.height) + + dc.SetBrush(wx.Brush(colour)) + dc.SetPen(wx.Pen(colour)) + + # If we have an even width, we can simply dedicate the middle-most pixels to both sides + # However, if there is an odd width, the middle pixel is shared between the left and right gauge + + if value >= 0: + padding = (half if is_even else math.ceil(half - 1)) + 1 + dc.DrawRectangle(padding, 1, w, rect.height) + else: + padding = half - w + 1 if is_even else math.ceil(half) - (w - 1) + dc.DrawRectangle(padding, 1, w, rect.height) + + if self.leading_edge and (self.edge_on_neutral or value != 0): + dc.SetPen(wx.Pen(wx.WHITE)) + dc.SetBrush(wx.Brush(wx.WHITE)) + + if value > 0: + dc.DrawRectangle(min(padding + w, rect.width), 1, 1, rect.height) + else: + dc.DrawRectangle(max(padding - 1, 1), 1, 1, rect.height) + + def OnTimer(self, event): + old_value = self._old_percentage + value = self._percentage + start = 0 + + # -1 = left direction, 1 = right direction + direction = 1 if old_value < value else -1 + + end = direction * (value - old_value) + + self._anim_direction = direction + step = self.anim_effect(self._anim_step, start, end, self._anim_duration) + + self._anim_step += self._period + + if self._timer_id == event.GetId(): + stop_timer = False + + if self._anim_step > self._anim_duration: + stop_timer = True + + # add new value to the animation if we haven't reached our goal + # otherwise, stop animation + if direction == 1: + if old_value + step < value: + self._anim_value = old_value + step + else: + stop_timer = True + else: + if old_value - step > value: + self._anim_value = old_value - step + else: + stop_timer = True + + if stop_timer: + self._timer.Stop() + + self.Refresh() + + +if __name__ == "__main__": + import random + + def frange(x, y, jump): + while x < y: + yield x + x += jump + + class MyPanel(wx.Panel): + def __init__(self, parent, size=(500, 500)): + wx.Panel.__init__(self, parent, size=size) + box = wx.BoxSizer(wx.VERTICAL) + + self.gauge = gauge = AttributeGauge(self, size=(204, 4)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(100) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge11 = gauge = AttributeGauge(self, size=(204, 6)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(100) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge12 = gauge = AttributeGauge(self, size=(204, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(100) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge13 = gauge = AttributeGauge(self, size=(204, 10)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(100) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.value = wx.StaticText(self, label="Text") + box.Add(self.value, 0, wx.ALL | wx.CENTER, 5) + + self.btn = wx.Button(self, label="Toggle Timer") + box.Add(self.btn, 0, wx.ALL | wx.CENTER, 5) + self.btn.Bind(wx.EVT_BUTTON, self.ToggleTimer) + + self.spinCtrl = wx.SpinCtrl(self, min=-10000, max=10000) + box.Add(self.spinCtrl, 0, wx.ALL | wx.CENTER, 5) + self.spinCtrl.Bind(wx.EVT_SPINCTRL, self.UpdateValue) + + self.m_staticline2 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + box.Add(self.m_staticline2, 0, wx.EXPAND, 5) + + self.spinCtrl2 = wx.SpinCtrl(self, min=0, max=10000) + box.Add(self.spinCtrl2, 0, wx.ALL | wx.CENTER, 5) + self.spinCtrl2.Bind(wx.EVT_SPINCTRL, self.UpdateValue2) + + box.Add(wx.StaticText(self, label="Large Even Pixel Test"), 0, wx.ALL | wx.CENTER, 5) + + guide_lines = False + + self.gauge2 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(204, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(2) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge3 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(204, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(-2) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + box.Add(wx.StaticText(self, label="Large Odd Pixel Test"), 0, wx.ALL | wx.CENTER, 5) + + self.gauge4 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(205, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(2) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge5 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(205, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(-2) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + box.Add(wx.StaticText(self, label="Small Even Pixel Test"), 0, wx.ALL | wx.CENTER, 5) + + self.gauge6 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(100, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(75) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge7 = gauge = AttributeGauge(self, guide_lines=guide_lines, size=(100, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(-75) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + box.Add(wx.StaticText(self, label="Small Odd Pixel Test"), 0, wx.ALL | wx.CENTER, 5) + + self.gauge8 = gauge = AttributeGauge(self, guide_lines=guide_lines, max_range=100, size=(101, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(1) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.gauge9 = gauge = AttributeGauge(self, guide_lines=guide_lines, max_range=100, size=(101, 8)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(-1) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL | wx.CENTER, 10) + + self.SetSizer(box) + self.Layout() + + self.animTimer = wx.Timer(self, wx.NewId()) + self.Bind(wx.EVT_TIMER, self.OnTimer) + + self.animTimer.Start(1000) + + def ToggleTimer(self, evt): + if self.animTimer.IsRunning: + self.animTimer.Stop() + else: + self.animTimer.Start(1000) + + def UpdateValue(self, event): + if self.animTimer.IsRunning: + self.animTimer.Stop() + num = self.spinCtrl.GetValue() + self.gauge.SetValue(num) + self.gauge11.SetValue(num) + self.gauge12.SetValue(num) + self.gauge13.SetValue(num) + self.value.SetLabel(str(num)) + + def UpdateValue2(self, event): + num = self.spinCtrl2.GetValue() + self.gauge2.SetValue(num) + self.gauge3.SetValue(num * -1) + self.gauge4.SetValue(num) + self.gauge5.SetValue(num * -1) + self.gauge6.SetValue(num) + self.gauge7.SetValue(num * -1) + self.gauge8.SetValue(num) + self.gauge9.SetValue(num * -1) + + def OnTimer(self, evt): + num = random.randint(-100, 100) + self.gauge.SetValue(num) + self.gauge11.SetValue(num) + self.gauge12.SetValue(num) + self.gauge13.SetValue(num) + self.value.SetLabel(str(num)) + + class Frame(wx.Frame): + def __init__(self, title, size=(500, 800)): + wx.Frame.__init__(self, None, title=title, size=size) + self.statusbar = self.CreateStatusBar() + main_sizer = wx.BoxSizer(wx.VERTICAL) + panel = MyPanel(self, size=size) + main_sizer.Add(panel) + self.SetSizer(main_sizer) + + app = wx.App(redirect=False) # Error messages go to popup window + top = Frame("Test Attribute Bar") + top.Show() + app.MainLoop() diff --git a/gui/bitmap_loader.py b/gui/bitmap_loader.py index 9330f1fc3..061e05a0c 100644 --- a/gui/bitmap_loader.py +++ b/gui/bitmap_loader.py @@ -32,12 +32,15 @@ logging = Logger(__name__) class BitmapLoader(object): - try: - archive = zipfile.ZipFile(os.path.join(config.pyfaPath, 'imgs.zip'), 'r') - logging.info("Using zipped image files.") - except (IOError, TypeError): - logging.info("Using local image files.") - archive = None + # try: + # archive = zipfile.ZipFile(os.path.join(config.pyfaPath, 'imgs.zip'), 'r') + # logging.info("Using zipped image files.") + # except (IOError, TypeError): + # logging.info("Using local image files.") + # archive = None + + logging.info("Using local image files.") + archive = None cached_bitmaps = OrderedDict() dont_use_cached_bitmaps = False @@ -46,7 +49,7 @@ class BitmapLoader(object): @classmethod def getStaticBitmap(cls, name, parent, location): static = wx.StaticBitmap(parent) - static.SetBitmap(cls.getBitmap(name, location)) + static.SetBitmap(cls.getBitmap(name or 0, location)) return static @classmethod 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/commandView.py b/gui/builtinAdditionPanes/commandView.py index 5ee0d1872..2bfa700c9 100644 --- a/gui/builtinAdditionPanes/commandView.py +++ b/gui/builtinAdditionPanes/commandView.py @@ -35,7 +35,7 @@ from service.fit import Fit class DummyItem(object): def __init__(self, txt): self.name = txt - self.icon = None + self.iconID = None class DummyEntry(object): diff --git a/gui/builtinAdditionPanes/projectedView.py b/gui/builtinAdditionPanes/projectedView.py index a5654d1a4..24ca11f3a 100644 --- a/gui/builtinAdditionPanes/projectedView.py +++ b/gui/builtinAdditionPanes/projectedView.py @@ -39,7 +39,7 @@ pyfalog = Logger(__name__) class DummyItem(object): def __init__(self, txt): self.name = txt - self.icon = None + self.iconID = None class DummyEntry(object): @@ -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/boosterSideEffects.py b/gui/builtinContextMenus/boosterSideEffects.py index 60f5e2ca5..a936e9340 100644 --- a/gui/builtinContextMenus/boosterSideEffects.py +++ b/gui/builtinContextMenus/boosterSideEffects.py @@ -34,6 +34,7 @@ class BoosterSideEffect(ContextMenu): label = ability.name id = ContextMenu.nextID() self.effectIds[id] = ability + menuItem = wx.MenuItem(menu, id, label, kind=wx.ITEM_CHECK) menu.Bind(wx.EVT_MENU, self.handleMode, menuItem) return menuItem 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/builtinContextMenus/moduleAmmoPicker.py b/gui/builtinContextMenus/moduleAmmoPicker.py index 81c02eab0..93ff48754 100644 --- a/gui/builtinContextMenus/moduleAmmoPicker.py +++ b/gui/builtinContextMenus/moduleAmmoPicker.py @@ -117,8 +117,8 @@ class ModuleAmmoPicker(ContextMenu): 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, "icons") + if charge is not None and charge.iconID is not None: + bitmap = BitmapLoader.getBitmap(charge.iconID, "icons") if bitmap is not None: item.SetBitmap(bitmap) diff --git a/gui/builtinContextMenus/mutaplasmids.py b/gui/builtinContextMenus/mutaplasmids.py new file mode 100644 index 000000000..253cdac60 --- /dev/null +++ b/gui/builtinContextMenus/mutaplasmids.py @@ -0,0 +1,78 @@ +from gui.contextMenu import ContextMenu +import gui.mainFrame +# noinspection PyPackageRequirements +import wx +import gui.globalEvents as GE +from service.fit import Fit +from service.settings import ContextMenuSettings + + +class MutaplasmidCM(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() + self.eventIDs = {} + + def display(self, srcContext, selection): + + # if not self.settings.get('ammoPattern'): + # return False + + if srcContext not in ("fittingModule") or self.mainFrame.getActiveFit() is None: + return False + + mod = selection[0] + if len(mod.item.mutaplasmids) == 0 and not mod.isMutated: + return False + + return True + + def getText(self, itmContext, selection): + mod = selection[0] + return "Apply Mutaplasmid" if not mod.isMutated else "Revert to {}".format(mod.baseItem.name) + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + if selection[0].isMutated: + return None + + msw = True if "wxMSW" in wx.PlatformInfo else False + self.skillIds = {} + sub = wx.Menu() + + mod = selection[0] + + menu = rootMenu if msw else sub + + for item in mod.item.mutaplasmids: + label = item.item.name + id = ContextMenu.nextID() + self.eventIDs[id] = (item, mod) + skillItem = wx.MenuItem(menu, id, label) + menu.Bind(wx.EVT_MENU, self.handleMenu, skillItem) + sub.Append(skillItem) + + return sub + + def handleMenu(self, event): + mutaplasmid, mod = self.eventIDs[event.Id] + fit = self.mainFrame.getActiveFit() + sFit = Fit.getInstance() + + # todo: dev out function to switch module to an abyssal module. Also, maybe open item stats here automatically + # with the attribute tab set? + sFit.convertMutaplasmid(fit, mod.modPosition, mutaplasmid) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fit)) + + def activate(self, fullContext, selection, i): + sFit = Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + + mod = selection[0] + sFit.changeModule(fitID, mod.modPosition, mod.baseItemID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + def getBitmap(self, context, selection): + return None + + +MutaplasmidCM.register() diff --git a/gui/builtinContextMenus/whProjector.py b/gui/builtinContextMenus/whProjector.py index 1ce95c906..ac0bd5c1b 100644 --- a/gui/builtinContextMenus/whProjector.py +++ b/gui/builtinContextMenus/whProjector.py @@ -195,7 +195,7 @@ class WhProjector(ContextMenu): def getLocalizedEnvironments(self): sMkt = Market.getInstance() - grp = sMkt.getGroup("Uninteractable Localized Effect Beacon") + grp = sMkt.getGroup("Abyssal Hazards") effects = dict() diff --git a/gui/builtinGraphs/fitDps.py b/gui/builtinGraphs/fitDps.py index d848d3f9c..183bc9ac8 100644 --- a/gui/builtinGraphs/fitDps.py +++ b/gui/builtinGraphs/fitDps.py @@ -55,7 +55,7 @@ class FitDpsGraph(Graph): icons = {} sAttr = Attribute.getInstance() for key, attrName in self.propertyAttributeMap.items(): - iconFile = sAttr.getAttributeInfo(attrName).icon.iconFile + iconFile = sAttr.getAttributeInfo(attrName).iconID bitmap = BitmapLoader.getBitmap(iconFile, "icons") if bitmap: icons[key] = bitmap diff --git a/gui/builtinItemStatsViews/attributeSlider.py b/gui/builtinItemStatsViews/attributeSlider.py new file mode 100644 index 000000000..69358278c --- /dev/null +++ b/gui/builtinItemStatsViews/attributeSlider.py @@ -0,0 +1,248 @@ +import wx +import wx.lib.newevent +from gui.attribute_gauge import AttributeGauge + +import eos +import eos.db + +_ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent() + + +class AttributeSliderChangeEvent: + def __init__(self, obj, old_value, new_value, old_percentage, new_percentage): + self.__obj = obj + self.__old = old_value + self.__new = new_value + self.__old_percent = old_percentage + self.__new_percent = new_percentage + + def GetObj(self): + return self.__obj + + def GetOldValue(self): + return self.__old + + def GetValue(self): + return self.__new + + def GetOldPercentage(self): + return self.__old_percent + + def GetPercentage(self): + return self.__new_percent + + Object = property(GetObj) + OldValue = property(GetOldValue) + Value = property(GetValue) + OldPercentage = property(GetOldPercentage) + Percentage = property(GetPercentage) + + +class ValueChanged(_ValueChanged, AttributeSliderChangeEvent): + def __init__(self, obj, old_value, new_value, old_percentage, new_percentage): + _ValueChanged.__init__(self) + AttributeSliderChangeEvent.__init__(self, obj, old_value, new_value, old_percentage, new_percentage) + + +class AttributeSlider(wx.Panel): + # Slider which abstracts users values from internal values (because the built in slider does not deal with floats + # and the like), based on http://wxpython-users.wxwidgets.narkive.com/ekgBzA7u/anyone-ever-thought-of-a-floating-point-slider + + def __init__(self, parent, baseValue, minMod, maxMod, inverse=False, id=-1): + wx.Panel.__init__(self, parent, id=id) + + self.parent = parent + + self.inverse = inverse + + self.base_value = baseValue + + self.UserMinValue = minMod + self.UserMaxValue = maxMod + + # The internal slider basically represents the percentage towards the end of the range. It has to be normalized + # in this way, otherwise when we start off with a base, if the range is skewed to one side, the base value won't + # be centered. We use a range of -100,100 so that we can depend on the SliderValue to contain the percentage + # toward one end + + # Additionally, since we want the slider to be accurate to 3 decimal places, we need to blow out the two ends here + # (if we have a slider that needs to land on 66.66% towards the right, it will actually be converted to 66%. Se we need it to support 6,666) + + self.SliderMinValue = -100 + self.SliderMaxValue = 100 + self.SliderValue = 0 + + range = [(self.UserMinValue * self.base_value), (self.UserMaxValue * self.base_value)] + + self.ctrl = wx.SpinCtrlDouble(self, min=min(range), max=max(range)) + self.ctrl.SetDigits(3) + + self.ctrl.Bind(wx.EVT_SPINCTRLDOUBLE, self.UpdateValue) + + self.slider = AttributeGauge(self, size=(-1, 8)) + + b = 4 + vsizer1 = wx.BoxSizer(wx.VERTICAL) + vsizer1.Add(self.ctrl, 0, wx.LEFT | wx.RIGHT | wx.CENTER, b) + vsizer1.Add(self.slider, 0, wx.EXPAND | wx.ALL , b) + + self.SetSizerAndFit(vsizer1) + self.parent.SetClientSize((500, vsizer1.GetSize()[1])) + + def UpdateValue(self, evt): + self.SetValue(self.ctrl.GetValue()) + evt.Skip() + + def SetValue(self, value, post_event=True): + # todo: check this against values that might be 2.5x and whatnot + mod = value / self.base_value + self.ctrl.SetValue(value) + slider_percentage = 0 + if mod < 1: + modEnd = self.UserMinValue + slider_percentage = (1 - mod) / (1 - modEnd) * -100 + elif mod > 1: + modEnd = self.UserMaxValue + slider_percentage = ((mod - 1) / (modEnd - 1)) * 100 + # print(slider_percentage) + if self.inverse: + slider_percentage *= -1 + self.slider.SetValue(slider_percentage) + if post_event: + wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage)) + + +class TestAttributeSlider(wx.Frame): + + def __init__(self, parent, id): + title = 'Slider...' + pos = wx.DefaultPosition + size = wx.DefaultSize + sty = wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, parent, id, title, pos, size, sty) + + self.panel = AttributeSlider(self, -50, 0.8, 1.5, False) + self.panel.Bind(EVT_VALUE_CHANGED, self.thing) + self.panel.SetValue(-55) + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + + def OnCloseWindow(self, event): + self.Destroy() + + def thing(self, evt): + print("thing") + + +if __name__ == "__main__": + app = wx.App() + frame = TestAttributeSlider(None, wx.ID_ANY) + frame.Show() + app.MainLoop() + + +# class AttributeSliderDEV(wx.Panel): +# # Slider which abstracts users values from internal values (because the built in slider does not deal with floats +# # and the like), based on http://wxpython-users.wxwidgets.narkive.com/ekgBzA7u/anyone-ever-thought-of-a-floating-point-slider +# +# def __init__(self, parent, baseValue, minMod, maxMod): +# wx.Panel.__init__(self, parent) +# +# self.parent = parent +# +# self.base_value = baseValue +# +# self.UserMinValue = minMod +# self.UserMaxValue = maxMod +# +# # The internal slider basically represents the percentage towards the end of the range. It has to be normalized +# # in this way, otherwise when we start off with a base, if the range is skewed to one side, the base value won't +# # be centered. We use a range of -100,100 so that we can depend on the SliderValue to contain the percentage +# # toward one end +# +# # Additionally, since we want the slider to be accurate to 3 decimal places, we need to blow out the two ends here +# # (if we have a slider that needs to land on 66.66% towards the right, it will actually be converted to 66%. Se we need it to support 6,666) +# +# self.SliderMinValue = -100_000 +# self.SliderMaxValue = 100_000 +# self.SliderValue = 0 +# +# self.statxt1 = wx.StaticText(self, wx.ID_ANY, 'left', +# style=wx.ST_NO_AUTORESIZE | wx.ALIGN_LEFT) +# self.statxt2 = wx.StaticText(self, wx.ID_ANY, 'middle', +# style=wx.ST_NO_AUTORESIZE | wx.ALIGN_CENTRE) +# self.statxt3 = wx.StaticText(self, wx.ID_ANY, 'right', +# style=wx.ST_NO_AUTORESIZE | wx.ALIGN_RIGHT) +# +# self.statxt1.SetLabel("{0:.3f}".format(self.UserMinValue * self.base_value)) +# self.statxt1.SetToolTip("{0:+f}%".format((1-self.UserMinValue)*-100)) +# self.statxt2.SetLabel("{0:.3f}".format(self.base_value)) +# self.statxt3.SetLabel("{0:.3f}".format(self.UserMaxValue * self.base_value)) +# self.statxt3.SetToolTip("{0:+f}%".format((1-self.UserMaxValue)*-100)) +# +# self.slider = wx.Slider( +# self, wx.ID_ANY, +# self.SliderValue, +# self.SliderMinValue, +# self.SliderMaxValue, +# style=wx.SL_HORIZONTAL) +# +# self.slider.SetTickFreq((self.SliderMaxValue - self.SliderMinValue) / 15) +# +# self.slider.Bind(wx.EVT_SCROLL, self.OnScroll) +# +# b = 20 +# hsizer1 = wx.BoxSizer(wx.HORIZONTAL) +# hsizer1.Add(self.statxt1, 1, wx.RIGHT, b) +# hsizer1.Add(self.statxt2, 1, wx.LEFT | wx.RIGHT, b) +# hsizer1.Add(self.statxt3, 1, wx.LEFT, b) +# +# b = 4 +# vsizer1 = wx.BoxSizer(wx.VERTICAL) +# vsizer1.Add(hsizer1, 0, wx.EXPAND | wx.ALL, b) +# vsizer1.Add(self.slider, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, b) +# +# self.SetSizerAndFit(vsizer1) +# self.parent.SetClientSize((500, vsizer1.GetSize()[1])) +# +# def OnScroll(self, event): +# self.CalculateUserValue() +# +# def SetValue(self, value): +# # todo: check this against values that might be 2.5x and whatnot +# mod = value / self.base_value +# slider_percentage = 0 +# if mod < 1: +# modEnd = -1 * self.UserMinValue +# slider_percentage = (modEnd / mod) * 10_000 +# elif mod > 1: +# modEnd = self.UserMaxValue +# slider_percentage = ((mod-1)/(modEnd-1)) * 100_000 +# +# self.slider.SetValue(slider_percentage) +# self.CalculateUserValue() +# +# def CalculateUserValue(self): +# self.SliderValue = self.slider.GetValue() +# +# mod = 1 +# +# # The slider value tells us when mod we're going to use, depending on its sign +# if self.SliderValue < 0: +# mod = self.UserMinValue +# elif self.SliderValue > 0: +# mod = self.UserMaxValue +# +# # Get the slider value percentage as an absolute value +# slider_mod = abs(self.SliderValue/1_000) / 100 +# +# # Gets our new mod by use the slider's percentage to determine where in the spectrum it is +# new_mod = mod + ((1 - mod) - ((1 - mod) * slider_mod)) +# +# # Modifies our base value, to get out modified value +# newValue = new_mod * self.base_value +# +# if mod == 1: +# self.statxt2.SetLabel("{0:.3f}".format(newValue)) +# else: +# self.statxt2.SetLabel("{0:.3f} ({1:+.3f})".format(newValue, newValue - self.base_value, )) +# self.statxt2.SetToolTip("{0:+f}%".format(new_mod*100)) diff --git a/gui/builtinItemStatsViews/itemAffectedBy.py b/gui/builtinItemStatsViews/itemAffectedBy.py index ed4280045..94808b734 100644 --- a/gui/builtinItemStatsViews/itemAffectedBy.py +++ b/gui/builtinItemStatsViews/itemAffectedBy.py @@ -237,16 +237,16 @@ class ItemAffectedBy(wx.Panel): displayName = attrInfo.displayName if attrInfo and attrInfo.displayName != "" else attrName if attrInfo: - if attrInfo.icon is not None: - iconFile = attrInfo.icon.iconFile + if attrInfo.iconID is not None: + iconFile = attrInfo.iconID icon = BitmapLoader.getBitmap(iconFile, "icons") if icon is None: icon = BitmapLoader.getBitmap("transparent16x16", "gui") attrIcon = self.imageList.Add(icon) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) if self.showRealNames: display = attrName @@ -267,8 +267,8 @@ class ItemAffectedBy(wx.Panel): if afflictorType == Ship: itemIcon = self.imageList.Add(BitmapLoader.getBitmap("ship_small", "gui")) - elif item.icon: - bitmap = BitmapLoader.getBitmap(item.icon.iconFile, "icons") + elif item.iconID: + bitmap = BitmapLoader.getBitmap(item.iconID, "icons") itemIcon = self.imageList.Add(bitmap) if bitmap else -1 else: itemIcon = -1 @@ -373,8 +373,8 @@ class ItemAffectedBy(wx.Panel): counter = len(afflictors) if afflictorType == Ship: itemIcon = self.imageList.Add(BitmapLoader.getBitmap("ship_small", "gui")) - elif item.icon: - bitmap = BitmapLoader.getBitmap(item.icon.iconFile, "icons") + elif item.iconID: + bitmap = BitmapLoader.getBitmap(item.iconID, "icons") itemIcon = self.imageList.Add(bitmap) if bitmap else -1 else: itemIcon = -1 @@ -398,17 +398,17 @@ class ItemAffectedBy(wx.Panel): displayName = attrInfo.displayName if attrInfo else "" if attrInfo: - if attrInfo.icon is not None: - iconFile = attrInfo.icon.iconFile + if attrInfo.iconID is not None: + iconFile = attrInfo.iconID icon = BitmapLoader.getBitmap(iconFile, "icons") if icon is None: icon = BitmapLoader.getBitmap("transparent16x16", "gui") attrIcon = self.imageList.Add(icon) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) penalized = "" if '*' in attrModifier: diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index e40710c45..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 @@ -102,7 +101,7 @@ class ItemParams(wx.Panel): if saveFileDialog.ShowModal() == wx.ID_CANCEL: return # the user hit cancel... - with open(saveFileDialog.GetPath(), "wb") as exportFile: + with open(saveFileDialog.GetPath(), "w") as exportFile: writer = csv.writer(exportFile, delimiter=',') writer.writerow( @@ -174,7 +173,12 @@ class ItemParams(wx.Panel): info = self.attrInfo.get(name) att = self.attrValues[name] - valDefault = getattr(info, "value", None) + # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function, + # which will return the value with respect to the effective base (with mutators / overrides in place) + valDefault = getattr(info, "value", None) # Get default value from attribute + if self.stuff is not None: + # if it's a stuff, overwrite default (with fallback to current value) + valDefault = self.stuff.getBaseAttrValue(name, valDefault) valueDefault = valDefault if valDefault is not None else att val = getattr(att, "value", None) @@ -189,8 +193,8 @@ class ItemParams(wx.Panel): attrName += " ({})".format(info.ID) if info: - if info.icon is not None: - iconFile = info.icon.iconFile + if info.iconID is not None: + iconFile = info.iconID icon = BitmapLoader.getBitmap(iconFile, "icons") if icon is None: @@ -198,9 +202,9 @@ class ItemParams(wx.Panel): attrIcon = self.imageList.Add(icon) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) + attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) index = self.paramList.InsertItem(self.paramList.GetItemCount(), attrName, attrIcon) idNameMap[idCount] = attrName @@ -210,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) @@ -232,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/itemDependants.py b/gui/builtinItemStatsViews/itemDependants.py index 9f7e459c9..84b8f8997 100644 --- a/gui/builtinItemStatsViews/itemDependants.py +++ b/gui/builtinItemStatsViews/itemDependants.py @@ -44,8 +44,8 @@ class ItemDependents(wx.Panel): child = self.reqTree.AppendItem(parent, "Level {}".format(self.romanNb[int(x)]), sbIconId) for item in items: - if item.icon: - bitmap = BitmapLoader.getBitmap(item.icon.iconFile, "icons") + if item.iconID: + bitmap = BitmapLoader.getBitmap(item.iconID, "icons") itemIcon = self.imageList.Add(bitmap) if bitmap else -1 else: itemIcon = -1 diff --git a/gui/builtinItemStatsViews/itemDescription.py b/gui/builtinItemStatsViews/itemDescription.py index ddf30dfb4..b03b892df 100644 --- a/gui/builtinItemStatsViews/itemDescription.py +++ b/gui/builtinItemStatsViews/itemDescription.py @@ -31,3 +31,36 @@ class ItemDescription(wx.Panel): mainSizer.Add(self.description, 1, wx.ALL | wx.EXPAND, 0) self.Layout() + + self.description.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu) + self.description.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) + + self.popupMenu = wx.Menu() + copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy') + self.popupMenu.Append(copyItem) + self.popupMenu.Bind(wx.EVT_MENU, self.menuClickHandler, copyItem) + + def onPopupMenu(self, event): + self.PopupMenu(self.popupMenu) + + def menuClickHandler(self, event): + selectedMenuItem = event.GetId() + if selectedMenuItem == 1: # Copy was chosen + self.copySelectionToClipboard() + + def onKeyDown(self, event): + keyCode = event.GetKeyCode() + # Ctrl + C + if keyCode == 67 and event.ControlDown(): + self.copySelectionToClipboard() + # Ctrl + A + if keyCode == 65 and event.ControlDown(): + self.description.SelectAll() + + def copySelectionToClipboard(self): + selectedText = self.description.SelectionToText() + if selectedText == '': # if no selection, copy all content + selectedText = self.description.ToText() + if wx.TheClipboard.Open(): + wx.TheClipboard.SetData(wx.TextDataObject(selectedText)) + wx.TheClipboard.Close() diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py new file mode 100644 index 000000000..614274e6e --- /dev/null +++ b/gui/builtinItemStatsViews/itemMutator.py @@ -0,0 +1,169 @@ +# noinspection PyPackageRequirements +import wx + +from service.fit import Fit +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 random + +from logbook import Logger + +pyfalog = Logger(__name__) + + +class ItemMutator(wx.Panel): + + def __init__(self, parent, stuff, item): + wx.Panel.__init__(self, parent) + self.stuff = stuff + self.item = item + self.timer = None + self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit() + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.goodColor = wx.Colour(96, 191, 0) + self.badColor = wx.Colour(255, 64, 0) + + self.event_mapping = {} + + for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): + 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) + + # create array for the two ranges + min_t = [round(m.minValue, 3), m.minMod, None] + max_t = [round(m.maxValue, 3), m.maxMod, None] + + # Then we need to determine if it's better than original, which will be the color + min_t[2] = min_t[1] < 1 if not m.highIsGood else 1 < min_t[1] + max_t[2] = max_t[1] < 1 if not m.highIsGood else 1 < max_t[1] + + # Lastly, we need to determine which range value is "worse" (left side) or "better" (right side) + if (m.highIsGood and min_t[1] > max_t[1]) or (not m.highIsGood and min_t[1] < max_t[1]): + better_range = min_t + else: + better_range = max_t + + if (m.highIsGood and max_t[1] < min_t[1]) or (not m.highIsGood and max_t[1] > min_t[1]): + 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) + + font = parent.GetFont() + font.SetWeight(wx.BOLD) + + headingSizer.Add(BitmapLoader.getStaticBitmap(m.attribute.iconID, self, "icons"), 0, wx.RIGHT, 10) + + displayName = wx.StaticText(self, wx.ID_ANY, m.attribute.displayName) + displayName.SetFont(font) + + headingSizer.Add(displayName, 3, wx.ALL | wx.EXPAND, 0) + + 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, 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(range_high, 0, wx.RIGHT | wx.EXPAND, 10) + + mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5) + + mainSizer.Add(slider, 0, wx.RIGHT | wx.LEFT | wx.EXPAND, 10) + mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5) + + mainSizer.AddStretchSpacer() + + self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + mainSizer.Add(self.m_staticline, 0, wx.EXPAND) + + bSizer = wx.BoxSizer(wx.HORIZONTAL) + + self.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) + + self.SetSizer(mainSizer) + self.Layout() + + 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 + sFit = Fit.getInstance() + + # 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/builtinItemStatsViews/itemTraits.py b/gui/builtinItemStatsViews/itemTraits.py index 12abd078d..1ea0514a5 100644 --- a/gui/builtinItemStatsViews/itemTraits.py +++ b/gui/builtinItemStatsViews/itemTraits.py @@ -13,5 +13,38 @@ class ItemTraits(wx.Panel): self.traits = wx.html.HtmlWindow(self) self.traits.SetPage(item.traits.traitText) + self.traits.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu) + self.traits.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) + mainSizer.Add(self.traits, 1, wx.ALL | wx.EXPAND, 0) self.Layout() + + self.popupMenu = wx.Menu() + copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy') + self.popupMenu.Append(copyItem) + self.popupMenu.Bind(wx.EVT_MENU, self.menuClickHandler, copyItem) + + def onPopupMenu(self, event): + self.PopupMenu(self.popupMenu) + + def menuClickHandler(self, event): + selectedMenuItem = event.GetId() + if selectedMenuItem == 1: # Copy was chosen + self.copySelectionToClipboard() + + def onKeyDown(self, event): + keyCode = event.GetKeyCode() + # Ctrl + C + if keyCode == 67 and event.ControlDown(): + self.copySelectionToClipboard() + # Ctrl + A + if keyCode == 65 and event.ControlDown(): + self.traits.SelectAll() + + def copySelectionToClipboard(self): + selectedText = self.traits.SelectionToText() + if selectedText == '': # if no selection, copy all content + selectedText = self.traits.ToText() + if wx.TheClipboard.Open(): + wx.TheClipboard.SetData(wx.TextDataObject(selectedText)) + wx.TheClipboard.Close() diff --git a/gui/builtinMarketBrowser/marketTree.py b/gui/builtinMarketBrowser/marketTree.py index 8b1286e50..f6c62732c 100644 --- a/gui/builtinMarketBrowser/marketTree.py +++ b/gui/builtinMarketBrowser/marketTree.py @@ -53,6 +53,7 @@ class MarketTree(wx.TreeCtrl): # And add real market group contents sMkt = self.sMkt currentMktGrp = sMkt.getMarketGroup(self.GetItemData(root), eager="children") + for childMktGrp in sMkt.getMarketGroupChildren(currentMktGrp): # If market should have items but it doesn't, do not show it if sMkt.marketGroupValidityCheck(childMktGrp) is False: diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 412e1a988..a31e56f8a 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -38,6 +38,10 @@ class PFGeneralPref(PreferenceView): 0) mainSizer.Add(self.cbGlobalChar, 0, wx.ALL | wx.EXPAND, 5) + self.cbDefaultCharImplants = wx.CheckBox(panel, wx.ID_ANY, "Use character implants by default for new fits", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbDefaultCharImplants, 0, wx.ALL | wx.EXPAND, 5) + self.cbGlobalDmgPattern = wx.CheckBox(panel, wx.ID_ANY, "Use global damage pattern", wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbGlobalDmgPattern, 0, wx.ALL | wx.EXPAND, 5) @@ -119,6 +123,7 @@ class PFGeneralPref(PreferenceView): self.sFit = Fit.getInstance() self.cbGlobalChar.SetValue(self.sFit.serviceFittingOptions["useGlobalCharacter"]) + self.cbDefaultCharImplants.SetValue(self.sFit.serviceFittingOptions["useCharacterImplantsByDefault"]) self.cbGlobalDmgPattern.SetValue(self.sFit.serviceFittingOptions["useGlobalDamagePattern"]) self.cbFitColorSlots.SetValue(self.sFit.serviceFittingOptions["colorFitBySlot"] or False) self.cbRackSlots.SetValue(self.sFit.serviceFittingOptions["rackSlots"] or False) @@ -136,6 +141,7 @@ class PFGeneralPref(PreferenceView): self.intDelay.SetValue(self.sFit.serviceFittingOptions["marketSearchDelay"]) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) + self.cbDefaultCharImplants.Bind(wx.EVT_CHECKBOX, self.OnCBDefaultCharImplantsStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) self.cbFitColorSlots.Bind(wx.EVT_CHECKBOX, self.onCBGlobalColorBySlot) self.cbRackSlots.Bind(wx.EVT_CHECKBOX, self.onCBGlobalRackSlots) @@ -187,6 +193,10 @@ class PFGeneralPref(PreferenceView): self.sFit.serviceFittingOptions["useGlobalCharacter"] = self.cbGlobalChar.GetValue() event.Skip() + def OnCBDefaultCharImplantsStateChange(self, event): + self.sFit.serviceFittingOptions["useCharacterImplantsByDefault"] = self.cbDefaultCharImplants.GetValue() + event.Skip() + def OnCBGlobalDmgPatternStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalDamagePattern"] = self.cbGlobalDmgPattern.GetValue() event.Skip() diff --git a/gui/builtinShipBrowser/fitItem.py b/gui/builtinShipBrowser/fitItem.py index 3d782fe5f..2f05229d0 100644 --- a/gui/builtinShipBrowser/fitItem.py +++ b/gui/builtinShipBrowser/fitItem.py @@ -23,7 +23,7 @@ pyfalog = Logger(__name__) class FitItem(SFItem.SFBrowserItem): def __init__(self, parent, fitID=None, shipFittingInfo=("Test", "TestTrait", "cnc's avatar", 0, 0, None), shipID=None, - itemData=None, + itemData=None, graphicID=None, id=wx.ID_ANY, pos=wx.DefaultPosition, size=(0, 40), style=0): @@ -51,7 +51,7 @@ class FitItem(SFItem.SFBrowserItem): self.deleted = False if shipID: - self.shipBmp = BitmapLoader.getBitmap(str(shipID), "renders") + self.shipBmp = BitmapLoader.getBitmap(str(graphicID), "renders") if not self.shipBmp: self.shipBmp = BitmapLoader.getBitmap("ship_no_image_big", "gui") diff --git a/gui/builtinShipBrowser/shipItem.py b/gui/builtinShipBrowser/shipItem.py index 4c679fb8a..517648ad6 100644 --- a/gui/builtinShipBrowser/shipItem.py +++ b/gui/builtinShipBrowser/shipItem.py @@ -18,7 +18,7 @@ pyfalog = Logger(__name__) class ShipItem(SFItem.SFBrowserItem): - def __init__(self, parent, shipID=None, shipFittingInfo=("Test", "TestTrait", 2), itemData=None, + def __init__(self, parent, shipID=None, shipFittingInfo=("Test", "TestTrait", 2), itemData=None, graphicID=None, id=wx.ID_ANY, pos=wx.DefaultPosition, size=(0, 40), style=0): SFItem.SFBrowserItem.__init__(self, parent, size=size) @@ -36,8 +36,8 @@ class ShipItem(SFItem.SFBrowserItem): self.fontSmall = wx.Font(fonts.SMALL, wx.SWISS, wx.NORMAL, wx.NORMAL) self.shipBmp = None - if shipID: - self.shipBmp = BitmapLoader.getBitmap(str(shipID), "renders") + if graphicID: + self.shipBmp = BitmapLoader.getBitmap(str(graphicID), "renders") if not self.shipBmp: self.shipBmp = BitmapLoader.getBitmap("ship_no_image_big", "gui") diff --git a/gui/builtinViewColumns/ammoIcon.py b/gui/builtinViewColumns/ammoIcon.py index 7459db6a4..647d235a0 100644 --- a/gui/builtinViewColumns/ammoIcon.py +++ b/gui/builtinViewColumns/ammoIcon.py @@ -43,7 +43,7 @@ class AmmoIcon(ViewColumn): if stuff.charge is None: return -1 else: - iconFile = stuff.charge.icon.iconFile if stuff.charge.icon else "" + iconFile = stuff.charge.iconID if stuff.charge.iconID else "" if iconFile: return self.fittingView.imageList.GetImageIndex(iconFile, "icons") else: diff --git a/gui/builtinViewColumns/attributeDisplay.py b/gui/builtinViewColumns/attributeDisplay.py index 7ad8743d7..b36cf7132 100644 --- a/gui/builtinViewColumns/attributeDisplay.py +++ b/gui/builtinViewColumns/attributeDisplay.py @@ -41,7 +41,7 @@ class AttributeDisplay(ViewColumn): iconFile = "pg_small" iconType = "gui" else: - iconFile = info.icon.iconFile if info.icon else None + iconFile = info.iconID iconType = "icons" if iconFile: self.imageId = fittingView.imageList.GetImageIndex(iconFile, iconType) diff --git a/gui/builtinViewColumns/baseIcon.py b/gui/builtinViewColumns/baseIcon.py index 8c09a9fa3..207d639c5 100644 --- a/gui/builtinViewColumns/baseIcon.py +++ b/gui/builtinViewColumns/baseIcon.py @@ -35,10 +35,10 @@ class BaseIcon(ViewColumn): return self.fittingView.imageList.GetImageIndex("slot_%s_small" % Slot.getName(stuff.slot).lower(), "gui") else: - return self.loadIconFile(stuff.item.icon.iconFile if stuff.item.icon else "") + return self.loadIconFile(stuff.item.iconID or "") item = getattr(stuff, "item", stuff) - return self.loadIconFile(item.icon.iconFile if item.icon else "") + return self.loadIconFile(item.iconID) def loadIconFile(self, iconFile): if iconFile: diff --git a/gui/builtinViewColumns/maxRange.py b/gui/builtinViewColumns/maxRange.py index 7fcd6cd44..24f72b3a1 100644 --- a/gui/builtinViewColumns/maxRange.py +++ b/gui/builtinViewColumns/maxRange.py @@ -40,7 +40,7 @@ class MaxRange(ViewColumn): info = sAttr.getAttributeInfo("maxRange") self.info = info if params["showIcon"]: - iconFile = info.icon.iconFile if info.icon else None + iconFile = info.iconID if iconFile: self.imageId = fittingView.imageList.GetImageIndex(iconFile, "icons") self.bitmap = BitmapLoader.getBitmap(iconFile, "icons") diff --git a/gui/builtinViewColumns/propertyDisplay.py b/gui/builtinViewColumns/propertyDisplay.py index b4faa177a..abd91f730 100644 --- a/gui/builtinViewColumns/propertyDisplay.py +++ b/gui/builtinViewColumns/propertyDisplay.py @@ -41,7 +41,7 @@ class PropertyDisplay(ViewColumn): iconFile = "pg_small" iconType = "gui" else: - iconFile = info.icon.iconFile if info.icon else None + iconFile = info.iconID if info.icon else None iconType = "icons" if iconFile: self.imageId = fittingView.imageList.GetImageIndex(iconFile, iconType) diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 4db377ddb..d6bf4ae0c 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -39,6 +39,7 @@ from service.fit import Fit from service.market import Market from gui.utils.staticHelpers import DragDropHelper +import gui.utils.fonts as fonts import gui.globalEvents as GE @@ -148,6 +149,7 @@ class FittingView(d.Display): self.mainFrame.Bind(EVT_FIT_RENAMED, self.fitRenamed) self.mainFrame.Bind(EVT_FIT_REMOVED, self.fitRemoved) self.mainFrame.Bind(ITEM_SELECTED, self.appendItem) + self.font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) self.Bind(wx.EVT_LEFT_DCLICK, self.removeItem) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) @@ -679,14 +681,27 @@ class FittingView(d.Display): slot = Slot.getValue(slotType) slotMap[slot] = fit.getSlotsFree(slot) < 0 - font = wx.Font(self.GetClassDefaultAttributes().font) - for i, mod in enumerate(self.mods): self.SetItemBackgroundColour(i, self.GetBackgroundColour()) # only consider changing color if we're dealing with a Module if type(mod) is Module: - if slotMap[mod.slot] or getattr(mod, 'restrictionOverridden', None): # Color too many modules as red + hasRestrictionOverriden = getattr(mod, 'restrictionOverridden', None) + # If module had broken fitting restrictions but now doesn't, + # ensure it is now valid, and remove restrictionOverridden + # variable. More in #1519 + if not fit.ignoreRestrictions and hasRestrictionOverriden: + clean = False + if mod.fits(fit, False): + if not mod.hardpoint: + clean = True + elif fit.getHardpointsFree(mod.hardpoint) >= 0: + clean = True + if clean: + del mod.restrictionOverridden + hasRestrictionOverriden = not hasRestrictionOverriden + + if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51)) elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled self.SetItemBackgroundColour(i, self.slotColour(mod.slot)) @@ -695,11 +710,11 @@ class FittingView(d.Display): if isinstance(mod, Rack) and \ sFit.serviceFittingOptions["rackSlots"] and \ sFit.serviceFittingOptions["rackLabels"]: - font.SetWeight(wx.FONTWEIGHT_BOLD) - self.SetItemFont(i, font) + self.font.SetWeight(wx.FONTWEIGHT_BOLD) + self.SetItemFont(i, self.font) else: - font.SetWeight(wx.FONTWEIGHT_NORMAL) - self.SetItemFont(i, font) + self.font.SetWeight(wx.FONTWEIGHT_NORMAL) + self.SetItemFont(i, self.font) self.Thaw() self.itemCount = self.GetItemCount() @@ -726,13 +741,12 @@ class FittingView(d.Display): # noinspection PyPropertyAccess def MakeSnapshot(self, maxColumns=1337): if self.FVsnapshot: - del self.FVsnapshot + self.FVsnapshot = None tbmp = wx.Bitmap(16, 16) tdc = wx.MemoryDC() tdc.SelectObject(tbmp) - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - tdc.SetFont(font) + tdc.SetFont(self.font) columnsWidths = [] for i in range(len(self.DEFAULT_COLS)): @@ -828,7 +842,7 @@ class FittingView(d.Display): mdc.SetBackground(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))) mdc.Clear() - mdc.SetFont(font) + mdc.SetFont(self.font) mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) cx = padding diff --git a/gui/builtinViews/implantEditor.py b/gui/builtinViews/implantEditor.py index b391c1812..d33ff9779 100644 --- a/gui/builtinViews/implantEditor.py +++ b/gui/builtinViews/implantEditor.py @@ -156,7 +156,7 @@ class BaseImplantEditorView(wx.Panel): currentMktGrp = sMkt.getMarketGroup(tree.GetItemData(parent)) items = sMkt.getItemsByMarketGroup(currentMktGrp) for item in items: - iconId = self.addMarketViewImage(item.icon.iconFile) + iconId = self.addMarketViewImage(item.iconID) tree.AppendItem(parent, item.name, iconId, data=item) tree.SortChildren(parent) diff --git a/gui/chrome_tabs.py b/gui/chrome_tabs.py index 10354b9be..521ac6168 100644 --- a/gui/chrome_tabs.py +++ b/gui/chrome_tabs.py @@ -20,6 +20,7 @@ from gui.bitmap_loader import BitmapLoader from gui.utils import draw from gui.utils import color as color_utils from service.fit import Fit +from gui.utils import fonts _PageChanging, EVT_NOTEBOOK_PAGE_CHANGING = wx.lib.newevent.NewEvent() _PageChanged, EVT_NOTEBOOK_PAGE_CHANGED = wx.lib.newevent.NewEvent() @@ -357,7 +358,7 @@ class _TabRenderer: self.tab_bitmap = None self.tab_back_bitmap = None self.padding = 4 - self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False) + self.font = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) self.tab_img = img self.position = (0, 0) # Not used internally for rendering - helper for tab container @@ -1322,7 +1323,7 @@ class PFNotebookPagePreview(wx.Frame): self.padding = 15 self.transp = 0 - hfont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False) + hfont = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) self.SetFont(hfont) tx, ty = self.GetTextExtent(self.title) @@ -1384,7 +1385,7 @@ class PFNotebookPagePreview(wx.Frame): mdc.SetBackground(wx.Brush(color)) mdc.Clear() - font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False) + font = wx.Font(11, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) mdc.SetFont(font) x, y = mdc.GetTextExtent(self.title) diff --git a/gui/contextMenu.py b/gui/contextMenu.py index 11d574157..08ea258d5 100644 --- a/gui/contextMenu.py +++ b/gui/contextMenu.py @@ -208,5 +208,6 @@ from gui.builtinContextMenus import ( # noqa: E402,F401 fighterAbilities, boosterSideEffects, commandFits, - tabbedFits + tabbedFits, + mutaplasmids, ) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 5675c835e..a70bea814 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -35,7 +35,7 @@ class CopySelectDialog(wx.Dialog): style=wx.DEFAULT_DIALOG_STYLE) mainSizer = wx.BoxSizer(wx.VERTICAL) - copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "CREST", "MultiBuy"] + copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "ESI", "MultiBuy"] copyFormatTooltips = {CopySelectDialog.copyFormatEft: "EFT text format", CopySelectDialog.copyFormatEftImps: "EFT text format", CopySelectDialog.copyFormatXml: "EVE native XML format", diff --git a/gui/esiFittings.py b/gui/esiFittings.py index ca04387e5..af3da77a5 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -157,6 +157,19 @@ class EveFittings(wx.Frame): self.statusbar.SetStatusText(msg) +class ESIServerExceptionHandler(object): + def __init__(self, parentWindow, ex): + dlg = wx.MessageDialog(parentWindow, + "There was an issue starting up the localized server, try setting " + "Login Authentication Method to Manual by going to Preferences -> EVE SS0 -> " + "Login Authentication Method. If this doesn't fix the problem please file an " + "issue on Github.", + "Add Character Error", + wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + pyfalog.error(ex) + + class ESIExceptionHandler(object): # todo: make this a generate excetpion handler for all calls def __init__(self, parentWindow, ex): @@ -231,6 +244,7 @@ class ExportToEve(wx.Frame): return self.charChoice.GetClientData(selection) if selection is not None else None def exportFitting(self, event): + sPort = Port.getInstance() fitID = self.mainFrame.getActiveFit() self.statusbar.SetStatusText("", 0) @@ -240,27 +254,32 @@ class ExportToEve(wx.Frame): return self.statusbar.SetStatusText("Sending request and awaiting response", 1) + sEsi = Esi.getInstance() + + sFit = Fit.getInstance() + data = sPort.exportESI(sFit.getFit(fitID)) + res = sEsi.postFitting(self.getActiveCharacter(), data) try: - + res.raise_for_status() self.statusbar.SetStatusText("", 0) - self.statusbar.SetStatusText("", 1) - # try: - # text = json.loads(res.text) - # self.statusbar.SetStatusText(text['message'], 1) - # except ValueError: - # pyfalog.warning("Value error on loading JSON.") - # self.statusbar.SetStatusText("", 1) + self.statusbar.SetStatusText(res.reason, 1) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) - self.statusbar.SetStatusText(msg) + self.statusbar.SetStatusText("ERROR", 0) + self.statusbar.SetStatusText(msg, 1) except ESIExportException as ex: pyfalog.error(ex) self.statusbar.SetStatusText("ERROR", 0) - self.statusbar.SetStatusText(ex.args[0], 1) + self.statusbar.SetStatusText("{} - {}".format(res.status_code, res.reason), 1) except APIException as ex: - ESIExceptionHandler(self, ex) + try: + ESIExceptionHandler(self, ex) + except Exception as ex: + self.statusbar.SetStatusText("ERROR", 0) + self.statusbar.SetStatusText("{} - {}".format(res.status_code, res.reason), 1) + pyfalog.error(ex) class SsoCharacterMgmt(wx.Dialog): @@ -319,10 +338,12 @@ class SsoCharacterMgmt(wx.Dialog): self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE) - @staticmethod - def addChar(event): - sEsi = Esi.getInstance() - sEsi.login() + def addChar(self, event): + try: + sEsi = Esi.getInstance() + sEsi.login() + except Exception as ex: + ESIServerExceptionHandler(self, ex) def delChar(self, event): item = self.lcCharacters.GetFirstSelected() diff --git a/gui/itemStats.py b/gui/itemStats.py index 3440fb322..3768780e2 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -34,6 +34,9 @@ from gui.builtinItemStatsViews.itemDependants import ItemDependents from gui.builtinItemStatsViews.itemEffects import ItemEffects from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy from gui.builtinItemStatsViews.itemProperties import ItemProperties +from gui.builtinItemStatsViews.itemMutator import ItemMutator + +from eos.saveddata.module import Module class ItemStatsDialog(wx.Dialog): @@ -79,10 +82,8 @@ class ItemStatsDialog(wx.Dialog): item = sMkt.getItem(victim.ID) victim = None self.context = itmContext - if item.icon is not None: - before, sep, after = item.icon.iconFile.rpartition("_") - iconFile = "%s%s%s" % (before, sep, "0%s" % after if len(after) < 2 else after) - itemImg = BitmapLoader.getBitmap(iconFile, "icons") + if item.iconID is not None: + itemImg = BitmapLoader.getBitmap(item.iconID, "icons") if itemImg is not None: self.SetIcon(wx.Icon(itemImg)) self.SetTitle("%s: %s%s" % ("%s Stats" % itmContext if itmContext is not None else "Stats", item.name, @@ -101,7 +102,7 @@ class ItemStatsDialog(wx.Dialog): if "wxGTK" in wx.PlatformInfo: self.closeBtn = wx.Button(self, wx.ID_ANY, "Close", wx.DefaultPosition, wx.DefaultSize, 0) self.mainSizer.Add(self.closeBtn, 0, wx.ALL | wx.ALIGN_RIGHT, 5) - self.closeBtn.Bind(wx.EVT_BUTTON, self.closeEvent) + self.closeBtn.Bind(wx.EVT_BUTTON, (lambda e: self.Close())) self.SetSizer(self.mainSizer) @@ -146,7 +147,7 @@ class ItemStatsDialog(wx.Dialog): ItemStatsDialog.counter -= 1 self.parentWnd.UnregisterStatsWindow(self) - self.Destroy() + event.Skip() class ItemStatsContainer(wx.Panel): @@ -163,6 +164,10 @@ class ItemStatsContainer(wx.Panel): self.traits = ItemTraits(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.traits, "Traits") + if isinstance(stuff, Module) and stuff.isMutated: + self.mutator = ItemMutator(self.nbContainer, stuff, item) + self.nbContainer.AddPage(self.mutator, "Mutations") + self.desc = ItemDescription(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.desc, "Description") diff --git a/gui/mainFrame.py b/gui/mainFrame.py index f8b3fb1f4..9b237663c 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -99,6 +99,8 @@ except ImportError as e: pyfalog = Logger(__name__) +pyfalog.debug("Done loading mainframe imports") + # dummy panel(no paint no erasebk) class PFPanel(wx.Panel): diff --git a/gui/pyfa_gauge.py b/gui/pyfa_gauge.py index 0144f651c..61379c5b0 100644 --- a/gui/pyfa_gauge.py +++ b/gui/pyfa_gauge.py @@ -130,8 +130,8 @@ class PyGauge(wx.Window): return self._max_range def Animate(self): - sFit = Fit.getInstance() - if sFit.serviceFittingOptions["enableGaugeAnimation"]: + # sFit = Fit.getInstance() + if True: if not self._timer: self._timer = wx.Timer(self, self._timer_id) @@ -425,6 +425,13 @@ if __name__ == "__main__": gauge.SetFractionDigits(1) box.Add(gauge, 0, wx.ALL, 2) + gauge = PyGauge(self, font, size=(100, 5)) + gauge.SetBackgroundColour(wx.Colour(52, 86, 98)) + gauge.SetBarColour(wx.Colour(255, 128, 0)) + gauge.SetValue(59) + gauge.SetFractionDigits(1) + box.Add(gauge, 0, wx.ALL, 2) + self.SetSizer(box) self.Layout() diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index afa9d6b71..75152a1b4 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -236,10 +236,10 @@ class ShipBrowser(wx.Panel): if self.filterShipsWithNoFits: if fits > 0: if filter_: - self.lpane.AddWidget(ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, fits), ship.race)) + self.lpane.AddWidget(ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, fits), ship.race, ship.graphicID)) else: if filter_: - self.lpane.AddWidget(ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, fits), ship.race)) + self.lpane.AddWidget(ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, fits), ship.race, ship.graphicID)) self.raceselect.RebuildRaces(racesList) @@ -335,8 +335,8 @@ class ShipBrowser(wx.Panel): shipTrait = ship.traits.traitText if (ship.traits is not None) else "" # empty string if no traits - for ID, name, booster, timestamp, notes in fitList: - self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID)) + for ID, name, booster, timestamp, notes, graphicID in fitList: + self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID, graphicID=graphicID)) self.lpane.RefreshList() self.lpane.Thaw() @@ -374,7 +374,7 @@ class ShipBrowser(wx.Panel): self.lpane.AddWidget( ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, len(sFit.getFitsWithShip(ship.ID))), - ship.race)) + ship.race, ship.graphicID)) for ID, name, shipID, shipName, booster, timestamp, notes in fitList: ship = sMkt.getItem(shipID) @@ -384,7 +384,7 @@ class ShipBrowser(wx.Panel): shipTrait = ship.traits.traitText if (ship.traits is not None) else "" # empty string if no traits - self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID)) + self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID, graphicID=ship.graphicID)) if len(ships) == 0 and len(fitList) == 0: self.lpane.AddWidget(PFStaticText(self.lpane, label="No matching results.")) self.lpane.RefreshList(doFocus=False) @@ -435,6 +435,7 @@ class ShipBrowser(wx.Panel): fit[4] ), shipItem.ID, + graphicID=shipItem.graphicID )) self.lpane.RefreshList(doFocus=False) self.lpane.Thaw() diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index 0d5456dd0..d1c0af9d0 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -90,7 +90,7 @@ class exportHtmlThread(threading.Thread):