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: environment:
global: global:
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # 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 # /E:ON and /V:ON options are not enabled in the batch script intepreter
@@ -8,76 +7,11 @@ environment:
matrix: matrix:
# Python 2.7.10 is the latest version and is not pre-installed. - PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.x"
- PYTHON: "C:\\Python27.10"
PYTHON_VERSION: "2.7.10"
PYTHON_ARCH: "32" PYTHON_ARCH: "32"
init:
#- PYTHON: "C:\\Python27.10-x64" - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
# 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"
install: install:
# If there is a newer build queued for the same PR, cancel this one. # 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 # 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) { ` Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
throw "There are newer queued builds for this pull request, failing early." } 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:" - ECHO "Filesystem root:"
- ps: "ls \"C:/\"" - ps: "ls \"C:/\""
- ECHO "Filesystem projects root:"
- ps: "ls \"C:\\projects\\\""
- ECHO "Filesystem pyfa root:" - ECHO "Filesystem pyfa root:"
- ps: "ls \"C:\\projects\\pyfa\\\"" - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\""
- ECHO "Installed SDKs:" - ECHO "Installed SDKs:"
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\"" - 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 # 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 # done from inside the powershell script as it would require to restart
# the parent CMD process). # the parent CMD process).
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
# Check that we have the expected version and architecture for Python
- "python --version" - "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - "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, # compiled extensions and are not provided as pre-built wheel packages,
# pip will build them from source using the MSVC compiler matching the # pip will build them from source using the MSVC compiler matching the
# target Python version and architecture # target Python version and architecture
# C:\\projects\\eve-gnosis\\
- ECHO "Install pip requirements:" - ECHO "Install pip requirements:"
- "pip install -r requirements.txt" - "pip install -r requirements.txt"
- "pip install -r requirements_test.txt" - "pip install PyInstaller"
- "pip install -r requirements_build_windows.txt"
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_script:
# Build the compiled extension
# - "python setup.py build"
- ECHO "Build pyfa:" - 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):" #- ECHO "Build pyfa (Debug):"
#- copy C:\projects\pyfa\dist_assets\win\pyfa_debug.spec C:\projects\pyfa\pyfa_debug.spec #- 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" #- "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: after_build:
- ps: "ls \"./\"" - ps: "ls \"./\""
#- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\"" #- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\""
- ps: "ls \"C:\\projects\\pyfa\\build\\\"" # - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\build\\exe.win32-2.7\\\""
- ps: "ls \"C:\\projects\\pyfa\\build\\exe.win32-2.7\\\""
# Zip # Zip
# APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER # APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER
#- 7z a build.zip -r C:\projects\pyfa\build\pyfa\*.* #- 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\*.* #- 7z a pyfa_debug.zip -r C:\projects\pyfa\dist\pyfa_debug\*.*
on_success: on_success:
@@ -176,11 +115,21 @@ after_test:
artifacts: artifacts:
# Archive the generated packages in the ci.appveyor.com build report. # Archive the generated packages in the ci.appveyor.com build report.
- path: pyfa.zip - path: pyfa*-win.zip
name: 'pyfa.zip' - path: pyfa*-win.exe
#- path: pyfa_debug.zip #- path: pyfa_debug.zip
# name: Pyfa_debug # 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: #on_success:
# - TODO: upload the content of dist/*.whl to a public wheelhouse # - TODO: upload the content of dist/*.whl to a public wheelhouse
# #

View File

@@ -1,36 +1,29 @@
dist: trusty os: linux
sudo: required
language: python language: python
cache: pip
python: python:
- '3.6' - 3.6
env: matrix:
- TOXENV=pep8 include:
addons: - os: osx
apt: osx_image: xcode7.3
packages: language: generic
env: PYTHON=3.6.1
before_install: 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 - bash scripts/setup-osx.sh
- pip install tox install:
# We're not actually installing Tox, but have to run it before we install wxPython via Conda. This is fugly but vOv - export PYFA_VERSION="$(python3 scripts/dump_version.py)"
- tox - bash scripts/package-osx.sh
- pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk2/ubuntu-14.04 wxPython==4.0.0b2 before_deploy:
# # get Conda - export RELEASE_PKG_FILE=$(ls *.deb)
# - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - echo "deploying $RELEASE_PKG_FILE to GitHub releases"
# wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; deploy:
# else provider: releases
# wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; api_key:
# fi 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=
# - bash miniconda.sh -b -p $HOME/miniconda file_glob: true
# - export PATH="$HOME/miniconda/bin:$PATH" file: "dist/pyfa-*.zip"
# - hash -r skip_cleanup: true
# - conda config --set always_yes yes --set changeps1 no draft: true
# - conda update -q conda on:
# # Useful for debugging any issues with conda tags: true
# - conda info -a repo: pyfa-org/Pyfa
#install:
# install wxPython 3.0.0.0
# - conda install -c https://conda.anaconda.org/travis wxpython=4.0.0b2
script:
- tox

View File

@@ -1,5 +1,6 @@
import os import os
import sys import sys
import yaml
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING StreamHandler, TimedRotatingFileHandler, WARNING
@@ -22,12 +23,6 @@ debug = False
# Defines if our saveddata will be in pyfa root or not # Defines if our saveddata will be in pyfa root or not
saveInRoot = False saveInRoot = False
# Version data
version = "2.6.1"
tag = "Stable"
expansionName = "Onslaught"
expansionVersion = "1.5"
evemonMinVersion = "4081" evemonMinVersion = "4081"
minItemSearchLength = 3 minItemSearchLength = 3
@@ -79,12 +74,7 @@ def getPyfaRoot():
def getVersion(): def getVersion():
if os.path.isfile(os.path.join(pyfaPath, '.version')): return 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)"
def getDefaultSave(): def getDefaultSave():
@@ -96,11 +86,12 @@ def defPaths(customSavePath=None):
global pyfaPath global pyfaPath
global savePath global savePath
global saveDB global saveDB
global gameDB global gameDB
global saveInRoot global saveInRoot
global logPath global logPath
global cipher global cipher
global clientHash global clientHash
global version
pyfalog.debug("Configuring Pyfa") pyfalog.debug("Configuring Pyfa")
@@ -110,6 +101,12 @@ def defPaths(customSavePath=None):
if pyfaPath is None: if pyfaPath is None:
pyfaPath = getPyfaRoot() 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 # Where we store the saved fits etc, default is the current users home directory
if saveInRoot is True: if saveInRoot is True:
savePath = getattr(configforced, "savePath", None) savePath = getattr(configforced, "savePath", None)

View File

@@ -24,11 +24,13 @@ added_files = [
('../../eve.db', '.'), ('../../eve.db', '.'),
('../../README.md', '.'), ('../../README.md', '.'),
('../../LICENSE', '.'), ('../../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") 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_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher) cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data, pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher) cipher=block_cipher)
exe = EXE(pyz, exe = EXE(pyz,
a.scripts, a.scripts,
a.binaries, a.binaries,
@@ -70,10 +74,16 @@ exe = EXE(pyz,
icon=icon, icon=icon,
) )
app = BUNDLE(exe, app = BUNDLE(
name='pyfa.app', exe,
icon=icon, name='pyfa.app',
bundle_identifier=None, icon=icon,
info_plist={ bundle_identifier=None,
'NSHighResolutionCapable': 'True' info_plist={
}) 'NSHighResolutionCapable': 'True',
'NSPrincipalClass': 'NSApplication',
'CFBundleName': 'pyfa',
'CFBundleDisplayName': 'pyfa',
'CFBundleIdentifier': 'org.pyfaorg.pyfa',
}
)

View File

@@ -3,44 +3,35 @@
import os.path import os.path
from subprocess import call from subprocess import call
import zipfile import zipfile
from packaging.version import Version
import yaml
def zipdir(path, zip): with open("version.yml", 'r') as file:
for root, dirs, files in os.walk(path): data = yaml.load(file)
for file in files: version = data['version']
zip.write(os.path.join(root, file))
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") fileName = "pyfa-{}-win".format(os.environ["PYFA_VERSION"])
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()
print("Compiling EXE") print("Compiling EXE")
expansion = "%s %s" % (config['expansionName'], config['expansionVersion']), v = Version(version)
print(v)
call([ call([
iscc, iscc,
os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"), os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"),
"/dMyAppVersion=%s" % (config['version']), "/dMyAppVersion=%s" % v,
"/dMyAppExpansion=%s" % expansion,
"/dMyAppDir=%s" % source, "/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 "/dMyOutputFile=%s" % fileName]) # stdout=devnull, stderr=devnull
print("Done") print("Done")

View File

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

View File

@@ -5,8 +5,7 @@ from itertools import chain
import subprocess import subprocess
import requests.certs import requests.certs
label = subprocess.check_output([ label = subprocess.check_output(["git", "describe", "--tags"]).strip()
"git", "describe", "--tags"]).strip()
with open('.version', 'w+') as f: with open('.version', 'w+') as f:
f.write(label.decode()) f.write(label.decode())
@@ -18,7 +17,7 @@ added_files = [
('../../imgs/gui/*.gif', 'imgs/gui'), ('../../imgs/gui/*.gif', 'imgs/gui'),
('../../imgs/icons/*.png', 'imgs/icons'), ('../../imgs/icons/*.png', 'imgs/icons'),
('../../imgs/renders/*.png', 'imgs/renders'), ('../../imgs/renders/*.png', 'imgs/renders'),
('../../service/jargon/*.yaml', 'service/jargon'), ('../../service/jargon/*.yaml', 'service/jargon'),
('../../dist_assets/win/pyfa.ico', '.'), ('../../dist_assets/win/pyfa.ico', '.'),
('../../dist_assets/win/pyfa.exe.manifest', '.'), ('../../dist_assets/win/pyfa.exe.manifest', '.'),
('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'), ('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'),
@@ -26,10 +25,12 @@ added_files = [
('../../eve.db', '.'), ('../../eve.db', '.'),
('../../README.md', '.'), ('../../README.md', '.'),
('../../LICENSE', '.'), ('../../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 # Walk directories that do dynamic importing
paths = ('eos/effects', 'eos/db/migrations', 'service/conversions') 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("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
Column("iconID", Integer), Column("iconID", Integer),
Column("graphicID", 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 .metaGroup import metatypes_table # noqa
from .traits import traits_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/>. # 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.collections import attribute_mapped_collection
from sqlalchemy.orm import relation, mapper from sqlalchemy.orm import relation, mapper
import datetime import datetime
@@ -40,6 +40,8 @@ modules_table = Table("modules", saveddata_meta,
Column("position", Integer), Column("position", Integer),
Column("created", DateTime, nullable=True, default=datetime.datetime.now), Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
Column("spoolType", Integer, nullable=True),
Column("spoolAmount", Float, nullable=True),
CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"')) CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"'))
mapper(Module, modules_table, mapper(Module, modules_table,

View File

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

View File

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

View File

@@ -141,6 +141,23 @@ class HandledModuleList(HandledList):
self.remove(mod) self.remove(mod)
return 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): def insert(self, index, mod):
mod.position = index mod.position = index
i = index i = index

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,4 +9,7 @@ type = "active"
def handler(fit, module, context): def handler(fit, module, context):
amount = module.getModifiedItemAttr("armorDamageAmount") amount = module.getModifiedItemAttr("armorDamageAmount")
speed = module.getModifiedItemAttr("duration") / 1000.0 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: Eifyr and Co. 'Alchemist' Neurotoxin Control NC (2 of 2)
# Implants named like: grade Edge (10 of 12) # Implants named like: grade Edge (10 of 12)
# Skill: Neurotoxin Control # Skill: Neurotoxin Control
runTime = 'early'
type = "passive" type = "passive"

View File

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

View File

@@ -6,5 +6,5 @@ type = "passive"
def handler(fit, ship, context): 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")) "duration", ship.getModifiedItemAttr("durationBonus"))

View File

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

View File

@@ -8,6 +8,6 @@ type = "passive"
def handler(fit, container, context): def handler(fit, container, context):
level = container.level if "skill" in context else 1 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", "consumptionQuantity",
container.getModifiedItemAttr("consumptionQuantityBonusPercentage") * level) container.getModifiedItemAttr("consumptionQuantityBonusPercentage") * level)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,4 +8,6 @@ runtime = "late"
def handler(fit, src, context): def handler(fit, src, context):
for dmgType in ('em', 'thermal', 'kinetic', 'explosive'): 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 amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier
speed = module.getModifiedItemAttr("duration") / 1000.0 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: # Used by:
# Variations of module: Ice Harvester Upgrade I (5 of 5) # Variations of module: Ice Harvester Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive" 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): 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: # Used by:
# Variations of module: Mining Laser Upgrade I (5 of 5) # Variations of module: Mining Laser Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive" 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: # Used by:
# Variations of module: Mining Laser Upgrade I (5 of 5) # Variations of module: Mining Laser Upgrade I (5 of 5)
# Module: Frostline 'Omnivore' Harvester Upgrade
type = "passive" type = "passive"

View File

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

View File

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

View File

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

View File

@@ -9,4 +9,7 @@ def handler(fit, container, context):
if "projected" in context: if "projected" in context:
bonus = container.getModifiedItemAttr("armorDamageAmount") bonus = container.getModifiedItemAttr("armorDamageAmount")
duration = container.getModifiedItemAttr("duration") / 1000.0 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: # Used by:
# Modules from group: Missile Launcher Torpedo (22 of 22) # 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 # Module: Interdiction Sphere Launcher I
type = "overheat" type = "overheat"

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# shieldTransportCpuNeedBonusEffect # shieldTransportCpuNeedBonusEffect
# #
# Used by: # Used by:
# Ships from group: Logistics (3 of 6) # Ships from group: Logistics (3 of 7)
type = "passive" 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 # shipBonusRole5RemoteArmorRepairPowergridBonus
# #
# Used by: # Used by:
# Ships from group: Logistics (3 of 6) # Ships from group: Logistics (3 of 7)
type = "passive" type = "passive"

View File

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

View File

@@ -2,8 +2,9 @@
# #
# Used by: # Used by:
# Modules from group: Ancillary Remote Armor Repairer (4 of 4) # Modules from group: Ancillary Remote Armor Repairer (4 of 4)
runTime = "late"
type = "projected", "active" type = "projected", "active"
runTime = "late"
def handler(fit, module, context, **kwargs): def handler(fit, module, context, **kwargs):
@@ -17,4 +18,7 @@ def handler(fit, module, context, **kwargs):
amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier
speed = module.getModifiedItemAttr("duration") / 1000.0 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: # Used by:
# Modules from group: Ancillary Remote Shield Booster (4 of 4) # Modules from group: Ancillary Remote Shield Booster (4 of 4)
runTime = "late"
type = "projected", "active" type = "projected", "active"
runTime = "late"
def handler(fit, module, context, **kwargs): 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: # Used by:
# Modules from group: Remote Armor Repairer (39 of 39) # Modules from group: Remote Armor Repairer (39 of 39)
type = "projected", "active" type = "projected", "active"
runTime = "late"
def handler(fit, container, context, **kwargs): def handler(fit, container, context, **kwargs):
if "projected" in context: if "projected" in context:
bonus = container.getModifiedItemAttr("armorDamageAmount") bonus = container.getModifiedItemAttr("armorDamageAmount")
duration = container.getModifiedItemAttr("duration") / 1000.0 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: # Used by:
# Modules from group: Remote Capacitor Transmitter (41 of 41) # Modules from group: Remote Capacitor Transmitter (41 of 41)
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "projected", "active" type = "projected", "active"
runTime = "late"
def handler(fit, src, context, **kwargs): def handler(fit, src, context, **kwargs):

View File

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

View File

@@ -7,4 +7,4 @@ type = "passive"
def handler(fit, skill, context): def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Missile Launcher Bomb", 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 # skillBonusDroneDurability
# #
# Used by: # Used by:
# Implants from group: Cyber Drones (4 of 4)
# Skill: Drone Durability # Skill: Drone Durability
type = "passive" 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 # skillBonusDroneInterfacing
# #
# Used by: # Used by:
# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D
# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T
# Skill: Drone Interfacing # Skill: Drone Interfacing
type = "passive" 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.__offensive = None
self.__assistive = None self.__assistive = None
self.__overrides = None self.__overrides = None
self.__price = None self.__priceObj = None
@property @property
def attributes(self): def attributes(self):
@@ -446,34 +446,33 @@ class Item(EqBase):
@property @property
def price(self): def price(self):
# todo: use `from sqlalchemy import inspect` instead (mac-deprecated doesn't have inspect(), was imp[lemented in 0.8) # 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)) 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) db_price = eos.db.getPrice(self.ID)
# do not yet have a price in the database for this item, create one # do not yet have a price in the database for this item, create one
if db_price is None: if db_price is None:
pyfalog.debug("Creating a price for {}".format(self.ID)) pyfalog.debug("Creating a price for {}".format(self.ID))
self.__price = types_Price(self.ID) self.__priceObj = types_Price(self.ID)
eos.db.add(self.__price) eos.db.add(self.__priceObj)
eos.db.commit() eos.db.flush()
else: else:
self.__price = db_price self.__priceObj = db_price
return self.__price return self.__priceObj
@property @property
def isAbyssal(self): def isAbyssal(self):
if Item.ABYSSAL_TYPES is None: if Item.ABYSSAL_TYPES is None:
Item.getAbyssalYypes() Item.getAbyssalTypes()
return self.ID in Item.ABYSSAL_TYPES return self.ID in Item.ABYSSAL_TYPES
@classmethod @classmethod
def getAbyssalYypes(cls): def getAbyssalTypes(cls):
cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes() cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes()
@property @property

View File

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

View File

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

View File

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

View File

@@ -24,12 +24,13 @@ from sqlalchemy.orm import validates, reconstructor
import eos.db import eos.db
from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
MINING_ATTRIBUTES = ("miningAmount",) MINING_ATTRIBUTES = ("miningAmount",)
def __init__(self, item): def __init__(self, item):
@@ -65,8 +66,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def build(self): def build(self):
""" Build object. Assumes proper and valid item already set """ """ Build object. Assumes proper and valid item already set """
self.__charge = None self.__charge = None
self.__dps = None self.__baseVolley = None
self.__volley = None self.__baseRemoteReps = None
self.__miningyield = None self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes self.__itemModifiedAttributes.original = self.__item.attributes
@@ -120,37 +121,67 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self): def hasAmmo(self):
return self.charge is not None return self.charge is not None
@property def getVolley(self, targetResists=None):
def dps(self): if not self.dealsDamage or self.amountActive <= 0:
return self.damageStats() 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): def changeType(self, typeID):
self.itemID = typeID self.itemID = typeID
self.init() 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 @property
def miningStats(self): def miningStats(self):
if self.__miningyield is None: if self.__miningyield is None:
@@ -208,8 +239,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val return val
def clear(self): def clear(self):
self.__dps = None self.__baseVolley = None
self.__volley = None self.__baseRemoteReps = None
self.__miningyield = None self.__miningyield = None
self.itemModifiedAttributes.clear() self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.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.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.saveddata.fighterAbility import FighterAbility from eos.saveddata.fighterAbility import FighterAbility
from eos.saveddata.module import Slot from eos.saveddata.module import Slot
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -87,8 +88,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def build(self): def build(self):
""" Build object. Assumes proper and valid item already set """ """ Build object. Assumes proper and valid item already set """
self.__charge = None self.__charge = None
self.__dps = None self.__baseVolley = None
self.__volley = None
self.__miningyield = None self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__chargeModifiedAttributes = ModifiedAttributeDict() self.__chargeModifiedAttributes = ModifiedAttributeDict()
@@ -172,43 +172,88 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def hasAmmo(self): def hasAmmo(self):
return self.charge is not None return self.charge is not None
@property def getVolley(self, targetResists=None):
def dps(self): if not self.active or self.amountActive <= 0:
return self.damageStats() 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): def getDps(self, targetResists=None):
if self.__dps is None: if not self.active or self.amountActive <= 0:
self.__volley = 0 return DmgTypes(0, 0, 0, 0)
self.__dps = 0 # Analyze cooldowns when reload is factored in
if self.active and self.amountActive > 0: if self.owner.factorReload:
for ability in self.abilities: activeTimes = []
dps, volley = ability.damageStats(targetResists) reloadTimes = []
self.__dps += dps peakEm = 0
self.__volley += volley peakTherm = 0
peakKin = 0
# For forward compatability this assumes a fighter peakExp = 0
# can have more than 2 damaging abilities and/or steadyEm = 0
# multiple that use charges. steadyTherm = 0
if self.owner.factorReload: steadyKin = 0
activeTimes = [] steadyExp = 0
reloadTimes = [] for ability in self.abilities:
constantDps = 0 abilityDps = ability.getDps(targetResists=targetResists)
for ability in self.abilities: # Peak dps
if not ability.active: peakEm += abilityDps.em
continue peakTherm += abilityDps.thermal
if ability.numShots == 0: peakKin += abilityDps.kinetic
dps, volley = ability.damageStats(targetResists) peakExp += abilityDps.explosive
constantDps += dps # Infinite use - add to steady dps
continue if ability.numShots == 0:
activeTimes.append(ability.numShots * ability.cycleTime) steadyEm += abilityDps.em
reloadTimes.append(ability.reloadTime) steadyTherm += abilityDps.thermal
steadyKin += abilityDps.kinetic
if len(activeTimes) > 0: steadyExp += abilityDps.explosive
shortestActive = sorted(activeTimes)[0] else:
longestReload = sorted(reloadTimes, reverse=True)[0] activeTimes.append(ability.numShots * ability.cycleTime)
self.__dps = max(constantDps, self.__dps * shortestActive / (shortestActive + longestReload)) reloadTimes.append(ability.reloadTime)
steadyDps = DmgTypes(steadyEm, steadyTherm, steadyKin, steadyExp)
return self.__dps, self.__volley 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 @property
def maxRange(self): def maxRange(self):
@@ -251,8 +296,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val return val
def clear(self): def clear(self):
self.__dps = None self.__baseVolley = None
self.__volley = None
self.__miningyield = None self.__miningyield = None
self.itemModifiedAttributes.clear() self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear() self.chargeModifiedAttributes.clear()

View File

@@ -21,12 +21,12 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor from sqlalchemy.orm import reconstructor
from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class FighterAbility(object): 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 # 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 # with the fighter squadron role
@@ -118,30 +118,38 @@ class FighterAbility(object):
return speed return speed
def damageStats(self, targetResists=None): def getVolley(self, targetResists=None):
if self.__dps is None: if not self.dealsDamage or not self.active:
self.__volley = 0 return DmgTypes(0, 0, 0, 0)
self.__dps = 0 if self.attrPrefix == "fighterAbilityLaunchBomb":
if self.dealsDamage and self.active: em = self.fighter.getModifiedChargeAttr("emDamage", 0)
cycleTime = self.cycleTime 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": def getDps(self, targetResists=None):
# bomb calcs volley = self.getVolley(targetResists=targetResists)
volley = sum([(self.fighter.getModifiedChargeAttr("%sDamage" % attr) or 0) * ( if not volley:
1 - getattr(targetResists, "%sAmount" % attr, 0)) for attr in self.DAMAGE_TYPES]) return DmgTypes(0, 0, 0, 0)
else: dpsFactor = 1 / (self.cycleTime / 1000)
volley = sum(map(lambda d2, d: dps = DmgTypes(
(self.fighter.getModifiedItemAttr( em=volley.em * dpsFactor,
"{}Damage{}".format(self.attrPrefix, d2)) or 0) * thermal=volley.thermal * dpsFactor,
(1 - getattr(targetResists, "{}Amount".format(d), 0)), kinetic=volley.kinetic * dpsFactor,
self.DAMAGE_TYPES2, self.DAMAGE_TYPES)) explosive=volley.explosive * dpsFactor)
return dps
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 clear(self): def clear(self):
self.__dps = None self.__dps = None

View File

@@ -34,6 +34,7 @@ from eos.saveddata.drone import Drone
from eos.saveddata.character import Character from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel from eos.saveddata.citadel import Citadel
from eos.saveddata.module import Module, State, Slot, Hardpoint from eos.saveddata.module import Module, State, Slot, Hardpoint
from eos.utils.stats import DmgTypes
from logbook import Logger from logbook import Logger
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -120,10 +121,11 @@ class Fit(object):
def build(self): def build(self):
self.__extraDrains = [] self.__extraDrains = []
self.__ehp = None self.__ehp = None
self.__weaponDPS = None self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None self.__minerYield = None
self.__weaponVolley = None self.__droneDps = None
self.__droneDPS = None
self.__droneVolley = None self.__droneVolley = None
self.__droneYield = None self.__droneYield = None
self.__sustainableTank = None self.__sustainableTank = None
@@ -135,12 +137,6 @@ class Fit(object):
self.__capUsed = None self.__capUsed = None
self.__capRecharge = None self.__capRecharge = None
self.__calculatedTargets = [] self.__calculatedTargets = []
self.__remoteReps = {
"Armor" : None,
"Shield" : None,
"Hull" : None,
"Capacitor": None,
}
self.factorReload = False self.factorReload = False
self.boostsFits = set() self.boostsFits = set()
self.gangBoosts = None self.gangBoosts = None
@@ -154,9 +150,9 @@ class Fit(object):
@targetResists.setter @targetResists.setter
def targetResists(self, targetResists): def targetResists(self, targetResists):
self.__targetResists = targetResists self.__targetResists = targetResists
self.__weaponDPS = None self.__weaponDpsMap = {}
self.__weaponVolley = None self.__weaponVolleyMap = {}
self.__droneDPS = None self.__droneDps = None
self.__droneVolley = None self.__droneVolley = None
@property @property
@@ -277,41 +273,31 @@ class Fit(object):
def projectedFighters(self): def projectedFighters(self):
return self.__projectedFighters return self.__projectedFighters
@property def getWeaponDps(self, spoolOptions=None):
def weaponDPS(self): if spoolOptions not in self.__weaponDpsMap:
if self.__weaponDPS is None: self.calculateWeaponDmgStats(spoolOptions)
self.calculateWeaponStats() 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 getDroneDps(self):
def weaponVolley(self): if self.__droneDps is None:
if self.__weaponVolley is None: self.calculateDroneDmgStats()
self.calculateWeaponStats() return self.__droneDps
return self.__weaponVolley def getDroneVolley(self):
@property
def droneDPS(self):
if self.__droneDPS is None:
self.calculateWeaponStats()
return self.__droneDPS
@property
def droneVolley(self):
if self.__droneVolley is None: if self.__droneVolley is None:
self.calculateWeaponStats() self.calculateDroneDmgStats()
return self.__droneVolley return self.__droneVolley
@property def getTotalDps(self, spoolOptions=None):
def totalDPS(self): return self.getDroneDps() + self.getWeaponDps(spoolOptions=spoolOptions)
return self.droneDPS + self.weaponDPS
@property def getTotalVolley(self, spoolOptions=None):
def totalVolley(self): return self.getDroneVolley() + self.getWeaponVolley(spoolOptions=spoolOptions)
return self.droneVolley + self.weaponVolley
@property @property
def minerYield(self): def minerYield(self):
@@ -409,12 +395,13 @@ class Fit(object):
def clear(self, projected=False, command=False): def clear(self, projected=False, command=False):
self.__effectiveTank = None self.__effectiveTank = None
self.__weaponDPS = None self.__weaponDpsMap = {}
self.__weaponVolleyMap = {}
self.__remoteRepMap = {}
self.__minerYield = None self.__minerYield = None
self.__weaponVolley = None
self.__effectiveSustainableTank = None self.__effectiveSustainableTank = None
self.__sustainableTank = None self.__sustainableTank = None
self.__droneDPS = None self.__droneDps = None
self.__droneVolley = None self.__droneVolley = None
self.__droneYield = None self.__droneYield = None
self.__ehp = None self.__ehp = None
@@ -426,9 +413,6 @@ class Fit(object):
self.ecmProjectedStr = 1 self.ecmProjectedStr = 1
# self.commandBonuses = {} # self.commandBonuses = {}
for remoterep_type in self.__remoteReps:
self.__remoteReps[remoterep_type] = None
del self.__calculatedTargets[:] del self.__calculatedTargets[:]
del self.__extraDrains[:] del self.__extraDrains[:]
@@ -1032,11 +1016,11 @@ class Fit(object):
@property @property
def pgUsed(self): def pgUsed(self):
return self.getItemAttrOnlineSum(self.modules, "power") return round(self.getItemAttrOnlineSum(self.modules, "power"), 2)
@property @property
def cpuUsed(self): def cpuUsed(self):
return self.getItemAttrOnlineSum(self.modules, "cpu") return round(self.getItemAttrOnlineSum(self.modules, "cpu"), 2)
@property @property
def droneBandwidthUsed(self): def droneBandwidthUsed(self):
@@ -1151,149 +1135,6 @@ class Fit(object):
return self.__capRecharge 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): def calculateCapRecharge(self, percent=PEAK_RECHARGE):
capacity = self.ship.getModifiedItemAttr("capacitorCapacity") capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0 rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
@@ -1377,92 +1218,27 @@ class Fit(object):
self.__capStable = True self.__capStable = True
self.__capState = 100 self.__capState = 100
@property def getRemoteReps(self, spoolOptions=None):
def remoteReps(self): if spoolOptions not in self.__remoteRepMap:
force_recalc = False remoteReps = {}
for remote_type in self.__remoteReps:
if self.__remoteReps[remote_type] is None:
force_recalc = True
break
if force_recalc is False: for module in self.modules:
return self.__remoteReps 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 for drone in self.drones:
# all values to 0. rrType, rrAmount = drone.getRemoteReps()
for remote_type in self.__remoteReps: if rrType:
self.__remoteReps[remote_type] = 0 if rrType not in remoteReps:
remoteReps[rrType] = 0
remoteReps[rrType] += rrAmount
for stuff in chain(self.modules, self.drones): self.__remoteRepMap[spoolOptions] = remoteReps
if stuff.item:
if stuff.item.ID == 10250:
pass
remote_type = None
# Only apply the charged multiplier if we have a charge in our ancil reppers (#1135) return self.__remoteRepMap[spoolOptions]
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
@property @property
def hp(self): def hp(self):
@@ -1485,11 +1261,14 @@ class Fit(object):
@property @property
def tank(self): def tank(self):
hps = {"passiveShield": self.calculateShieldRecharge()} reps = {
for type in ("shield", "armor", "hull"): "passiveShield": self.calculateShieldRecharge(),
hps["%sRepair" % type] = self.extraAttributes["%sRepair" % type] "shieldRepair": self.extraAttributes["shieldRepair"],
"armorRepair": self.extraAttributes["armorRepair"],
return hps "armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"],
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"],
"hullRepair": self.extraAttributes["hullRepair"]}
return reps
@property @property
def effectiveTank(self): def effectiveTank(self):
@@ -1497,24 +1276,153 @@ class Fit(object):
if self.damagePattern is None: if self.damagePattern is None:
ehps = self.tank ehps = self.tank
else: else:
ehps = self.damagePattern.calculateEffectiveTank(self, self.extraAttributes) ehps = self.damagePattern.calculateEffectiveTank(self, self.tank)
self.__effectiveTank = ehps self.__effectiveTank = ehps
return self.__effectiveTank return self.__effectiveTank
@property
def sustainableTank(self):
if self.__sustainableTank is None:
self.calculateSustainableTank()
return self.__sustainableTank
@property @property
def effectiveSustainableTank(self): def effectiveSustainableTank(self):
if self.__effectiveSustainableTank is None: if self.__effectiveSustainableTank is None:
if self.damagePattern is None: if self.damagePattern is None:
eshps = self.sustainableTank tank = self.sustainableTank
else: else:
eshps = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank) tank = self.damagePattern.calculateEffectiveTank(self, self.sustainableTank)
self.__effectiveSustainableTank = tank
self.__effectiveSustainableTank = eshps
return self.__effectiveSustainableTank 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): def calculateLockTime(self, radius):
scanRes = self.ship.getModifiedItemAttr("scanResolution") scanRes = self.ship.getModifiedItemAttr("scanResolution")
if scanRes is not None and scanRes > 0: if scanRes is not None and scanRes > 0:
@@ -1537,30 +1445,30 @@ class Fit(object):
self.__minerYield = minerYield self.__minerYield = minerYield
self.__droneYield = droneYield self.__droneYield = droneYield
def calculateWeaponStats(self): def calculateWeaponDmgStats(self, spoolOptions):
weaponDPS = 0 weaponVolley = DmgTypes(0, 0, 0, 0)
droneDPS = 0 weaponDps = DmgTypes(0, 0, 0, 0)
weaponVolley = 0
droneVolley = 0
for mod in self.modules: for mod in self.modules:
dps, volley = mod.damageStats(self.targetResists) weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetResists=self.targetResists)
weaponDPS += dps weaponDps += mod.getDps(spoolOptions=spoolOptions, targetResists=self.targetResists)
weaponVolley += volley
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: for drone in self.drones:
dps, volley = drone.damageStats(self.targetResists) droneVolley += drone.getVolley(targetResists=self.targetResists)
droneDPS += dps droneDps += drone.getDps(targetResists=self.targetResists)
droneVolley += volley
for fighter in self.fighters: for fighter in self.fighters:
dps, volley = fighter.damageStats(self.targetResists) droneVolley += fighter.getVolley(targetResists=self.targetResists)
droneDPS += dps droneDps += fighter.getDps(targetResists=self.targetResists)
droneVolley += volley
self.__weaponDPS = weaponDPS self.__droneDps = droneDps
self.__weaponVolley = weaponVolley
self.__droneDPS = droneDPS
self.__droneVolley = droneVolley self.__droneVolley = droneVolley
@property @property

View File

@@ -28,6 +28,9 @@ from eos.enum import Enum
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.citadel import Citadel from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator 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__) pyfalog = Logger(__name__)
@@ -95,7 +98,6 @@ class Hardpoint(Enum):
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes""" """An instance of this class represents a module together with its charge and modified attributes"""
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
MINING_ATTRIBUTES = ("miningAmount",) MINING_ATTRIBUTES = ("miningAmount",)
SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object") 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": if self.__charge and self.__charge.category.name != "Charge":
self.__charge = None self.__charge = None
self.__dps = None self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None self.__miningyield = None
self.__volley = None
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None
@@ -247,8 +249,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if chargeVolume is None or containerCapacity is None: if chargeVolume is None or containerCapacity is None:
charges = 0 charges = 0
else: else:
charges = floor(containerCapacity / chargeVolume) charges = int(floatUnerr(containerCapacity / chargeVolume))
return int(charges) return charges
@property @property
def numShots(self): def numShots(self):
@@ -411,35 +413,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__itemModifiedAttributes.clear() 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 @property
def miningStats(self): def miningStats(self):
if self.__miningyield is None: if self.__miningyield is None:
@@ -459,13 +432,110 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__miningyield return self.__miningyield
@property def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False):
def dps(self): if self.isEmpty or (self.state < State.ACTIVE and not ignoreState):
return self.damageStats(None)[0] 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 getDps(self, spoolOptions=None, targetResists=None, ignoreState=False):
def volley(self): volley = self.getVolley(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
return self.damageStats(None)[1] 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 @property
def reloadTime(self): def reloadTime(self):
@@ -718,9 +788,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return val return val
def clear(self): def clear(self):
self.__dps = None self.__baseVolley = None
self.__baseRemoteReps = None
self.__miningyield = None self.__miningyield = None
self.__volley = None
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None

View File

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

View File

@@ -29,14 +29,16 @@ pyfalog = Logger(__name__)
class Ship(ItemAttrShortcut, HandledItem): class Ship(ItemAttrShortcut, HandledItem):
EXTRA_ATTRIBUTES = { EXTRA_ATTRIBUTES = {
"armorRepair" : 0, "armorRepair": 0,
"hullRepair" : 0, "armorRepairPreSpool": 0,
"shieldRepair" : 0, "armorRepairFullSpool": 0,
"maxActiveDrones" : 0, "hullRepair": 0,
"shieldRepair": 0,
"maxActiveDrones": 0,
"maxTargetsLockedFromSkills": 2, "maxTargetsLockedFromSkills": 2,
"droneControlRange" : 20000, "droneControlRange": 20000,
"cloaked" : False, "cloaked": False,
"siege" : False "siege": False
# We also have speedLimit for Entosis Link, but there seems to be an # We also have speedLimit for Entosis Link, but there seems to be an
# issue with naming it exactly "speedLimit" due to unknown reasons. # issue with naming it exactly "speedLimit" due to unknown reasons.
# Regardless, we don't have to put it here anyways - it will come up # 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 import config
versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion) versionString = "{0}".format(config.version)
licenses = ( licenses = (
"pyfa is released under GNU GPLv3 - see included LICENSE file", "pyfa is released under GNU GPLv3 - see included LICENSE file",
"All EVE-Online related materials are property of CCP hf.", "All EVE-Online related materials are property of CCP hf.",

View File

@@ -4,6 +4,7 @@ import wx
from service.fit import Fit from service.fit import Fit
import gui.globalEvents as GE import gui.globalEvents as GE
import gui.mainFrame import gui.mainFrame
from gui.utils.helpers_wxPython import HandleCtrlBackspace
class NotesView(wx.Panel): class NotesView(wx.Panel):
@@ -17,9 +18,16 @@ class NotesView(wx.Panel):
self.SetSizer(mainSizer) self.SetSizer(mainSizer)
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.Bind(wx.EVT_TEXT, self.onText) self.Bind(wx.EVT_TEXT, self.onText)
self.editNotes.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.saveTimer = wx.Timer(self) self.saveTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.delayedSave, self.saveTimer) 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): def fitChanged(self, event):
sFit = Fit.getInstance() sFit = Fit.getInstance()
fit = sFit.getFit(event.fitID) 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() sMkt = Market.getInstance()
item = getattr(selection[0], "item", selection[0]) item = getattr(selection[0], "item", selection[0])
isMutated = getattr(selection[0], "isMutated", False)
mktGrp = sMkt.getMarketGroupByItem(item) 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 # 1663 is Special Edition Festival Assets, we don't have root group for it
if mktGrp is None or mktGrp.ID == 1663: if mktGrp is None or mktGrp.ID == 1663:
@@ -43,7 +46,10 @@ class MarketJump(ContextMenu):
if srcContext in ("fittingCharge", "projectedCharge"): if srcContext in ("fittingCharge", "projectedCharge"):
item = selection[0].charge item = selection[0].charge
elif hasattr(selection[0], "item"): 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: else:
item = selection[0] item = selection[0]

View File

@@ -1,7 +1,10 @@
import math
import wx import wx
import wx.lib.newevent import wx.lib.newevent
from gui.attribute_gauge import AttributeGauge from gui.attribute_gauge import AttributeGauge
from eos.utils.float import floatUnerr
_ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent() _ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent()
@@ -58,22 +61,41 @@ class AttributeSlider(wx.Panel):
self.inverse = inverse self.inverse = inverse
# The internal slider basically represents the percentage towards the end of the range. It has to be normalized def getStep(valRange):
# 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 Find step for the passed range, which is based on 1, 2 or 5.
# toward one end 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 def getDigitPlaces(minValue, maxValue):
# (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) minDigits = 3
# maxDigits = 5
# self.SliderMinValue = -100 currentDecision = minDigits
# self.SliderMaxValue = 100 for value in (floatUnerr(minValue), floatUnerr(maxValue)):
# self.SliderValue = 0 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) 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 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): class ItemCompare(wx.Panel):
def __init__(self, parent, stuff, item, items, context=None): def __init__(self, parent, stuff, item, items, context=None):
# Start dealing with Price stuff to get that thread going # Start dealing with Price stuff to get that thread going
@@ -27,8 +31,7 @@ class ItemCompare(wx.Panel):
self.currentSort = None self.currentSort = None
self.sortReverse = False self.sortReverse = False
self.item = item self.item = item
self.items = sorted(items, self.items = sorted(items, key=defaultSort)
key=lambda x: x.attributes['metaLevel'].value if 'metaLevel' in x.attributes else 0)
self.attrs = {} self.attrs = {}
# get a dict of attrName: attrInfo of all unique attributes across all items # 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. # starts at 0 while the list has the item name as column 0.
attr = str(list(self.attrs.keys())[sort - 1]) attr = str(list(self.attrs.keys())[sort - 1])
func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0 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: except IndexError:
# Clicked on a column that's not part of our array (price most likely) # Price
self.sortReverse = False if sort == len(self.attrs) + 1:
func = lambda _val: _val.attributes['metaLevel'].value if 'metaLevel' in _val.attributes else 0.0 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) 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) self.paramList.SetItem(i, x + 1, valueUnit)
# Add prices # 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.paramList.RefreshRows()
self.Layout() self.Layout()

View File

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

View File

@@ -22,38 +22,67 @@ class ItemMutator(wx.Panel):
self.item = item self.item = item
self.timer = None self.timer = None
self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit() self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit()
font = parent.GetFont()
font.SetWeight(wx.BOLD)
mainSizer = wx.BoxSizer(wx.VERTICAL) 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.goodColor = wx.Colour(96, 191, 0)
self.badColor = wx.Colour(255, 64, 0) self.badColor = wx.Colour(255, 64, 0)
self.event_mapping = {} self.event_mapping = {}
for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName):
# create array for the two ranges # Format: [raw value, modifier applied to base raw value, display value]
min_t = [m.minValue, m.minMod, None] range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue))
max_t = [m.maxValue, m.maxMod, None] 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 # minValue/maxValue do not always correspond to min/max, because these are
min_t[2] = min_t[1] < 1 if not m.highIsGood else 1 < min_t[1] # just base value multiplied by minMod/maxMod, and in case base is negative
max_t[2] = max_t[1] < 1 if not m.highIsGood else 1 < max_t[1] # minValue is actually bigger than maxValue
if range1[0] <= range2[0]:
# Lastly, we need to determine which range value is "worse" (left side) or "better" (right side) minRange = range1
if (m.highIsGood and min_t[1] > max_t[1]) or (not m.highIsGood and min_t[1] < max_t[1]): maxRange = range2
better_range = min_t
else: 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]): if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]):
worse_range = max_t betterRange = minRange
worseRange = maxRange
else: 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) 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) headingSizer.Add(BitmapLoader.getStaticBitmap(m.attribute.iconID, self, "icons"), 0, wx.RIGHT, 10)
displayName = wx.StaticText(self, wx.ID_ANY, m.attribute.displayName) 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) headingSizer.Add(displayName, 3, wx.ALL | wx.EXPAND, 0)
worst_val = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(worse_range[0]), rounding='dec') worseVal = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(worseRange[0]), rounding='dec')
worst_text = wx.StaticText(self, wx.ID_ANY, worst_val) worseText = wx.StaticText(self, wx.ID_ANY, worseVal)
worst_text.SetForegroundColour(self.goodColor if worse_range[2] else self.badColor) worseText.SetForegroundColour(self.badColor)
best_val = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(better_range[0]), rounding='dec') betterVal = ItemParams.FormatValue(*m.attribute.unit.PreformatValue(betterRange[0]), rounding='dec')
best_text = wx.StaticText(self, wx.ID_ANY, best_val) betterText = wx.StaticText(self, wx.ID_ANY, betterVal)
best_text.SetForegroundColour(self.goodColor if better_range[2] else self.badColor) 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(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) mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5)
slider = AttributeSlider(parent=self, slider = AttributeSlider(parent=self,
baseValue=m.attribute.unit.SimplifyValue(m.baseValue), baseValue=m.attribute.unit.SimplifyValue(sliderBaseValue),
minValue=m.attribute.unit.SimplifyValue(min_t[0]), minValue=displayMinRange[1],
maxValue=m.attribute.unit.SimplifyValue(max_t[0]), maxValue=displayMaxRange[1],
inverse=better_range is min_t) inverse=displayMaxRange is worseRange)
slider.SetValue(m.attribute.unit.SimplifyValue(m.value), False) slider.SetValue(m.attribute.unit.SimplifyValue(m.value), False)
slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue) slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue)
self.event_mapping[slider] = m self.event_mapping[slider] = m

View File

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

View File

@@ -81,6 +81,11 @@ class PFContextMenuPref(PreferenceView):
rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5) rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5)
self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change) 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) mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0)
panel.SetSizer(mainSizer) panel.SetSizer(mainSizer)
@@ -107,6 +112,9 @@ class PFContextMenuPref(PreferenceView):
def OnSetting7Change(self, event): def OnSetting7Change(self, event):
self.settings.set('project', event.GetInt()) self.settings.set('project', event.GetInt())
def OnSetting8Change(self, event):
self.settings.set('moduleFill', event.GetInt())
def getImage(self): def getImage(self):
return BitmapLoader.getBitmap("settings_menu", "gui") 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) 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) 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", self.cbOpenFitInNew = wx.CheckBox(panel, wx.ID_ANY, "Open fittings in a new page by default",
wx.DefaultPosition, wx.DefaultSize, 0) wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbOpenFitInNew, 0, wx.ALL | wx.EXPAND, 5) 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.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False)
self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False) self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False)
self.cbGaugeAnimation.SetValue(self.sFit.serviceFittingOptions["enableGaugeAnimation"]) self.cbGaugeAnimation.SetValue(self.sFit.serviceFittingOptions["enableGaugeAnimation"])
self.cbExportCharges.SetValue(self.sFit.serviceFittingOptions["exportCharges"])
self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"]) self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"])
self.chPriceSource.SetStringSelection(self.sFit.serviceFittingOptions["priceSource"]) self.chPriceSource.SetStringSelection(self.sFit.serviceFittingOptions["priceSource"])
self.chPriceSystem.SetStringSelection(self.sFit.serviceFittingOptions["priceSystem"]) self.chPriceSystem.SetStringSelection(self.sFit.serviceFittingOptions["priceSystem"])
@@ -151,7 +146,6 @@ class PFGeneralPref(PreferenceView):
self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip) self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip)
self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts) self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts)
self.cbGaugeAnimation.Bind(wx.EVT_CHECKBOX, self.onCBGaugeAnimation) 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.cbOpenFitInNew.Bind(wx.EVT_CHECKBOX, self.onCBOpenFitInNew)
self.chPriceSource.Bind(wx.EVT_CHOICE, self.onPricesSourceSelection) self.chPriceSource.Bind(wx.EVT_CHOICE, self.onPricesSourceSelection)
self.chPriceSystem.Bind(wx.EVT_CHOICE, self.onPriceSelection) self.chPriceSystem.Bind(wx.EVT_CHOICE, self.onPriceSelection)
@@ -220,9 +214,6 @@ class PFGeneralPref(PreferenceView):
def onCBGaugeAnimation(self, event): def onCBGaugeAnimation(self, event):
self.sFit.serviceFittingOptions["enableGaugeAnimation"] = self.cbGaugeAnimation.GetValue() self.sFit.serviceFittingOptions["enableGaugeAnimation"] = self.cbGaugeAnimation.GetValue()
def onCBExportCharges(self, event):
self.sFit.serviceFittingOptions["exportCharges"] = self.cbExportCharges.GetValue()
def onCBOpenFitInNew(self, event): def onCBOpenFitInNew(self, event):
self.sFit.serviceFittingOptions["openFitInNew"] = self.cbOpenFitInNew.GetValue() 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 .events import FitSelected, SearchSelected, ImportSelected, Stage1Selected, Stage2Selected, Stage3Selected
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from service.fit import Fit from service.fit import Fit
from gui.utils.helpers_wxPython import HandleCtrlBackspace
pyfalog = Logger(__name__) 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_TEXT_ENTER, self.OnBrowserSearchBoxEnter)
# self.BrowserSearchBox.Bind(wx.EVT_KILL_FOCUS, self.OnBrowserSearchBoxLostFocus) # 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.BrowserSearchBox.Bind(wx.EVT_TEXT, self.OnScheduleSearch)
self.SetMinSize(size) self.SetMinSize(size)
@@ -103,9 +104,11 @@ class NavigationPanel(SFItem.SFBrowserItem):
def OnBrowserSearchBoxLostFocus(self, event): def OnBrowserSearchBoxLostFocus(self, event):
self.BrowserSearchBox.Show(False) self.BrowserSearchBox.Show(False)
def OnBrowserSearchBoxEsc(self, event): def OnBrowserSearchBoxKeyPress(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE: if event.GetKeyCode() == wx.WXK_ESCAPE:
self.BrowserSearchBox.Show(False) self.BrowserSearchBox.Show(False)
elif event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.BrowserSearchBox)
else: else:
event.Skip() event.Skip()

View File

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

View File

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

View File

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

View File

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

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