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):