Merge branch 'master' into singularity

This commit is contained in:
DarkPhoenix
2018-08-21 19:40:39 +03:00
1963 changed files with 2602 additions and 523 deletions

1
.gitignore vendored
View File

@@ -122,3 +122,4 @@ gitversion
/.version
*.swp
*.fsdbinary

View File

@@ -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

View File

@@ -73,4 +73,7 @@ exe = EXE(pyz,
app = BUNDLE(exe,
name='pyfa.app',
icon=icon,
bundle_identifier=None)
bundle_identifier=None,
info_plist={
'NSHighResolutionCapable': 'True'
})

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInheritable/>
<assemblyIdentity
type="win32"
name="Microsoft.VC90.CRT"
version="9.0.21022.8"
processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"/>
<file name="MSVCR90.DLL"/>
<file name="MSVCM90.DLL"/>
<file name="MSVCP90.DLL"/>
</assembly>

View File

@@ -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

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="pyfa" processorArchitecture="x86" type="win32" version="1.0.0.0"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -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', '.'),

View File

@@ -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

View File

@@ -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"]

View File

@@ -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"),

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
# ===============================================================================
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")

View File

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

View File

@@ -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")

View File

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

View File

@@ -17,15 +17,16 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
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)

View File

@@ -14,7 +14,7 @@ def upgrade(saveddata_engine):
"boosters": 2,
"cargo": 2,
"characters": 2,
"crest": 1,
# "crest": 1,
"damagePatterns": 2,
"drones": 2,
"fighters": 2,

View File

@@ -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;")

View File

@@ -1,6 +1,7 @@
__all__ = [
"character",
"fit",
"mutator",
"module",
"user",
"skill",

View File

@@ -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),

View File

@@ -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')
)
})

View File

@@ -17,19 +17,18 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
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)

View File

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

View File

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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

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

View File

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

View File

@@ -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"))

View File

@@ -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"

View File

@@ -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")

View File

@@ -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"

View File

@@ -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")

View File

@@ -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",

View File

@@ -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"),

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

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

View File

@@ -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: ""),
"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):

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

131
eos/saveddata/mutator.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
# ===============================================================================
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

View File

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

BIN
eve.db

Binary file not shown.

503
gui/attribute_gauge.py Normal file
View File

@@ -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()

View File

@@ -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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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()

View File

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

View File

@@ -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()

View File

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

View File

@@ -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()

View File

@@ -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")

View File

@@ -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")

View File

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

View File

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

View File

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

View File

@@ -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")

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -208,5 +208,6 @@ from gui.builtinContextMenus import ( # noqa: E402,F401
fighterAbilities,
boosterSideEffects,
commandFits,
tabbedFits
tabbedFits,
mutaplasmids,
)

View File

@@ -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",

View File

@@ -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()

View File

@@ -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")

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -90,7 +90,7 @@ class exportHtmlThread(threading.Thread):
<link rel="stylesheet" href="https://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" />
<script src="https://code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
// http://stackoverflow.com/questions/32453806/uncaught-securityerror-failed-to-execute-replacestate-on-history-cannot-be
//http://stackoverflow.com/questions/32453806/uncaught-securityerror-failed-to-execute-replacestate-on-history-cannot-be
$(document).bind('mobileinit',function(){
$.mobile.changePage.defaults.changeHash = false;
$.mobile.hashListeningEnabled = false;
@@ -195,55 +195,51 @@ class exportHtmlThread(threading.Thread):
if len(fits) > 0:
groupFits += len(fits)
HTMLship = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
'data-corners="false">\n'
' <h2>' + ship.name + ' <span class="ui-li-count">' + str(
len(fits)) + '</span></h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" '
'data-corners="false">\n'
)
if len(fits) == 1:
for fit in fits:
if self.stopRunning:
return
fit = fits[0]
try:
dnaFit = Port.exportDna(getFit(fit[0]))
HTMLgroup += ' <li><a data-dna="' + dnaFit + '" target="_blank">' + ship.name + ": " + \
fit[1] + '</a></li>\n'
eftFit = Port.exportEft(getFit(fit[0]))
print(eftFit)
HTMLfit = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
'data-corners="false">\n'
' <h2>' + fit[1] + '</h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" '
'data-corners="false">\n'
)
HTMLfit += ' <li><pre>' + eftFit + '\n </pre></li>\n'
HTMLfit += ' </ul>\n </li>\n'
HTMLship += HTMLfit
except:
pyfalog.warning("Failed to export line")
pass
continue
finally:
if self.callback:
wx.CallAfter(self.callback, count)
count += 1
else:
# Ship group header
HTMLship = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" data-corners="false">\n'
' <h2>' + ship.name + ' <span class="ui-li-count">' + str(
len(fits)) + '</span></h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" data-corners="false">\n'
)
for fit in fits:
if self.stopRunning:
return
try:
dnaFit = Port.exportDna(getFit(fit[0]))
print(dnaFit)
HTMLship += ' <li><a data-dna="' + dnaFit + '" target="_blank">' + fit[
1] + '</a></li>\n'
except:
pyfalog.warning("Failed to export line")
continue
finally:
if self.callback:
wx.CallAfter(self.callback, count)
count += 1
HTMLgroup += HTMLship + (' </ul>\n'
' </li>\n')
HTMLgroup += HTMLship + (' </ul>\n'
' </li>\n')
if groupFits > 0:
# Market group header
HTML += (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" data-corners="false">\n'
' <h2>' + group.groupName + ' <span class="ui-li-count">' + str(groupFits) + '</span></h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" data-corners="false">\n' + HTMLgroup +
' <ul data-role="listview" data-shadow="false" data-inset="true" data-corners="false">\n' +
HTMLgroup +
' </ul>\n'
' </li>'
)
@@ -279,7 +275,8 @@ class exportHtmlThread(threading.Thread):
return
try:
dnaFit = Port.exportDna(getFit(fit[0]))
HTML += '<a class="outOfGameBrowserLink" target="_blank" href="' + dnaUrl + dnaFit + '">' + ship.name + ': ' + \
HTML += '<a class="outOfGameBrowserLink" target="_blank" href="' + dnaUrl + dnaFit + '">' \
+ ship.name + ': ' + \
fit[1] + '</a><br> \n'
except:
pyfalog.error("Failed to export line")

View File

Before

Width:  |  Height:  |  Size: 452 B

After

Width:  |  Height:  |  Size: 452 B

BIN
imgs/icons/10012.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Some files were not shown because too many files have changed in this diff Show More