Merge remote-tracking branch 'origin/master' into attrGroup

# Conflicts:
#	eve.db
This commit is contained in:
blitzmann
2019-02-28 18:51:52 -05:00
1371 changed files with 2257 additions and 1148 deletions

View File

@@ -1,5 +1,4 @@
environment:
global:
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
# /E:ON and /V:ON options are not enabled in the batch script intepreter
@@ -8,76 +7,11 @@ environment:
matrix:
# Python 2.7.10 is the latest version and is not pre-installed.
- PYTHON: "C:\\Python27.10"
PYTHON_VERSION: "2.7.10"
- PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.x"
PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python27.10-x64"
# PYTHON_VERSION: "2.7.10"
# PYTHON_ARCH: "64"
# Pre-installed Python versions, which Appveyor may upgrade to
# a later point release.
# See: http://www.appveyor.com/docs/installed-software#python
#- PYTHON: "C:\\Python27"
# PYTHON_VERSION: "2.7.x" # currently 2.7.9
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python27-x64"
# PYTHON_VERSION: "2.7.x" # currently 2.7.9
# PYTHON_ARCH: "64"
#- PYTHON: "C:\\Python33"
# PYTHON_VERSION: "3.3.x" # currently 3.3.5
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python33-x64"
# PYTHON_VERSION: "3.3.x" # currently 3.3.5
# PYTHON_ARCH: "64"
#- PYTHON: "C:\\Python34"
# PYTHON_VERSION: "3.4.x" # currently 3.4.3
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python34-x64"
# PYTHON_VERSION: "3.4.x" # currently 3.4.3
# PYTHON_ARCH: "64"
# Python versions not pre-installed
# Python 2.6.6 is the latest Python 2.6 with a Windows installer
# See: https://github.com/ogrisel/python-appveyor-demo/issues/10
#- PYTHON: "C:\\Python266"
# PYTHON_VERSION: "2.6.6"
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python266-x64"
# PYTHON_VERSION: "2.6.6"
# PYTHON_ARCH: "64"
#- PYTHON: "C:\\Python35"
# PYTHON_VERSION: "3.5.0"
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python35-x64"
# PYTHON_VERSION: "3.5.0"
# PYTHON_ARCH: "64"
# Major and minor releases (i.e x.0.0 and x.y.0) prior to 3.3.0 use
# a different naming scheme.
#- PYTHON: "C:\\Python270"
# PYTHON_VERSION: "2.7.0"
# PYTHON_ARCH: "32"
#- PYTHON: "C:\\Python270-x64"
# PYTHON_VERSION: "2.7.0"
# PYTHON_ARCH: "64"
init:
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
install:
# If there is a newer build queued for the same PR, cancel this one.
# The AppVeyor 'rollout builds' option is supposed to serve the same
@@ -89,34 +23,23 @@ install:
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
throw "There are newer queued builds for this pull request, failing early." }
# Install wxPython
- 'ECHO Downloading wxPython.'
- "appveyor DownloadFile https://goo.gl/yvO8PB -FileName C:\\wxpython.exe"
#- "appveyor DownloadFile https://goo.gl/Uj0jV3 -FileName C:\\wxpython64.exe"
- 'ECHO Install wxPython'
- "C:\\wxpython.exe /SP- /VERYSILENT /NORESTART"
#- "C:\\wxpython64.exe /SP- /VERYSILENT /NORESTART"
- ECHO "Filesystem root:"
- ps: "ls \"C:/\""
- ECHO "Filesystem projects root:"
- ps: "ls \"C:\\projects\\\""
- ECHO "Filesystem pyfa root:"
- ps: "ls \"C:\\projects\\pyfa\\\""
- ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\""
- ECHO "Installed SDKs:"
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
# Install Python (from the official .msi of http://python.org) and pip when
# not already installed.
# - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
# Prepend newly installed Python to the PATH of this build (this cannot be
# done from inside the powershell script as it would require to restart
# the parent CMD process).
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
# Check that we have the expected version and architecture for Python
- "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
@@ -128,19 +51,36 @@ install:
# compiled extensions and are not provided as pre-built wheel packages,
# pip will build them from source using the MSVC compiler matching the
# target Python version and architecture
# C:\\projects\\eve-gnosis\\
- ECHO "Install pip requirements:"
- "pip install -r requirements.txt"
- "pip install -r requirements_test.txt"
- "pip install -r requirements_build_windows.txt"
- "pip install PyInstaller"
before_build:
# directory that will contain the built files
- ps: $env:PYFA_DIST_DIR = "c:\projects\$env:APPVEYOR_PROJECT_SLUG\dist"
- ps: $env:PYFA_VERSION = (python ./scripts/dump_version.py)
- ps: echo("pyfa version ")
- ps: echo ($env:PYFA_VERSION)
build_script:
# Build the compiled extension
# - "python setup.py build"
- ECHO "Build pyfa:"
#- copy C:\projects\pyfa\dist_assets\win\pyfa.spec C:\projects\pyfa\pyfa.spec
- "python C:\\projects\\pyfa\\setup.py build"
##########
# PyInstaller - create binaries for pyfa
##########
# Build command for PyInstaller
- "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
# Copy over manifest (See pyfa-org/pyfa#1622)
- ps: xcopy /y dist_assets\win\pyfa.exe.manifest $env:PYFA_DIST_DIR\pyfa\
# Not really sure if this is needed, but why not
- ps: xcopy /y dist_assets\win\Microsoft.VC90.CRT.manifest $env:PYFA_DIST_DIR\pyfa\
##########
# InnoScript EXE building
# This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
##########
- "python dist_assets/win/dist.py"
- ps: dir $env:PYFA_DIST_DIR/
#- ECHO "Build pyfa (Debug):"
#- copy C:\projects\pyfa\dist_assets\win\pyfa_debug.spec C:\projects\pyfa\pyfa_debug.spec
#- "pyinstaller.exe --clean --noconfirm --windowed --upx-dir=C:\\projects\\pyfa\\scripts\\upx.exe C:\\projects\\pyfa\\pyfa_debug.spec"
@@ -150,12 +90,11 @@ build: on
after_build:
- ps: "ls \"./\""
#- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\""
- ps: "ls \"C:\\projects\\pyfa\\build\\\""
- ps: "ls \"C:\\projects\\pyfa\\build\\exe.win32-2.7\\\""
# - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\build\\exe.win32-2.7\\\""
# Zip
# APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER
#- 7z a build.zip -r C:\projects\pyfa\build\pyfa\*.*
- 7z a pyfa.zip -r C:\projects\pyfa\build\exe.win32-2.7\*.*
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*.*"
#- 7z a pyfa_debug.zip -r C:\projects\pyfa\dist\pyfa_debug\*.*
on_success:
@@ -176,11 +115,21 @@ after_test:
artifacts:
# Archive the generated packages in the ci.appveyor.com build report.
- path: pyfa.zip
name: 'pyfa.zip'
- path: pyfa*-win.zip
- path: pyfa*-win.exe
#- path: pyfa_debug.zip
# name: Pyfa_debug
deploy:
tag: $(pyfa_version)
release: pyfa $(pyfa_version)
description: 'Release description'
provider: GitHub
auth_token:
secure: BfNHO66ff5hVx2O2ORbl49X0U/5h2V2T0IuRZDwm7fd1HvsVluF0wRCbl29oRp1M
draft: true
on:
APPVEYOR_REPO_TAG: true # deploy on tag push only
#on_success:
# - TODO: upload the content of dist/*.whl to a public wheelhouse
#

View File

@@ -1,36 +1,29 @@
dist: trusty
sudo: required
os: linux
language: python
cache: pip
python:
- '3.6'
env:
- TOXENV=pep8
addons:
apt:
packages:
- 3.6
matrix:
include:
- os: osx
osx_image: xcode7.3
language: generic
env: PYTHON=3.6.1
before_install:
- sudo apt-get update && sudo apt-get --reinstall install -qq language-pack-en language-pack-ru language-pack-he language-pack-zh-hans
- pip install tox
# We're not actually installing Tox, but have to run it before we install wxPython via Conda. This is fugly but vOv
- tox
- pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk2/ubuntu-14.04 wxPython==4.0.0b2
# # get Conda
# - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
# wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;
# else
# wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
# fi
# - bash miniconda.sh -b -p $HOME/miniconda
# - export PATH="$HOME/miniconda/bin:$PATH"
# - hash -r
# - conda config --set always_yes yes --set changeps1 no
# - conda update -q conda
# # Useful for debugging any issues with conda
# - conda info -a
#install:
# install wxPython 3.0.0.0
# - conda install -c https://conda.anaconda.org/travis wxpython=4.0.0b2
script:
- tox
- bash scripts/setup-osx.sh
install:
- export PYFA_VERSION="$(python3 scripts/dump_version.py)"
- bash scripts/package-osx.sh
before_deploy:
- export RELEASE_PKG_FILE=$(ls *.deb)
- echo "deploying $RELEASE_PKG_FILE to GitHub releases"
deploy:
provider: releases
api_key:
secure: Xfu0xApoB0zUPLXl29aYUulVC3iA4/3bXQwwADKCfAKZwxgNon4dLbO7Rie5/7Ukf2POL0KwmRaQGN3kOr+XSoIVTE4M5sXxnhiaaLGKQ+48hDizLE6JuXcZGJvkxUaghaTzIdCwHsG7VGBsPfQgfGsjJcfBp8tFNLmRyM/Jpsr8T6BR2MxtBIEUVy8zrOWFNZqnmWrY2pWMsB9fYt3JFNdpqeIgRAYqbBsBcZQ1MngLTi3ztuYS5IaF+lk06RrnBlHmUsJu/5nCvIpvPvD0i2BLZ3Uu0+Fn+8QWUgjJEL9MNseXZMXynu05xd8YRk7Ajc9CUrzQIIbAktyteYp85kE3pUJHmrMLcXhh7nqkwttR5/47Zwa3OLJLJFKBxMx6wY5jFkJjkV08850B7aWrmTFl/Eqc3Q5nZMuiEt3wFRbjxHi9h1mTN/fkxfRRHg8u3ENGPR+ZPiFC3J18qtks/B/hsKjjHvZP1i79OYlET4V/zyLyyQkCbpDaARQANuotLYJyZ7tH+KWEyRsvTi0M9Yev9mNNw6aI4vzh4HfkEhvcvnWnYwckPj1dnjQ573Qpw0Z9wsconoWfHAn+hBDt3+YLMrrFZl++mCRskHH1mZChX3aGMDi49zD0kfxBUkYPOAhguc6PwudBxHUZP+O6T/SoHylff6EizCE/k5dGeAk=
file_glob: true
file: "dist/pyfa-*.zip"
skip_cleanup: true
draft: true
on:
tags: true
repo: pyfa-org/Pyfa

View File

@@ -1,5 +1,6 @@
import os
import sys
import yaml
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING
@@ -22,12 +23,6 @@ debug = False
# Defines if our saveddata will be in pyfa root or not
saveInRoot = False
# Version data
version = "2.6.1"
tag = "Stable"
expansionName = "Onslaught"
expansionVersion = "1.5"
evemonMinVersion = "4081"
minItemSearchLength = 3
@@ -79,12 +74,7 @@ def getPyfaRoot():
def getVersion():
if os.path.isfile(os.path.join(pyfaPath, '.version')):
with open(os.path.join(pyfaPath, '.version')) as f:
gitVersion = f.readline()
return gitVersion
# if no version file exists, then user is running from source or not an official build
return version + " (git)"
return version
def getDefaultSave():
@@ -96,11 +86,12 @@ def defPaths(customSavePath=None):
global pyfaPath
global savePath
global saveDB
global gameDB
global gameDB
global saveInRoot
global logPath
global cipher
global clientHash
global version
pyfalog.debug("Configuring Pyfa")
@@ -110,6 +101,12 @@ def defPaths(customSavePath=None):
if pyfaPath is None:
pyfaPath = getPyfaRoot()
# Version data
with open(os.path.join(pyfaPath, "version.yml"), 'r') as file:
data = yaml.load(file)
version = data['version']
# Where we store the saved fits etc, default is the current users home directory
if saveInRoot is True:
savePath = getattr(configforced, "savePath", None)

View File

@@ -24,11 +24,13 @@ added_files = [
('../../eve.db', '.'),
('../../README.md', '.'),
('../../LICENSE', '.'),
('../../.version', '.'),
('../../version.yml', '.'),
]
import_these = []
import_these = [
'numpy.core._dtype_ctypes' # https://github.com/pyinstaller/pyinstaller/issues/3982
]
icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns")
@@ -54,8 +56,10 @@ a = Analysis([r'../../pyfa.py'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
@@ -70,10 +74,16 @@ exe = EXE(pyz,
icon=icon,
)
app = BUNDLE(exe,
name='pyfa.app',
icon=icon,
bundle_identifier=None,
info_plist={
'NSHighResolutionCapable': 'True'
})
app = BUNDLE(
exe,
name='pyfa.app',
icon=icon,
bundle_identifier=None,
info_plist={
'NSHighResolutionCapable': 'True',
'NSPrincipalClass': 'NSApplication',
'CFBundleName': 'pyfa',
'CFBundleDisplayName': 'pyfa',
'CFBundleIdentifier': 'org.pyfaorg.pyfa',
}
)

View File

@@ -3,44 +3,35 @@
import os.path
from subprocess import call
import zipfile
from packaging.version import Version
import yaml
def zipdir(path, zip):
for root, dirs, files in os.walk(path):
for file in files:
zip.write(os.path.join(root, file))
with open("version.yml", 'r') as file:
data = yaml.load(file)
version = data['version']
config = {}
os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
exec(compile(open("config.py").read(), "config.py", 'exec'), config)
os.environ["PYFA_VERSION"] = version
iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")
print("Creating archive")
source = os.path.join(os.getcwd(), "dist", "pyfa")
fileName = "pyfa-{}-{}-{}-win".format(
config['version'],
config['expansionName'].lower(),
config['expansionVersion']
)
archive = zipfile.ZipFile(os.path.join(os.getcwd(), "dist", fileName + ".zip"), 'w', compression=zipfile.ZIP_DEFLATED)
zipdir(source, archive)
archive.close()
fileName = "pyfa-{}-win".format(os.environ["PYFA_VERSION"])
print("Compiling EXE")
expansion = "%s %s" % (config['expansionName'], config['expansionVersion']),
v = Version(version)
print(v)
call([
iscc,
os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"),
"/dMyAppVersion=%s" % (config['version']),
"/dMyAppExpansion=%s" % expansion,
"/dMyAppVersion=%s" % v,
"/dMyAppDir=%s" % source,
"/dMyOutputDir=%s" % os.path.join(os.getcwd(), "dist"),
"/dMyOutputDir=%s" % os.path.join(os.getcwd()),
"/dMyOutputFile=%s" % fileName]) # stdout=devnull, stderr=devnull
print("Done")

View File

@@ -7,15 +7,12 @@
#ifndef MyAppVersion
#define MyAppVersion "2.1.0"
#endif
#ifndef MyAppExpansion
#define MyAppExpansion "Vanguard 1.0"
#endif
; Other config
#define MyAppName "pyfa"
#define MyAppPublisher "pyfa"
#define MyAppURL "https://forums.eveonline.com/t/27156"
#define MyAppURL "https://github.com/pyfa-org/Pyfa/"
#define MyAppExeName "pyfa.exe"
; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup
@@ -23,7 +20,7 @@
#define MinorVersionFlag 0
#ifndef MyOutputFile
#define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-'+MyAppExpansion+'-win-wx3', " ", "-"))
#define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-win', " ", "-"))
#endif
#ifndef MyAppDir
#define MyAppDir "pyfa"
@@ -39,7 +36,7 @@
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{3DA39096-C08D-49CD-90E0-1D177F32C8AA}
AppName={#MyAppName}
AppVersion={#MyAppVersion} ({#MyAppExpansion})
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
@@ -51,10 +48,8 @@ LicenseFile={#MyAppDir}\LICENSE
OutputDir={#MyOutputDir}
OutputBaseFilename={#MyOutputFile}
SetupIconFile={#MyAppDir}\pyfa.ico
Compression=lzma
SolidCompression=yes
CloseApplications=yes
AppReadmeFile=https://github.com/pyfa-org/Pyfa/blob/v{#MyAppVersion}/readme.txt
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

View File

@@ -5,8 +5,7 @@ from itertools import chain
import subprocess
import requests.certs
label = subprocess.check_output([
"git", "describe", "--tags"]).strip()
label = subprocess.check_output(["git", "describe", "--tags"]).strip()
with open('.version', 'w+') as f:
f.write(label.decode())
@@ -18,7 +17,7 @@ added_files = [
('../../imgs/gui/*.gif', 'imgs/gui'),
('../../imgs/icons/*.png', 'imgs/icons'),
('../../imgs/renders/*.png', 'imgs/renders'),
('../../service/jargon/*.yaml', 'service/jargon'),
('../../service/jargon/*.yaml', 'service/jargon'),
('../../dist_assets/win/pyfa.ico', '.'),
('../../dist_assets/win/pyfa.exe.manifest', '.'),
('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'),
@@ -26,10 +25,12 @@ added_files = [
('../../eve.db', '.'),
('../../README.md', '.'),
('../../LICENSE', '.'),
('../../.version', '.'),
('../../version.yml', '.'),
]
import_these = []
import_these = [
'numpy.core._dtype_ctypes' # https://github.com/pyinstaller/pyinstaller/issues/3982
]
# Walk directories that do dynamic importing
paths = ('eos/effects', 'eos/db/migrations', 'service/conversions')

View File

@@ -40,7 +40,9 @@ items_table = Table("invtypes", gamedata_meta,
Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
Column("iconID", Integer),
Column("graphicID", Integer),
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True))
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
Column("replaceSame", String),
Column("replaceBetter", String))
from .metaGroup import metatypes_table # noqa
from .traits import traits_table # noqa

View File

@@ -0,0 +1,18 @@
"""
Migration 29
- adds spoolType and spoolAmount to modules table
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT spoolType FROM modules LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE modules ADD COLUMN spoolType INT;")
try:
saveddata_engine.execute("SELECT spoolAmount FROM modules LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE modules ADD COLUMN spoolAmount FLOAT;")

View File

@@ -0,0 +1,17 @@
"""
Migration 30
- changes to prices table
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT status FROM prices LIMIT 1")
except sqlalchemy.exc.DatabaseError:
# Just drop table, table will be re-created by sqlalchemy and
# data will be re-fetched
saveddata_engine.execute("DROP TABLE prices;")

View File

@@ -17,7 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, CheckConstraint, Boolean, DateTime
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm import relation, mapper
import datetime
@@ -40,6 +40,8 @@ modules_table = Table("modules", saveddata_meta,
Column("position", Integer),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("spoolType", Integer, nullable=True),
Column("spoolAmount", Float, nullable=True),
CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"'))
mapper(Module, modules_table,

View File

@@ -17,17 +17,20 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from sqlalchemy import Table, Column, Float, Integer
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.saveddata.price import Price
prices_table = Table("prices", saveddata_meta,
Column("typeID", Integer, primary_key=True),
Column("price", Float, default=0.0),
Column("time", Integer, nullable=False),
Column("failed", Integer))
Column("status", Integer, nullable=False))
mapper(Price, prices_table, properties={
"_Price__price": prices_table.c.price,

View File

@@ -542,8 +542,17 @@ def commit():
with sd_lock:
try:
saveddata_session.commit()
saveddata_session.flush()
except Exception as ex:
except Exception:
saveddata_session.rollback()
exc_info = sys.exc_info()
raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
def flush():
with sd_lock:
try:
saveddata_session.flush()
except Exception:
saveddata_session.rollback()
exc_info = sys.exc_info()
raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])

View File

@@ -141,6 +141,23 @@ class HandledModuleList(HandledList):
self.remove(mod)
return
def replaceRackPosition(self, rackPosition, mod):
listPositions = []
for currMod in self:
if currMod.slot == mod.slot:
listPositions.append(currMod.position)
listPositions.sort()
try:
modListPosition = listPositions[rackPosition]
except IndexError:
self.appendIgnoreEmpty(mod)
else:
self.toDummy(modListPosition)
if not mod.isEmpty:
self.toModule(modListPosition, mod)
if mod.isInvalid:
self.toDummy(modListPosition)
def insert(self, index, mod):
mod.position = index
i = index

View File

@@ -1,7 +1,7 @@
# ammoInfluenceCapNeed
#
# Used by:
# Items from category: Charge (493 of 947)
# Items from category: Charge (493 of 949)
type = "passive"

View File

@@ -1,7 +1,7 @@
# ammoInfluenceRange
#
# Used by:
# Items from category: Charge (587 of 947)
# Items from category: Charge (587 of 949)
type = "passive"

View File

@@ -1,10 +1,9 @@
# ammoSpeedMultiplier
#
# Used by:
# Charges from group: Festival Charges (23 of 23)
# Charges from group: Festival Charges (26 of 26)
# Charges from group: Interdiction Probe (2 of 2)
# Charges from group: Structure Festival Charges (3 of 3)
# Special Edition Assetss from group: Festival Charges Expired (2 of 2)
# Items from market group: Special Edition Assets > Special Edition Festival Assets (30 of 33)
type = "passive"

View File

@@ -1,7 +1,7 @@
# ammoTrackingMultiplier
#
# Used by:
# Items from category: Charge (182 of 947)
# Items from category: Charge (182 of 949)
# Charges from group: Projectile Ammo (128 of 128)
type = "passive"

View File

@@ -9,4 +9,7 @@ type = "active"
def handler(fit, module, context):
amount = module.getModifiedItemAttr("armorDamageAmount")
speed = module.getModifiedItemAttr("duration") / 1000.0
fit.extraAttributes.increase("armorRepair", amount / speed)
rps = amount / speed
fit.extraAttributes.increase("armorRepair", rps)
fit.extraAttributes.increase("armorRepairPreSpool", rps)
fit.extraAttributes.increase("armorRepairFullSpool", rps)

View File

@@ -4,6 +4,7 @@
# Implants named like: Eifyr and Co. 'Alchemist' Neurotoxin Control NC (2 of 2)
# Implants named like: grade Edge (10 of 12)
# Skill: Neurotoxin Control
runTime = 'early'
type = "passive"

View File

@@ -1,7 +1,7 @@
# boosterShieldCapacityPenalty
#
# Used by:
# Implants from group: Booster (12 of 69)
# Implants from group: Booster (12 of 70)
type = "boosterSideEffect"
# User-friendly name for the side effect

View File

@@ -6,5 +6,5 @@ type = "passive"
def handler(fit, ship, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field",
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator",
"duration", ship.getModifiedItemAttr("durationBonus"))

View File

@@ -1,7 +1,7 @@
# cynosuralGeneration
#
# Used by:
# Modules from group: Cynosural Field (2 of 2)
# Modules from group: Cynosural Field Generator (2 of 2)
type = "active"

View File

@@ -8,6 +8,6 @@ type = "passive"
def handler(fit, container, context):
level = container.level if "skill" in context else 1
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field",
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator",
"consumptionQuantity",
container.getModifiedItemAttr("consumptionQuantityBonusPercentage") * level)

View File

@@ -11,6 +11,5 @@ def handler(fit, module, context):
bonus = "%s%sDamageResonance" % (attrPrefix, damageType)
bonus = "%s%s" % (bonus[0].lower(), bonus[1:])
booster = "%s%sDamageResonance" % (layer, damageType)
penalize = False if layer == 'hull' else True
fit.ship.multiplyItemAttr(bonus, module.getModifiedItemAttr(booster),
stackingPenalties=penalize, penaltyGroup="preMul")
stackingPenalties=True, penaltyGroup="preMul")

View File

@@ -1,7 +1,7 @@
# doHacking
#
# Used by:
# Modules from group: Data Miners (9 of 9)
# Modules from group: Data Miners (10 of 10)
type = "active"

View File

@@ -1,7 +1,7 @@
# droneArmorDamageBonusEffect
#
# Used by:
# Ships from group: Logistics (5 of 6)
# Ships from group: Logistics (6 of 7)
# Ship: Exequror
# Ship: Scythe
type = "passive"

View File

@@ -1,7 +1,7 @@
# droneHullRepairBonusEffect
#
# Used by:
# Ships from group: Logistics (5 of 6)
# Ships from group: Logistics (6 of 7)
# Ship: Exequror
# Ship: Scythe
type = "passive"

View File

@@ -1,7 +1,7 @@
# droneShieldBonusBonusEffect
#
# Used by:
# Ships from group: Logistics (5 of 6)
# Ships from group: Logistics (6 of 7)
# Ship: Exequror
# Ship: Scythe
type = "passive"

View File

@@ -8,4 +8,6 @@ runtime = "late"
def handler(fit, src, context):
for dmgType in ('em', 'thermal', 'kinetic', 'explosive'):
fit.ship.forceItemAttr('{}DamageResonance'.format(dmgType), src.getModifiedItemAttr("hull{}DamageResonance".format(dmgType.title())))
fit.ship.multiplyItemAttr('{}DamageResonance'.format(dmgType),
src.getModifiedItemAttr("hull{}DamageResonance".format(dmgType.title())),
stackingPenalties=True, penaltyGroup="postMul")

View File

@@ -14,4 +14,7 @@ def handler(fit, module, context):
amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier
speed = module.getModifiedItemAttr("duration") / 1000.0
fit.extraAttributes.increase("armorRepair", amount / speed)
rps = amount / speed
fit.extraAttributes.increase("armorRepair", rps)
fit.extraAttributes.increase("armorRepairPreSpool", rps)
fit.extraAttributes.increase("armorRepairFullSpool", rps)

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Variations of module: Ice Harvester Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive"

View File

@@ -0,0 +1,10 @@
# implantWarpScrambleRangeBonus
#
# Used by:
# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3)
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Warp Scrambler", "maxRange",
src.getModifiedItemAttr("warpScrambleRangeBonus"), stackingPenalties=False)

View File

@@ -6,4 +6,4 @@ type = "passive"
def handler(fit, module, context):
fit.ship.boostItemAttr("signatureRadius", module.getModifiedItemAttr("signatureRadiusBonus"))
fit.ship.boostItemAttr("signatureRadius", module.getModifiedItemAttr("signatureRadiusBonus"), stackingPenalties=True)

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Variations of module: Mining Laser Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive"

View File

@@ -0,0 +1,10 @@
# miningDurationMultiplierOnline
#
# Used by:
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mining Laser",
"duration", module.getModifiedItemAttr("miningDurationMultiplier"))

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Variations of module: Mining Laser Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive"

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Modules from group: Missile Guidance Enhancer (3 of 3)
# Module: ML-EKP 'Polybolos' Ballistic Control System
type = "passive"

View File

@@ -1,7 +1,7 @@
# missileDMGBonus
#
# Used by:
# Modules from group: Ballistic Control system (21 of 21)
# Modules from group: Ballistic Control system (22 of 22)
type = "passive"

View File

@@ -1,7 +1,7 @@
# missileLauncherSpeedMultiplier
#
# Used by:
# Modules from group: Ballistic Control system (21 of 21)
# Modules from group: Ballistic Control system (22 of 22)
type = "passive"

View File

@@ -9,4 +9,7 @@ def handler(fit, container, context):
if "projected" in context:
bonus = container.getModifiedItemAttr("armorDamageAmount")
duration = container.getModifiedItemAttr("duration") / 1000.0
fit.extraAttributes.increase("armorRepair", bonus / duration)
rps = bonus / duration
fit.extraAttributes.increase("armorRepair", rps)
fit.extraAttributes.increase("armorRepairPreSpool", rps)
fit.extraAttributes.increase("armorRepairFullSpool", rps)

View File

@@ -2,7 +2,7 @@
#
# Used by:
# Modules from group: Missile Launcher Torpedo (22 of 22)
# Items from market group: Ship Equipment > Turrets & Bays (429 of 881)
# Items from market group: Ship Equipment > Turrets & Bays (429 of 883)
# Module: Interdiction Sphere Launcher I
type = "overheat"

View File

@@ -1,17 +1,17 @@
# overloadSelfDurationBonus
#
# Used by:
# Modules from group: Ancillary Remote Shield Booster (4 of 4)
# Modules from group: Capacitor Booster (59 of 59)
# Modules from group: Energy Neutralizer (54 of 54)
# Modules from group: Energy Nosferatu (54 of 54)
# Modules from group: Hull Repair Unit (25 of 25)
# Modules from group: Remote Armor Repairer (39 of 39)
# Modules from group: Remote Capacitor Transmitter (41 of 41)
# Modules from group: Remote Hull Repairer (8 of 8)
# Modules from group: Remote Shield Booster (38 of 38)
# Modules from group: Smart Bomb (118 of 118)
# Modules from group: Warp Disrupt Field Generator (7 of 7)
# Modules named like: Ancillary Remote (8 of 8)
# Modules named like: Remote Repairer (56 of 56)
# Module: Reactive Armor Hardener
# Module: Target Spectrum Breaker
type = "overheat"

View File

@@ -1,7 +1,7 @@
# remoteCapacitorTransmitterPowerNeedBonusEffect
#
# Used by:
# Ships from group: Logistics (3 of 6)
# Ships from group: Logistics (3 of 7)
type = "passive"

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Implants named like: Inquest 'Eros' Stasis Webifier MR (3 of 3)
# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3)
type = "passive"

View File

@@ -1,7 +1,7 @@
# shieldTransportCpuNeedBonusEffect
#
# Used by:
# Ships from group: Logistics (3 of 6)
# Ships from group: Logistics (3 of 7)
type = "passive"

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRemoteRepairRangeRole3
#
# Used by:
# Ship: Rodiva
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "maxRange", src.getModifiedItemAttr("shipBonusRole3"))

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRemoteRepAmounteliteBonusLogisitics2
#
# Used by:
# Ship: Zarmazd
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "armorDamageAmount", src.getModifiedItemAttr("eliteBonusLogistics2"), skill="Logistics Cruisers")

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRemoteRepCapNeedeliteBonusLogisitics1
#
# Used by:
# Ship: Zarmazd
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "capacitorNeed", src.getModifiedItemAttr("eliteBonusLogistics1"), skill="Logistics Cruisers")

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRemoteRepRangePC1
#
# Used by:
# Ship: Zarmazd
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "maxRange", src.getModifiedItemAttr("shipBonusPC1"), skill="Precursor Cruiser")

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRepAmountPC1
#
# Used by:
# Ship: Rodiva
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "armorDamageAmount", src.getModifiedItemAttr("shipBonusPC1"), skill="Precursor Cruiser")

View File

@@ -0,0 +1,7 @@
# shipBonusMutadaptiveRepCapNeedPC2
#
# Used by:
# Ship: Rodiva
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mutadaptive Remote Armor Repairer", "capacitorNeed", src.getModifiedItemAttr("shipBonusPC2"), skill="Precursor Cruiser")

View File

@@ -0,0 +1,7 @@
# shipBonusNosNeutCapNeedRoleBonus2
#
# Used by:
# Variations of ship: Rodiva (2 of 2)
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capacitor Emission Systems"), "capacitorNeed", src.getModifiedItemAttr("shipBonusRole2"))

View File

@@ -0,0 +1,7 @@
# shipBonusRemoteCapacitorTransferRangeRole1
#
# Used by:
# Variations of ship: Rodiva (2 of 2)
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Remote Capacitor Transmitter", "maxRange", src.getModifiedItemAttr("shipBonusRole1"))

View File

@@ -1,7 +1,7 @@
# shipBonusRole5RemoteArmorRepairPowergridBonus
#
# Used by:
# Ships from group: Logistics (3 of 6)
# Ships from group: Logistics (3 of 7)
type = "passive"

View File

@@ -1,6 +1,7 @@
# shipBonusSmartbombCapNeedRoleBonus2
#
# Used by:
# Variations of ship: Rodiva (2 of 2)
# Ship: Damavik
# Ship: Drekavac
# Ship: Hydra

View File

@@ -2,8 +2,9 @@
#
# Used by:
# Modules from group: Ancillary Remote Armor Repairer (4 of 4)
runTime = "late"
type = "projected", "active"
runTime = "late"
def handler(fit, module, context, **kwargs):
@@ -17,4 +18,7 @@ def handler(fit, module, context, **kwargs):
amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier
speed = module.getModifiedItemAttr("duration") / 1000.0
fit.extraAttributes.increase("armorRepair", amount / speed, **kwargs)
rps = amount / speed
fit.extraAttributes.increase("armorRepair", rps)
fit.extraAttributes.increase("armorRepairPreSpool", rps)
fit.extraAttributes.increase("armorRepairFullSpool", rps)

View File

@@ -2,8 +2,9 @@
#
# Used by:
# Modules from group: Ancillary Remote Shield Booster (4 of 4)
runTime = "late"
type = "projected", "active"
runTime = "late"
def handler(fit, module, context, **kwargs):

View File

@@ -0,0 +1,28 @@
# ShipModuleRemoteArmorMutadaptiveRepairer
#
# Used by:
# Modules from group: Mutadaptive Remote Armor Repairer (5 of 5)
from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, resolveSpoolOptions
type = "projected", "active"
runTime = "late"
def handler(fit, container, context, **kwargs):
if "projected" in context:
repAmountBase = container.getModifiedItemAttr("armorDamageAmount")
cycleTime = container.getModifiedItemAttr("duration") / 1000.0
repSpoolMax = container.getModifiedItemAttr("repairMultiplierBonusMax")
repSpoolPerCycle = container.getModifiedItemAttr("repairMultiplierBonusPerCycle")
# TODO: fetch spoolup option
defaultSpoolValue = 1
spoolType, spoolAmount = resolveSpoolOptions(SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False), container)
rps = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, spoolType, spoolAmount)[0]) / cycleTime
rpsPreSpool = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, SpoolType.SCALE, 0)[0]) / cycleTime
rpsFullSpool = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, SpoolType.SCALE, 1)[0]) / cycleTime
fit.extraAttributes.increase("armorRepair", rps, **kwargs)
fit.extraAttributes.increase("armorRepairPreSpool", rpsPreSpool, **kwargs)
fit.extraAttributes.increase("armorRepairFullSpool", rpsFullSpool, **kwargs)

View File

@@ -2,11 +2,16 @@
#
# Used by:
# Modules from group: Remote Armor Repairer (39 of 39)
type = "projected", "active"
runTime = "late"
def handler(fit, container, context, **kwargs):
if "projected" in context:
bonus = container.getModifiedItemAttr("armorDamageAmount")
duration = container.getModifiedItemAttr("duration") / 1000.0
fit.extraAttributes.increase("armorRepair", bonus / duration, **kwargs)
rps = bonus / duration
fit.extraAttributes.increase("armorRepair", rps)
fit.extraAttributes.increase("armorRepairPreSpool", rps)
fit.extraAttributes.increase("armorRepairFullSpool", rps)

View File

@@ -2,8 +2,13 @@
#
# Used by:
# Modules from group: Remote Capacitor Transmitter (41 of 41)
from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "projected", "active"
runTime = "late"
def handler(fit, src, context, **kwargs):

View File

@@ -2,6 +2,7 @@
#
# Used by:
# Modules from group: Remote Hull Repairer (8 of 8)
type = "projected", "active"
runTime = "late"

View File

@@ -7,4 +7,4 @@ type = "passive"
def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Missile Launcher Bomb",
"moduleReactivationDelay", skill.getModifiedItemAttr("rofBonus") * skill.level)
"moduleReactivationDelay", skill.getModifiedItemAttr("reactivationDelayBonus") * skill.level)

View File

@@ -1,7 +1,6 @@
# skillBonusDroneDurability
#
# Used by:
# Implants from group: Cyber Drones (4 of 4)
# Skill: Drone Durability
type = "passive"

View File

@@ -0,0 +1,14 @@
# skillBonusDroneDurabilityNotFighters
#
# Used by:
# Implants from group: Cyber Drones (4 of 4)
type = "passive"
def handler(fit, src, context):
fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "hp",
src.getModifiedItemAttr("hullHpBonus"))
fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "armorHP",
src.getModifiedItemAttr("armorHpBonus"))
fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "shieldCapacity",
src.getModifiedItemAttr("shieldCapacityBonus"))

View File

@@ -1,8 +1,6 @@
# skillBonusDroneInterfacing
#
# Used by:
# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D
# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T
# Skill: Drone Interfacing
type = "passive"

View File

@@ -0,0 +1,11 @@
# skillBonusDroneInterfacingNotFighters
#
# Used by:
# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D
# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T
type = "passive"
def handler(fit, src, context):
fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "damageMultiplier",
src.getModifiedItemAttr("damageMultiplierBonus"))

View File

@@ -0,0 +1,10 @@
# stripMinerDurationMultiplier
#
# Used by:
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Strip Miner",
"duration", module.getModifiedItemAttr("miningDurationMultiplier"))

View File

@@ -239,7 +239,7 @@ class Item(EqBase):
self.__offensive = None
self.__assistive = None
self.__overrides = None
self.__price = None
self.__priceObj = None
@property
def attributes(self):
@@ -446,34 +446,33 @@ class Item(EqBase):
@property
def price(self):
# todo: use `from sqlalchemy import inspect` instead (mac-deprecated doesn't have inspect(), was imp[lemented in 0.8)
if self.__price is not None and getattr(self.__price, '_sa_instance_state', None) and self.__price._sa_instance_state.deleted:
if self.__priceObj is not None and getattr(self.__priceObj, '_sa_instance_state', None) and self.__priceObj._sa_instance_state.deleted:
pyfalog.debug("Price data for {} was deleted (probably from a cache reset), resetting object".format(self.ID))
self.__price = None
self.__priceObj = None
if self.__price is None:
if self.__priceObj is None:
db_price = eos.db.getPrice(self.ID)
# do not yet have a price in the database for this item, create one
if db_price is None:
pyfalog.debug("Creating a price for {}".format(self.ID))
self.__price = types_Price(self.ID)
eos.db.add(self.__price)
eos.db.commit()
self.__priceObj = types_Price(self.ID)
eos.db.add(self.__priceObj)
eos.db.flush()
else:
self.__price = db_price
self.__priceObj = db_price
return self.__price
return self.__priceObj
@property
def isAbyssal(self):
if Item.ABYSSAL_TYPES is None:
Item.getAbyssalYypes()
Item.getAbyssalTypes()
return self.ID in Item.ABYSSAL_TYPES
@classmethod
def getAbyssalYypes(cls):
def getAbyssalTypes(cls):
cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes()
@property

View File

@@ -75,7 +75,7 @@ class FitDpsGraph(Graph):
pyfalog.critical(e)
for mod in fit.modules:
dps, _ = mod.damageStats(fit.targetResists)
dps = mod.getDps(targetResists=fit.targetResists).total
if mod.hardpoint == Hardpoint.TURRET:
if mod.state >= State.ACTIVE:
total += dps * self.calculateTurretMultiplier(mod, data)
@@ -88,7 +88,7 @@ class FitDpsGraph(Graph):
for drone in fit.drones:
multiplier = 1 if drone.getModifiedItemAttr("maxVelocity") > 1 else self.calculateTurretMultiplier(
drone, data)
dps, _ = drone.damageStats(fit.targetResists)
dps = drone.getDps(targetResists=fit.targetResists).total
total += dps * multiplier
# this is janky as fuck
@@ -98,7 +98,7 @@ class FitDpsGraph(Graph):
for ability in fighter.abilities:
if ability.dealsDamage and ability.active:
multiplier = self.calculateFighterMissileMultiplier(ability, data)
dps, _ = ability.damageStats(fit.targetResists)
dps = ability.getDps(targetResists=fit.targetResists).total
total += dps * multiplier
return total

View File

@@ -215,6 +215,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
if force is not None:
if cappingValue is not None:
force = min(force, cappingValue)
if key in (50, 30, 48, 11):
force = round(force, 2)
return force
# Grab our values if they're there, otherwise we'll take default values
preIncrease = self.__preIncreases.get(key, 0)
@@ -268,7 +270,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Cap value if we have cap defined
if cappingValue is not None:
val = min(val, cappingValue)
if key in (50, 30, 48, 11):
val = round(val, 2)
return val
def __handleSkill(self, skillName):

View File

@@ -42,13 +42,18 @@ class DamagePattern(object):
return ehp
def calculateEffectiveTank(self, fit, tankInfo):
ehps = {}
passiveShield = fit.calculateShieldRecharge()
ehps["passiveShield"] = self.effectivify(fit, passiveShield, "shield")
for type in ("shield", "armor", "hull"):
ehps["%sRepair" % type] = self.effectivify(fit, tankInfo["%sRepair" % type], type)
return ehps
typeMap = {
"passiveShield": "shield",
"shieldRepair": "shield",
"armorRepair": "armor",
"armorRepairPreSpool": "armor",
"armorRepairFullSpool": "armor",
"hullRepair": "hull"}
ereps = {}
for field in tankInfo:
if field in typeMap:
ereps[field] = self.effectivify(fit, tankInfo[field], typeMap[field])
return ereps
def effectivify(self, fit, amount, type):
type = type if type != "hull" else ""

View File

@@ -24,12 +24,13 @@ from sqlalchemy.orm import validates, reconstructor
import eos.db
from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
MINING_ATTRIBUTES = ("miningAmount",)
def __init__(self, item):
@@ -65,8 +66,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__charge = None
self.__dps = None
self.__volley = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
@@ -120,37 +121,67 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self):
return self.charge is not None
@property
def dps(self):
return self.damageStats()
def getVolley(self, targetResists=None):
if not self.dealsDamage or self.amountActive <= 0:
return DmgTypes(0, 0, 0, 0)
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
self.__baseVolley = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes(
em=self.__baseVolley.em * (1 - getattr(targetResists, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
return volley
def getDps(self, targetResists=None):
volley = self.getVolley(targetResists=targetResists)
if not volley:
return DmgTypes(0, 0, 0, 0)
cycleAttr = "missileLaunchDuration" if self.hasAmmo else "speed"
cycleTime = self.getModifiedItemAttr(cycleAttr)
dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
return dps
def getRemoteReps(self, ignoreState=False):
if self.amountActive <= 0 and not ignoreState:
return (None, 0)
if self.__baseRemoteReps is None:
rrShield = self.getModifiedItemAttr("shieldBonus", 0)
rrArmor = self.getModifiedItemAttr("armorDamageAmount", 0)
rrHull = self.getModifiedItemAttr("structureDamageAmount", 0)
if rrShield:
rrType = "Shield"
rrAmount = rrShield
elif rrArmor:
rrType = "Armor"
rrAmount = rrArmor
elif rrHull:
rrType = "Hull"
rrAmount = rrHull
else:
rrType = None
rrAmount = 0
if rrAmount:
droneAmount = self.amount if ignoreState else self.amountActive
rrAmount *= droneAmount / (self.cycleTime / 1000)
self.__baseRemoteReps = (rrType, rrAmount)
return self.__baseRemoteReps
def changeType(self, typeID):
self.itemID = typeID
self.init()
def damageStats(self, targetResists=None):
if self.__dps is None:
self.__volley = 0
self.__dps = 0
if self.dealsDamage is True and self.amountActive > 0:
if self.hasAmmo:
attr = "missileLaunchDuration"
getter = self.getModifiedChargeAttr
else:
attr = "speed"
getter = self.getModifiedItemAttr
cycleTime = self.getModifiedItemAttr(attr)
volley = sum(
[(getter("%sDamage" % d) or 0) * (1 - getattr(targetResists, "%sAmount" % d, 0)) for d in self.DAMAGE_TYPES])
volley *= self.amountActive
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
self.__volley = volley
self.__dps = volley / (cycleTime / 1000.0)
return self.__dps, self.__volley
@property
def miningStats(self):
if self.__miningyield is None:
@@ -208,8 +239,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val
def clear(self):
self.__dps = None
self.__volley = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None
self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear()

View File

@@ -26,6 +26,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.saveddata.fighterAbility import FighterAbility
from eos.saveddata.module import Slot
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
@@ -87,8 +88,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__charge = None
self.__dps = None
self.__volley = None
self.__baseVolley = None
self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__chargeModifiedAttributes = ModifiedAttributeDict()
@@ -172,43 +172,88 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self):
return self.charge is not None
@property
def dps(self):
return self.damageStats()
def getVolley(self, targetResists=None):
if not self.active or self.amountActive <= 0:
return DmgTypes(0, 0, 0, 0)
if self.__baseVolley is None:
em = 0
therm = 0
kin = 0
exp = 0
for ability in self.abilities:
# Not passing resists here as we want to calculate and store base volley
abilityVolley = ability.getVolley()
em += abilityVolley.em
therm += abilityVolley.thermal
kin += abilityVolley.kinetic
exp += abilityVolley.explosive
self.__baseVolley = DmgTypes(em, therm, kin, exp)
volley = DmgTypes(
em=self.__baseVolley.em * (1 - getattr(targetResists, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
return volley
def damageStats(self, targetResists=None):
if self.__dps is None:
self.__volley = 0
self.__dps = 0
if self.active and self.amountActive > 0:
for ability in self.abilities:
dps, volley = ability.damageStats(targetResists)
self.__dps += dps
self.__volley += volley
# For forward compatability this assumes a fighter
# can have more than 2 damaging abilities and/or
# multiple that use charges.
if self.owner.factorReload:
activeTimes = []
reloadTimes = []
constantDps = 0
for ability in self.abilities:
if not ability.active:
continue
if ability.numShots == 0:
dps, volley = ability.damageStats(targetResists)
constantDps += dps
continue
activeTimes.append(ability.numShots * ability.cycleTime)
reloadTimes.append(ability.reloadTime)
if len(activeTimes) > 0:
shortestActive = sorted(activeTimes)[0]
longestReload = sorted(reloadTimes, reverse=True)[0]
self.__dps = max(constantDps, self.__dps * shortestActive / (shortestActive + longestReload))
return self.__dps, self.__volley
def getDps(self, targetResists=None):
if not self.active or self.amountActive <= 0:
return DmgTypes(0, 0, 0, 0)
# Analyze cooldowns when reload is factored in
if self.owner.factorReload:
activeTimes = []
reloadTimes = []
peakEm = 0
peakTherm = 0
peakKin = 0
peakExp = 0
steadyEm = 0
steadyTherm = 0
steadyKin = 0
steadyExp = 0
for ability in self.abilities:
abilityDps = ability.getDps(targetResists=targetResists)
# Peak dps
peakEm += abilityDps.em
peakTherm += abilityDps.thermal
peakKin += abilityDps.kinetic
peakExp += abilityDps.explosive
# Infinite use - add to steady dps
if ability.numShots == 0:
steadyEm += abilityDps.em
steadyTherm += abilityDps.thermal
steadyKin += abilityDps.kinetic
steadyExp += abilityDps.explosive
else:
activeTimes.append(ability.numShots * ability.cycleTime)
reloadTimes.append(ability.reloadTime)
steadyDps = DmgTypes(steadyEm, steadyTherm, steadyKin, steadyExp)
if len(activeTimes) > 0:
shortestActive = sorted(activeTimes)[0]
longestReload = sorted(reloadTimes, reverse=True)[0]
peakDps = DmgTypes(peakEm, peakTherm, peakKin, peakExp)
peakAdjustFactor = shortestActive / (shortestActive + longestReload)
peakDpsAdjusted = DmgTypes(
em=peakDps.em * peakAdjustFactor,
thermal=peakDps.thermal * peakAdjustFactor,
kinetic=peakDps.kinetic * peakAdjustFactor,
explosive=peakDps.explosive * peakAdjustFactor)
dps = max(steadyDps, peakDpsAdjusted, key=lambda d: d.total)
return dps
else:
return steadyDps
# Just sum all abilities when not taking reload into consideration
else:
em = 0
therm = 0
kin = 0
exp = 0
for ability in self.abilities:
abilityDps = ability.getDps(targetResists=targetResists)
em += abilityDps.em
therm += abilityDps.thermal
kin += abilityDps.kinetic
exp += abilityDps.explosive
return DmgTypes(em, therm, kin, exp)
@property
def maxRange(self):
@@ -251,8 +296,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val
def clear(self):
self.__dps = None
self.__volley = None
self.__baseVolley = None
self.__miningyield = None
self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear()

View File

@@ -21,12 +21,12 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
class FighterAbility(object):
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
DAMAGE_TYPES2 = ("EM", "Kin", "Exp", "Therm")
# We aren't able to get data on the charges that can be stored with fighters. So we hardcode that data here, keyed
# with the fighter squadron role
@@ -118,30 +118,38 @@ class FighterAbility(object):
return speed
def damageStats(self, targetResists=None):
if self.__dps is None:
self.__volley = 0
self.__dps = 0
if self.dealsDamage and self.active:
cycleTime = self.cycleTime
def getVolley(self, targetResists=None):
if not self.dealsDamage or not self.active:
return DmgTypes(0, 0, 0, 0)
if self.attrPrefix == "fighterAbilityLaunchBomb":
em = self.fighter.getModifiedChargeAttr("emDamage", 0)
therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0)
kin = self.fighter.getModifiedChargeAttr("kineticDamage", 0)
exp = self.fighter.getModifiedChargeAttr("explosiveDamage", 0)
else:
em = self.fighter.getModifiedItemAttr("{}DamageEM".format(self.attrPrefix), 0)
therm = self.fighter.getModifiedItemAttr("{}DamageTherm".format(self.attrPrefix), 0)
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amountActive * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes(
em=em * dmgMult * (1 - getattr(targetResists, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetResists, "explosiveAmount", 0)))
return volley
if self.attrPrefix == "fighterAbilityLaunchBomb":
# bomb calcs
volley = sum([(self.fighter.getModifiedChargeAttr("%sDamage" % attr) or 0) * (
1 - getattr(targetResists, "%sAmount" % attr, 0)) for attr in self.DAMAGE_TYPES])
else:
volley = sum(map(lambda d2, d:
(self.fighter.getModifiedItemAttr(
"{}Damage{}".format(self.attrPrefix, d2)) or 0) *
(1 - getattr(targetResists, "{}Amount".format(d), 0)),
self.DAMAGE_TYPES2, self.DAMAGE_TYPES))
volley *= self.fighter.amountActive
volley *= self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix)) or 1
self.__volley += volley
self.__dps += volley / (cycleTime / 1000.0)
return self.__dps, self.__volley
def getDps(self, targetResists=None):
volley = self.getVolley(targetResists=targetResists)
if not volley:
return DmgTypes(0, 0, 0, 0)
dpsFactor = 1 / (self.cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
return dps
def clear(self):
self.__dps = None

View File

@@ -34,6 +34,7 @@ from eos.saveddata.drone import Drone
from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel
from eos.saveddata.module import Module, State, Slot, Hardpoint
from eos.utils.stats import DmgTypes
from logbook import Logger
pyfalog = Logger(__name__)
@@ -120,10 +121,11 @@ class Fit(object):
def build(self):
self.__extraDrains = []
self.__ehp = None
self.__weaponDPS = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None
self.__weaponVolley = None
self.__droneDPS = None
self.__droneDps = None
self.__droneVolley = None
self.__droneYield = None
self.__sustainableTank = None
@@ -135,12 +137,6 @@ class Fit(object):
self.__capUsed = None
self.__capRecharge = None
self.__calculatedTargets = []
self.__remoteReps = {
"Armor" : None,
"Shield" : None,
"Hull" : None,
"Capacitor": None,
}
self.factorReload = False
self.boostsFits = set()
self.gangBoosts = None
@@ -154,9 +150,9 @@ class Fit(object):
@targetResists.setter
def targetResists(self, targetResists):
self.__targetResists = targetResists
self.__weaponDPS = None
self.__weaponVolley = None
self.__droneDPS = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__droneDps = None
self.__droneVolley = None
@property
@@ -277,41 +273,31 @@ class Fit(object):
def projectedFighters(self):
return self.__projectedFighters
@property
def weaponDPS(self):
if self.__weaponDPS is None:
self.calculateWeaponStats()
def getWeaponDps(self, spoolOptions=None):
if spoolOptions not in self.__weaponDpsMap:
self.calculateWeaponDmgStats(spoolOptions)
return self.__weaponDpsMap[spoolOptions]
return self.__weaponDPS
def getWeaponVolley(self, spoolOptions=None):
if spoolOptions not in self.__weaponVolleyMap:
self.calculateWeaponDmgStats(spoolOptions)
return self.__weaponVolleyMap[spoolOptions]
@property
def weaponVolley(self):
if self.__weaponVolley is None:
self.calculateWeaponStats()
def getDroneDps(self):
if self.__droneDps is None:
self.calculateDroneDmgStats()
return self.__droneDps
return self.__weaponVolley
@property
def droneDPS(self):
if self.__droneDPS is None:
self.calculateWeaponStats()
return self.__droneDPS
@property
def droneVolley(self):
def getDroneVolley(self):
if self.__droneVolley is None:
self.calculateWeaponStats()
self.calculateDroneDmgStats()
return self.__droneVolley
@property
def totalDPS(self):
return self.droneDPS + self.weaponDPS
def getTotalDps(self, spoolOptions=None):
return self.getDroneDps() + self.getWeaponDps(spoolOptions=spoolOptions)
@property
def totalVolley(self):
return self.droneVolley + self.weaponVolley
def getTotalVolley(self, spoolOptions=None):
return self.getDroneVolley() + self.getWeaponVolley(spoolOptions=spoolOptions)
@property
def minerYield(self):
@@ -409,12 +395,13 @@ class Fit(object):
def clear(self, projected=False, command=False):
self.__effectiveTank = None
self.__weaponDPS = None
self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None
self.__weaponVolley = None
self.__effectiveSustainableTank = None
self.__sustainableTank = None
self.__droneDPS = None
self.__droneDps = None
self.__droneVolley = None
self.__droneYield = None
self.__ehp = None
@@ -426,9 +413,6 @@ class Fit(object):
self.ecmProjectedStr = 1
# self.commandBonuses = {}
for remoterep_type in self.__remoteReps:
self.__remoteReps[remoterep_type] = None
del self.__calculatedTargets[:]
del self.__extraDrains[:]
@@ -1032,11 +1016,11 @@ class Fit(object):
@property
def pgUsed(self):
return self.getItemAttrOnlineSum(self.modules, "power")
return round(self.getItemAttrOnlineSum(self.modules, "power"), 2)
@property
def cpuUsed(self):
return self.getItemAttrOnlineSum(self.modules, "cpu")
return round(self.getItemAttrOnlineSum(self.modules, "cpu"), 2)
@property
def droneBandwidthUsed(self):
@@ -1151,149 +1135,6 @@ class Fit(object):
return self.__capRecharge
@property
def sustainableTank(self):
if self.__sustainableTank is None:
self.calculateSustainableTank()
return self.__sustainableTank
def calculateSustainableTank(self, effective=True):
if self.__sustainableTank is None:
if self.capStable and not self.factorReload:
sustainable = {
"armorRepair" : self.extraAttributes["armorRepair"],
"shieldRepair": self.extraAttributes["shieldRepair"],
"hullRepair" : self.extraAttributes["hullRepair"]
}
else:
sustainable = {}
repairers = []
# Map a repairer type to the attribute it uses
groupAttrMap = {
"Shield Booster": "shieldBonus",
"Ancillary Shield Booster": "shieldBonus",
"Remote Shield Booster": "shieldBonus",
"Ancillary Remote Shield Booster": "shieldBonus",
"Armor Repair Unit": "armorDamageAmount",
"Ancillary Armor Repairer": "armorDamageAmount",
"Remote Armor Repairer": "armorDamageAmount",
"Ancillary Remote Armor Repairer": "armorDamageAmount",
"Hull Repair Unit": "structureDamageAmount",
"Remote Hull Repairer": "structureDamageAmount",
}
# Map repairer type to attribute
groupStoreMap = {
"Shield Booster": "shieldRepair",
"Remote Shield Booster": "shieldRepair",
"Ancillary Shield Booster": "shieldRepair",
"Ancillary Remote Shield Booster": "shieldRepair",
"Armor Repair Unit": "armorRepair",
"Remote Armor Repairer": "armorRepair",
"Ancillary Armor Repairer": "armorRepair",
"Ancillary Remote Armor Repairer": "armorRepair",
"Hull Repair Unit": "hullRepair",
"Remote Hull Repairer": "hullRepair",
}
capUsed = self.capUsed
for attr in ("shieldRepair", "armorRepair", "hullRepair"):
sustainable[attr] = self.extraAttributes[attr]
dict = self.extraAttributes.getAfflictions(attr)
if self in dict:
for mod, _, amount, used in dict[self]:
if not used:
continue
if mod.projected is False:
usesCap = True
try:
if mod.capUse:
capUsed -= mod.capUse
else:
usesCap = False
except AttributeError:
usesCap = False
# Normal Repairers
if usesCap and not mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
sustainable[attr] -= amount / (cycleTime / 1000.0)
repairers.append(mod)
# Ancillary Armor reps etc
elif usesCap and mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
sustainable[attr] -= amount * multiplier / (cycleTime / 1000.0)
repairers.append(mod)
# Ancillary Shield boosters etc
elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
else:
reloadtime = 0.0
offdutycycle = reloadtime / ((max(mod.numShots, 1) * cycleTime) + reloadtime)
sustainable[attr] -= amount * offdutycycle / (cycleTime / 1000.0)
# Sort repairers by efficiency. We want to use the most efficient repairers first
repairers.sort(key=lambda _mod: _mod.getModifiedItemAttr(
groupAttrMap[_mod.item.group.name]) * (_mod.getModifiedItemAttr(
"chargedArmorDamageMultiplier") or 1) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True)
# Loop through every module until we're above peak recharge
# Most efficient first, as we sorted earlier.
# calculate how much the repper can rep stability & add to total
totalPeakRecharge = self.capRecharge
for mod in repairers:
if capUsed > totalPeakRecharge:
break
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
else:
reloadtime = 0.0
cycleTime = mod.rawCycleTime
capPerSec = mod.capUse
if capPerSec is not None and cycleTime is not None:
# Check how much this repper can work
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
# Add the sustainable amount
if not mod.charge:
sustainable[groupStoreMap[mod.item.group.name]] += sustainability * amount / (
cycleTime / 1000.0)
else:
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
ondutycycle = (max(mod.numShots, 1) * cycleTime) / (
(max(mod.numShots, 1) * cycleTime) + reloadtime)
sustainable[groupStoreMap[
mod.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
cycleTime / 1000.0)
capUsed += capPerSec
sustainable["passiveShield"] = self.calculateShieldRecharge()
self.__sustainableTank = sustainable
return self.__sustainableTank
def calculateCapRecharge(self, percent=PEAK_RECHARGE):
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
@@ -1377,92 +1218,27 @@ class Fit(object):
self.__capStable = True
self.__capState = 100
@property
def remoteReps(self):
force_recalc = False
for remote_type in self.__remoteReps:
if self.__remoteReps[remote_type] is None:
force_recalc = True
break
def getRemoteReps(self, spoolOptions=None):
if spoolOptions not in self.__remoteRepMap:
remoteReps = {}
if force_recalc is False:
return self.__remoteReps
for module in self.modules:
rrType, rrAmount = module.getRemoteReps(spoolOptions=spoolOptions)
if rrType:
if rrType not in remoteReps:
remoteReps[rrType] = 0
remoteReps[rrType] += rrAmount
# We are rerunning the recalcs. Explicitly set to 0 to make sure we don't duplicate anything and correctly set
# all values to 0.
for remote_type in self.__remoteReps:
self.__remoteReps[remote_type] = 0
for drone in self.drones:
rrType, rrAmount = drone.getRemoteReps()
if rrType:
if rrType not in remoteReps:
remoteReps[rrType] = 0
remoteReps[rrType] += rrAmount
for stuff in chain(self.modules, self.drones):
if stuff.item:
if stuff.item.ID == 10250:
pass
remote_type = None
self.__remoteRepMap[spoolOptions] = remoteReps
# Only apply the charged multiplier if we have a charge in our ancil reppers (#1135)
if stuff.charge:
modifier = stuff.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
else:
modifier = 1
if isinstance(stuff, Module) and (stuff.isEmpty or stuff.state < State.ACTIVE):
continue
elif isinstance(stuff, Drone):
# drones don't have fueled charges, so simply override modifier with the amount of drones active
modifier = stuff.amountActive
# Covert cycleTime to seconds
duration = stuff.cycleTime / 1000
# Skip modules with no duration.
if not duration:
continue
remote_module_groups = {
"Remote Armor Repairer" : "Armor",
"Ancillary Remote Armor Repairer": "Armor",
"Remote Hull Repairer" : "Hull",
"Remote Shield Booster" : "Shield",
"Ancillary Remote Shield Booster": "Shield",
"Remote Capacitor Transmitter" : "Capacitor",
}
module_group = stuff.item.group.name
if module_group in remote_module_groups:
remote_type = remote_module_groups[module_group]
elif not isinstance(stuff, Drone):
# Module isn't in our list of remote rep modules, bail
continue
if remote_type == "Hull":
hp = stuff.getModifiedItemAttr("structureDamageAmount", 0)
elif remote_type == "Armor":
hp = stuff.getModifiedItemAttr("armorDamageAmount", 0)
elif remote_type == "Shield":
hp = stuff.getModifiedItemAttr("shieldBonus", 0)
elif remote_type == "Capacitor":
hp = stuff.getModifiedItemAttr("powerTransferAmount", 0)
else:
droneShield = stuff.getModifiedItemAttr("shieldBonus", 0)
droneArmor = stuff.getModifiedItemAttr("armorDamageAmount", 0)
droneHull = stuff.getModifiedItemAttr("structureDamageAmount", 0)
if droneShield:
remote_type = "Shield"
hp = droneShield
elif droneArmor:
remote_type = "Armor"
hp = droneArmor
elif droneHull:
remote_type = "Hull"
hp = droneHull
else:
hp = 0
if hp > 0 and duration > 0:
self.__remoteReps[remote_type] += (hp * modifier) / duration
return self.__remoteReps
return self.__remoteRepMap[spoolOptions]
@property
def hp(self):
@@ -1485,11 +1261,14 @@ class Fit(object):
@property
def tank(self):
hps = {"passiveShield": self.calculateShieldRecharge()}
for type in ("shield", "armor", "hull"):
hps["%sRepair" % type] = self.extraAttributes["%sRepair" % type]
return hps
reps = {
"passiveShield": self.calculateShieldRecharge(),
"shieldRepair": self.extraAttributes["shieldRepair"],
"armorRepair": self.extraAttributes["armorRepair"],
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"],
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"],
"hullRepair": self.extraAttributes["hullRepair"]}
return reps
@property
def effectiveTank(self):
@@ -1497,24 +1276,153 @@ class Fit(object):
if self.damagePattern is None:
ehps = self.tank
else:
ehps = self.damagePattern.calculateEffectiveTank(self, self.extraAttributes)
ehps = self.damagePattern.calculateEffectiveTank(self, self.tank)
self.__effectiveTank = ehps
return self.__effectiveTank
@property
def sustainableTank(self):
if self.__sustainableTank is None:
self.calculateSustainableTank()
return self.__sustainableTank
@property
def effectiveSustainableTank(self):
if self.__effectiveSustainableTank is None:
if self.damagePattern is None:
eshps = self.sustainableTank
tank = self.sustainableTank
else:
eshps = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank)
self.__effectiveSustainableTank = eshps
tank = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank)
self.__effectiveSustainableTank = tank
return self.__effectiveSustainableTank
def calculateSustainableTank(self):
if self.__sustainableTank is None:
sustainable = {
"passiveShield": self.calculateShieldRecharge(),
"shieldRepair": self.extraAttributes["shieldRepair"],
"armorRepair": self.extraAttributes["armorRepair"],
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"],
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"],
"hullRepair": self.extraAttributes["hullRepair"]}
if not self.capStable or self.factorReload:
# Map a local repairer type to the attribute it uses
groupAttrMap = {
"Shield Booster": "shieldBonus",
"Ancillary Shield Booster": "shieldBonus",
"Armor Repair Unit": "armorDamageAmount",
"Ancillary Armor Repairer": "armorDamageAmount",
"Hull Repair Unit": "structureDamageAmount"}
# Map local repairer type to tank type
groupStoreMap = {
"Shield Booster": "shieldRepair",
"Ancillary Shield Booster": "shieldRepair",
"Armor Repair Unit": "armorRepair",
"Ancillary Armor Repairer": "armorRepair",
"Hull Repair Unit": "hullRepair"}
repairers = []
localAdjustment = {"shieldRepair": 0, "armorRepair": 0, "hullRepair": 0}
capUsed = self.capUsed
for tankType in localAdjustment:
dict = self.extraAttributes.getAfflictions(tankType)
if self in dict:
for mod, _, amount, used in dict[self]:
if not used:
continue
if mod.projected:
continue
if mod.item.group.name not in groupAttrMap:
continue
usesCap = True
try:
if mod.capUse:
capUsed -= mod.capUse
else:
usesCap = False
except AttributeError:
usesCap = False
# Normal Repairers
if usesCap and not mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
localAdjustment[tankType] -= amount / (cycleTime / 1000.0)
repairers.append(mod)
# Ancillary Armor reps etc
elif usesCap and mod.charge:
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
localAdjustment[tankType] -= amount * multiplier / (cycleTime / 1000.0)
repairers.append(mod)
# Ancillary Shield boosters etc
elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
cycleTime = mod.rawCycleTime
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
else:
reloadtime = 0.0
offdutycycle = reloadtime / ((max(mod.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[tankType] -= amount * offdutycycle / (cycleTime / 1000.0)
# Sort repairers by efficiency. We want to use the most efficient repairers first
repairers.sort(key=lambda _mod: _mod.getModifiedItemAttr(
groupAttrMap[_mod.item.group.name]) * (_mod.getModifiedItemAttr(
"chargedArmorDamageMultiplier") or 1) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True)
# Loop through every module until we're above peak recharge
# Most efficient first, as we sorted earlier.
# calculate how much the repper can rep stability & add to total
totalPeakRecharge = self.capRecharge
for mod in repairers:
if capUsed > totalPeakRecharge:
break
if self.factorReload and mod.charge:
reloadtime = mod.reloadTime
else:
reloadtime = 0.0
cycleTime = mod.rawCycleTime
capPerSec = mod.capUse
if capPerSec is not None and cycleTime is not None:
# Check how much this repper can work
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
# Add the sustainable amount
if not mod.charge:
localAdjustment[groupStoreMap[mod.item.group.name]] += sustainability * amount / (
cycleTime / 1000.0)
else:
if mod.charge.name == "Nanite Repair Paste":
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
else:
multiplier = 1
ondutycycle = (max(mod.numShots, 1) * cycleTime) / (
(max(mod.numShots, 1) * cycleTime) + reloadtime)
localAdjustment[groupStoreMap[
mod.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
cycleTime / 1000.0)
capUsed += capPerSec
sustainable["shieldRepair"] += localAdjustment["shieldRepair"]
sustainable["armorRepair"] += localAdjustment["armorRepair"]
sustainable["armorRepairPreSpool"] += localAdjustment["armorRepair"]
sustainable["armorRepairFullSpool"] += localAdjustment["armorRepair"]
sustainable["hullRepair"] += localAdjustment["hullRepair"]
self.__sustainableTank = sustainable
return self.__sustainableTank
def calculateLockTime(self, radius):
scanRes = self.ship.getModifiedItemAttr("scanResolution")
if scanRes is not None and scanRes > 0:
@@ -1537,30 +1445,30 @@ class Fit(object):
self.__minerYield = minerYield
self.__droneYield = droneYield
def calculateWeaponStats(self):
weaponDPS = 0
droneDPS = 0
weaponVolley = 0
droneVolley = 0
def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes(0, 0, 0, 0)
weaponDps = DmgTypes(0, 0, 0, 0)
for mod in self.modules:
dps, volley = mod.damageStats(self.targetResists)
weaponDPS += dps
weaponVolley += volley
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetResists=self.targetResists)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetResists=self.targetResists)
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self):
droneVolley = DmgTypes(0, 0, 0, 0)
droneDps = DmgTypes(0, 0, 0, 0)
for drone in self.drones:
dps, volley = drone.damageStats(self.targetResists)
droneDPS += dps
droneVolley += volley
droneVolley += drone.getVolley(targetResists=self.targetResists)
droneDps += drone.getDps(targetResists=self.targetResists)
for fighter in self.fighters:
dps, volley = fighter.damageStats(self.targetResists)
droneDPS += dps
droneVolley += volley
droneVolley += fighter.getVolley(targetResists=self.targetResists)
droneDps += fighter.getDps(targetResists=self.targetResists)
self.__weaponDPS = weaponDPS
self.__weaponVolley = weaponVolley
self.__droneDPS = droneDPS
self.__droneDps = droneDps
self.__droneVolley = droneVolley
@property

View File

@@ -28,6 +28,9 @@ from eos.enum import Enum
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
@@ -95,7 +98,6 @@ class Hardpoint(Enum):
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes"""
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
MINING_ATTRIBUTES = ("miningAmount",)
SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object")
@@ -168,9 +170,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.__charge and self.__charge.category.name != "Charge":
self.__charge = None
self.__dps = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None
self.__volley = None
self.__reloadTime = None
self.__reloadForce = None
self.__chargeCycles = None
@@ -247,8 +249,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if chargeVolume is None or containerCapacity is None:
charges = 0
else:
charges = floor(containerCapacity / chargeVolume)
return int(charges)
charges = int(floatUnerr(containerCapacity / chargeVolume))
return charges
@property
def numShots(self):
@@ -411,35 +413,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__itemModifiedAttributes.clear()
def damageStats(self, targetResists):
if self.__dps is None:
self.__dps = 0
self.__volley = 0
if not self.isEmpty and self.state >= State.ACTIVE:
if self.charge:
func = self.getModifiedChargeAttr
else:
func = self.getModifiedItemAttr
volley = sum([(func("%sDamage" % attr) or 0) * (1 - getattr(targetResists, "%sAmount" % attr, 0)) for attr in self.DAMAGE_TYPES])
volley *= self.getModifiedItemAttr("damageMultiplier") or 1
# Disintegrator-specific ramp-up multiplier
volley *= (self.getModifiedItemAttr("damageMultiplierBonusMax") or 0) + 1
if volley:
cycleTime = self.cycleTime
# Some weapons repeat multiple times in one cycle (think doomsdays)
# Get the number of times it fires off
weaponDoT = max(
self.getModifiedItemAttr("doomsdayDamageDuration", 1) / self.getModifiedItemAttr("doomsdayDamageCycleTime", 1),
1
)
self.__volley = volley
self.__dps = (volley * weaponDoT) / (cycleTime / 1000.0)
return self.__dps, self.__volley
@property
def miningStats(self):
if self.__miningyield is None:
@@ -459,13 +432,110 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__miningyield
@property
def dps(self):
return self.damageStats(None)[0]
def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False):
if self.isEmpty or (self.state < State.ACTIVE and not ignoreState):
return DmgTypes(0, 0, 0, 0)
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
self.__baseVolley = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost
volley = DmgTypes(
em=self.__baseVolley.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)),
thermal=self.__baseVolley.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0)))
return volley
@property
def volley(self):
return self.damageStats(None)[1]
def getDps(self, spoolOptions=None, targetResists=None, ignoreState=False):
volley = self.getVolley(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
if not volley:
return DmgTypes(0, 0, 0, 0)
# Some weapons repeat multiple times in one cycle (bosonic doomsdays). Get the number of times it fires off
volleysPerCycle = max(self.getModifiedItemAttr("doomsdayDamageDuration", 1) / self.getModifiedItemAttr("doomsdayDamageCycleTime", 1), 1)
dpsFactor = volleysPerCycle / (self.cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
return dps
def getRemoteReps(self, spoolOptions=None, ignoreState=False):
if self.isEmpty or (self.state < State.ACTIVE and not ignoreState):
return None, 0
def getBaseRemoteReps(module):
remoteModuleGroups = {
"Remote Armor Repairer": "Armor",
"Ancillary Remote Armor Repairer": "Armor",
"Mutadaptive Remote Armor Repairer": "Armor",
"Remote Hull Repairer": "Hull",
"Remote Shield Booster": "Shield",
"Ancillary Remote Shield Booster": "Shield",
"Remote Capacitor Transmitter": "Capacitor"}
rrType = remoteModuleGroups.get(module.item.group.name, None)
if not rrType:
return None, 0
if rrType == "Hull":
rrAmount = module.getModifiedItemAttr("structureDamageAmount", 0)
elif rrType == "Armor":
rrAmount = module.getModifiedItemAttr("armorDamageAmount", 0)
elif rrType == "Shield":
rrAmount = module.getModifiedItemAttr("shieldBonus", 0)
elif rrType == "Capacitor":
rrAmount = module.getModifiedItemAttr("powerTransferAmount", 0)
else:
return None, 0
if rrAmount:
rrAmount *= 1 / (self.cycleTime / 1000)
if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge:
rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
return rrType, rrAmount
if self.__baseRemoteReps is None:
self.__baseRemoteReps = getBaseRemoteReps(self)
rrType, rrAmount = self.__baseRemoteReps
if rrType and rrAmount and self.item.group.name == "Mutadaptive Remote Armor Repairer":
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("repairMultiplierBonusMax", 0),
self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
rrAmount *= (1 + spoolBoost)
return rrType, rrAmount
def getSpoolData(self, spoolOptions=None):
weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0)
weaponMultPerCycle = self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0)
if weaponMultMax and weaponMultPerCycle:
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
_, spoolCycles, spoolTime = calculateSpoolup(
weaponMultMax, weaponMultPerCycle,
self.rawCycleTime / 1000, spoolType, spoolAmount)
return spoolCycles, spoolTime
rrMultMax = self.getModifiedItemAttr("repairMultiplierBonusMax", 0)
rrMultPerCycle = self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0)
if rrMultMax and rrMultPerCycle:
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
_, spoolCycles, spoolTime = calculateSpoolup(
rrMultMax, rrMultPerCycle,
self.rawCycleTime / 1000, spoolType, spoolAmount)
return spoolCycles, spoolTime
return 0, 0
@property
def reloadTime(self):
@@ -718,9 +788,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val
def clear(self):
self.__dps = None
self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None
self.__volley = None
self.__reloadTime = None
self.__reloadForce = None
self.__chargeCycles = None

View File

@@ -18,24 +18,30 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import time
from sqlalchemy.orm import reconstructor
import time
from enum import IntEnum, unique
from logbook import Logger
pyfalog = Logger(__name__)
@unique
class PriceStatus(IntEnum):
notFetched = 0
success = 1
fail = 2
notSupported = 3
class Price(object):
def __init__(self, typeID):
self.typeID = typeID
self.time = 0
self.__price = 0
self.failed = None
@reconstructor
def init(self):
self.__item = None
self.status = PriceStatus.notFetched
@property
def isValid(self):
@@ -43,7 +49,10 @@ class Price(object):
@property
def price(self):
return self.__price or 0.0
if self.status != PriceStatus.success:
return 0
else:
return self.__price or 0
@price.setter
def price(self, price):

View File

@@ -29,14 +29,16 @@ pyfalog = Logger(__name__)
class Ship(ItemAttrShortcut, HandledItem):
EXTRA_ATTRIBUTES = {
"armorRepair" : 0,
"hullRepair" : 0,
"shieldRepair" : 0,
"maxActiveDrones" : 0,
"armorRepair": 0,
"armorRepairPreSpool": 0,
"armorRepairFullSpool": 0,
"hullRepair": 0,
"shieldRepair": 0,
"maxActiveDrones": 0,
"maxTargetsLockedFromSkills": 2,
"droneControlRange" : 20000,
"cloaked" : False,
"siege" : False
"droneControlRange": 20000,
"cloaked": False,
"siege": False
# We also have speedLimit for Entosis Link, but there seems to be an
# issue with naming it exactly "speedLimit" due to unknown reasons.
# Regardless, we don't have to put it here anyways - it will come up

0
eos/utils/__init__.py Normal file
View File

26
eos/utils/float.py Normal file
View File

@@ -0,0 +1,26 @@
"""
Sometimes use of floats may lead to undesirable results, e.g.
int(2.3 / 0.1) = 22.
We cannot afford to use different number representations (e.g. representations
provided by decimal or fraction modules), thus consequences are worked around by
this module.
"""
import math
import sys
# As we will be rounding numbers after operations (which introduce higher error
# than base float representation error), we need to keep less significant
# numbers than for single float number w/o operations
keepDigits = int(sys.float_info.dig / 2)
def floatUnerr(value):
"""Round possible float number error, killing some precision in process."""
if value == 0:
return value
# Find round factor, taking into consideration that we want to keep at least
# predefined amount of significant digits
roundFactor = int(keepDigits - math.ceil(math.log10(abs(value))))
return round(value, roundFactor)

70
eos/utils/spoolSupport.py Normal file
View File

@@ -0,0 +1,70 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from collections import namedtuple
from enum import IntEnum, unique
from eos.utils.float import floatUnerr
SpoolOptions = namedtuple('SpoolOptions', ('spoolType', 'spoolAmount', 'force'))
@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
def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount):
"""
Calculate damage multiplier increment based on passed parameters. Module cycle time
is specified in seconds.
Returns spoolup value, amount of cycles to reach it and time to reach it.
"""
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
elif spoolType == SpoolType.TIME:
cycles = min(int(floatUnerr(spoolAmount / modCycleTime)), int(floatUnerr(modMaxValue / modStepValue)))
return cycles * modStepValue, cycles, cycles * modCycleTime
elif spoolType == SpoolType.CYCLES:
cycles = min(int(spoolAmount), int(floatUnerr(modMaxValue / modStepValue)))
return cycles * modStepValue, cycles, cycles * modCycleTime
else:
return 0, 0, 0
def resolveSpoolOptions(spoolOptions, module):
# Rely on passed options if they are forcing us to do so
if spoolOptions is not None and spoolOptions.force:
return spoolOptions.spoolType, spoolOptions.spoolAmount
# If we're not forced to use options and module has options set, prefer on-module values
elif module is not None and module.spoolType is not None:
return module.spoolType, module.spoolAmount
# Otherwise - rely on passed options
elif spoolOptions is not None:
return spoolOptions.spoolType, spoolOptions.spoolAmount
else:
return None, None

70
eos/utils/stats.py Normal file
View File

@@ -0,0 +1,70 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
class DmgTypes:
"""Container for damage data stats."""
def __init__(self, em, thermal, kinetic, explosive):
self.em = em
self.thermal = thermal
self.kinetic = kinetic
self.explosive = explosive
self._calcTotal()
# Iterator is needed to support tuple-style unpacking
def __iter__(self):
yield self.em
yield self.thermal
yield self.kinetic
yield self.explosive
yield self.total
def __eq__(self, other):
if not isinstance(other, DmgTypes):
return NotImplemented
return all((
self.em == other.em,
self.thermal == other.thermal,
self.kinetic == other.kinetic,
self.explosive == other.explosive,
self.total == other.total))
def __bool__(self):
return any((
self.em, self.thermal, self.kinetic,
self.explosive, self.total))
def _calcTotal(self):
self.total = self.em + self.thermal + self.kinetic + self.explosive
def __add__(self, other):
return type(self)(
em=self.em + other.em,
thermal=self.thermal + other.thermal,
kinetic=self.kinetic + other.kinetic,
explosive=self.explosive + other.explosive)
def __iadd__(self, other):
self.em += other.em
self.thermal += other.thermal
self.kinetic += other.kinetic
self.explosive += other.explosive
self._calcTotal()
return self

BIN
eve.db

Binary file not shown.

View File

@@ -19,7 +19,7 @@
import config
versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion)
versionString = "{0}".format(config.version)
licenses = (
"pyfa is released under GNU GPLv3 - see included LICENSE file",
"All EVE-Online related materials are property of CCP hf.",

View File

@@ -4,6 +4,7 @@ import wx
from service.fit import Fit
import gui.globalEvents as GE
import gui.mainFrame
from gui.utils.helpers_wxPython import HandleCtrlBackspace
class NotesView(wx.Panel):
@@ -17,9 +18,16 @@ class NotesView(wx.Panel):
self.SetSizer(mainSizer)
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.Bind(wx.EVT_TEXT, self.onText)
self.editNotes.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.saveTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.delayedSave, self.saveTimer)
def OnKeyDown(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.editNotes)
else:
event.Skip()
def fitChanged(self, event):
sFit = Fit.getInstance()
fit = sFit.getFit(event.fitID)

View File

@@ -0,0 +1,34 @@
from gui.contextMenu import ContextMenu
import gui.mainFrame
# noinspection PyPackageRequirements
import wx
import gui.globalEvents as GE
from service.settings import ContextMenuSettings
import gui.fitCommands as cmd
class FillWithModule(ContextMenu):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, srcContext, selection):
if not self.settings.get('moduleFill'):
return False
return srcContext in ("fittingModule")
def getText(self, itmContext, selection):
return u"Fill With {0}".format(itmContext if itmContext is not None else "Module")
def activate(self, fullContext, selection, i):
srcContext = fullContext[0]
fitID = self.mainFrame.getActiveFit()
if srcContext == "fittingModule":
self.mainFrame.command.Submit(cmd.GuiFillWithModuleCommand(fitID, selection[0].itemID))
return # the command takes care of the PostEvent
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
FillWithModule.register()

View File

@@ -26,7 +26,10 @@ class MarketJump(ContextMenu):
sMkt = Market.getInstance()
item = getattr(selection[0], "item", selection[0])
isMutated = getattr(selection[0], "isMutated", False)
mktGrp = sMkt.getMarketGroupByItem(item)
if mktGrp is None and isMutated:
mktGrp = sMkt.getMarketGroupByItem(selection[0].baseItem)
# 1663 is Special Edition Festival Assets, we don't have root group for it
if mktGrp is None or mktGrp.ID == 1663:
@@ -43,7 +46,10 @@ class MarketJump(ContextMenu):
if srcContext in ("fittingCharge", "projectedCharge"):
item = selection[0].charge
elif hasattr(selection[0], "item"):
item = selection[0].item
if getattr(selection[0], "isMutated", False):
item = selection[0].baseItem
else:
item = selection[0].item
else:
item = selection[0]

View File

@@ -1,7 +1,10 @@
import math
import wx
import wx.lib.newevent
from gui.attribute_gauge import AttributeGauge
from eos.utils.float import floatUnerr
_ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent()
@@ -58,22 +61,41 @@ class AttributeSlider(wx.Panel):
self.inverse = inverse
# The internal slider basically represents the percentage towards the end of the range. It has to be normalized
# in this way, otherwise when we start off with a base, if the range is skewed to one side, the base value won't
# be centered. We use a range of -100,100 so that we can depend on the SliderValue to contain the percentage
# toward one end
def getStep(valRange):
"""
Find step for the passed range, which is based on 1, 2 or 5.
Step returned will make sure that range fits 10..50 of them,
as close to 10 as possible.
"""
steps = {1: None, 2: None, 5: None}
for baseInc in steps:
baseIncAmount = valRange / baseInc
incScale = math.floor(math.log10(baseIncAmount) - 1)
steps[baseInc] = baseInc * 10 ** incScale
chosenBase = min(steps, key=lambda base: valRange / steps[base])
chosenStep = steps[chosenBase]
if inverse:
chosenStep *= -1
return chosenStep
# Additionally, since we want the slider to be accurate to 3 decimal places, we need to blow out the two ends here
# (if we have a slider that needs to land on 66.66% towards the right, it will actually be converted to 66%. Se we need it to support 6,666)
#
# self.SliderMinValue = -100
# self.SliderMaxValue = 100
# self.SliderValue = 0
def getDigitPlaces(minValue, maxValue):
minDigits = 3
maxDigits = 5
currentDecision = minDigits
for value in (floatUnerr(minValue), floatUnerr(maxValue)):
for currentDigit in range(minDigits, maxDigits + 1):
if round(value, currentDigit) == value:
if currentDigit > currentDecision:
currentDecision = currentDigit
break
# Max decimal places we can afford to show was not enough
else:
return maxDigits
return currentDecision
range = [self.UserMinValue, self.UserMaxValue]
self.ctrl = wx.SpinCtrlDouble(self, min=minValue, max=maxValue, inc=getStep(maxValue - minValue))
self.ctrl.SetDigits(getDigitPlaces(minValue, maxValue))
self.ctrl = wx.SpinCtrlDouble(self, min=min(range), max=max(range))
self.ctrl.SetDigits(3)
self.ctrl.Bind(wx.EVT_SPINCTRLDOUBLE, self.UpdateValue)

View File

@@ -8,6 +8,10 @@ from service.attribute import Attribute
from gui.utils.numberFormatter import formatAmount
def defaultSort(item):
return (item.attributes['metaLevel'].value if 'metaLevel' in item.attributes else 0, item.name)
class ItemCompare(wx.Panel):
def __init__(self, parent, stuff, item, items, context=None):
# Start dealing with Price stuff to get that thread going
@@ -27,8 +31,7 @@ class ItemCompare(wx.Panel):
self.currentSort = None
self.sortReverse = False
self.item = item
self.items = sorted(items,
key=lambda x: x.attributes['metaLevel'].value if 'metaLevel' in x.attributes else 0)
self.items = sorted(items, key=defaultSort)
self.attrs = {}
# get a dict of attrName: attrInfo of all unique attributes across all items
@@ -126,10 +129,15 @@ class ItemCompare(wx.Panel):
# starts at 0 while the list has the item name as column 0.
attr = str(list(self.attrs.keys())[sort - 1])
func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0
# Clicked on a column that's not part of our array (price most likely)
except IndexError:
# Clicked on a column that's not part of our array (price most likely)
self.sortReverse = False
func = lambda _val: _val.attributes['metaLevel'].value if 'metaLevel' in _val.attributes else 0.0
# Price
if sort == len(self.attrs) + 1:
func = lambda i: i.price.price if i.price.price != 0 else float("Inf")
# Something else
else:
self.sortReverse = False
func = defaultSort
self.items = sorted(self.items, key=func, reverse=self.sortReverse)
@@ -160,7 +168,7 @@ class ItemCompare(wx.Panel):
self.paramList.SetItem(i, x + 1, valueUnit)
# Add prices
self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True))
self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "")
self.paramList.RefreshRows()
self.Layout()

View File

@@ -15,7 +15,6 @@ class ItemDescription(wx.Panel):
fgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
self.description = wx.html.HtmlWindow(self)
if not item.description:
return
@@ -24,8 +23,11 @@ class ItemDescription(wx.Panel):
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", "\g<inside>", desc)
# Strip URLs
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", "\g<inside>", desc)
desc = "<body bgcolor='" + bgcolor.GetAsString(wx.C2S_HTML_SYNTAX) + "' text='" + fgcolor.GetAsString(
wx.C2S_HTML_SYNTAX) + "' >" + desc + "</body>"
desc = "<body style='background-color: {}; color: {}'>{}</body>".format(
bgcolor.GetAsString(wx.C2S_CSS_SYNTAX),
fgcolor.GetAsString(wx.C2S_CSS_SYNTAX),
desc
)
self.description.SetPage(desc)

View File

@@ -22,38 +22,67 @@ class ItemMutator(wx.Panel):
self.item = item
self.timer = None
self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit()
font = parent.GetFont()
font.SetWeight(wx.BOLD)
mainSizer = wx.BoxSizer(wx.VERTICAL)
sourceItemsSizer = wx.BoxSizer(wx.HORIZONTAL)
sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.item.iconID, self, "icons"), 0, wx.LEFT, 5)
sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.mutaplasmid.item.iconID, self, "icons"), 0, wx.LEFT, 0)
sourceItemShort = "{} {}".format(stuff.mutaplasmid.item.name.split(" ")[0], stuff.baseItem.name)
sourceItemText = wx.StaticText(self, wx.ID_ANY, sourceItemShort)
sourceItemText.SetFont(font)
sourceItemsSizer.Add(sourceItemText, 0, wx.LEFT, 10)
mainSizer.Add(sourceItemsSizer, 0, wx.TOP | wx.EXPAND, 10)
mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5)
self.goodColor = wx.Colour(96, 191, 0)
self.badColor = wx.Colour(255, 64, 0)
self.event_mapping = {}
for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName):
# create array for the two ranges
min_t = [m.minValue, m.minMod, None]
max_t = [m.maxValue, m.maxMod, None]
# Format: [raw value, modifier applied to base raw value, display value]
range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue))
range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue))
# Then we need to determine if it's better than original, which will be the color
min_t[2] = min_t[1] < 1 if not m.highIsGood else 1 < min_t[1]
max_t[2] = max_t[1] < 1 if not m.highIsGood else 1 < max_t[1]
# Lastly, we need to determine which range value is "worse" (left side) or "better" (right side)
if (m.highIsGood and min_t[1] > max_t[1]) or (not m.highIsGood and min_t[1] < max_t[1]):
better_range = min_t
# minValue/maxValue do not always correspond to min/max, because these are
# just base value multiplied by minMod/maxMod, and in case base is negative
# minValue is actually bigger than maxValue
if range1[0] <= range2[0]:
minRange = range1
maxRange = range2
else:
better_range = max_t
minRange = range2
maxRange = range1
if (m.highIsGood and max_t[1] < min_t[1]) or (not m.highIsGood and max_t[1] > min_t[1]):
worse_range = max_t
if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]):
betterRange = minRange
worseRange = maxRange
else:
worse_range = min_t
betterRange = maxRange
worseRange = minRange
if minRange[1] >= maxRange[1]:
displayMaxRange = minRange
displayMinRange = maxRange
else:
displayMaxRange = maxRange
displayMinRange = minRange
# If base value is outside of mutation range, make sure that center of slider
# corresponds to the value which is closest available to actual base value. It's
# how EVE handles it
if minRange[0] <= m.baseValue <= maxRange[0]:
sliderBaseValue = m.baseValue
else:
sliderBaseValue = max(minRange[0], min(maxRange[0], m.baseValue))
headingSizer = wx.BoxSizer(wx.HORIZONTAL)
font = parent.GetFont()
font.SetWeight(wx.BOLD)
headingSizer.Add(BitmapLoader.getStaticBitmap(m.attribute.iconID, self, "icons"), 0, wx.RIGHT, 10)
displayName = wx.StaticText(self, wx.ID_ANY, m.attribute.displayName)
@@ -61,25 +90,25 @@ class ItemMutator(wx.Panel):
headingSizer.Add(displayName, 3, wx.ALL | wx.EXPAND, 0)
worst_val = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(worse_range[0]), rounding='dec')
worst_text = wx.StaticText(self, wx.ID_ANY, worst_val)
worst_text.SetForegroundColour(self.goodColor if worse_range[2] else self.badColor)
worseVal = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(worseRange[0]), rounding='dec')
worseText = wx.StaticText(self, wx.ID_ANY, worseVal)
worseText.SetForegroundColour(self.badColor)
best_val = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(better_range[0]), rounding='dec')
best_text = wx.StaticText(self, wx.ID_ANY, best_val)
best_text.SetForegroundColour(self.goodColor if better_range[2] else self.badColor)
betterVal = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(betterRange[0]), rounding='dec')
betterText = wx.StaticText(self, wx.ID_ANY, betterVal)
betterText.SetForegroundColour(self.goodColor)
headingSizer.Add(worst_text, 0, wx.ALL | wx.EXPAND, 0)
headingSizer.Add(worseText, 0, wx.ALL | wx.EXPAND, 0)
headingSizer.Add(wx.StaticText(self, wx.ID_ANY, ""), 0, wx.RIGHT | wx.LEFT | wx.EXPAND, 5)
headingSizer.Add(best_text, 0, wx.RIGHT | wx.EXPAND, 10)
headingSizer.Add(betterText, 0, wx.RIGHT | wx.EXPAND, 10)
mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5)
slider = AttributeSlider(parent=self,
baseValue=m.attribute.unit.SimplifyValue(m.baseValue),
minValue=m.attribute.unit.SimplifyValue(min_t[0]),
maxValue=m.attribute.unit.SimplifyValue(max_t[0]),
inverse=better_range is min_t)
baseValue=m.attribute.unit.SimplifyValue(sliderBaseValue),
minValue=displayMinRange[1],
maxValue=displayMaxRange[1],
inverse=displayMaxRange is worseRange)
slider.SetValue(m.attribute.unit.SimplifyValue(m.value), False)
slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue)
self.event_mapping[slider] = m

View File

@@ -2,6 +2,7 @@
import wx
import gui.utils.color as colorUtils
import gui.utils.draw as drawUtils
from gui.utils.helpers_wxPython import HandleCtrlBackspace
SearchButton, EVT_SEARCH_BTN = wx.lib.newevent.NewEvent()
CancelButton, EVT_CANCEL_BTN = wx.lib.newevent.NewEvent()
@@ -55,7 +56,7 @@ class PFSearchBox(wx.Window):
self.EditBox.Bind(wx.EVT_SET_FOCUS, self.OnEditSetFocus)
self.EditBox.Bind(wx.EVT_KILL_FOCUS, self.OnEditKillFocus)
self.EditBox.Bind(wx.EVT_KEY_DOWN, self.OnKeyPress)
self.EditBox.Bind(wx.EVT_TEXT, self.OnText)
self.EditBox.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
@@ -83,6 +84,12 @@ class PFSearchBox(wx.Window):
self.Clear()
event.Skip()
def OnKeyPress(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.EditBox)
else:
event.Skip()
def Clear(self):
self.EditBox.Clear()
# self.EditBox.ChangeValue(self.descriptiveText)

View File

@@ -81,6 +81,11 @@ class PFContextMenuPref(PreferenceView):
rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5)
self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change)
self.rbBox8 = wx.RadioBox(panel, -1, "Fill with module", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox8.SetSelection(self.settings.get('moduleFill'))
rbSizerRow3.Add(self.rbBox8, 1, wx.TOP | wx.RIGHT, 5)
self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change)
mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0)
panel.SetSizer(mainSizer)
@@ -107,6 +112,9 @@ class PFContextMenuPref(PreferenceView):
def OnSetting7Change(self, event):
self.settings.set('project', event.GetInt())
def OnSetting8Change(self, event):
self.settings.set('moduleFill', event.GetInt())
def getImage(self):
return BitmapLoader.getBitmap("settings_menu", "gui")

View File

@@ -76,10 +76,6 @@ class PFGeneralPref(PreferenceView):
self.cbGaugeAnimation = wx.CheckBox(panel, wx.ID_ANY, "Animate gauges", wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbGaugeAnimation, 0, wx.ALL | wx.EXPAND, 5)
self.cbExportCharges = wx.CheckBox(panel, wx.ID_ANY, "Export loaded charges", wx.DefaultPosition,
wx.DefaultSize, 0)
mainSizer.Add(self.cbExportCharges, 0, wx.ALL | wx.EXPAND, 5)
self.cbOpenFitInNew = wx.CheckBox(panel, wx.ID_ANY, "Open fittings in a new page by default",
wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbOpenFitInNew, 0, wx.ALL | wx.EXPAND, 5)
@@ -133,7 +129,6 @@ class PFGeneralPref(PreferenceView):
self.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False)
self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False)
self.cbGaugeAnimation.SetValue(self.sFit.serviceFittingOptions["enableGaugeAnimation"])
self.cbExportCharges.SetValue(self.sFit.serviceFittingOptions["exportCharges"])
self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"])
self.chPriceSource.SetStringSelection(self.sFit.serviceFittingOptions["priceSource"])
self.chPriceSystem.SetStringSelection(self.sFit.serviceFittingOptions["priceSystem"])
@@ -151,7 +146,6 @@ class PFGeneralPref(PreferenceView):
self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip)
self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts)
self.cbGaugeAnimation.Bind(wx.EVT_CHECKBOX, self.onCBGaugeAnimation)
self.cbExportCharges.Bind(wx.EVT_CHECKBOX, self.onCBExportCharges)
self.cbOpenFitInNew.Bind(wx.EVT_CHECKBOX, self.onCBOpenFitInNew)
self.chPriceSource.Bind(wx.EVT_CHOICE, self.onPricesSourceSelection)
self.chPriceSystem.Bind(wx.EVT_CHOICE, self.onPriceSelection)
@@ -220,9 +214,6 @@ class PFGeneralPref(PreferenceView):
def onCBGaugeAnimation(self, event):
self.sFit.serviceFittingOptions["enableGaugeAnimation"] = self.cbGaugeAnimation.GetValue()
def onCBExportCharges(self, event):
self.sFit.serviceFittingOptions["exportCharges"] = self.cbExportCharges.GetValue()
def onCBOpenFitInNew(self, event):
self.sFit.serviceFittingOptions["openFitInNew"] = self.cbOpenFitInNew.GetValue()

View File

@@ -11,6 +11,7 @@ import gui.utils.fonts as fonts
from .events import FitSelected, SearchSelected, ImportSelected, Stage1Selected, Stage2Selected, Stage3Selected
from gui.bitmap_loader import BitmapLoader
from service.fit import Fit
from gui.utils.helpers_wxPython import HandleCtrlBackspace
pyfalog = Logger(__name__)
@@ -72,7 +73,7 @@ class NavigationPanel(SFItem.SFBrowserItem):
# self.BrowserSearchBox.Bind(wx.EVT_TEXT_ENTER, self.OnBrowserSearchBoxEnter)
# self.BrowserSearchBox.Bind(wx.EVT_KILL_FOCUS, self.OnBrowserSearchBoxLostFocus)
self.BrowserSearchBox.Bind(wx.EVT_KEY_DOWN, self.OnBrowserSearchBoxEsc)
self.BrowserSearchBox.Bind(wx.EVT_KEY_DOWN, self.OnBrowserSearchBoxKeyPress)
self.BrowserSearchBox.Bind(wx.EVT_TEXT, self.OnScheduleSearch)
self.SetMinSize(size)
@@ -103,9 +104,11 @@ class NavigationPanel(SFItem.SFBrowserItem):
def OnBrowserSearchBoxLostFocus(self, event):
self.BrowserSearchBox.Show(False)
def OnBrowserSearchBoxEsc(self, event):
def OnBrowserSearchBoxKeyPress(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE:
self.BrowserSearchBox.Show(False)
elif event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.BrowserSearchBox)
else:
event.Skip()

View File

@@ -22,7 +22,8 @@ import wx
import gui.mainFrame
from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from service.fit import Fit
@@ -148,27 +149,55 @@ class FirepowerViewFull(StatsView):
else:
self.stEff.Hide()
stats = (("labelFullDpsWeapon", lambda: fit.weaponDPS, 3, 0, 0, "%s DPS", None),
("labelFullDpsDrone", lambda: fit.droneDPS, 3, 0, 0, "%s DPS", None),
("labelFullVolleyTotal", lambda: fit.totalVolley, 3, 0, 0, "%s", "Volley: %.1f"),
("labelFullDpsTotal", lambda: fit.totalDPS, 3, 0, 0, "%s", None))
# See GH issue #
# if fit is not None and fit.totalYield > 0:
# self.miningyield.Show()
# else:
# self.miningyield.Hide()
def dpsToolTip(preSpool, fullSpool, prec, lowest, highest):
if roundToPrec(preSpool, prec) == roundToPrec(fullSpool, prec):
return ""
else:
return "Spool up: {}-{}".format(
formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option
defaultSpoolValue = 1
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,
3, 0, 0, "{}{} DPS"),
(
"labelFullDpsDrone",
lambda: fit.getDroneDps().total,
lambda: fit.getDroneDps().total,
lambda: fit.getDroneDps().total,
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,
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,
3, 0, 0, "{}{}"))
counter = 0
for labelName, value, prec, lowest, highest, valueFormat, altFormat in stats:
for labelName, val, preSpoolVal, fullSpoolVal, prec, lowest, highest, valueFormat in stats:
label = getattr(self, labelName)
value = value() if fit is not None else 0
value = value if value is not None else 0
if self._cachedValues[counter] != value:
valueStr = formatAmount(value, prec, lowest, highest)
label.SetLabel(valueFormat % valueStr)
tipStr = valueFormat % valueStr if altFormat is None else altFormat % value
label.SetToolTip(wx.ToolTip(tipStr))
self._cachedValues[counter] = value
val = val() if fit is not None else 0
preSpoolVal = preSpoolVal() if fit is not None else 0
fullSpoolVal = fullSpoolVal() if fit is not None else 0
if self._cachedValues[counter] != val:
tooltipText = dpsToolTip(preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(val, prec, lowest, highest),
"\u02e2" if tooltipText else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
counter += 1
self.panel.Layout()

View File

@@ -21,7 +21,35 @@
import wx
from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount
from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions
stats = [
(
"labelRemoteCapacitor", "Capacitor:", "{}{} GJ/s", "capacitorInfo", "Capacitor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Capacitor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Capacitor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Capacitor", 0),
3, 0, 0),
(
"labelRemoteShield", "Shield:", "{}{} HP/s", "shieldActive", "Shield restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Shield", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Shield", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Shield", 0),
3, 0, 0),
(
"labelRemoteArmor", "Armor:", "{}{} HP/s", "armorActive", "Armor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Armor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Armor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Armor", 0),
3, 0, 0),
(
"labelRemoteHull", "Hull:", "{}{} HP/s", "hullActive", "Hull restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Hull", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Hull", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Hull", 0),
3, 0, 0)]
class OutgoingViewFull(StatsView):
@@ -48,56 +76,46 @@ class OutgoingViewFull(StatsView):
contentSizer.Add(sizerOutgoing, 0, wx.EXPAND, 0)
counter = 0
rr_list = [
("RemoteCapacitor", "Capacitor:", "capacitorInfo", "Capacitor GJ/s per second transferred remotely."),
("RemoteShield", "Shield:", "shieldActive", "Shield hitpoints per second repaired remotely."),
("RemoteArmor", "Armor:", "armorActive", "Armor hitpoints per second repaired remotely."),
("RemoteHull", "Hull:", "hullActive", "Hull hitpoints per second repaired remotely."),
]
for outgoingType, label, image, tooltip in rr_list:
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
baseBox = wx.BoxSizer(wx.VERTICAL)
baseBox.Add(BitmapLoader.getStaticBitmap("%s_big" % image, parent, "gui"), 0, wx.ALIGN_CENTER)
if "Capacitor" in outgoingType:
lbl = wx.StaticText(parent, wx.ID_ANY, "0 GJ/s")
else:
lbl = wx.StaticText(parent, wx.ID_ANY, "0 HP/s")
lbl = wx.StaticText(parent, wx.ID_ANY, valueFormat.format(0, ""))
lbl.SetToolTip(wx.ToolTip(tooltip))
setattr(self, "label%s" % outgoingType, lbl)
setattr(self, labelName, lbl)
baseBox.Add(lbl, 0, wx.ALIGN_CENTER)
self._cachedValues.append(0)
counter += 1
sizerOutgoing.Add(baseBox, 1, wx.ALIGN_LEFT)
def refreshPanel(self, fit):
# If we did anything intresting, we'd update our labels to reflect the new fit's stats here
stats = [
("labelRemoteArmor", lambda: fit.remoteReps["Armor"], 3, 0, 0, "%s HP/s", None),
("labelRemoteShield", lambda: fit.remoteReps["Shield"], 3, 0, 0, "%s HP/s", None),
("labelRemoteHull", lambda: fit.remoteReps["Hull"], 3, 0, 0, "%s HP/s", None),
("labelRemoteCapacitor", lambda: fit.remoteReps["Capacitor"], 3, 0, 0, "%s GJ/s", None),
]
def formatTooltip(text, preSpool, fullSpool, prec, lowest, highest):
if roundToPrec(preSpool, prec) == roundToPrec(fullSpool, prec):
return False, text
else:
return True, "{}\nSpool up: {}-{}".format(
text,
formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option
defaultSpoolValue = 1
counter = 0
for labelName, value, prec, lowest, highest, valueFormat, altFormat in stats:
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
label = getattr(self, labelName)
value = value() if fit is not None else 0
value = value if value is not None else 0
if self._cachedValues[counter] != value:
valueStr = formatAmount(value, prec, lowest, highest)
label.SetLabel(valueFormat % valueStr)
tipStr = valueFormat % valueStr if altFormat is None else altFormat % value
label.SetToolTip(wx.ToolTip(tipStr))
self._cachedValues[counter] = value
val = val(fit, defaultSpoolValue) if fit is not None else 0
preSpoolVal = preSpoolVal(fit) if fit is not None else 0
fullSpoolVal = fullSpoolVal(fit) if fit is not None else 0
if self._cachedValues[counter] != val:
hasSpool, tooltipText = formatTooltip(tooltip, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(val, prec, lowest, highest),
"\u02e2" if hasSpool else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
counter += 1
self.panel.Layout()
self.headerPanel.Layout()

View File

@@ -20,7 +20,35 @@
# noinspection PyPackageRequirements
import wx
from gui.statsView import StatsView
from gui.utils.numberFormatter import formatAmount
from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions
stats = [
(
"labelRemoteCapacitor", "Capacitor:", "{}{} GJ/s", "capacitorInfo", "Capacitor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Capacitor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Capacitor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Capacitor", 0),
3, 0, 0),
(
"labelRemoteShield", "Shield:", "{}{} HP/s", "shieldActive", "Shield restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Shield", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Shield", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Shield", 0),
3, 0, 0),
(
"labelRemoteArmor", "Armor:", "{}{} HP/s", "armorActive", "Armor restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Armor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Armor", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Armor", 0),
3, 0, 0),
(
"labelRemoteHull", "Hull:", "{}{} HP/s", "hullActive", "Hull restored",
lambda fit, spool: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, spool, False)).get("Hull", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True)).get("Hull", 0),
lambda fit: fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True)).get("Hull", 0),
3, 0, 0)]
class OutgoingViewMinimal(StatsView):
@@ -47,56 +75,46 @@ class OutgoingViewMinimal(StatsView):
contentSizer.Add(sizerOutgoing, 0, wx.EXPAND, 0)
counter = 0
rr_list = [
("RemoteCapacitor", "Capacitor:", "capacitorInfo", "Capacitor GJ/s per second transferred remotely."),
("RemoteShield", "Shield:", "shieldActive", "Shield hitpoints per second repaired remotely."),
("RemoteArmor", "Armor:", "armorActive", "Armor hitpoints per second repaired remotely."),
("RemoteHull", "Hull:", "hullActive", "Hull hitpoints per second repaired remotely."),
]
for outgoingType, label, image, tooltip in rr_list:
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
baseBox = wx.BoxSizer(wx.VERTICAL)
baseBox.Add(wx.StaticText(contentPanel, wx.ID_ANY, label), 0, wx.ALIGN_CENTER)
if "Capacitor" in outgoingType:
lbl = wx.StaticText(parent, wx.ID_ANY, "0 GJ/s")
else:
lbl = wx.StaticText(parent, wx.ID_ANY, "0 HP/s")
baseBox.Add(wx.StaticText(contentPanel, wx.ID_ANY, labelDesc), 0, wx.ALIGN_CENTER)
lbl = wx.StaticText(parent, wx.ID_ANY, valueFormat.format(0, ""))
lbl.SetToolTip(wx.ToolTip(tooltip))
setattr(self, "label%s" % outgoingType, lbl)
setattr(self, labelName, lbl)
baseBox.Add(lbl, 0, wx.ALIGN_CENTER)
self._cachedValues.append(0)
counter += 1
sizerOutgoing.Add(baseBox, 1, wx.ALIGN_LEFT)
def refreshPanel(self, fit):
# If we did anything intresting, we'd update our labels to reflect the new fit's stats here
stats = [
("labelRemoteArmor", lambda: fit.remoteReps["Armor"], 3, 0, 0, "%s HP/s", None),
("labelRemoteShield", lambda: fit.remoteReps["Shield"], 3, 0, 0, "%s HP/s", None),
("labelRemoteHull", lambda: fit.remoteReps["Hull"], 3, 0, 0, "%s HP/s", None),
("labelRemoteCapacitor", lambda: fit.remoteReps["Capacitor"], 3, 0, 0, "%s GJ/s", None),
]
def formatTooltip(text, preSpool, fullSpool, prec, lowest, highest):
if roundToPrec(preSpool, prec) == roundToPrec(fullSpool, prec):
return False, text
else:
return True, "{}\nSpool up: {}-{}".format(
text,
formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option
defaultSpoolValue = 1
counter = 0
for labelName, value, prec, lowest, highest, valueFormat, altFormat in stats:
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
label = getattr(self, labelName)
value = value() if fit is not None else 0
value = value if value is not None else 0
if self._cachedValues[counter] != value:
valueStr = formatAmount(value, prec, lowest, highest)
label.SetLabel(valueFormat % valueStr)
tipStr = valueFormat % valueStr if altFormat is None else altFormat % value
label.SetToolTip(wx.ToolTip(tipStr))
self._cachedValues[counter] = value
val = val(fit, defaultSpoolValue) if fit is not None else 0
preSpoolVal = preSpoolVal(fit) if fit is not None else 0
fullSpoolVal = fullSpoolVal(fit) if fit is not None else 0
if self._cachedValues[counter] != val:
hasSpool, tooltipText = formatTooltip(tooltip, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(val, prec, lowest, highest),
"\u02e2" if hasSpool else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
counter += 1
self.panel.Layout()
self.headerPanel.Layout()

View File

@@ -63,15 +63,20 @@ class RechargeViewFull(StatsView):
# Add an empty label first for correct alignment.
sizerTankStats.Add(wx.StaticText(contentPanel, wx.ID_ANY, ""), 0)
toolTipText = {"shieldPassive": "Passive shield recharge", "shieldActive": "Active shield boost",
"armorActive": "Armor repair amount", "hullActive": "Hull repair amount"}
toolTipText = {
"shieldPassive": "Passive shield recharge",
"shieldActive": "Active shield boost",
"armorActive": "Armor repair amount",
"hullActive": "Hull repair amount"}
for tankType in ("shieldPassive", "shieldActive", "armorActive", "hullActive"):
bitmap = BitmapLoader.getStaticBitmap("%s_big" % tankType, contentPanel, "gui")
tooltip = wx.ToolTip(toolTipText[tankType])
bitmap.SetToolTip(tooltip)
sizerTankStats.Add(bitmap, 0, wx.ALIGN_CENTER)
toolTipText = {"reinforced": "Reinforced", "sustained": "Sustained"}
toolTipText = {
"reinforced": "Reinforced",
"sustained": "Sustained"}
for stability in ("reinforced", "sustained"):
bitmap = BitmapLoader.getStaticBitmap("regen%s_big" % stability.capitalize(), contentPanel, "gui")
tooltip = wx.ToolTip(toolTipText[stability])
@@ -85,7 +90,6 @@ class RechargeViewFull(StatsView):
tankTypeCap = tankType[0].capitalize() + tankType[1:]
lbl = wx.StaticText(contentPanel, wx.ID_ANY, "0.0", style=wx.ALIGN_RIGHT)
setattr(self, "labelTank%s%s" % (stability.capitalize(), tankTypeCap), lbl)
box = wx.BoxSizer(wx.HORIZONTAL)
box.Add(lbl, 0, wx.EXPAND)
@@ -115,9 +119,23 @@ class RechargeViewFull(StatsView):
unitlbl = getattr(self, "unitLabelTank%s%sActive" % (stability.capitalize(), name.capitalize()))
unitlbl.SetLabel(unit)
if tank is not None:
lbl.SetLabel("%.1f" % tank["%sRepair" % name])
amount = tank["{}Repair".format(name)]
else:
lbl.SetLabel("0.0")
amount = 0
if tank is not None and name == "armor":
preSpoolAmount = tank["armorRepairPreSpool"]
fullSpoolAmount = tank["armorRepairFullSpool"]
if round(preSpoolAmount, 1) != round(fullSpoolAmount, 1):
ttText = "Spool up: {:.1f}-{:.1f}".format(preSpoolAmount, fullSpoolAmount)
else:
ttText = ""
else:
ttText = ""
lbl.SetLabel("{:.1f}{}".format(amount, "\u02e2" if ttText else ""))
lbl.SetToolTip(wx.ToolTip(ttText))
unitlbl.SetToolTip(wx.ToolTip(ttText))
if fit is not None:
label = getattr(self, "labelTankSustainedShieldPassive")

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