Compare commits

...

83 Commits

Author SHA1 Message Date
DarkPhoenix
813db9340f Make sure to select right-clicked item on windows when Control is pressed 2019-10-02 13:19:46 +03:00
DarkPhoenix
acbd8a3298 Allow context menu-related batch actions to be triggered on ctrl too
Windows hides context menu when user presses alt
2019-10-02 12:44:27 +03:00
DarkPhoenix
561e22e894 Bump version 2019-10-02 12:11:25 +03:00
DarkPhoenix
05ac0a528a Show extra labels only when there's something inside 2019-10-02 10:56:01 +03:00
DarkPhoenix
c040353f6e Move some common functionality into common space of tab sizing method 2019-10-02 10:37:14 +03:00
DarkPhoenix
f23a8fa0c8 Adjust tab shadows to tab sizes 2019-10-02 10:33:11 +03:00
DarkPhoenix
ba93467646 Implement logic to have tabs of different sizes in additions pane and in fitting pane 2019-10-02 10:16:36 +03:00
DarkPhoenix
00d480860f Change tab outlook when option value is changed 2019-10-01 22:28:24 +03:00
DarkPhoenix
c94384acb8 Show amount of items hidden in additions tabs 2019-10-01 22:22:10 +03:00
DarkPhoenix
0c2c0ac6ef Add modules to session even when they were added via appendIgnoreEmpty 2019-10-01 10:25:31 +03:00
DarkPhoenix
61a33a331e Copy projection range when copying fit 2019-10-01 09:29:10 +03:00
DarkPhoenix
e374a6f2c6 Do not add null drains to not affect cap sim calculations 2019-09-30 17:27:10 +03:00
DarkPhoenix
dbd84dce28 Allow to change projected items' metas regardless of ability of target ship to fit them 2019-09-30 17:24:14 +03:00
DarkPhoenix
9d554f9c68 Make sure to do fit recalculation if it was changed after the last one 2019-09-30 17:20:22 +03:00
DarkPhoenix
576cf56735 Expose chance of jamming to stats pane itself rather than tooltip 2019-09-30 17:06:01 +03:00
DarkPhoenix
e2aaabbc16 Do not let jamming strength exceed 100% 2019-09-30 16:40:48 +03:00
DarkPhoenix
ef226898c0 Add projection range calculation to effects where it makes sense 2019-09-30 14:43:52 +03:00
DarkPhoenix
a0db235e5a Add support for projection range to bunch of effects 2019-09-30 03:17:21 +03:00
DarkPhoenix
5bf05ba775 Allow batch changes of projection range 2019-09-30 02:47:12 +03:00
DarkPhoenix
c073b1fa2a Do not show context menu on system-wide effects 2019-09-30 02:10:11 +03:00
DarkPhoenix
5f58307bf3 Add projection range commands to projected fighters 2019-09-30 02:04:24 +03:00
DarkPhoenix
8741b17a5e Add projection range commands for projected drones 2019-09-30 01:55:16 +03:00
DarkPhoenix
4c1fa09795 Apply drones and fighters from projected fit at range of 0 2019-09-30 01:21:39 +03:00
DarkPhoenix
ce7df2d01f Allow to change projection range for projected modules 2019-09-30 01:18:55 +03:00
DarkPhoenix
b433b0ea7c Change fit projection so that projection range actually counts for the sake of calculations overall
No effect support still
2019-09-30 00:19:31 +03:00
DarkPhoenix
20868d6b44 Add ability to change projection range of fits 2019-09-29 23:41:45 +03:00
DarkPhoenix
33103dbee9 Add column which shows projected item range 2019-09-29 22:16:19 +03:00
DarkPhoenix
2a05ac5a85 Pass projection range parameter to effects 2019-09-29 22:02:10 +03:00
DarkPhoenix
a013828128 Add projectionRange to actual objects built from database 2019-09-29 16:04:44 +03:00
DarkPhoenix
e19510b3d4 Move function which calculates range factor to eos 2019-09-29 16:00:37 +03:00
DarkPhoenix
390f2048f2 Add projection range column to projectable entities 2019-09-29 15:54:45 +03:00
DarkPhoenix
0bb732300e Do not rely on resistance view being available 2019-09-29 11:21:00 +03:00
DarkPhoenix
fd017df561 Add lock range limit support to ewar graph 2019-09-27 20:43:28 +03:00
DarkPhoenix
0ed16b9a6f Add lockrange support to DPS graphs 2019-09-27 20:19:29 +03:00
DarkPhoenix
865978fcc1 Add context menu which controls if graphs ignore drone control range or not, and add support for this option to RR graph 2019-09-27 18:40:33 +03:00
DarkPhoenix
a43f9930de Allow to change meta level of standup fighters 2019-09-23 16:44:26 +03:00
DarkPhoenix
c13cd23d54 Change parent of fit deletion dialog
Try out tip mentioned in https://github.com/wxWidgets/Phoenix/issues/1343
2019-09-23 15:32:16 +03:00
DarkPhoenix
ed1f52a114 Show implant description in tooltip of implant editor 2019-09-23 15:25:58 +03:00
DarkPhoenix
7dd063f04e Add graph setting to ignore drone control range 2019-09-17 13:03:21 +03:00
DarkPhoenix
6e9fc1d1d9 Do not crash on Nones in value 2019-09-11 08:32:13 +03:00
DarkPhoenix
cae0172e48 Bump version 2019-09-10 15:54:23 +03:00
DarkPhoenix
e2b492ee8d Update database and effects to 1564394 2019-09-10 15:53:37 +03:00
DarkPhoenix
545ddc7492 Adjust DB script to changes in phobos 2019-09-10 15:31:02 +03:00
DarkPhoenix
d0b7c58a1d Merge branch 'singularity' 2019-09-10 14:30:51 +03:00
DarkPhoenix
a9ad094422 Fix fax link amount bonus 2019-09-04 15:10:07 +03:00
DarkPhoenix
68154333c2 Merge branch 'master' into singularity 2019-09-02 02:25:25 +03:00
DarkPhoenix
5df2db5879 Bump version 2019-09-02 01:21:32 +03:00
DarkPhoenix
5a34db0d2f Change how fit deletion confirmation dialog is destroyed 2019-09-02 01:14:35 +03:00
DarkPhoenix
6f50be1e7e Fix another set of crashes with manual login 2019-08-30 15:36:34 +03:00
DarkPhoenix
d15fefcf1b Avoid various crashes when working with SslLoginServer dialog 2019-08-30 15:26:58 +03:00
DarkPhoenix
07bf1b400c Fix spool scale mode on the very final cycle with spoolup set 2019-08-30 09:42:26 +03:00
DarkPhoenix
9f975a958e Merge branch 'master' into singularity 2019-08-27 11:34:17 +03:00
DarkPhoenix
c2a240bab0 Fix mutadaptive rep group 2019-08-26 23:12:44 +03:00
DarkPhoenix
40c3bf723f Make drone RR rigs stacking penalized for shield RR bots 2019-08-26 19:43:44 +03:00
DarkPhoenix
7a92ace2db Remove stacking penalty from scorpion ECM strength bonus 2019-08-26 19:33:55 +03:00
DarkPhoenix
500f5b8310 Add redraw delay on fit changes 2019-08-26 14:47:34 +03:00
DarkPhoenix
44830a4de6 Add capacity to container name if it was added to cargo 2019-08-26 14:13:26 +03:00
DarkPhoenix
f3f13e7ba8 Make cargo containers searchable 2019-08-26 13:38:03 +03:00
DarkPhoenix
0269a64ae1 Add maximize button to resizeable windows and make character editor resizeable 2019-08-26 12:59:00 +03:00
DarkPhoenix
5d6cdcbd23 Fix indent 2019-08-26 12:28:55 +03:00
DarkPhoenix
81906a7bd2 Do not store item-specific resistance attrs on effects 2019-08-26 12:27:43 +03:00
DarkPhoenix
b25b038934 Fix on-effect resistance definition 2019-08-26 09:28:48 +03:00
DarkPhoenix
b469fa520e Do not crash on cap boosters only in fit 2019-08-26 09:02:55 +03:00
DarkPhoenix
4f865896c7 Force variations menu to respect meta group overrides 2019-08-26 04:01:23 +03:00
DarkPhoenix
3b50dddef2 Allow to online extra amount of command bursts on various caps 2019-08-26 03:48:17 +03:00
DarkPhoenix
380e9c2e87 Change how rounding on Y ticks happens - now it relies on shown Y range 2019-08-26 03:35:35 +03:00
DarkPhoenix
1c1443c862 Move calculation of normalization shift to separate function 2019-08-26 02:56:11 +03:00
DarkPhoenix
0529baac4a Merge branch 'master' into singularity 2019-08-23 13:44:40 +03:00
DarkPhoenix
7dab220009 Ignore non-active scrams and scrammables 2019-08-23 13:44:15 +03:00
DarkPhoenix
0ea0f8cdf2 Merge branch 'master' into singularity 2019-08-23 13:24:29 +03:00
DarkPhoenix
eebd59413b Apply scrams in DPS graph when projected mods is enabled 2019-08-23 13:19:17 +03:00
DarkPhoenix
f4a635eb43 Implement hybrid extended attribute getter and few scram-related functions for DPS graph 2019-08-23 11:53:13 +03:00
DarkPhoenix
0e57258cc5 Add ability to pass multiple afflictors to no-afflictor getter 2019-08-23 09:13:40 +03:00
DarkPhoenix
67462c3278 Do not crash on attempt to export blank fitting 2019-08-23 09:07:31 +03:00
DarkPhoenix
fce8129fa2 Add support for extended target profile stats export/import 2019-08-23 08:55:13 +03:00
DarkPhoenix
1d76f3ec31 Merge branch 'master' into singularity 2019-08-22 21:32:34 +03:00
DarkPhoenix
707dbeecf8 Fix subsystems giving fitting bonuses to non-med RRs 2019-08-22 21:30:10 +03:00
DarkPhoenix
56672f5830 Merge branch 'master' into singularity 2019-08-22 19:35:27 +03:00
DarkPhoenix
13a0bf9d42 Do not delay any damage besides doomsday mods 2019-08-22 19:34:38 +03:00
DarkPhoenix
1bff10c973 Bump version 2019-08-22 17:56:51 +03:00
DarkPhoenix
1d4aece7cc Limit max range of FoF missiles 2019-08-22 17:54:44 +03:00
DarkPhoenix
cdb79f499a Allow non-integer amount of cycles in spool calculator 2019-08-22 17:35:16 +03:00
DarkPhoenix
837dbb3677 Update database and effects to 1554701 2019-08-22 16:51:33 +03:00
97 changed files with 4312 additions and 2826 deletions

33
eos/calc.py Normal file
View File

@@ -0,0 +1,33 @@
# =============================================================================
# Copyright (C) 2019 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
if distance is None:
return 1
if srcFalloffRange > 0:
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
return 0
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
elif distance <= srcOptimalRange:
return 1
else:
return 0

View File

@@ -148,6 +148,7 @@ class CapSimulator:
stability_precision = self.stability_precision
period = self.period
activation = None
iterations = 0
capCapacity = self.capacitorCapacity
@@ -162,7 +163,12 @@ class CapSimulator:
t_max = self.t_max
while 1:
activation = pop(state)
# Nothing to pop - might happen when no mods are activated, or when
# only cap injectors are active (and are postponed by code below)
try:
activation = pop(state)
except IndexError:
break
t_now, duration, capNeed, shot, clipSize, reloadTime, isInjector = activation
# Max time reached, stop simulation - we're stable
@@ -275,7 +281,8 @@ class CapSimulator:
activation[3] = shot
push(state, activation)
push(state, activation)
if activation is not None:
push(state, activation)
# update instance with relevant results.
self.t = t_last

View File

@@ -90,9 +90,12 @@ class FittingHardpoint(IntEnum):
@unique
class SpoolType(IntEnum):
SCALE = 0 # [0..1]
TIME = 1 # Expressed via time in seconds since spool up started
CYCLES = 2 # Expressed in amount of cycles since spool up started
# Spool and cycle scale are different in case if max spool amount cannot
# be divided by spool step without remainder
SPOOL_SCALE = 0 # [0..1]
CYCLE_SCALE = 1 # [0..1]
TIME = 2 # Expressed via time in seconds since spool up started
CYCLES = 3 # Expressed in amount of cycles since spool up started
@unique

View File

@@ -0,0 +1,25 @@
"""
Migration 34
- Adds projection range columns to projectable entities
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT projectionRange FROM projectedFits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE projectedFits ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM modules LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE modules ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM drones LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE drones ADD COLUMN projectionRange FLOAT;")
try:
saveddata_engine.execute("SELECT projectionRange FROM fighters LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fighters ADD COLUMN projectionRange FLOAT;")

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
from sqlalchemy.orm import mapper, relation
import datetime
@@ -33,7 +33,8 @@ drones_table = Table("drones", saveddata_meta,
Column("amountActive", Integer, nullable=False),
Column("projected", Boolean, default=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True)
)
mapper(Drone, drones_table,

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
from sqlalchemy.orm import mapper, relation
import datetime
@@ -34,7 +34,8 @@ fighters_table = Table("fighters", saveddata_meta,
Column("amount", Integer, nullable=False),
Column("projected", Boolean, default=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True),
)
fighter_abilities_table = Table("fightersAbilities", saveddata_meta,

View File

@@ -19,7 +19,7 @@
import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Float, String, Table
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import mapper, reconstructor, relation, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
@@ -70,7 +70,8 @@ projectedFits_table = Table("projectedFits", saveddata_meta,
Column("amount", Integer, nullable=False, default=1),
Column("active", Boolean, nullable=False, default=1),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("projectionRange", Float, nullable=True),
)
commandFits_table = Table("commandFits", saveddata_meta,
@@ -83,6 +84,7 @@ commandFits_table = Table("commandFits", saveddata_meta,
class ProjectedFit:
def __init__(self, sourceID, source_fit, amount=1, active=True):
self.sourceID = sourceID
self.source_fit = source_fit

View File

@@ -42,6 +42,7 @@ modules_table = Table("modules", saveddata_meta,
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("spoolType", Integer, nullable=True),
Column("spoolAmount", Float, nullable=True),
Column("projectionRange", Float, nullable=True),
CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"'))
mapper(Module, modules_table,

View File

@@ -19,6 +19,7 @@
from logbook import Logger
from sqlalchemy.orm.collections import collection
pyfalog = Logger(__name__)
@@ -138,9 +139,10 @@ class HandledModuleList(HandledList):
else:
self.appendIgnoreEmpty(mod)
@collection.appender
def appendIgnoreEmpty(self, mod):
mod.position = len(self)
HandledList.append(self, mod)
super().append(mod)
if mod.isInvalid:
self.remove(mod)

File diff suppressed because it is too large Load Diff

View File

@@ -530,6 +530,14 @@ class Item(EqBase):
def isBooster(self):
return self.group.name == 'Booster' and self.category.name == 'Implant'
@property
def isStandup(self):
if self.category.name == "Structure Module":
return True
if self.isFighter and {'fighterSquadronIsStandupLight', 'fighterSquadronIsStandupHeavy', 'fighterSquadronIsStandupSupport'}.intersection(self.attributes):
return True
return False
def __repr__(self):
return "Item(ID={}, name={}) at {}".format(
self.ID, self.name, hex(id(self))

View File

@@ -29,6 +29,7 @@ from eos.db.gamedata.queries import getAttributeInfo
defaultValuesCache = {}
cappingAttrKeyCache = {}
resistanceCache = {}
def getAttrDefault(key, fallback=None):
@@ -46,19 +47,23 @@ def getAttrDefault(key, fallback=None):
def getResistanceAttrID(modifyingItem, effect):
# If it doesn't exist on the effect, check the modifying modules attributes. If it's there, set it on the
# effect for this session so that we don't have to look here again (won't always work when it's None, but
# will catch most)
if not effect.getattr('resistanceCalculated'):
# If it doesn't exist on the effect, check the modifying module's attributes.
# If it's there, cache it and return
if effect.resistanceID:
return effect.resistanceID
cacheKey = (modifyingItem.item.ID, effect.ID)
try:
return resistanceCache[cacheKey]
except KeyError:
attrPrefix = effect.getattr('prefix')
if attrPrefix:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
if not effect.resistanceID:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
if not resistanceID:
resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
else:
effect.resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
effect.resistanceCalculated = True
return effect.resistanceID
resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
resistanceCache[cacheKey] = resistanceID
return resistanceID
class ItemAttrShortcut:
@@ -67,14 +72,8 @@ class ItemAttrShortcut:
return_value = self.itemModifiedAttributes.get(key)
return return_value or default
def getModifiedItemAttrWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.itemModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers)
return return_value or default
def getModifiedItemAttrWithoutAfflictor(self, key, afflictor, default=0):
"""Returns attribute value with passed afflictor modification removed."""
return_value = self.itemModifiedAttributes.getWithoutAfflictor(key, afflictor)
def getModifiedItemAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.itemModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getItemBaseAttrValue(self, key, default=0):
@@ -88,14 +87,8 @@ class ChargeAttrShortcut:
return_value = self.chargeModifiedAttributes.get(key)
return return_value or default
def getModifiedChargeAttrWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.chargeModifiedAttributes.getWithExtraMods(key, extraMultipliers=extraMultipliers)
return return_value or default
def getModifiedChargeAttrWithoutAfflictor(self, key, afflictor, default=0):
"""Returns attribute value with passed modifiers applied to it."""
return_value = self.chargeModifiedAttributes.getWithoutAfflictor(key, afflictor)
def getModifiedChargeAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
return_value = self.chargeModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
return return_value or default
def getChargeBaseAttrValue(self, key, default=0):
@@ -211,32 +204,11 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Original value is the least priority
return self.getOriginal(key)
def getWithExtraMods(self, key, extraMultipliers=None, default=0):
"""Copy of __getitem__ with some modifications."""
if not extraMultipliers:
return self.get(key, default=default)
val = self.__calculateValue(key, extraMultipliers=extraMultipliers)
if val is not None:
return val
# Then in values which are not yet calculated
if self.__intermediary:
val = self.__intermediary.get(key)
else:
val = None
if val is not None:
return val
# Original value
val = self.getOriginal(key)
if val is not None:
return val
# Passed in default value
return default
def getWithoutAfflictor(self, key, afflictor, default=0):
def getExtended(self, key, extraMultipliers=None, ignoreAfflictors=None, default=0):
"""
Here we consider couple of parameters. If they affect final result, we do
not store result, and if they are - we do.
"""
# Here we do not have support for preAssigns/forceds, as doing them would
# mean that we have to store all of them in a list which increases memory use,
# and we do not actually need those operators atm
@@ -245,8 +217,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
ignorePenalizedMultipliers = {}
postIncreaseAdjustment = 0
for fit, afflictors in self.getAfflictions(key).items():
for innerAfflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if innerAfflictor is afflictor:
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if afflictor in ignoreAfflictors:
if operator == Operator.MULTIPLY:
if stackingGroup is None:
multiplierAdjustment /= postResAmount
@@ -257,29 +229,31 @@ class ModifiedAttributeDict(collections.MutableMapping):
elif operator == Operator.POSTINCREASE:
postIncreaseAdjustment -= postResAmount
if preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0:
# If we apply no customizations - use regular getter
if (
not extraMultipliers and
preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and
postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0
):
return self.get(key, default=default)
# Try to calculate custom values
val = self.__calculateValue(
key, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
key, extraMultipliers=extraMultipliers, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
postIncAdj=postIncreaseAdjustment, ignorePenMult=ignorePenalizedMultipliers)
if val is not None:
return val
# Then in values which are not yet calculated
# Then the same fallbacks as in regular getter
if self.__intermediary:
val = self.__intermediary.get(key)
else:
val = None
if val is not None:
return val
# Original value
val = self.getOriginal(key)
if val is not None:
return val
# Passed in default value
return default
def __delitem__(self, key):
@@ -584,10 +558,12 @@ class ModifiedAttributeDict(collections.MutableMapping):
if 'projected' not in effectType:
return 1
remoteResistID = getResistanceAttrID(modifyingItem=fit.getModifier(), effect=effect)
if not remoteResistID:
return 1
attrInfo = getAttributeInfo(remoteResistID)
# Get the attribute of the resist
resist = fit.ship.itemModifiedAttributes[attrInfo.attributeName] or None
return resist or 1.0
return resist or 1
class Affliction:

View File

@@ -122,7 +122,7 @@ class Booster(HandledItem, ItemAttrShortcut):
(effect.isType("passive") or effect.isType("boosterSideEffect")):
if effect.isType("boosterSideEffect") and effect not in self.activeSideEffectEffects:
continue
effect.handler(fit, self, ("booster",))
effect.handler(fit, self, ("booster",), None)
@validates("ID", "itemID", "ammoID", "active")
def validator(self, key, val):

View File

@@ -422,7 +422,7 @@ class Skill(HandledItem):
(not fit.isStructure or effect.isType("structure")) and \
effect.activeByDefault:
try:
effect.handler(fit, self, ("skill",))
effect.handler(fit, self, ("skill",), None)
except AttributeError:
continue

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -25,6 +26,7 @@ import eos.db
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.utils.cycles import CycleInfo
from eos.utils.default import DEFAULT
from eos.utils.stats import DmgTypes, RRTypes
@@ -45,6 +47,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.amount = 0
self.amountActive = 0
self.projected = False
self.projectionRange = None
self.build()
@reconstructor
@@ -304,7 +307,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
else:
return True
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
if self.projected or forceProjected:
context = "projected", "drone"
projected = True
@@ -312,6 +315,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("drone",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
for effect in self.item.effects.values():
if effect.runTime == runTime and \
effect.activeByDefault and \
@@ -319,36 +324,34 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
projected is False and effect.isType("passive")):
# See GH issue #765
if effect.getattr('grouped'):
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
else:
i = 0
while i != self.amountActive:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
i += 1
if self.charge:
for effect in self.charge.effects.values():
if effect.runTime == runTime and effect.activeByDefault:
effect.handler(fit, self, ("droneCharge",))
effect.handler(fit, self, ("droneCharge",), projectionRange)
def __deepcopy__(self, memo):
copy = Drone(self.item)
copy.amount = self.amount
copy.amountActive = self.amountActive
copy.projectionRange = self.projectionRange
return copy
def rebase(self, item):
amount = self.amount
amountActive = self.amountActive
projectionRange = self.projectionRange
Drone.__init__(self, item)
self.amount = amount
self.amountActive = amountActive
self.projectionRange = projectionRange
def fits(self, fit):
fitDroneGroupLimits = set()

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -27,8 +28,9 @@ from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.fighterAbility import FighterAbility
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.stats import DmgTypes
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
@@ -47,6 +49,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.itemID = item.ID if item is not None else None
self.projected = False
self.projectionRange = None
self.active = True
# -1 is a placeholder that represents max squadron size, which we may not know yet as ships may modify this with
@@ -380,7 +383,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
else:
return True
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
if not self.active:
return
@@ -391,6 +394,8 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("fighter",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
for ability in self.abilities:
if not ability.active:
continue
@@ -399,17 +404,11 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and effect.activeByDefault and \
((projected and effect.isType("projected")) or not projected):
if ability.grouped:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
else:
i = 0
while i != self.amount:
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
i += 1
def __deepcopy__(self, memo):
@@ -419,18 +418,22 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
copyAbility = next(filter(lambda a: a.effectID == ability.effectID, copy.abilities))
copyAbility.active = ability.active
copy.projectionRange = self.projectionRange
return copy
def rebase(self, item):
amount = self._amount
active = self.active
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
projectionRange = self.projectionRange
Fighter.__init__(self, item)
self._amount = amount
self.active = active
for ability in self.abilities:
if ability.effectID in abilityEffectStates:
ability.active = abilityEffectStates[ability.effectID]
self.projectionRange = projectionRange
def fits(self, fit):
# If ships doesn't support this type of fighter, don't add it

View File

@@ -21,9 +21,9 @@ import datetime
import time
from copy import deepcopy
from itertools import chain
from math import asinh, log, sqrt
from logbook import Logger
from math import asinh, log, sqrt
from sqlalchemy.orm import reconstructor, validates
import eos.db
@@ -440,10 +440,8 @@ class Fit:
return False
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
if isinstance(self.ship, Citadel) and item.category.name != "Structure Module" or \
not isinstance(self.ship, Citadel) and item.category.name == "Structure Module":
if isinstance(self.ship, Citadel) is not item.isStandup:
return False
return True
def clear(self, projected=False, command=False):
@@ -934,13 +932,20 @@ class Fit:
To support a simpler way of doing self projections (so that we don't have to make a copy of the fit and
recalculate), this function was developed to be a common source of projected effect application.
"""
c = chain(self.drones, self.fighters, self.modules)
for item in c:
for item in chain(self.drones, self.fighters):
if item is not None:
# apply effects onto target fit x amount of times
for _ in range(projectionInfo.amount):
targetFit.register(item, origin=self)
item.calculateModifiedAttributes(targetFit, runTime, True)
item.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=0)
for mod in self.modules:
for _ in range(projectionInfo.amount):
targetFit.register(mod, origin=self)
mod.calculateModifiedAttributes(
targetFit, runTime, forceProjected=True,
forcedProjRange=projectionInfo.projectionRange)
def fill(self):
"""
@@ -1219,8 +1224,8 @@ class Fit:
# Signature reduction, uses the bomb formula as per CCP Larrikin
if energyNeutralizerSignatureResolution:
capNeed = capNeed * min(1, signatureRadius / energyNeutralizerSignatureResolution)
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
if capNeed:
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
def removeDrain(self, i):
del self.__extraDrains[i]
@@ -1320,8 +1325,8 @@ class Fit:
"""Return how much cap regen do we gain from having this module"""
currentRegen = self.calculateCapRecharge()
nomodRegen = self.calculateCapRecharge(
capacity=self.ship.getModifiedItemAttrWithoutAfflictor("capacitorCapacity", mod),
rechargeRate=self.ship.getModifiedItemAttrWithoutAfflictor("rechargeRate", mod) / 1000.0)
capacity=self.ship.getModifiedItemAttrExtended("capacitorCapacity", ignoreAfflictors=[mod]),
rechargeRate=self.ship.getModifiedItemAttrExtended("rechargeRate", ignoreAfflictors=[mod]) / 1000.0)
return currentRegen - nomodRegen
def getRemoteReps(self, spoolOptions=None):
@@ -1671,6 +1676,8 @@ class Fit:
copyProjectionInfo = fit.getProjectionInfo(fitCopy.ID)
originalProjectionInfo = fit.getProjectionInfo(self.ID)
copyProjectionInfo.active = originalProjectionInfo.active
copyProjectionInfo.amount = originalProjectionInfo.amount
copyProjectionInfo.projectionRange = originalProjectionInfo.projectionRange
forceUpdateSavedata(fit)
return fitCopy

View File

@@ -95,7 +95,7 @@ class Implant(HandledItem, ItemAttrShortcut):
return
for effect in self.item.effects.values():
if effect.runTime == runTime and effect.isType("passive") and effect.activeByDefault:
effect.handler(fit, self, ("implant",))
effect.handler(fit, self, ("implant",), None)
@validates("fitID", "itemID", "active")
def validator(self, key, val):

View File

@@ -54,7 +54,7 @@ class Mode(ItemAttrShortcut, HandledItem):
if self.item:
for effect in self.item.effects.values():
if effect.runTime == runTime and effect.activeByDefault:
effect.handler(fit, self, context=("module",))
effect.handler(fit, self, ("module",), None)
def __deepcopy__(self, memo):
copy = Mode(self.item)

View File

@@ -17,8 +17,9 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from logbook import Logger
import math
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
import eos.db
@@ -28,6 +29,7 @@ from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, Modi
from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator
from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes, RRTypes
@@ -90,6 +92,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__charge = None
self.projected = False
self.projectionRange = None
self.state = FittingModuleState.ONLINE
self.build()
@@ -339,7 +342,12 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
duringAcceleration = maxVelocity / 2 * accelTime
# Distance done after being at full speed
fullSpeed = maxVelocity * (flightTime - accelTime)
return duringAcceleration + fullSpeed
maxRange = duringAcceleration + fullSpeed
if 'fofMissileLaunching' in self.charge.effects:
rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange")
if rangeLimit:
maxRange = min(maxRange, rangeLimit)
return maxRange
@property
def falloff(self):
@@ -428,7 +436,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) or self.getModifiedItemAttr("doomsdayWarningDuration", 0)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
# Reaper DD can damage each target only once
@@ -826,7 +840,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear()
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False):
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False, forcedProjRange=DEFAULT):
# We will run the effect when two conditions are met:
# 1: It makes sense to run the effect
# The effect is either offline
@@ -843,6 +857,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
context = ("module",)
projected = False
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
if self.charge is not None:
# fix for #82 and it's regression #106
if not projected or (self.projected and not forceProjected) or gang:
@@ -856,13 +872,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
(not gang or (gang and effect.isType("gang")))
):
contexts = ("moduleCharge",)
# For gang effects, we pass in the effect itself as an argument. However, to avoid going through all
# the effect definitions and defining this argument, do a simple try/catch here and be done with it.
# @todo: possibly fix this
try:
effect.handler(fit, self, contexts, effect=effect)
except:
effect.handler(fit, self, contexts)
effect.handler(fit, self, contexts, projectionRange, effect=effect)
if self.item:
if self.state >= FittingModuleState.OVERHEATED:
@@ -872,7 +882,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
and not forceProjected \
and effect.activeByDefault \
and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange)
for effect in self.item.effects.values():
if effect.runTime == runTime and \
@@ -882,10 +892,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
(effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \
and ((projected and effect.isType("projected")) or not projected) \
and ((gang and effect.isType("gang")) or not gang):
try:
effect.handler(fit, self, context, effect=effect)
except:
effect.handler(fit, self, context)
effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well"""
@@ -1016,6 +1023,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
copy.state = self.state
copy.spoolType = self.spoolType
copy.spoolAmount = self.spoolAmount
copy.projectionRange = self.projectionRange
for x in self.mutators.values():
Mutator(copy, x.attribute, x.value)
@@ -1025,10 +1033,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def rebase(self, item):
state = self.state
charge = self.charge
spoolType = self.spoolType
spoolAmount = self.spoolAmount
projectionRange = self.projectionRange
Module.__init__(self, item, self.baseItem, self.mutaplasmid)
self.state = state
if self.isValidCharge(charge):
self.charge = charge
self.spoolType = spoolType
self.spoolAmount = spoolAmount
self.projectionRange = projectionRange
for x in self.mutators.values():
Mutator(self, x.attribute, x.value)

View File

@@ -98,7 +98,7 @@ class Ship(ItemAttrShortcut, HandledItem):
# skillbook modifiers will use the stale modifier value
# GH issue #351
fit.register(self)
effect.handler(fit, self, ("ship",))
effect.handler(fit, self, ("ship",), None)
def validateModeItem(self, item, owner=None):
""" Checks if provided item is a valid mode """

View File

@@ -77,6 +77,8 @@ class TargetProfile:
@signatureRadius.setter
def signatureRadius(self, val):
if val is not None and math.isinf(val):
val = None
self._signatureRadius = val
@property
@@ -106,7 +108,7 @@ class TargetProfile:
continue
line = line.split('#', 1)[0] # allows for comments
type, data = line.rsplit('=', 1)
type, data = type.strip(), data.split(',')
type, data = type.strip(), [d.strip() for d in data.split(',')]
except:
pyfalog.warning("Data isn't in correct format, continue to next line.")
continue
@@ -115,11 +117,13 @@ class TargetProfile:
continue
numPatterns += 1
name, data = data[0], data[1:5]
name, dataRes, dataMisc = data[0], data[1:5], data[5:8]
fields = {}
for index, val in enumerate(data):
val = float(val)
for index, val in enumerate(dataRes):
val = float(val) if val else 0
if math.isinf(val):
val = 0
try:
assert 0 <= val <= 100
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val / 100
@@ -127,7 +131,18 @@ class TargetProfile:
pyfalog.warning("Caught unhandled exception in import patterns.")
continue
if len(fields) == 4: # Avoid possible blank lines
if len(dataMisc) == 3:
for index, val in enumerate(dataMisc):
try:
fieldName = ("maxVelocity", "signatureRadius", "radius")[index]
except IndexError:
break
val = float(val) if val else 0
if fieldName != "signatureRadius" and math.isinf(val):
val = 0
fields[fieldName] = val
if len(fields) in (4, 7): # Avoid possible blank lines
if name.strip() in lookup:
pattern = lookup[name.strip()]
pattern.update(**fields)
@@ -142,20 +157,23 @@ class TargetProfile:
return patterns, numPatterns
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f\n"
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f\n"
@classmethod
def exportPatterns(cls, *patterns):
out = "# Exported from pyfa\n#\n"
out += "# Values are in following format:\n"
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %]\n\n"
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %],[Max velocity m/s],[Signature radius m],[Radius m]\n\n"
for dp in patterns:
out += cls.EXPORT_FORMAT % (
dp.name,
dp.emAmount * 100,
dp.thermalAmount * 100,
dp.kineticAmount * 100,
dp.explosiveAmount * 100
dp.explosiveAmount * 100,
dp.maxVelocity,
dp.signatureRadius,
dp.radius
)
return out.strip()

3
eos/utils/default.py Normal file
View File

@@ -0,0 +1,3 @@
class DEFAULT:
"""Singleton class to signify default argument value."""
pass

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import math
from collections import namedtuple
from eos.const import SpoolType
@@ -36,15 +37,33 @@ def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAm
"""
if not modMaxValue or not modStepValue:
return 0, 0, 0
if spoolType == SpoolType.SCALE:
cycles = int(floatUnerr(spoolAmount * modMaxValue / modStepValue))
return cycles * modStepValue, cycles, cycles * modCycleTime
if spoolType == SpoolType.SPOOL_SCALE:
# Find out at which point of spoolup scale we're on, find out how many cycles
# is enough to reach it and recalculate spoolup value for that amount of cycles
cycles = math.ceil(floatUnerr(modMaxValue * spoolAmount / modStepValue))
spoolValue = min(modMaxValue, cycles * modStepValue)
return spoolValue, cycles, cycles * modCycleTime
elif spoolType == SpoolType.CYCLE_SCALE:
# For cycle scale, find out max amount of cycles and scale against it
cycles = round(spoolAmount * math.ceil(floatUnerr(modMaxValue / modStepValue)))
spoolValue = min(modMaxValue, cycles * modStepValue)
return spoolValue, cycles, cycles * modCycleTime
elif spoolType == SpoolType.TIME:
cycles = min(int(floatUnerr(spoolAmount / modCycleTime)), int(floatUnerr(modMaxValue / modStepValue)))
return cycles * modStepValue, cycles, cycles * modCycleTime
cycles = min(
# How many full cycles mod had by passed time
math.floor(floatUnerr(spoolAmount / modCycleTime)),
# Max amount of cycles
math.ceil(floatUnerr(modMaxValue / modStepValue)))
spoolValue = min(modMaxValue, cycles * modStepValue)
return spoolValue, cycles, cycles * modCycleTime
elif spoolType == SpoolType.CYCLES:
cycles = min(int(spoolAmount), int(floatUnerr(modMaxValue / modStepValue)))
return cycles * modStepValue, cycles, cycles * modCycleTime
cycles = min(
# Consider full cycles only
math.floor(spoolAmount),
# Max amount of cycles
math.ceil(floatUnerr(modMaxValue / modStepValue)))
spoolValue = min(modMaxValue, cycles * modStepValue)
return spoolValue, cycles, cycles * modCycleTime
else:
return 0, 0, 0

BIN
eve.db

Binary file not shown.

View File

@@ -20,20 +20,7 @@
import math
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
if distance is None:
return 1
if srcFalloffRange > 0:
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
return 0
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
elif distance <= srcOptimalRange:
return 1
else:
return 0
from service.settings import GraphSettings
# Just copy-paste penalization chain calculation code (with some modifications,
@@ -63,3 +50,19 @@ def calculateMultiplier(multipliers):
bonus = l[i]
val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289)
return val
def checkLockRange(src, distance):
if distance is None:
return True
if GraphSettings.getInstance().get('ignoreLockRange'):
return True
return distance <= src.item.maxTargetRange
def checkDroneControlRange(src, distance):
if distance is None:
return True
if GraphSettings.getInstance().get('ignoreDCR'):
return True
return distance <= src.item.extraAttributes['droneControlRange']

View File

@@ -21,37 +21,47 @@
import math
from functools import lru_cache
from eos.calc import calculateRangeFactor
from eos.const import FittingHardpoint
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from service.attribute import Attribute
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
applicationMap = {}
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
applicationMap[mod] = getTurretMult(
mod=mod,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
if inLockRange:
applicationMap[mod] = getTurretMult(
mod=mod,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[mod] = 0
elif mod.hardpoint == FittingHardpoint.MISSILE:
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# FoF missiles can shoot beyond lock range
if inLockRange or (mod.charge is not None and 'fofMissileLaunching' in mod.charge.effects):
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[mod] = 0
elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'):
applicationMap[mod] = getSmartbombMult(
mod=mod,
@@ -64,44 +74,58 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
applicationMap[mod] = getGuidedBombMult(
mod=mod,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
if inLockRange:
applicationMap[mod] = getGuidedBombMult(
mod=mod,
src=src,
distance=distance,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[mod] = 0
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
# Only single-target DDs need locks
if not inLockRange and {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(mod.item.effects):
applicationMap[mod] = 0
else:
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
drone=drone,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
if inLockRange and inDroneRange:
applicationMap[drone] = getDroneMult(
drone=drone,
src=src,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[drone] = 0
for fighter in src.item.activeFightersIter():
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
if not ability.dealsDamage or not ability.active:
continue
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
src=src,
tgt=tgt,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# Bomb launching doesn't need locks
if inLockRange or ability.effect.name == 'fighterAbilityLaunchBomb':
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
src=src,
tgt=tgt,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
else:
applicationMap[(fighter, ability.effectID)] = 0
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)
@@ -200,7 +224,11 @@ def getGuidedBombMult(mod, src, distance, tgtSigRadius):
def getDroneMult(drone, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
if distance is not None and distance > src.item.extraAttributes['droneControlRange']:
if (
distance is not None and (
(not GraphSettings.getInstance().get('ignoreDCR') and distance > src.item.extraAttributes['droneControlRange']) or
(not GraphSettings.getInstance().get('ignoreLockRange') and distance > src.item.maxTargetRange))
):
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones

View File

@@ -20,37 +20,85 @@
import math
from eos.calc import calculateRangeFactor
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance):
def _isRegularScram(mod):
if not mod.item:
return False
if not {'warpScrambleBlockMWDWithNPCEffect', 'structureWarpScrambleBlockMWDWithNPCEffect'}.intersection(mod.item.effects):
return False
if not mod.getModifiedItemAttr('activationBlockedStrenght', 0):
return False
return True
def _isHicScram(mod):
if not mod.item:
return False
if 'warpDisruptSphere' not in mod.item.effects:
return False
if not mod.charge:
return False
if 'shipModuleFocusedWarpScramblingScript' not in mod.charge.effects:
return False
return True
def getScramRange(src):
scramRange = None
for mod in src.item.activeModulesIter():
if _isRegularScram(mod) or _isHicScram(mod):
scramRange = max(scramRange or 0, mod.maxRange or 0)
return scramRange
def getScrammables(tgt):
scrammables = []
if tgt.isFit:
for mod in tgt.item.activeModulesIter():
if not mod.item:
continue
if {'moduleBonusMicrowarpdrive', 'microJumpDrive', 'microJumpPortalDrive'}.intersection(mod.item.effects):
scrammables.append(mod)
return scrammables
def getTackledSpeed(src, tgt, currentUntackledSpeed, srcScramRange, tgtScrammables, webMods, webDrones, webFighters, distance):
# Can slow down non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return currentUnwebbedSpeed
maxUnwebbedSpeed = tgt.getMaxVelocity()
return currentUntackledSpeed
maxUntackledSpeed = tgt.getMaxVelocity()
# What's immobile cannot be slowed
if maxUnwebbedSpeed == 0:
return maxUnwebbedSpeed
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
if maxUntackledSpeed == 0:
return maxUntackledSpeed
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
speedRatio = currentUntackledSpeed / maxUntackledSpeed
# No scrams or distance is longer than longest scram - nullify scrammables list
if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange):
tgtScrammables = ()
appliedMultipliers = {}
# Modules first, they are applied always the same way
for wData in webMods:
appliedBoost = wData.boost * calculateRangeFactor(
srcOptimalRange=wData.optimal,
srcFalloffRange=wData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Modules first, they are always applied the same way
if inLockRange:
for wData in webMods:
appliedBoost = wData.boost * calculateRangeFactor(
srcOptimalRange=wData.optimal,
srcFalloffRange=wData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Drones and fighters
mobileWebs = []
mobileWebs.extend(webFighters)
# Drones have range limit
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
if inLockRange:
mobileWebs.extend(webFighters)
if inLockRange and inDroneRange:
mobileWebs.extend(webDrones)
atkRadius = src.getRadius()
# As mobile webs either follow the target or stick to the attacking ship,
@@ -60,8 +108,8 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
for mwData in longEnoughMws:
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Apply remaining webs, from fastest to slowest
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
while mobileWebs:
@@ -70,7 +118,7 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
for mwData in fastestMws:
# Faster than target or set to follow it - apply full slowdown
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentTackledSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
appliedMwBoost = mwData.boost
# Otherwise project from the center of the ship
else:
@@ -84,31 +132,37 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
maxTackledSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
currentTackledSpeed = maxTackledSpeed * speedRatio
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(currentWebbedSpeed)
return floatUnerr(currentTackledSpeed)
def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
def getSigRadiusMult(src, tgt, tgtSpeed, srcScramRange, tgtScrammables, tpMods, tpDrones, tpFighters, distance):
# Can blow non-immune ships and target profiles
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return 1
untpedSig = tgt.getSigRadius()
# Modules
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
initSig = tgt.getSigRadius()
# No scrams or distance is longer than longest scram - nullify scrammables list
if not inLockRange or srcScramRange is None or (distance is not None and distance > srcScramRange):
tgtScrammables = ()
# TPing modules
appliedMultipliers = {}
for tpData in tpMods:
appliedBoost = tpData.boost * calculateRangeFactor(
srcOptimalRange=tpData.optimal,
srcFalloffRange=tpData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
# Drones and fighters
if inLockRange:
for tpData in tpMods:
appliedBoost = tpData.boost * calculateRangeFactor(
srcOptimalRange=tpData.optimal,
srcFalloffRange=tpData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
# TPing drones
mobileTps = []
mobileTps.extend(tpFighters)
# Drones have range limit
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
if inLockRange:
mobileTps.extend(tpFighters)
if inLockRange and inDroneRange:
mobileTps.extend(tpDrones)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
atkRadius = src.getRadius()
@@ -127,9 +181,9 @@ def getTpMult(src, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
srcFalloffRange=mtpData.falloff,
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID))
tpedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers)
if tpedSig == math.inf and untpedSig == math.inf:
modifiedSig = tgt.getSigRadius(extraMultipliers=appliedMultipliers, ignoreAfflictors=tgtScrammables)
if modifiedSig == math.inf and initSig == math.inf:
return 1
mult = tpedSig / untpedSig
mult = modifiedSig / initSig
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(mult)

View File

@@ -24,7 +24,7 @@ from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter
from service.settings import GraphSettings
from .calc.application import getApplicationPerKey
from .calc.projected import getTpMult, getWebbedSpeed
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists):
@@ -54,7 +54,7 @@ class YDpsMixin:
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False))
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
@@ -88,7 +88,7 @@ class YVolleyMixin:
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False))
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
@@ -138,8 +138,11 @@ class XDistanceMixin(SmoothPointGetter):
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(src=src, maxTime=miscParams['time'])
applyProjected = GraphSettings.getInstance().get('applyProjected')
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'applyProjected': applyProjected,
'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
@@ -151,18 +154,22 @@ class XDistanceMixin(SmoothPointGetter):
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=distance)
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=commonData['srcScramRange'],
tgtScrammables=commonData['tgtScrammables'],
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -189,21 +196,27 @@ class XTimeMixin(PointGetter):
tgtSpeed = miscParams['tgtSpeed']
tgtSigRadius = tgt.getSigRadius()
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -303,21 +316,27 @@ class XTgtSpeedMixin(SmoothPointGetter):
tgtSpeed = x
tgtSigRadius = tgt.getSigRadius()
if commonData['applyProjected']:
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
tgtSigRadius = tgtSigRadius * getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
@@ -347,21 +366,27 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
tgtSpeed = miscParams['tgtSpeed']
tgtSigMult = 1
if GraphSettings.getInstance().get('applyProjected'):
srcScramRange = getScramRange(src=src)
tgtScrammables = getScrammables(tgt=tgt)
webMods, tpMods = self.graph._projectedCache.getProjModData(src)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(src)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(src)
tgtSpeed = getWebbedSpeed(
tgtSpeed = getTackledSpeed(
src=src,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
currentUntackledSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParams['distance'])
tgtSigMult = getTpMult(
tgtSigMult = getSigRadiusMult(
src=src,
tgt=tgt,
tgtSpeed=tgtSpeed,
srcScramRange=srcScramRange,
tgtScrammables=tgtScrammables,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,

View File

@@ -20,7 +20,8 @@
import math
from graphs.calc import calculateMultiplier, calculateRangeFactor
from eos.calc import calculateRangeFactor
from graphs.calc import calculateMultiplier, checkLockRange, checkDroneControlRange
from graphs.data.base import SmoothPointGetter
@@ -37,33 +38,37 @@ class Distance2NeutingStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'energyNosferatuFalloff' in mod.item.effects and mod.getModifiedItemAttr('nosOverride'):
neuts.append((
mod.getModifiedItemAttr('powerTransferAmount') / self.__getDuration(mod) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'doomsdayAOENeut' in mod.item.effects:
neuts.append((
mod.getModifiedItemAttr('energyNeutralizerAmount') / self.__getDuration(mod) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
mod.falloff or 0, False, False))
for drone in src.item.activeDronesIter():
if 'entityEnergyNeutralizerFalloff' in drone.item.effects:
neuts.extend(drone.amountActive * ((
drone.getModifiedItemAttr('energyNeutralizerAmount') / (drone.getModifiedItemAttr('energyNeutralizerDuration') / 1000) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
math.inf, 0, True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityEnergyNeutralizer':
nps = fighter.getModifiedItemAttr('fighterAbilityEnergyNeutralizerAmount') / (ability.cycleTime / 1000)
neuts.append((
nps * fighter.amount * resonance,
math.inf, 0))
math.inf, 0, True, False))
return {'neuts': neuts}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
combinedStr = 0
for strength, optimal, falloff in commonData['neuts']:
for strength, optimal, falloff, needsLock, needsDcr in commonData['neuts']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
@@ -84,28 +89,32 @@ class Distance2WebbingStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEWeb' in mod.item.effects:
webs.append((
mod.getModifiedItemAttr('speedFactor') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteWebifierEntity' in drone.item.effects:
webs.extend(drone.amountActive * ((
drone.getModifiedItemAttr('speedFactor') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityStasisWebifier':
webs.append((
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amount * resonance,
math.inf, 0, 'default'))
math.inf, 0, 'default', True, False))
return {'webs': webs}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['webs']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['webs']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -129,28 +138,32 @@ class Distance2EcmStrMaxGetter(SmoothPointGetter):
if effectName in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
mod.maxRange or 0, mod.falloff or 0))
mod.maxRange or 0, mod.falloff or 0, True, False))
if 'doomsdayAOEECM' in mod.item.effects:
ecms.append((
max(mod.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0))
mod.falloff or 0, False, False))
for drone in src.item.activeDronesIter():
if 'entityECMFalloff' in drone.item.effects:
ecms.extend(drone.amountActive * ((
max(drone.getModifiedItemAttr(a) for a in self.ECM_ATTRS_GENERAL) * resonance,
src.item.extraAttributes['droneControlRange'], 0),))
math.inf, 0, True, True),))
for fighter, ability in src.item.activeFighterAbilityIter():
if ability.effect.name == 'fighterAbilityECM':
ecms.append((
max(fighter.getModifiedItemAttr(a) for a in self.ECM_ATTRS_FIGHTERS) * fighter.amount * resonance,
math.inf, 0))
math.inf, 0, True, False))
return {'ecms': ecms}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
combinedStr = 0
for strength, optimal, falloff in commonData['ecms']:
for strength, optimal, falloff, needsLock, needsDcr in commonData['ecms']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
combinedStr += strength * calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
return combinedStr
@@ -168,23 +181,27 @@ class Distance2DampStrLockRangeGetter(SmoothPointGetter):
if effectName in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEDamp' in mod.item.effects:
damps.append((
mod.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteSensorDampEntity' in drone.item.effects:
damps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxTargetRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'damps': damps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['damps']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['damps']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -205,23 +222,27 @@ class Distance2TdStrOptimalGetter(SmoothPointGetter):
if effectName in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOETrack' in mod.item.effects:
tds.append((
mod.getModifiedItemAttr('maxRangeBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'npcEntityWeaponDisruptor' in drone.item.effects:
tds.extend(drone.amountActive * ((
drone.getModifiedItemAttr('maxRangeBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'tds': tds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tds']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['tds']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)
@@ -243,20 +264,24 @@ class Distance2GdStrRangeGetter(SmoothPointGetter):
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOETrack' in mod.item.effects:
gds.append((
mod.getModifiedItemAttr('missileVelocityBonus') * resonance,
mod.getModifiedItemAttr('explosionDelayBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
return {'gds': gds}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
velocityStrMults = {}
timeStrMults = {}
for velocityStr, timeStr, optimal, falloff, stackingGroup in commonData['gds']:
for velocityStr, timeStr, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['gds']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
rangeFactor = calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
velocityStr *= rangeFactor
timeStr *= rangeFactor
@@ -281,23 +306,27 @@ class Distance2TpStrGetter(SmoothPointGetter):
if effectName in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
mod.maxRange or 0, mod.falloff or 0, 'default'))
mod.maxRange or 0, mod.falloff or 0, 'default', True, False))
if 'doomsdayAOEPaint' in mod.item.effects:
tps.append((
mod.getModifiedItemAttr('signatureRadiusBonus') * resonance,
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - src.getRadius()),
mod.falloff or 0, 'default'))
mod.falloff or 0, 'default', False, False))
for drone in src.item.activeDronesIter():
if 'remoteTargetPaintEntity' in drone.item.effects:
tps.extend(drone.amountActive * ((
drone.getModifiedItemAttr('signatureRadiusBonus') * resonance,
src.item.extraAttributes['droneControlRange'], 0, 'default'),))
math.inf, 0, 'default', True, True),))
return {'tps': tps}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
strMults = {}
for strength, optimal, falloff, stackingGroup in commonData['tps']:
for strength, optimal, falloff, stackingGroup, needsLock, needsDcr in commonData['tps']:
if (needsLock and not inLockRange) or (needsDcr and not inDroneRange):
continue
strength *= calculateRangeFactor(srcOptimalRange=optimal, srcFalloffRange=falloff, distance=distance)
strMults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
strMult = calculateMultiplier(strMults)

View File

@@ -18,23 +18,32 @@
# =============================================================================
from eos.calc import calculateRangeFactor
from eos.utils.float import floatUnerr
from graphs.calc import calculateRangeFactor
from graphs.calc import checkLockRange, checkDroneControlRange
def getApplicationPerKey(src, distance):
inLockRange = checkLockRange(src=src, distance=distance)
inDroneRange = checkDroneControlRange(src=src, distance=distance)
applicationMap = {}
for mod in src.item.activeModulesIter():
if not mod.isRemoteRepping():
continue
applicationMap[mod] = 1 if distance is None else calculateRangeFactor(
srcOptimalRange=mod.maxRange or 0,
srcFalloffRange=mod.falloff or 0,
distance=distance)
if not inLockRange:
applicationMap[mod] = 0
else:
applicationMap[mod] = calculateRangeFactor(
srcOptimalRange=mod.maxRange or 0,
srcFalloffRange=mod.falloff or 0,
distance=distance)
for drone in src.item.activeDronesIter():
if not drone.isRemoteRepping():
continue
applicationMap[drone] = 1 if distance is None or distance <= src.item.extraAttributes['droneControlRange'] else 0
if not inLockRange or not inDroneRange:
applicationMap[drone] = 0
else:
applicationMap[drone] = 1
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)

View File

@@ -50,7 +50,7 @@ class YRpsMixin:
isAncShield = 'shipModuleAncillaryRemoteShieldBooster' in mod.item.effects
isAncArmor = 'shipModuleAncillaryRemoteArmorRepairer' in mod.item.effects
rpsMap[mod] = mod.getRemoteReps(
spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False),
spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False),
reloadOverride=ancReload if (isAncShield or isAncArmor) else None)
for drone in src.item.activeDronesIter():
if not drone.isRemoteRepping():

View File

@@ -106,7 +106,9 @@ class GraphCanvasPanel(wx.Panel):
legendData = []
chosenX = self.graphFrame.ctrlPanel.xType
chosenY = self.graphFrame.ctrlPanel.yType
self.subplot.set(xlabel=self.graphFrame.ctrlPanel.formatLabel(chosenX), ylabel=self.graphFrame.ctrlPanel.formatLabel(chosenY))
self.subplot.set(
xlabel=self.graphFrame.ctrlPanel.formatLabel(chosenX),
ylabel=self.graphFrame.ctrlPanel.formatLabel(chosenY))
mainInput, miscInputs = self.graphFrame.ctrlPanel.getValues()
view = self.graphFrame.getView()
@@ -186,6 +188,7 @@ class GraphCanvasPanel(wx.Panel):
if minX is not None and maxX is not None:
minY = min(allYs, default=None)
maxY = max(allYs, default=None)
yDiff = (maxY or 0) - (minY or 0)
xMark = max(min(self.xMark, maxX), minX)
# If in top 10% of X coordinates, align labels differently
if xMark > canvasMinX + 0.9 * (canvasMaxX - canvasMinX):
@@ -212,14 +215,16 @@ class GraphCanvasPanel(wx.Panel):
def addYMark(val):
if val is None:
return
# Round according to shown Y range - the bigger the range,
# the rougher the rounding
if yDiff != 0:
rounded = roundToPrec(val, 4, nsValue=yDiff)
else:
rounded = val
# If due to some bug or insufficient plot density we're
# out of bounds, do not add anything
if minY <= val <= maxY:
if abs(val) < 0.0001:
val = 0
else:
val = roundToPrec(val, 4)
yMarks.add(val)
if minY <= val <= maxY or minY <= rounded <= maxY:
yMarks.add(rounded)
for source, target in iterList:
xs, ys = plotData[(source, target)]

View File

@@ -38,6 +38,9 @@ from .ctrlPanel import GraphControlPanel
pyfalog = Logger(__name__)
REDRAW_DELAY = 500
class GraphFrame(AuxiliaryFrame):
def __init__(self, parent):
@@ -45,7 +48,7 @@ class GraphFrame(AuxiliaryFrame):
pyfalog.warning('Matplotlib is not enabled. Skipping initialization.')
return
super().__init__(parent, title='Graphs', style=wx.RESIZE_BORDER, size=(520, 390))
super().__init__(parent, title='Graphs', size=(520, 390), resizeable=True)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
@@ -90,6 +93,9 @@ class GraphFrame(AuxiliaryFrame):
self.mainFrame.Bind(GE.GRAPH_OPTION_CHANGED, self.OnGraphOptionChanged)
self.mainFrame.Bind(GE.EFFECTIVE_HP_TOGGLED, self.OnEffectiveHpToggled)
self.drawTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer)
self.Layout()
self.UpdateWindowSize()
self.draw()
@@ -128,7 +134,10 @@ class GraphFrame(AuxiliaryFrame):
for fitID in event.fitIDs:
self.clearCache(reason=GraphCacheCleanupReason.fitChanged, extraData=fitID)
self.ctrlPanel.OnFitChanged(event)
self.draw()
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
def OnFitRemoved(self, event):
event.Skip()
@@ -184,7 +193,10 @@ class GraphFrame(AuxiliaryFrame):
self.ctrlPanel.refreshAxeLabels(restoreSelection=True)
self.Layout()
self.clearCache(reason=GraphCacheCleanupReason.hpEffectivityChanged)
self.draw()
# Data has to be recalculated - delay redraw
# to give time to finish UI update in main window
self.drawTimer.Stop()
self.drawTimer.Start(REDRAW_DELAY, True)
# Even if graph is not selected, keep it updated
for idx in range(self.graphSelection.GetCount()):
view = self.getView(idx=idx)
@@ -202,6 +214,10 @@ class GraphFrame(AuxiliaryFrame):
self.draw()
event.Skip()
def OnDrawTimer(self, event):
event.Skip()
self.draw()
def OnClose(self, event):
self.mainFrame.Unbind(GE.FIT_RENAMED, handler=self.OnFitRenamed)
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)

View File

@@ -54,10 +54,13 @@ class BaseWrapper:
return self.item.name
return ''
def getMaxVelocity(self, extraMultipliers=None):
def getMaxVelocity(self, extraMultipliers=None, ignoreAfflictors=()):
if self.isFit:
if extraMultipliers:
maxVelocity = self.item.ship.getModifiedItemAttrWithExtraMods('maxVelocity', extraMultipliers=extraMultipliers)
if extraMultipliers or ignoreAfflictors:
maxVelocity = self.item.ship.getModifiedItemAttrExtended(
'maxVelocity',
extraMultipliers=extraMultipliers,
ignoreAfflictors=ignoreAfflictors)
else:
maxVelocity = self.item.ship.getModifiedItemAttr('maxVelocity')
elif self.isProfile:
@@ -68,10 +71,13 @@ class BaseWrapper:
maxVelocity = None
return maxVelocity
def getSigRadius(self, extraMultipliers=None):
def getSigRadius(self, extraMultipliers=None, ignoreAfflictors=()):
if self.isFit:
if extraMultipliers:
sigRadius = self.item.ship.getModifiedItemAttrWithExtraMods('signatureRadius', extraMultipliers=extraMultipliers)
if extraMultipliers or ignoreAfflictors:
sigRadius = self.item.ship.getModifiedItemAttrExtended(
'signatureRadius',
extraMultipliers=extraMultipliers,
ignoreAfflictors=ignoreAfflictors)
else:
sigRadius = self.item.ship.getModifiedItemAttr('signatureRadius')
elif self.isProfile:

View File

@@ -20,6 +20,7 @@
# noinspection PyPackageRequirements
import wx
import gui.globalEvents as GE
from gui.bitmap_loader import BitmapLoader
from gui.builtinAdditionPanes.boosterView import BoosterView
from gui.builtinAdditionPanes.cargoView import CargoView
@@ -35,9 +36,10 @@ from gui.toggle_panel import TogglePanel
class AdditionsPane(TogglePanel):
def __init__(self, parent):
def __init__(self, parent, mainFrame):
TogglePanel.__init__(self, parent, force_layout=1)
self.mainFrame = mainFrame
self.SetLabel("Additions")
pane = self.GetContentPanel()
@@ -45,7 +47,7 @@ class AdditionsPane(TogglePanel):
baseSizer = wx.BoxSizer(wx.HORIZONTAL)
pane.SetSizer(baseSizer)
self.notebook = ChromeNotebook(pane, False)
self.notebook = ChromeNotebook(pane, can_add=False, tabWidthMode=1)
self.notebook.SetMinSize((-1, 1000))
baseSizer.Add(self.notebook, 1, wx.EXPAND)
@@ -83,6 +85,8 @@ class AdditionsPane(TogglePanel):
self.notes = NotesView(self.notebook)
self.notebook.AddPage(self.notes, "Notes", image=notesImg, closeable=False)
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
self.notebook.SetSelection(0)
PANES = ["Drones", "Fighters", "Cargo", "Implants", "Boosters", "Projected", "Command", "Notes"]
@@ -106,3 +110,21 @@ class AdditionsPane(TogglePanel):
self.parent.SetSashInvisible(False)
self.parent.SetMinimumPaneSize(200)
self.parent.SetSashPosition(self.old_pos, True)
def OnFitChanged(self, event):
event.Skip()
activeFitID = self.mainFrame.getActiveFit()
if activeFitID is not None and activeFitID not in event.fitIDs:
return
self.updateExtraText()
def updateExtraText(self):
refresh = False
for i in range(self.notebook.GetPageCount()):
page = self.notebook.GetPage(i)
if hasattr(page, 'getTabExtraText'):
refresh = True
self.notebook.SetPageTitleExtra(i, page.getTabExtraText() or '', refresh=False)
if refresh:
self.notebook.tabs_container.AdjustTabsSize()
self.notebook.Refresh()

View File

@@ -26,8 +26,10 @@ class AuxiliaryFrame(wx.Frame):
_instance = None
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=None):
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=None, resizeable=False):
baseStyle = wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
if resizeable:
baseStyle = baseStyle | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX
kwargs = {
'parent': parent,
'style': baseStyle if style is None else baseStyle | style}

View File

@@ -226,3 +226,23 @@ class BoosterView(d.Display):
continue
boosters.append(booster)
return boosters
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active boosters
if opt == 1:
amount = len([b for b in fit.boosters if b.active])
return ' ({})'.format(amount) if amount else None
# Total amount of boosters
elif opt == 2:
amount = len(fit.boosters)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -214,3 +214,19 @@ class CargoView(d.Display):
continue
cargos.append(cargo)
return cargos
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Total amount of cargo items
if opt in (1, 2):
amount = len(fit.cargo)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -247,3 +247,27 @@ class CommandView(d.Display):
self.mainFrame.command.Submit(cmd.GuiAddCommandFitsCommand(
fitID=self.mainFrame.getActiveFit(),
commandFitIDs=fitIDs))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active command fits
if opt == 1:
amount = 0
for commandFit in fit.commandFits:
info = commandFit.getCommandInfo(fitID)
if info is not None and info.active:
amount += 1
return ' ({})'.format(amount) if amount else None
# Total amount of command fits
elif opt == 2:
amount = len(fit.commandFits)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -337,3 +337,27 @@ class DroneView(Display):
continue
drones.append(drone)
return drones
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active drones
if opt == 1:
amount = 0
for droneStack in fit.drones:
amount += droneStack.amountActive
return ' ({})'.format(amount) if amount else None
# Total amount of drones
elif opt == 2:
amount = 0
for droneStack in fit.drones:
amount += droneStack.amount
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -117,6 +117,26 @@ class FighterView(wx.Panel):
self.Refresh()
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active fighter squads
if opt == 1:
amount = len([f for f in fit.fighters if f.active])
return ' ({})'.format(amount) if amount else None
# Total amount of fighter squads
elif opt == 2:
amount = len(fit.fighters)
return ' ({})'.format(amount) if amount else None
else:
return None
class FighterDisplay(d.Display):

View File

@@ -101,6 +101,25 @@ class ImplantView(wx.Panel):
self.mainFrame.command.Submit(cmd.GuiChangeImplantLocationCommand(
fitID=fitID, source=ImplantLocation.FIT if self.rbFit.GetValue() else ImplantLocation.CHARACTER))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active implants
if opt == 1:
amount = len([i for i in fit.appliedImplants if i.active])
return ' ({})'.format(amount) if amount else None
# Total amount of implants
elif opt == 2:
amount = len(fit.appliedImplants)
return ' ({})'.format(amount) if amount else None
else:
return None
class ImplantDisplay(d.Display):

View File

@@ -27,6 +27,7 @@ import gui.builtinAdditionPanes.droneView
import gui.display as d
import gui.fitCommands as cmd
import gui.globalEvents as GE
from eos.const import FittingModuleState
from eos.saveddata.drone import Drone as EosDrone
from eos.saveddata.fighter import Fighter as EosFighter
from eos.saveddata.fit import Fit as EosFit
@@ -74,7 +75,8 @@ class ProjectedView(d.Display):
'Ammo Icon',
'Base Icon',
'Base Name',
'Ammo']
'Ammo',
'Projection Range']
def __init__(self, parent):
d.Display.__init__(self, parent, style=wx.BORDER_NONE)
@@ -396,3 +398,34 @@ class ProjectedView(d.Display):
fitID=self.mainFrame.getActiveFit(),
projectedFitIDs=fitIDs,
amount=1))
def getTabExtraText(self):
fitID = self.mainFrame.getActiveFit()
if fitID is None:
return None
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
return None
opt = sFit.serviceFittingOptions["additionsLabels"]
# Amount of active projected items
if opt == 1:
amount = 0
for projectedFit in fit.projectedFits:
info = projectedFit.getProjectionInfo(fitID)
if info is not None and info.active:
amount += 1
amount += len([m for m in fit.projectedModules if m.state > FittingModuleState.OFFLINE])
amount += len([d for d in fit.projectedDrones if d.amountActive > 0])
amount += len([f for f in fit.projectedFighters if f.active])
return ' ({})'.format(amount) if amount else None
# Total amount of projected items
elif opt == 2:
amount = 0
amount += len(fit.projectedFits)
amount += len(fit.projectedModules)
amount += len(fit.projectedDrones)
amount += len(fit.projectedFighters)
return ' ({})'.format(amount) if amount else None
else:
return None

View File

@@ -22,6 +22,7 @@ from gui.builtinContextMenus import shipJump
# Generic item manipulations
from gui.builtinContextMenus import itemRemove
from gui.builtinContextMenus import itemAmountChange
from gui.builtinContextMenus import itemProjectionRange
from gui.builtinContextMenus import droneSplitStack
from gui.builtinContextMenus import itemVariationChange
from gui.builtinContextMenus import moduleMutations
@@ -44,8 +45,10 @@ from gui.builtinContextMenus import damagePatternChange
from gui.builtinContextMenus import factorReload
from gui.builtinContextMenus.targetProfile import switcher
# Graph extra options
from gui.builtinContextMenus import graphDmgIgnoreResists
from gui.builtinContextMenus import graphDmgApplyProjected
from gui.builtinContextMenus import graphDmgIgnoreResists
from gui.builtinContextMenus import graphLockRange
from gui.builtinContextMenus import graphDroneControlRange
from gui.builtinContextMenus import graphDmgDroneMode
# Additions panel menus
from gui.builtinContextMenus import additionsExportSelection

View File

@@ -69,7 +69,7 @@ class FighterAbilities(ContextMenuCombined):
command = cmd.GuiToggleLocalFighterAbilityStateCommand
if self.fighter in container:
mainPosition = container.index(self.fighter)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fighters = getSimilarFighters(container, self.fighter)
else:
fighters = self.selection

View File

@@ -17,7 +17,7 @@ class GraphDmgApplyProjectedMenu(ContextMenuUnconditional):
return srcContext == 'dmgStatsGraph'
def getText(self, callingWindow, itmContext):
return 'Apply Attacker Webs and TPs'
return 'Apply Projected Items'
def activate(self, callingWindow, fullContext, i):
self.settings.set('applyProjected', not self.settings.get('applyProjected'))

View File

@@ -0,0 +1,30 @@
# noinspection PyPackageRequirements
import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.settings import GraphSettings
class GraphIgnoreDcrMenu(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = GraphSettings.getInstance()
def display(self, callingWindow, srcContext):
return srcContext in ('dmgStatsGraph', 'remoteRepsGraph', 'ewarStatsGraph')
def getText(self, callingWindow, itmContext):
return 'Ignore Drone Control Range'
def activate(self, callingWindow, fullContext, i):
self.settings.set('ignoreDCR', not self.settings.get('ignoreDCR'))
wx.PostEvent(self.mainFrame, GE.GraphOptionChanged())
def isChecked(self, i):
return self.settings.get('ignoreDCR')
GraphIgnoreDcrMenu.register()

View File

@@ -0,0 +1,30 @@
# noinspection PyPackageRequirements
import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.settings import GraphSettings
class GraphIgnoreLockRangeMenu(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = GraphSettings.getInstance()
def display(self, callingWindow, srcContext):
return srcContext in ('dmgStatsGraph', 'remoteRepsGraph', 'ewarStatsGraph')
def getText(self, callingWindow, itmContext):
return 'Ignore Lock Range'
def activate(self, callingWindow, fullContext, i):
self.settings.set('ignoreLockRange', not self.settings.get('ignoreLockRange'))
wx.PostEvent(self.mainFrame, GE.GraphOptionChanged())
def isChecked(self, i):
return self.settings.get('ignoreLockRange')
GraphIgnoreLockRangeMenu.register()

View File

@@ -0,0 +1,126 @@
import re
# noinspection PyPackageRequirements
import wx
import gui.fitCommands as cmd
import gui.mainFrame
from eos.saveddata.fighter import Fighter as EosFighter
from eos.saveddata.fit import Fit as EosFit
from eos.saveddata.module import Module as EosModule
from gui.contextMenu import ContextMenuCombined
from gui.fitCommands.helpers import getSimilarFighters, getSimilarModPositions
from service.fit import Fit
class ChangeItemProjectionRange(ContextMenuCombined):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, mainItem, selection):
if srcContext not in ('projectedFit', 'projectedModule', 'projectedDrone', 'projectedFighter'):
return False
if mainItem is None:
return False
if getattr(mainItem, 'isExclusiveSystemEffect', False):
return False
return True
def getText(self, callingWindow, itmContext, mainItem, selection):
return 'Change {} Range'.format(itmContext)
def activate(self, callingWindow, fullContext, mainItem, selection, i):
fitID = self.mainFrame.getActiveFit()
if isinstance(mainItem, EosFit):
try:
value = mainItem.getProjectionInfo(fitID).projectionRange
except AttributeError:
return
else:
value = mainItem.projectionRange
if value is not None:
value /= 1000
with RangeChanger(self.mainFrame, value) as dlg:
if dlg.ShowModal() == wx.ID_OK:
cleanInput = re.sub(r'[^0-9.]', '', dlg.input.GetLineText(0).strip())
if cleanInput:
try:
cleanInputFloat = float(cleanInput)
except ValueError:
return
newRange = cleanInputFloat * 1000
else:
newRange = None
fitID = self.mainFrame.getActiveFit()
items = selection
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
if isinstance(mainItem, EosModule):
fit = Fit.getInstance().getFit(fitID)
positions = getSimilarModPositions(fit.projectedModules, mainItem)
items = [fit.projectedModules[p] for p in positions]
elif isinstance(mainItem, EosFighter):
fit = Fit.getInstance().getFit(fitID)
items = getSimilarFighters(fit.projectedFighters, mainItem)
self.mainFrame.command.Submit(cmd.GuiChangeProjectedItemsProjectionRangeCommand(
fitID=fitID, items=items, projectionRange=newRange))
ChangeItemProjectionRange.register()
class RangeChanger(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title='Change Projection Range', style=wx.DEFAULT_DIALOG_STYLE)
self.SetMinSize((346, 156))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
bSizer2 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, 'New Range, km:')
bSizer2.Add(text, 0)
bSizer1.Add(bSizer2, 0, wx.ALL, 10)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
if value is None:
value = ''
else:
if value == int(value):
value = int(value)
value = str(value)
self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 15)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)
# checks to make sure it's valid number
@staticmethod
def onChar(event):
key = event.GetKeyCode()
acceptable_characters = '1234567890.'
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -65,7 +65,7 @@ class RemoveItem(ContextMenuCombined):
def __handleModule(self, callingWindow, mainItem, selection):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
positions = getSimilarModPositions(fit.modules, mainItem)
else:
positions = []
@@ -88,7 +88,7 @@ class RemoveItem(ContextMenuCombined):
def __handleFighter(self, callingWindow, mainItem, selection):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fighters = getSimilarFighters(fit.fighters, mainItem)
else:
fighters = selection
@@ -131,7 +131,7 @@ class RemoveItem(ContextMenuCombined):
self.mainFrame.command.Submit(cmd.GuiRemoveProjectedItemsCommand(
fitID=fitID, items=selection, amount=math.inf))
elif isinstance(mainItem, EosModule):
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fit = Fit.getInstance().getFit(fitID)
positions = getSimilarModPositions(fit.projectedModules, mainItem)
items = [fit.projectedModules[p] for p in positions]
@@ -143,7 +143,7 @@ class RemoveItem(ContextMenuCombined):
self.mainFrame.command.Submit(cmd.GuiRemoveProjectedItemsCommand(
fitID=fitID, items=selection, amount=math.inf))
elif isinstance(mainItem, EosFighter):
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fit = Fit.getInstance().getFit(fitID)
items = getSimilarFighters(fit.projectedFighters, mainItem)
else:

View File

@@ -43,6 +43,7 @@ class ChangeItemToVariation(ContextMenuCombined):
self.mainItem = mainItem
self.selection = selection
self.srcContext = srcContext
return True
def getText(self, callingWindow, itmContext, mainItem, selection):
@@ -51,6 +52,7 @@ class ChangeItemToVariation(ContextMenuCombined):
def getSubMenu(self, callingWindow, context, mainItem, selection, rootMenu, i, pitem):
self.moduleLookup = {}
sFit = Fit.getInstance()
sMkt = Market.getInstance()
fit = sFit.getFit(self.mainFrame.getActiveFit())
def get_metalevel(x):
@@ -61,7 +63,8 @@ class ChangeItemToVariation(ContextMenuCombined):
def get_metagroup(x):
# We want deadspace before officer mods
remap = {5: 6, 6: 5}
return remap.get(x.metaGroup.ID, x.metaGroup.ID) if x.metaGroup is not None else 0
metaGroup = sMkt.getMetaGroupByItem(x)
return remap.get(metaGroup.ID, metaGroup.ID) if metaGroup is not None else 0
def get_boosterrank(x):
# If we're returning a lot of items, sort my name
@@ -84,7 +87,9 @@ class ChangeItemToVariation(ContextMenuCombined):
bindmenu = m
# Do not show abyssal items
items = list(i for i in self.mainVariations if i.metaGroup is None or i.metaGroup.ID != 15)
items = list(
i for i in self.mainVariations
if sMkt.getMetaGroupByItem(i) is None or sMkt.getMetaGroupByItem(i).ID != 15)
# Sort items by metalevel, and group within that metalevel
# Sort all items by name first
items.sort(key=lambda x: x.name)
@@ -102,12 +107,13 @@ class ChangeItemToVariation(ContextMenuCombined):
group = None
for item in items:
# Apparently no metaGroup for the Tech I variant:
metaGroup = sMkt.getMetaGroupByItem(item)
if 'subSystem' in item.effects:
thisgroup = item.marketGroup.marketGroupName
elif item.metaGroup is None:
elif metaGroup is None:
thisgroup = 'Tech I'
else:
thisgroup = item.metaGroup.name
thisgroup = metaGroup.name
if thisgroup != group and context not in ('implantItem', 'boosterItem'):
group = thisgroup
@@ -121,7 +127,7 @@ class ChangeItemToVariation(ContextMenuCombined):
self.moduleLookup[id] = item, context
m.Append(mitem)
mitem.Enable(fit.canFit(item))
mitem.Enable(self.srcContext in ('projectedModule', 'projectedDrone', 'projectedFighter') or fit.canFit(item))
return m
@@ -148,7 +154,7 @@ class ChangeItemToVariation(ContextMenuCombined):
def __handleModule(self, varItem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
positions = getSimilarModPositions(fit.modules, self.mainItem)
else:
sMkt = Market.getInstance()
@@ -187,7 +193,7 @@ class ChangeItemToVariation(ContextMenuCombined):
def __handleFighter(self, varItem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fighters = getSimilarFighters(fit.fighters, self.mainItem)
else:
fighters = self.selection
@@ -240,7 +246,7 @@ class ChangeItemToVariation(ContextMenuCombined):
def __handleProjectedModule(self, varItem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
positions = getSimilarModPositions(fit.projectedModules, self.mainItem)
else:
sMkt = Market.getInstance()
@@ -277,7 +283,7 @@ class ChangeItemToVariation(ContextMenuCombined):
def __handleProjectedFighter(self, varItem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if wx.GetMouseState().GetModifiers() == wx.MOD_ALT:
if wx.GetMouseState().GetModifiers() in (wx.MOD_ALT, wx.MOD_CONTROL):
fighters = getSimilarFighters(fit.projectedFighters, self.mainItem)
else:
fighters = self.selection

View File

@@ -45,10 +45,10 @@ class ChangeModuleSpool(ContextMenuSingle):
bindmenu = m
isNotDefault = self.mod.spoolType is not None and self.mod.spoolAmount is not None
cycleDefault = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], True))[0]
cycleCurrent = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], False))[0]
cycleMin = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True))[0]
cycleMax = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True))[0]
cycleDefault = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], True))[0]
cycleCurrent = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], False))[0]
cycleMin = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True))[0]
cycleMax = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True))[0]
cycleTotalMin = min(cycleDefault, cycleCurrent, cycleMin)
cycleTotalMax = max(cycleDefault, cycleCurrent, cycleMax)

View File

@@ -86,6 +86,10 @@ class PFGeneralPref(PreferenceView):
'When disabled, reloads charges just in selected modules. Action can be reversed by holding Ctrl or Alt key while changing charge.'))
mainSizer.Add(self.cbReloadAll, 0, wx.ALL | wx.EXPAND, 5)
self.rbAddLabels = wx.RadioBox(panel, -1, "Extra info in Additions panel tab names", wx.DefaultPosition, wx.DefaultSize, ["None", "Quantity of active items", "Quantity of all items"], 1, wx.RA_SPECIFY_COLS)
mainSizer.Add(self.rbAddLabels, 0, wx.EXPAND | wx.TOP | wx.RIGHT | wx.BOTTOM, 10)
self.rbAddLabels.Bind(wx.EVT_RADIOBOX, self.OnAddLabelsChange)
self.sFit = Fit.getInstance()
self.cbGlobalChar.SetValue(self.sFit.serviceFittingOptions["useGlobalCharacter"])
@@ -101,6 +105,7 @@ class PFGeneralPref(PreferenceView):
self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"])
self.cbShowShipBrowserTooltip.SetValue(self.sFit.serviceFittingOptions["showShipBrowserTooltip"])
self.cbReloadAll.SetValue(self.sFit.serviceFittingOptions["ammoChangeAll"])
self.rbAddLabels.SetSelection(self.sFit.serviceFittingOptions["additionsLabels"])
self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange)
self.cbDefaultCharImplants.Bind(wx.EVT_CHECKBOX, self.OnCBDefaultCharImplantsStateChange)
@@ -187,6 +192,13 @@ class PFGeneralPref(PreferenceView):
def onCBReloadAll(self, event):
self.sFit.serviceFittingOptions["ammoChangeAll"] = self.cbReloadAll.GetValue()
def OnAddLabelsChange(self, event):
self.sFit.serviceFittingOptions["additionsLabels"] = event.GetInt()
fitID = self.mainFrame.getActiveFit()
self.sFit.refreshFit(fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
event.Skip()
def getImage(self):
return BitmapLoader.getBitmap("prefs_settings", "gui")

View File

@@ -345,9 +345,7 @@ class FitItem(SFItem.SFBrowserItem):
self.deleteFit()
else:
with wx.MessageDialog(
self,
"Do you really want to delete this fit?",
"Confirm Delete",
self.GetTopLevelParent(), "Do you really want to delete this fit?", "Confirm Delete",
wx.YES | wx.NO | wx.ICON_QUESTION
) as dlg:
if dlg.ShowModal() == wx.ID_YES:

View File

@@ -163,9 +163,9 @@ class FirepowerViewFull(StatsView):
stats = (
(
"labelFullDpsWeapon",
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getWeaponDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
3, 0, 0, "{}{} DPS"),
(
"labelFullDpsDrone",
@@ -175,15 +175,15 @@ class FirepowerViewFull(StatsView):
3, 0, 0, "{}{} DPS"),
(
"labelFullVolleyTotal",
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
3, 0, 0, "{}{}"),
(
"labelFullDpsTotal",
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).total,
lambda: fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).total,
3, 0, 0, "{}{}"))
counter = 0

View File

@@ -29,27 +29,27 @@ import eos.config
stats = [
(
"labelRemoteCapacitor", "Capacitor:", "{}{} GJ/s", "capacitorInfo", "Capacitor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).capacitor,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).capacitor,
3, 0, 0),
(
"labelRemoteShield", "Shield:", "{}{} HP/s", "shieldActive", "Shield restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).shield,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).shield,
3, 0, 0),
(
"labelRemoteArmor", "Armor:", "{}{} HP/s", "armorActive", "Armor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).armor,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).armor,
3, 0, 0),
(
"labelRemoteHull", "Hull:", "{}{} HP/s", "hullActive", "Hull restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).hull,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).hull,
3, 0, 0)]

View File

@@ -28,27 +28,27 @@ import eos.config
stats = [
(
"labelRemoteCapacitor", "Capacitor:", "{}{} GJ/s", "capacitorInfo", "Capacitor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).capacitor,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).capacitor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).capacitor,
3, 0, 0),
(
"labelRemoteShield", "Shield:", "{}{} HP/s", "shieldActive", "Shield restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).shield,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).shield,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).shield,
3, 0, 0),
(
"labelRemoteArmor", "Armor:", "{}{} HP/s", "armorActive", "Armor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).armor,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).armor,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).armor,
3, 0, 0),
(
"labelRemoteHull", "Hull:", "{}{} HP/s", "hullActive", "Hull restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).hull,
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, spool, False)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)).hull,
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)).hull,
3, 0, 0)]

View File

@@ -36,7 +36,6 @@ class RechargeViewFull(StatsView):
self.parent = parent
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.mainFrame.Bind(GE.EFFECTIVE_HP_TOGGLED, self.toggleEffective)
self.effective = True
def getHeaderText(self, fit):
return "Recharge rates"
@@ -45,9 +44,15 @@ class RechargeViewFull(StatsView):
width, height = self.parent.GetTextExtent(text)
return width
@property
def effective(self):
try:
return self.parent.nameViewMap['resistancesViewFull'].showEffective
except KeyError:
return False
def toggleEffective(self, event):
event.Skip()
self.effective = event.effective
sFit = Fit.getInstance()
self.refreshPanel(sFit.getFit(self.mainFrame.getActiveFit()))
@@ -104,8 +109,7 @@ class RechargeViewFull(StatsView):
def refreshPanel(self, fit):
# If we did anything interesting, we'd update our labels to reflect the new fit's stats here
unit = " EHP/s" if self.parent.nameViewMap['resistancesViewFull'].showEffective else " HP/s"
unit = " EHP/s" if self.effective else " HP/s"
for stability in ("reinforced", "sustained"):
if stability == "reinforced" and fit is not None:

View File

@@ -100,6 +100,10 @@ class TargetingMiscViewMinimal(StatsView):
def refreshPanel(self, fit):
# If we did anything interesting, we'd update our labels to reflect the new fit's stats here
sensorValues = {
"main": lambda: fit.scanStrength,
"jamChance": lambda: fit.jamChance}
cargoNamesOrder = OrderedDict((
("fleetHangarCapacity", "Fleet hangar"),
("shipMaintenanceBayCapacity", "Maintenance bay"),
@@ -117,8 +121,7 @@ class TargetingMiscViewMinimal(StatsView):
("specialSalvageHoldCapacity", "Salvage hold"),
("specialCommandCenterHoldCapacity", "Command center hold"),
("specialPlanetaryCommoditiesHoldCapacity", "Planetary goods hold"),
("specialQuafeHoldCapacity", "Quafe hold")
))
("specialQuafeHoldCapacity", "Quafe hold")))
cargoValues = {
"main": lambda: fit.ship.getModifiedItemAttr("capacity"),
@@ -138,13 +141,12 @@ class TargetingMiscViewMinimal(StatsView):
"specialSalvageHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialSalvageHoldCapacity"),
"specialCommandCenterHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialCommandCenterHoldCapacity"),
"specialPlanetaryCommoditiesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialPlanetaryCommoditiesHoldCapacity"),
"specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity")
}
"specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity")}
stats = (("labelTargets", {"main": lambda: fit.maxTargets}, 3, 0, 0, ""),
("labelRange", {"main": lambda: fit.maxTargetRange / 1000}, 3, 0, 0, "km"),
("labelScanRes", {"main": lambda: fit.ship.getModifiedItemAttr("scanResolution")}, 3, 0, 0, "mm"),
("labelSensorStr", {"main": lambda: fit.scanStrength}, 3, 0, 0, ""),
("labelSensorStr", sensorValues, 3, 0, 0, ""),
("labelCtrlRange", {"main": lambda: fit.extraAttributes["droneControlRange"] / 1000}, 3, 0, 0, "km"),
("labelFullSpeed", {"main": lambda: fit.maxSpeed}, 3, 0, 0, "m/s"),
("labelFullAlignTime", {"main": lambda: fit.alignTime}, 3, 0, 0, "s"),
@@ -176,6 +178,15 @@ class TargetingMiscViewMinimal(StatsView):
unit))
else:
label.SetLabel("%s %s" % (formatAmount(mainValue, prec, lowest, highest), unit))
elif labelName == "labelSensorStr":
ecmChance = otherValues["jamChance"]
ecmChance = round(ecmChance, 1)
if ecmChance:
label.SetLabel("{} ({}%)".format(
formatAmount(mainValue, prec, lowest, highest),
formatAmount(ecmChance, 3, 0, 0)))
else:
label.SetLabel("{}".format(formatAmount(mainValue, prec, lowest, highest)))
else:
label.SetLabel("%s %s" % (formatAmount(mainValue, prec, lowest, highest), unit))
# Tooltip stuff
@@ -195,10 +206,14 @@ class TargetingMiscViewMinimal(StatsView):
warpScrambleStatus = "Warp Core Strength: %.1f" % 0
label.SetToolTip(wx.ToolTip("%s\n%s" % (maxWarpDistance, warpScrambleStatus)))
elif labelName == "labelSensorStr":
if fit.jamChance > 0:
label.SetToolTip(wx.ToolTip("Type: %s\n%.1f%% Chance of Jam" % (fit.scanType, fit.jamChance)))
ecmChance = otherValues["jamChance"]
ecmChance = round(ecmChance, 1)
if ecmChance > 0:
label.SetToolTip(wx.ToolTip("Type: {}\n{}% chance to be jammed".format(
fit.scanType,
formatAmount(ecmChance, 3, 0, 0))))
else:
label.SetToolTip(wx.ToolTip("Type: %s" % fit.scanType))
label.SetToolTip(wx.ToolTip("Type: {}".format(fit.scanType)))
elif labelName == "labelFullAlignTime":
alignTime = "Align:\t%.3fs" % mainValue
mass = 'Mass:\t{:,.0f}kg'.format(fit.ship.getModifiedItemAttr("mass"))
@@ -225,14 +240,6 @@ class TargetingMiscViewMinimal(StatsView):
label.SetToolTip(wx.ToolTip("%s\n%s" % (maxWarpDistance, warpScrambleStatus)))
else:
label.SetToolTip(wx.ToolTip(""))
elif labelName == "labelSensorStr":
if fit:
if fit.jamChance > 0:
label.SetToolTip(wx.ToolTip("Type: %s\n%.1f%% Chance of Jam" % (fit.scanType, fit.jamChance)))
else:
label.SetToolTip(wx.ToolTip("Type: %s" % fit.scanType))
else:
label.SetToolTip(wx.ToolTip(""))
elif labelName == "labelFullCargo":
if fit:
cachedCargo = self._cachedValues[counter]

View File

@@ -84,11 +84,12 @@ class AttributeDisplay(ViewColumn):
return ""
if self.info.name == "volume":
str_ = (formatAmount(attr, 3, 0, 3))
if hasattr(mod, "amount"):
str_ += "m\u00B3 (%s m\u00B3)" % (formatAmount(attr * mod.amount, 3, 0, 3))
attr = str_
if getattr(mod, "amount", 1) != 1:
attr = "{} m\u00B3 ({} m\u00B3)".format(
formatAmount(attr, 3, 0, 6),
formatAmount(attr * mod.amount, 3, 0, 6))
else:
attr = "{} m\u00B3".format(formatAmount(attr, 3, 0, 6))
if isinstance(attr, (float, int)):
attr = (formatAmount(attr, 3, 0, 3))

View File

@@ -84,7 +84,7 @@ class DpsColumn(GraphColumn):
def _getValue(self, fit):
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
return fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total, None
return fit.getTotalDps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total, None
def _getFitTooltip(self):
return 'Declared DPS'
@@ -102,7 +102,7 @@ class VolleyColumn(GraphColumn):
def _getValue(self, fit):
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
return fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total, None
return fit.getTotalVolley(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).total, None
def _getFitTooltip(self):
return 'Declared volley'
@@ -329,7 +329,7 @@ class ShieldRRColumn(GraphColumn):
def _getValue(self, fit):
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).shield, 'HP/s'
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).shield, 'HP/s'
def _getFitTooltip(self):
return 'Declared shield repair speed'
@@ -348,7 +348,7 @@ class ArmorRRColumn(GraphColumn):
def _getValue(self, fit):
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).armor, 'HP/s'
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).armor, 'HP/s'
def _getFitTooltip(self):
return 'Declared armor repair speed'
@@ -367,7 +367,7 @@ class HullRRColumn(GraphColumn):
def _getValue(self, fit):
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).hull, 'HP/s'
return fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)).hull, 'HP/s'
def _getFitTooltip(self):
return 'Declared hull repair speed'

View File

@@ -33,6 +33,7 @@ from eos.saveddata.module import Module, Rack
from eos.saveddata.targetProfile import TargetProfile
from graphs.wrapper import BaseWrapper
from gui.builtinContextMenus.envEffectAdd import AddEnvironmentEffect
from gui.utils.numberFormatter import formatAmount
from gui.viewColumn import ViewColumn
from service.fit import Fit as FitSvc
from service.market import Market
@@ -64,7 +65,11 @@ class BaseName(ViewColumn):
return "%d/%d %s" % \
(stuff.amount, stuff.getModifiedItemAttr("fighterSquadronMaxSize"), stuff.item.name)
elif isinstance(stuff, Cargo):
return "%dx %s" % (stuff.amount, stuff.item.name)
if stuff.item.group.name in ("Cargo Container", "Secure Cargo Container", "Audit Log Secure Container", "Freight Container"):
capacity = stuff.item.getAttribute('capacity')
if capacity:
return "{:d}x {} ({} m\u00B3)".format(stuff.amount, stuff.item.name, formatAmount(capacity, 3, 0, 6))
return "{:d}x {}".format(stuff.amount, stuff.item.name)
elif isinstance(stuff, Fit):
if self.projectedView:
# we need a little more information for the projected view

View File

@@ -113,7 +113,7 @@ class Miscellanea(ViewColumn):
info.append((text, tooltip))
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
spoolTime = stuff.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))[1]
spoolTime = stuff.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False))[1]
if spoolTime:
text = "{0}s".format(formatAmount(spoolTime, 3, 0, 3))
tooltip = "spool up time"
@@ -396,9 +396,9 @@ class Miscellanea(ViewColumn):
return text, tooltip
elif itemGroup == "Mutadaptive Remote Armor Repairer":
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
spoolOptDefault = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)
spoolOptPre = SpoolOptions(SpoolType.SCALE, 0, True)
spoolOptFull = SpoolOptions(SpoolType.SCALE, 1, True)
spoolOptDefault = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, False)
spoolOptPre = SpoolOptions(SpoolType.SPOOL_SCALE, 0, True)
spoolOptFull = SpoolOptions(SpoolType.SPOOL_SCALE, 1, True)
rps = stuff.getRemoteReps(spoolOptions=spoolOptDefault, ignoreState=True).armor
rpsPre = stuff.getRemoteReps(spoolOptions=spoolOptPre, ignoreState=True).armor
rpsFull = stuff.getRemoteReps(spoolOptions=spoolOptFull, ignoreState=True).armor
@@ -612,7 +612,10 @@ class Miscellanea(ViewColumn):
fit = Fit.getInstance().getFit(self.fittingView.getActiveFit())
ehpTotal = fit.ehp
hpTotal = fit.hp
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
try:
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
except KeyError:
useEhp = False
tooltip = "{0} restored over duration using charges (plus reload)".format(boosted_attribute)
if useEhp and boosted_attribute == "HP" and "Remote" not in itemGroup:

View File

@@ -0,0 +1,61 @@
# coding: utf-8
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
# noinspection PyPackageRequirements
import wx
from logbook import Logger
import gui.mainFrame
from eos.saveddata.fit import Fit
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
from gui.viewColumn import ViewColumn
pyfalog = Logger(__name__)
class ProjectionRangeColumn(ViewColumn):
name = 'Projection Range'
def __init__(self, fittingView, params):
super().__init__(fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.imageId = fittingView.imageList.GetImageIndex(1391, "icons")
self.bitmap = BitmapLoader.getBitmap(1391, "icons")
self.mask = wx.LIST_MASK_IMAGE
def getText(self, stuff):
if isinstance(stuff, Fit):
fitID = self.mainFrame.getActiveFit()
info = stuff.getProjectionInfo(fitID)
projRange = info.projectionRange
else:
projRange = getattr(stuff, 'projectionRange', None)
if projRange is None:
return ''
return formatAmount(projRange, 3, 0, 3, unitName='m')
def getToolTip(self, mod):
return 'Projection Range'
ProjectionRangeColumn.register()

View File

@@ -1,3 +1,5 @@
import re
# noinspection PyPackageRequirements
import wx
# noinspection PyPackageRequirements
@@ -10,6 +12,12 @@ from gui.marketBrowser import SearchBox
from service.market import Market
def stripHtml(text):
text = re.sub('<\s*br\s*/?\s*>', '\n', text)
text = re.sub('</?[^/]+?(/\s*)?>', '', text)
return text
class BaseImplantEditorView(wx.Panel):
def addMarketViewImage(self, iconFile):
@@ -68,8 +76,10 @@ class BaseImplantEditorView(wx.Panel):
self.SetSizer(pmainSizer)
# Populate the market tree
self.hoveredLeftTreeTypeID = None
self.hoveredRightListRow = None
# Populate the market tree
sMkt = Market.getInstance()
for mktGrp in sMkt.getImplantTree():
iconId = self.addMarketViewImage(sMkt.getIconByMarketGroup(mktGrp))
@@ -82,9 +92,13 @@ class BaseImplantEditorView(wx.Panel):
# Bind the event to replace dummies by real data
self.availableImplantsTree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup)
self.availableImplantsTree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.itemSelected)
self.availableImplantsTree.Bind(wx.EVT_MOTION, self.OnLeftTreeMouseMove)
self.availableImplantsTree.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeftTreeMouseLeave)
self.itemView.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemSelected)
self.pluggedImplantsTree.Bind(wx.EVT_MOTION, self.OnRightListMouseMove)
# Bind add & remove buttons
self.btnAdd.Bind(wx.EVT_BUTTON, self.itemSelected)
self.btnRemove.Bind(wx.EVT_BUTTON, self.removeItem)
@@ -193,6 +207,55 @@ class BaseImplantEditorView(wx.Panel):
self.removeImplantFromContext(self.implants[pos])
self.update()
# Due to https://github.com/wxWidgets/Phoenix/issues/1372 we cannot set tooltips on
# tree itself; work this around with following two methods, by setting tooltip to
# parent window
def OnLeftTreeMouseMove(self, event):
event.Skip()
treeItemId, _ = self.availableImplantsTree.HitTest(event.Position)
if not treeItemId:
if self.hoveredLeftTreeTypeID is not None:
self.hoveredLeftTreeTypeID = None
self.SetToolTip(None)
return
item = self.availableImplantsTree.GetItemData(treeItemId)
isImplant = getattr(item, 'isImplant', False)
if not isImplant:
if self.hoveredLeftTreeTypeID is not None:
self.hoveredLeftTreeTypeID = None
self.SetToolTip(None)
return
if self.hoveredLeftTreeTypeID == item.ID:
return
if self.ToolTip is not None:
self.SetToolTip(None)
else:
self.hoveredLeftTreeTypeID = item.ID
toolTip = wx.ToolTip(stripHtml(item.description))
toolTip.SetMaxWidth(self.GetSize().Width)
self.SetToolTip(toolTip)
def OnLeftTreeMouseLeave(self, event):
event.Skip()
self.SetToolTip(None)
def OnRightListMouseMove(self, event):
event.Skip()
row, _, col = self.pluggedImplantsTree.HitTestSubItem(event.Position)
if row != self.hoveredRightListRow:
if self.pluggedImplantsTree.ToolTip is not None:
self.pluggedImplantsTree.SetToolTip(None)
else:
self.hoveredRightListRow = row
try:
implant = self.implants[row]
except IndexError:
self.pluggedImplantsTree.SetToolTip(None)
else:
toolTip = wx.ToolTip(stripHtml(implant.item.description))
toolTip.SetMaxWidth(self.pluggedImplantsTree.GetSize().Width)
self.pluggedImplantsTree.SetToolTip(toolTip)
class AvailableImplantsView(d.Display):
DEFAULT_COLS = ["attr:implantness",
@@ -212,6 +275,7 @@ class ItemView(d.Display):
self.parent = parent
self.searchBox = parent.searchBox
self.hoveredRow = None
self.items = []
# Bind search actions
@@ -220,6 +284,8 @@ class ItemView(d.Display):
self.searchBox.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.scheduleSearch)
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
def clearSearch(self, event=None):
if self.IsShown():
self.parent.availableImplantsTree.Show()
@@ -255,3 +321,20 @@ class ItemView(d.Display):
self.items = sorted(list(items), key=lambda i: i.name)
self.update(self.items)
def OnMouseMove(self, event):
event.Skip()
row, _, col = self.HitTestSubItem(event.Position)
if row != self.hoveredRow:
if self.ToolTip is not None:
self.SetToolTip(None)
else:
self.hoveredRow = row
try:
item = self.items[row]
except IndexError:
self.SetToolTip(None)
else:
toolTip = wx.ToolTip(stripHtml(item.description))
toolTip.SetMaxWidth(self.GetSize().Width)
self.SetToolTip(toolTip)

View File

@@ -154,7 +154,9 @@ class CharacterEntityEditor(EntityEditor):
class CharacterEditor(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(parent, id=wx.ID_ANY, title="Character Editor", pos=wx.DefaultPosition, size=wx.Size(640, 600))
super().__init__(
parent, id=wx.ID_ANY, title="Character Editor", resizeable=True, pos=wx.DefaultPosition,
size=wx.Size(950, 650) if "wxGTK" in wx.PlatformInfo else wx.Size(850, 600))
i = wx.Icon(BitmapLoader.getBitmap("character_small", "gui"))
self.SetIcon(i)
@@ -207,6 +209,7 @@ class CharacterEditor(AuxiliaryFrame):
self.SetSizer(mainSizer)
self.Layout()
self.SetMinSize(self.GetSize())
self.Centre(wx.BOTH)
self.Bind(wx.EVT_CLOSE, self.OnClose)

View File

@@ -13,14 +13,17 @@
#
# ===============================================================================
import math
from functools import lru_cache
import wx
import wx.lib.newevent
from gui.bitmap_loader import BitmapLoader
from gui.utils import draw
from gui.utils import color as color_utils
from gui.utils import color as color_utils, draw, fonts
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()
@@ -89,11 +92,15 @@ class PageAdding(_PageAdding, VetoAble):
class ChromeNotebook(wx.Panel):
def __init__(self, parent, can_add=True):
def __init__(self, parent, can_add=True, tabWidthMode=0):
"""
Instance of Notebook. Initializes general layout, includes methods
for setting current page, replacing pages, any public function for the
notebook
width modes:
- 0: legacy (all tabs have equal width)
- 1: all tabs take just enough space to fit text
"""
super().__init__(parent, wx.ID_ANY, size=(-1, -1))
@@ -103,7 +110,7 @@ class ChromeNotebook(wx.Panel):
main_sizer = wx.BoxSizer(wx.VERTICAL)
tabs_sizer = wx.BoxSizer(wx.VERTICAL)
self.tabs_container = _TabsContainer(self, can_add=can_add)
self.tabs_container = _TabsContainer(self, can_add=can_add, tabWidthMode=tabWidthMode)
tabs_sizer.Add(self.tabs_container, 0, wx.EXPAND)
if 'wxMSW' in wx.PlatformInfo:
@@ -296,7 +303,14 @@ class ChromeNotebook(wx.Panel):
def SetPageTitle(self, i, text, refresh=True):
tab = self.tabs_container.tabs[i]
tab.text = text
tab.baseText = text
if refresh:
self.tabs_container.AdjustTabsSize()
self.Refresh()
def SetPageTitleExtra(self, i, text, refresh=True):
tab = self.tabs_container.tabs[i]
tab.extraText = text
if refresh:
self.tabs_container.AdjustTabsSize()
self.Refresh()
@@ -354,7 +368,8 @@ class _TabRenderer:
height = max(height, self.min_height)
self.disabled = False
self.text = text
self.baseText = text
self.extraText = ''
self.tab_size = (width, height)
self.closeable = closeable
self.selected = False
@@ -368,6 +383,10 @@ class _TabRenderer:
self.position = (0, 0) # Not used internally for rendering - helper for tab container
self.InitTab()
@property
def text(self):
return self.baseText + self.extraText
def SetPosition(self, position):
self.position = position
@@ -685,15 +704,15 @@ class _AddRenderer:
class _TabsContainer(wx.Panel):
def __init__(self, parent, pos=(50, 0), size=(100, 22), id=wx.ID_ANY,
can_add=True):
can_add=True, tabWidthMode=0):
"""
Defines the tab container. Handles functions such as tab selection and
dragging, and defines minimum width of tabs (all tabs are of equal
width, which is determined via widest tab). Also handles the tab
preview, if any.
"""
super().__init__(parent, id, pos, size)
self.tabWidthMode = tabWidthMode
self.tabs = []
self.width, self.height = size
@@ -720,8 +739,7 @@ class _TabsContainer(wx.Panel):
self.show_add_button = can_add
self.tab_container_width = self.width - self.reserved
self.tab_min_width = self.width
self.tab_shadow = _TabRenderer((self.tab_min_width, self.height + 1))
self.fxBmps = {}
self.add_button = _AddRenderer()
self.add_bitmap = self.add_button.Render()
@@ -1173,7 +1191,7 @@ class _TabsContainer(wx.Panel):
if not tab.IsSelected():
# drop shadow first
mdc.DrawBitmap(self.fx_bmp, posx, posy, True)
mdc.DrawBitmap(self.fxBmps[tab], posx, posy, True)
bmp = tab.Render()
img = bmp.ConvertToImage()
img = img.AdjustChannels(1, 1, 1, 0.85)
@@ -1191,7 +1209,7 @@ class _TabsContainer(wx.Panel):
if selected:
posx, posy = selected.GetPosition()
# drop shadow first
mdc.DrawBitmap(self.fx_bmp, posx, posy, True)
mdc.DrawBitmap(self.fxBmps[selected], posx, posy, True)
bmp = selected.Render()
@@ -1211,16 +1229,21 @@ class _TabsContainer(wx.Panel):
def UpdateTabFX(self):
""" Updates tab drop shadow bitmap """
self.tab_shadow.SetSize((self.tab_min_width, self.height + 1))
fx_bmp = self.tab_shadow.Render()
self.fxBmps.clear()
for tab in self.tabs:
tabW, tabH = tab.tab_size
self.fxBmps[tab] = self.GetTabFx(tabW, self.height + 1)
@lru_cache(maxsize=50)
def GetTabFx(self, width, height):
renderer = _TabRenderer((width, height))
fx_bmp = renderer.Render()
img = fx_bmp.ConvertToImage()
if not img.HasAlpha():
img.InitAlpha()
img = img.Blur(2)
img = img.AdjustChannels(0.3, 0.3, 0.3, 0.35)
self.fx_bmp = wx.Bitmap(img)
return wx.Bitmap(img)
def AddTab(self, title=wx.EmptyString, img=None, closeable=False):
self.ClearTabsSelected()
@@ -1262,29 +1285,74 @@ class _TabsContainer(wx.Panel):
Adjust tab sizes to ensure that they are all consistent and can fit into
the tab container.
"""
if self.tabWidthMode == 1:
if self.GetTabsCount() > 0:
availableW = self.tab_container_width
overlapSavedW = max(0, len(self.tabs)) * self.inclination * 2
tabsGrouped = {}
for tab in self.tabs:
tabW, _ = tab.GetMinSize()
tabsGrouped.setdefault(math.ceil(tabW), []).append(tab)
clippedTabs = []
clipW = max(tabsGrouped, default=0)
# first we loop through our tabs and calculate the the largest tab. This
# is the size that we will base our calculations off
def getUnclippedW():
unclippedW = 0
for w, tabs in tabsGrouped.items():
unclippedW += w * len(tabs)
return unclippedW
while tabsGrouped:
# Check if we're within width limit
neededW = 0
for w, tabs in tabsGrouped.items():
neededW += w * len(tabs)
if clippedTabs:
neededW += clipW * len(clippedTabs)
if neededW <= availableW + overlapSavedW:
break
# If we're not, extract widest tab group and mark it for clipping
currentTabs = tabsGrouped.pop(max(tabsGrouped))
clippedTabs.extend(currentTabs)
proposedClipWidth = math.floor((availableW + overlapSavedW - getUnclippedW()) / len(clippedTabs))
if not tabsGrouped or proposedClipWidth >= max(tabsGrouped, default=0):
clipW = max(0, proposedClipWidth)
break
else:
clipW = max(tabsGrouped)
# Assign width for unclipped tabs
for w, tabs in tabsGrouped.items():
for tab in tabs:
tab.SetSize((w, self.height))
if clippedTabs:
# Some width remains to be used due to rounding to integer
extraWTotal = availableW + overlapSavedW - getUnclippedW() - clipW * len(clippedTabs)
extraWPerTab = math.ceil(extraWTotal / len(clippedTabs))
# Assign width for clipped tabs
for tab in clippedTabs:
extraW = min(extraWTotal, extraWPerTab)
extraWTotal -= extraW
tab.SetSize((clipW + extraW, self.height))
else:
# first we loop through our tabs and calculate the the largest tab. This
# is the size that we will base our calculations off
max_width = 100 # Tab should be at least 100
for tab in self.tabs:
mw, _ = tab.GetMinSize() # Tab min size includes tab contents
max_width = max(mw, max_width)
max_width = 100 # Tab should be at least 100
for tab in self.tabs:
mw, _ = tab.GetMinSize() # Tab min size includes tab contents
max_width = max(mw, max_width)
tabWidth = 0
# Divide tab container by number of tabs and add inclination. This will
# return the ideal max size for the containers size
if self.GetTabsCount() > 0:
dx = self.tab_container_width / self.GetTabsCount() + self.inclination * 2
tabWidth = min(dx, max_width)
# Divide tab container by number of tabs and add inclination. This will
# return the ideal max size for the containers size
if self.GetTabsCount() > 0:
dx = self.tab_container_width / self.GetTabsCount() + self.inclination * 2
self.tab_min_width = min(dx, max_width)
# Apply new size to all tabs
for tab in self.tabs:
tab.SetSize((self.tab_min_width, self.height))
if self.GetTabsCount() > 0:
# update drop shadow based on new sizes
self.UpdateTabFX()
# Apply new size to all tabs
for tab in self.tabs:
tab.SetSize((tabWidth, self.height))
# update drop shadow based on new sizes
self.UpdateTabFX()
self.UpdateTabsPosition()
def UpdateTabsPosition(self, skip_tab=None):

View File

@@ -39,7 +39,7 @@ class DevTools(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Development Tools", style=wx.RESIZE_BORDER,
parent, id=wx.ID_ANY, title="Development Tools", resizeable=True,
size=wx.Size(400, 320) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240))
self.mainFrame = parent
self.block = False

View File

@@ -301,10 +301,13 @@ class Display(wx.ListCtrl):
def ensureSelection(self, clickedPos):
"""
On mac, when right-click on any item happens, it doesn't get selected.
This method ensures that selection actually happens.
On windows with Ctrl is pressed, or on Mac, when right-click on any item happens,
the item doesn't get selected. This method ensures that only clicked item is selected.
"""
if 'wxMac' in wx.PlatformInfo:
if (
'wxMac' in wx.PlatformInfo or
('wxMSW' in wx.PlatformInfo and wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL)
):
if clickedPos != -1:
selectedPoss = self.getSelectedRows()
if clickedPos not in selectedPoss:

View File

@@ -25,7 +25,7 @@ class EveFittings(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Browse EVE Fittings", pos=wx.DefaultPosition,
size=wx.Size(750, 450), style=wx.RESIZE_BORDER)
size=wx.Size(750, 450), resizeable=True)
self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.VERTICAL)
@@ -204,7 +204,7 @@ class ExportToEve(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Export fit to EVE", pos=wx.DefaultPosition,
size=wx.Size(400, 120) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 100), style=wx.RESIZE_BORDER)
size=wx.Size(400, 120) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 100), resizeable=True)
self.mainFrame = parent
@@ -273,7 +273,15 @@ class ExportToEve(AuxiliaryFrame):
sEsi = Esi.getInstance()
sFit = Fit.getInstance()
data = sPort.exportESI(sFit.getFit(fitID))
try:
data = sPort.exportESI(sFit.getFit(fitID))
except ESIExportException as e:
msg = str(e)
if not msg:
msg = "Failed to generate export data"
pyfalog.warning(msg)
self.statusbar.SetStatusText(msg, 1)
return
activeChar = self.getActiveCharacter()
if activeChar is None:
msg = "Need at least one ESI character to export"
@@ -309,7 +317,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="SSO Character Management", pos=wx.DefaultPosition,
size=wx.Size(550, 250), style=wx.RESIZE_BORDER)
size=wx.Size(550, 250), resizeable=True)
self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.HORIZONTAL)

View File

@@ -56,6 +56,7 @@ from .gui.localModule.replace import GuiReplaceLocalModuleCommand
from .gui.localModule.swap import GuiSwapLocalModulesCommand
from .gui.localModuleCargo.cargoToLocalModule import GuiCargoToLocalModuleCommand
from .gui.localModuleCargo.localModuleToCargo import GuiLocalModuleToCargoCommand
from .gui.projectedChangeProjectionRange import GuiChangeProjectedItemsProjectionRangeCommand
from .gui.projectedChangeStates import GuiChangeProjectedItemStatesCommand
from .gui.projectedDrone.add import GuiAddProjectedDroneCommand
from .gui.projectedDrone.changeAmount import GuiChangeProjectedDroneAmountCommand

View File

@@ -0,0 +1,40 @@
import wx
from logbook import Logger
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeProjectedDroneProjectionRangeCommand(wx.Command):
def __init__(self, fitID, itemID, projectionRange):
wx.Command.__init__(self, True, 'Change Projected Drone Projection Range')
self.fitID = fitID
self.itemID = itemID
self.projectionRange = projectionRange
self.savedProjectionRange = None
def Do(self):
pyfalog.debug('Doing change of projected drone {} projection range to {} on fit {}'.format(
self.itemID, self.projectionRange, self.fitID))
fit = Fit.getInstance().getFit(self.fitID)
drone = next((pd for pd in fit.projectedDrones if pd.itemID == self.itemID), None)
if drone is None:
pyfalog.warning('Cannot find projected drone')
return False
if drone.projectionRange == self.projectionRange:
return False
self.savedProjectionRange = drone.projectionRange
drone.projectionRange = self.projectionRange
return True
def Undo(self):
pyfalog.debug('Undoing change of projected drone {} projection range to {} on fit {}'.format(
self.itemID, self.projectionRange, self.fitID))
cmd = CalcChangeProjectedDroneProjectionRangeCommand(
fitID=self.fitID,
itemID=self.itemID,
projectionRange=self.savedProjectionRange)
return cmd.Do()

View File

@@ -0,0 +1,37 @@
import wx
from logbook import Logger
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeProjectedFighterProjectionRangeCommand(wx.Command):
def __init__(self, fitID, position, projectionRange):
wx.Command.__init__(self, True, 'Change Projected Fighter Projection Range')
self.fitID = fitID
self.position = position
self.projectionRange = projectionRange
self.savedProjectionRange = None
def Do(self):
pyfalog.debug('Doing changing of projected fighter projection range to {} at position {} for fit {}'.format(
self.projectionRange, self.position, self.fitID))
fit = Fit.getInstance().getFit(self.fitID)
fighter = fit.projectedFighters[self.position]
if fighter.projectionRange == self.projectionRange:
return False
self.savedProjectionRange = fighter.projectionRange
fighter.projectionRange = self.projectionRange
return True
def Undo(self):
pyfalog.debug('Undoing changing of projected fighter projection range to {} at position {} for fit {}'.format(
self.projectionRange, self.position, self.fitID))
cmd = CalcChangeProjectedFighterProjectionRangeCommand(
fitID=self.fitID,
position=self.position,
projectionRange=self.savedProjectionRange)
return cmd.Do()

View File

@@ -0,0 +1,55 @@
import wx
from logbook import Logger
from gui.fitCommands.helpers import restoreCheckedStates
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeProjectedModuleProjectionRangeCommand(wx.Command):
def __init__(self, fitID, position, projectionRange):
wx.Command.__init__(self, True)
self.fitID = fitID
self.position = position
self.projectionRange = projectionRange
self.savedProjectionRange = None
self.savedStateCheckChanges = None
def Do(self):
pyfalog.debug('Doing change of projected module projection range at position {} to range {} on fit {}'.format(
self.position, self.projectionRange, self.fitID))
sFit = Fit.getInstance()
fit = sFit.getFit(self.fitID)
mod = fit.projectedModules[self.position]
if mod.projectionRange == self.projectionRange:
return False
self.savedProjectionRange = mod.projectionRange
mod.projectionRange = self.projectionRange
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, mod)
return True
def Undo(self):
pyfalog.debug('Undoing change of projected module projection range at position {} to range {} on fit {}'.format(
self.position, self.projectionRange, self.fitID))
cmd = CalcChangeProjectedModuleProjectionRangeCommand(
fitID=self.fitID,
position=self.position,
projectionRange=self.savedProjectionRange)
result = cmd.Do()
restoreCheckedStates(Fit.getInstance().getFit(self.fitID), self.savedStateCheckChanges)
return result
@property
def needsGuiRecalc(self):
if self.savedStateCheckChanges is None:
return True
for container in self.savedStateCheckChanges:
if len(container) > 0:
return True
return False

View File

@@ -0,0 +1,60 @@
import wx
from logbook import Logger
from gui.fitCommands.helpers import restoreCheckedStates
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeProjectedFitProjectionRangeCommand(wx.Command):
def __init__(self, fitID, projectedFitID, projectionRange):
wx.Command.__init__(self, True, 'Change Projected Fit Projection Range')
self.fitID = fitID
self.projectedFitID = projectedFitID
self.projectionRange = projectionRange
self.savedProjectionRange = None
self.savedStateCheckChanges = None
def Do(self):
pyfalog.debug('Doing change of projected fit {} range to {} for fit {}'.format(self.projectedFitID, self.projectionRange, self.fitID))
sFit = Fit.getInstance()
fit = sFit.getFit(self.fitID)
projectedFit = sFit.getFit(self.projectedFitID, projected=True)
# Projected fit could have been deleted if we are redoing
if projectedFit is None:
pyfalog.debug('Projected fit is not available')
return False
projectionInfo = projectedFit.getProjectionInfo(self.fitID)
if projectionInfo is None:
pyfalog.warning('Fit projection info is not available')
return False
if projectionInfo.projectionRange == self.projectionRange:
return False
self.savedProjectionRange = projectionInfo.projectionRange
projectionInfo.projectionRange = self.projectionRange
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, None)
return True
def Undo(self):
pyfalog.debug('Undoing change of projected fit {} range to {} for fit {}'.format(self.projectedFitID, self.projectionRange, self.fitID))
cmd = CalcChangeProjectedFitProjectionRangeCommand(
fitID=self.fitID,
projectedFitID=self.projectedFitID,
projectionRange=self.savedProjectionRange)
result = cmd.Do()
restoreCheckedStates(Fit.getInstance().getFit(self.fitID), self.savedStateCheckChanges)
return result
@property
def needsGuiRecalc(self):
if self.savedStateCheckChanges is None:
return True
for container in self.savedStateCheckChanges:
if len(container) > 0:
return True
return False

View File

@@ -0,0 +1,91 @@
import wx
import eos.db
import gui.mainFrame
from eos.saveddata.drone import Drone as EosDrone
from eos.saveddata.fighter import Fighter as EosFighter
from eos.saveddata.fit import Fit as EosFit
from eos.saveddata.module import Module as EosModule
from gui import globalEvents as GE
from gui.fitCommands.calc.drone.projectedChangeProjectionRange import CalcChangeProjectedDroneProjectionRangeCommand
from gui.fitCommands.calc.fighter.projectedChangeProjectionRange import CalcChangeProjectedFighterProjectionRangeCommand
from gui.fitCommands.calc.module.projectedChangeProjectionRange import CalcChangeProjectedModuleProjectionRangeCommand
from gui.fitCommands.calc.projectedFit.changeProjectionRange import CalcChangeProjectedFitProjectionRangeCommand
from gui.fitCommands.helpers import InternalCommandHistory
from service.fit import Fit
class GuiChangeProjectedItemsProjectionRangeCommand(wx.Command):
def __init__(self, fitID, items, projectionRange):
wx.Command.__init__(self, True, 'Change Projected Items Projection Range')
self.internalHistory = InternalCommandHistory()
self.fitID = fitID
self.projectionRange = projectionRange
self.pModPositions = []
self.pDroneItemIDs = []
self.pFighterPositions = []
self.pFitIDs = []
fit = Fit.getInstance().getFit(fitID)
for item in items:
if isinstance(item, EosModule):
if item in fit.projectedModules and not getattr(item, 'isExclusiveSystemEffect', False):
self.pModPositions.append(fit.projectedModules.index(item))
elif isinstance(item, EosDrone):
self.pDroneItemIDs.append(item.itemID)
elif isinstance(item, EosFighter):
if item in fit.projectedFighters:
self.pFighterPositions.append(fit.projectedFighters.index(item))
elif isinstance(item, EosFit):
self.pFitIDs.append(item.ID)
def Do(self):
results = []
needRecalc = True
for pModPosition in self.pModPositions:
cmd = CalcChangeProjectedModuleProjectionRangeCommand(
fitID=self.fitID,
position=pModPosition,
projectionRange=self.projectionRange)
results.append(self.internalHistory.submit(cmd))
needRecalc = cmd.needsGuiRecalc
for pDroneItemID in self.pDroneItemIDs:
cmd = CalcChangeProjectedDroneProjectionRangeCommand(
fitID=self.fitID,
itemID=pDroneItemID,
projectionRange=self.projectionRange)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFighterPosition in self.pFighterPositions:
cmd = CalcChangeProjectedFighterProjectionRangeCommand(
fitID=self.fitID,
position=pFighterPosition,
projectionRange=self.projectionRange)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFitID in self.pFitIDs:
cmd = CalcChangeProjectedFitProjectionRangeCommand(
fitID=self.fitID,
projectedFitID=pFitID,
projectionRange=self.projectionRange)
results.append(self.internalHistory.submit(cmd))
needRecalc = cmd.needsGuiRecalc
success = any(results)
sFit = Fit.getInstance()
if needRecalc:
eos.db.flush()
sFit.recalc(self.fitID)
sFit.fill(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success
def Undo(self):
success = self.internalHistory.undoAll()
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
sFit.fill(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success

View File

@@ -72,18 +72,21 @@ class GuiChangeProjectedItemStatesCommand(wx.Command):
itemID=pDroneItemID,
state=False if self.proposedState == 'inactive' else True)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFighterPosition in self.pFighterPositions:
cmd = CalcChangeProjectedFighterStateCommand(
fitID=self.fitID,
position=pFighterPosition,
state=False if self.proposedState == 'inactive' else True)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFitID in self.pFitIDs:
cmd = CalcChangeProjectedFitStateCommand(
fitID=self.fitID,
projectedFitID=pFitID,
state=False if self.proposedState == 'inactive' else True)
results.append(self.internalHistory.submit(cmd))
needRecalc = cmd.needsGuiRecalc
success = any(results)
sFit = Fit.getInstance()
if needRecalc:

View File

@@ -7,7 +7,7 @@ import gui.mainFrame
from gui import globalEvents as GE
from gui.fitCommands.calc.drone.projectedChangeAmount import CalcChangeProjectedDroneAmountCommand
from gui.fitCommands.calc.drone.projectedRemove import CalcRemoveProjectedDroneCommand
from gui.fitCommands.helpers import DroneInfo, InternalCommandHistory
from gui.fitCommands.helpers import InternalCommandHistory
from service.fit import Fit

View File

@@ -49,12 +49,15 @@ class GuiRemoveProjectedItemsCommand(wx.Command):
for pDroneItemID in self.pDroneItemIDs:
cmd = CalcRemoveProjectedDroneCommand(fitID=self.fitID, itemID=pDroneItemID, amount=self.amount)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFighterPosition in sorted(self.pFighterPositions, reverse=True):
cmd = CalcRemoveProjectedFighterCommand(fitID=self.fitID, position=pFighterPosition)
results.append(self.internalHistory.submit(cmd))
needRecalc = True
for pFitID in self.pFitIDs:
cmd = CalcRemoveProjectedFitCommand(fitID=self.fitID, projectedFitID=pFitID, amount=self.amount)
results.append(self.internalHistory.submit(cmd))
needRecalc = cmd.needsGuiRecalc
success = any(results)
sFit = Fit.getInstance()
if needRecalc:

View File

@@ -56,7 +56,7 @@ class ItemStatsFrame(AuxiliaryFrame):
title="Item stats",
pos=pos,
size=size,
style=wx.RESIZE_BORDER)
resizeable=True)
empty = getattr(victim, "isEmpty", False)

View File

@@ -162,7 +162,7 @@ class MainFrame(wx.Frame):
mainSizer.Add(self.browser_fitting_split, 1, wx.EXPAND | wx.LEFT, 2)
self.fitMultiSwitch = MultiSwitch(self.fitting_additions_split)
self.additionsPane = AdditionsPane(self.fitting_additions_split)
self.additionsPane = AdditionsPane(self.fitting_additions_split, self)
self.notebookBrowsers = ChromeNotebook(self.browser_fitting_split, False)

View File

@@ -23,7 +23,7 @@ import gui.builtinViews.emptyView
class MultiSwitch(ChromeNotebook):
def __init__(self, parent):
ChromeNotebook.__init__(self, parent)
ChromeNotebook.__init__(self, parent, can_add=True, tabWidthMode=1)
# self.AddPage() # now handled by mainFrame
self.handlers = handlers = []
for type in TabSpawner.tabTypes:

View File

@@ -94,7 +94,7 @@ class DmgPatternEditor(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Damage Pattern Editor", style=wx.RESIZE_BORDER,
parent, id=wx.ID_ANY, title="Damage Pattern Editor", resizeable=True,
# Dropdown list widget is scaled to its longest content line on GTK, adapt to that
size=wx.Size(500, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240))

View File

@@ -24,7 +24,7 @@ class AttributeEditor(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, wx.ID_ANY, title="Attribute Editor", pos=wx.DefaultPosition,
size=wx.Size(650, 600), style=wx.RESIZE_BORDER)
size=wx.Size(650, 600), resizeable=True)
i = wx.Icon(BitmapLoader.getBitmap("fit_rename_small", "gui"))
self.SetIcon(i)

View File

@@ -119,7 +119,7 @@ class ImplantSetEditor(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Implant Set Editor", style=wx.RESIZE_BORDER,
parent, id=wx.ID_ANY, title="Implant Set Editor", resizeable=True,
size=wx.Size(950, 500) if "wxGTK" in wx.PlatformInfo else wx.Size(850, 420))
self.block = False

View File

@@ -33,23 +33,18 @@ class SsoLogin(wx.Dialog):
self.SetSizer(bSizer1)
self.Center()
mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
from service.esi import Esi
self.sEsi = Esi.getInstance()
uri = self.sEsi.getLoginURI(None)
webbrowser.open(uri)
def OnLogin(self, event):
self.Close()
event.Skip()
class SsoLoginServer(wx.Dialog):
def __init__(self, port):
mainFrame = gui.mainFrame.MainFrame.getInstance()
super().__init__(mainFrame, id=wx.ID_ANY, title="SSO Login", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
super().__init__(self.mainFrame, id=wx.ID_ANY, title="SSO Login", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE)
from service.esi import Esi
@@ -59,8 +54,8 @@ class SsoLoginServer(wx.Dialog):
uri = self.sEsi.getLoginURI(serverAddr)
bSizer1 = wx.BoxSizer(wx.VERTICAL)
mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
text = wx.StaticText(self, wx.ID_ANY, "Waiting for character login through EVE Single Sign-On.")
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
@@ -78,9 +73,10 @@ class SsoLoginServer(wx.Dialog):
webbrowser.open(uri)
def OnLogin(self, event):
self.Close()
self.EndModal(wx.ID_OK)
event.Skip()
def OnClose(self, event):
def OnDestroy(self, event):
self.mainFrame.Unbind(GE.EVT_SSO_LOGIN, handler=self.OnLogin)
self.sEsi.stopServer()
event.Skip()

View File

@@ -126,7 +126,7 @@ class TargetProfileEditor(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title="Target Profile Editor", style=wx.RESIZE_BORDER,
parent, id=wx.ID_ANY, title="Target Profile Editor", resizeable=True,
# Dropdown list widget is scaled to its longest content line on GTK, adapt to that
size=wx.Size(500, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 240))

View File

@@ -110,7 +110,11 @@ class FloatRangeBox(wx.TextCtrl):
super().__init__(parent=parent, id=id, style=style, **kwargs)
self.Bind(wx.EVT_TEXT, self.OnText)
self._storedValue = ''
self.ChangeValue('{}-{}'.format(valToStr(min(value)), valToStr(max(value))))
value = [v for v in value if v is not None]
if not value:
self.ChangeValue('')
else:
self.ChangeValue('{}-{}'.format(valToStr(min(value)), valToStr(max(value))))
def ChangeValue(self, value):
self._storedValue = value

View File

@@ -99,14 +99,15 @@ def formatAmount(val, prec=3, lowest=0, highest=0, currency=False, forceSign=Fal
return result
def roundToPrec(val, prec):
def roundToPrec(val, prec, nsValue=None):
"""
nsValue: custom value which should be used to determine normalization shift
"""
# We're not rounding integers anyway
# Also make sure that we do not ask to calculate logarithm of zero
if int(val) == val:
return int(val)
# Find round factor, taking into consideration that we want to keep at least prec
# positions for fractions with zero integer part (e.g. 0.0000354 for prec=3)
roundFactor = int(prec - math.ceil(math.log10(abs(val))))
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
# But we don't want to round integers
if roundFactor < 0:
roundFactor = 0

View File

@@ -83,6 +83,7 @@ from gui.builtinViewColumns import ( # noqa: E402, F401
maxRange,
misc,
price,
projectionRange,
propertyDisplay,
state,
sideEffects,

View File

@@ -55,23 +55,23 @@ def main(db, json_path):
# Config dict
tables = {
'clonegrades': eos.gamedata.AlphaCloneSkill,
'dgmattribs': eos.gamedata.AttributeInfo,
'dgmeffects': eos.gamedata.Effect,
'dgmtypeattribs': eos.gamedata.Attribute,
'dgmtypeeffects': eos.gamedata.ItemEffect,
'dgmunits': eos.gamedata.Unit,
'evecategories': eos.gamedata.Category,
'evegroups': eos.gamedata.Group,
'invmetagroups': eos.gamedata.MetaGroup,
'invmetatypes': eos.gamedata.MetaType,
'evetypes': eos.gamedata.Item,
'phbtraits': eos.gamedata.Traits,
'phbmetadata': eos.gamedata.MetaData,
'marketGroups': eos.gamedata.MarketGroup}
'clonegrades': ('fsd_lite', eos.gamedata.AlphaCloneSkill),
'dgmattribs': ('bulkdata', eos.gamedata.AttributeInfo),
'dgmeffects': ('bulkdata', eos.gamedata.Effect),
'dgmtypeattribs': ('bulkdata', eos.gamedata.Attribute),
'dgmtypeeffects': ('bulkdata', eos.gamedata.ItemEffect),
'dgmunits': ('bulkdata', eos.gamedata.Unit),
'evecategories': ('fsd_lite', eos.gamedata.Category),
'evegroups': ('fsd_lite', eos.gamedata.Group),
'invmetagroups': ('bulkdata', eos.gamedata.MetaGroup),
'invmetatypes': ('bulkdata', eos.gamedata.MetaType),
'evetypes': ('fsd_lite', eos.gamedata.Item),
'traits': ('phobos', eos.gamedata.Traits),
'metadata': ('phobos', eos.gamedata.MetaData),
'marketgroups': ('fsd_binary', eos.gamedata.MarketGroup)}
fieldMapping = {
'marketGroups': {
'marketgroups': {
'id': 'marketGroupID',
'name': 'marketGroupName'}}
@@ -79,7 +79,7 @@ def main(db, json_path):
'evetypes',
'evegroups',
'evecategories',
'marketGroups')
'marketgroups')
def convertIcons(data):
new = []
@@ -271,8 +271,8 @@ def main(db, json_path):
data = {}
# Dump all data to memory so we can easely cross check ignored rows
for jsonName, cls in tables.items():
with open(os.path.join(jsonPath, '{}.json'.format(jsonName)), encoding='utf-8') as f:
for jsonName, (minerName, cls) in tables.items():
with open(os.path.join(jsonPath, minerName, '{}.json'.format(jsonName)), encoding='utf-8') as f:
tableData = json.load(f)
if jsonName in rowsInValues:
newTableData = []
@@ -285,7 +285,7 @@ def main(db, json_path):
tableData = newTableData
if jsonName == 'icons':
tableData = convertIcons(tableData)
if jsonName == 'phbtraits':
if jsonName == 'traits':
tableData = convertTraits(tableData)
if jsonName == 'clonegrades':
tableData = convertClones(tableData)
@@ -342,7 +342,7 @@ def main(db, json_path):
):
row['published'] = True
instance = tables[jsonName]()
instance = tables[jsonName][1]()
# fix for issue 80
if jsonName is 'icons' and 'res:/ui/texture/icons/' in str(row['iconFile']).lower():
row['iconFile'] = row['iconFile'].lower().replace('res:/ui/texture/icons/', '').replace('.png', '')
@@ -370,7 +370,7 @@ def main(db, json_path):
eos.db.gamedata_session.add(instance)
# quick and dirty hack to get this data in
with open(os.path.join(jsonPath, 'dynamicattributes.json'), encoding='utf-8') as f:
with open(os.path.join(jsonPath, 'fsd_binary', 'dynamicitemattributes.json'), encoding='utf-8') as f:
bulkdata = json.load(f)
for mutaID, data in bulkdata.items():
muta = eos.gamedata.DynamicItem()

View File

@@ -92,6 +92,7 @@ class Fit:
"showShipBrowserTooltip": True,
"marketSearchDelay": 250,
"ammoChangeAll": False,
"additionsLabels": 1,
}
self.serviceFittingOptions = SettingsProvider.getInstance().getSettings(

View File

@@ -305,9 +305,10 @@ class Market:
self.ITEMS_FORCEDMARKETGROUP_R = self.__makeRevDict(self.ITEMS_FORCEDMARKETGROUP)
self.FORCEDMARKETGROUP = {
685: False, # Ship Equipment > Electronic Warfare > ECCM
681: False, # Ship Equipment > Electronic Warfare > Sensor Backup Arrays
1639: False # Ship Equipment > Fleet Assistance > Command Processors
685: False, # Ship Equipment > Electronic Warfare > ECCM
681: False, # Ship Equipment > Electronic Warfare > Sensor Backup Arrays
1639: False, # Ship Equipment > Fleet Assistance > Command Processors
2527: True, # Ship Equipment > Hull & Armor > Mutadaptive Remote Armor Repairers - has hasTypes set to 1 while actually having no types
}
# Misc definitions
@@ -328,7 +329,7 @@ class Market:
"Structure",
"Structure Module",
)
self.SEARCH_GROUPS = ("Ice Product",)
self.SEARCH_GROUPS = ("Ice Product", "Cargo Container", "Secure Cargo Container", "Audit Log Secure Container", "Freight Container")
self.ROOT_MARKET_GROUPS = (9, # Ship Equipment
1111, # Rigs
157, # Drones

View File

@@ -363,7 +363,7 @@ class EfsPort:
groups = {}
# Export at maximum spool for consistency, spoolup data is exported anyway.
defaultSpoolValue = 1
spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True)
spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)
for mod in fit.modules:
if mod.getDps(spoolOptions=spoolOptions).total > 0:
# Group weapon + ammo combinations that occur more than once
@@ -689,7 +689,7 @@ class EfsPort:
shipSize = EfsPort.getShipSize(fit.ship.item.groupID)
# Export at maximum spool for consistency, spoolup data is exported anyway.
defaultSpoolValue = 1
spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True)
spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)
cargoIDs = []
for cargo in fit.cargo:

View File

@@ -519,7 +519,9 @@ class GraphSettings:
def __init__(self):
defaults = {
'mobileDroneMode': GraphDpsDroneMode.auto,
'ignoreDCR': False,
'ignoreResists': True,
'ignoreLockRange': True,
'applyProjected': True}
self.settings = SettingsProvider.getInstance().getSettings('graphSettings', defaults)

View File

@@ -1 +1 @@
version: v2.10.0
version: v2.11.1