Merge branch 'pyfa-org:master' into master

This commit is contained in:
StormDelay
2025-05-20 18:33:48 +02:00
committed by GitHub
791 changed files with 382527 additions and 28685 deletions

View File

@@ -1,75 +1,49 @@
image:
- Visual Studio 2019
- Ubuntu
- macos
clone_depth: 1
- Ubuntu2204
- Visual Studio 2022
- macos-catalina
for:
-
matrix:
only:
- image: Ubuntu
- image: Ubuntu2204
environment:
APPVEYOR_SSH_KEY: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJDW/+oYNGOiPvwuwAL9tc/LQgg58aosIVpMYfepQZ20V+VZnHpZh8IRDA8Jo5xht19p2PksA+hFgqA0kpKtrSkuiWdE8rATQItfk4gf7yB0yGasJGGQZYazy9k/9XtmYkq2HHOOeEqdxvrICddJQ88MLCLT9lJENSUP/YS/yGcjZFXVxE11pTeIcqlCRU+3eYa1v7BeNvXIKNhZoK5orXWrtuH3cy8jrSns/u70aYfJ6B2jA8CnWnDbuvpeQtEY61SQqlKUsSArNa8NAsXj41wr3Ar9gAG9330w7EMTqlutk8HZO35uHI0q5qinUhaQYufPPrVkb2L/N+ZCfu0fnh appveyor"
APPIMAGE_TOOL: appimagetool-x86_64.AppImage
PYTHON_APPIMAGE: python3.7.16-cp37-cp37m-manylinux2014_x86_64.AppImage
APPVEYOR_SSH_KEY: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDhb96UEXy8yOy/f+riX/8kKbNx/lOfIZ4pP4Cw3Gj3DmnTwEnxtRtyc+xtaxOsKbt+7+EAXFpCzYX+jHMhtd0QtWB7dbey8DBg31g0f8C5EPquqROibVbhzr/F3f6/d52FFfq6Y/CWaAvLjezvipr+zOOsIFcVusqtXdPJQ/LtUJ0LS5d4lFiw5ELHSxHIpqwGwyb7PbR3ufEFoqbr8eYiCH+vlBob72ArPfo2f3u0sMvpGYmjVVu2jj4FEY2h89sLrGyFdNWBoyumRhkb38+WSAuyPa/Y21+g+S8sRzIlkwbxicGNMtrMIi6zHEIGAgA06Sw2psP807h730PPOVaWjUcU3ojNW8hH3nPizF74pT82+iP7/fFC4PXLP+tBa+8OoHC5yiO7QKUKprMSqVa1qOm8fHbrzglplKJXfzSfUtSE+AQ+HtHhuUWKI+0LBLDrsOJwI5hbsPOAuiZ5I3VfqfAOck6SH9TcmlapVmQEypc7d7oeeUtZSOuIWKXp068= dfx@aw"
APPIMAGE_TOOL: appimage-builder-x86_64.AppImage
DEPLOY_DIR: AppDir/opt/pyfa
# APPVEYOR_SSH_BLOCK: true
# APPVEYOR_SSH_BLOCK: true
cache:
- /home/appveyor/.cache/pip -> requirements.txt
init:
- sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
# init:
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
install:
- sh: git fetch --prune --unshallow # to fix the version dump issues
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3.7-dev libgtk-3-dev python3-pip libwebkit2gtk-4.0-dev
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update --allow-releaseinfo-change
# AppImage dependencies
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install libfuse2
# Preparation script dependencies
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-wxgtk4.0 python3-sqlalchemy python3-logbook
before_build:
- sh: mkdir build && cd build
- sh: curl -LO https://github.com/AppImage/AppImageKit/releases/download/13/$APPIMAGE_TOOL && chmod +x $APPIMAGE_TOOL
- sh: curl -LO https://github.com/niess/python-appimage/releases/download/python3.7/$PYTHON_APPIMAGE && chmod +x $PYTHON_APPIMAGE
build_script:
# Prepare Python base AppImage, stripping Python metadata
- sh: ./$PYTHON_APPIMAGE --appimage-extract
- sh: mv squashfs-root AppDir
- sh: rm AppDir/python*.desktop
- sh: rm AppDir/usr/share/applications/*.desktop
- sh: rm AppDir/usr/share/metainfo/*.appdata.xml
- sh: unlink AppDir/AppRun
- sh: mkdir -p $DEPLOY_DIR
# run install pyfa packages and any other requirements
- sh: AppDir/usr/bin/python -s -m pip install -U pip setuptools==41.6.0 wheel pathlib2
- sh: AppDir/usr/bin/python -s -m pip install -r ../requirements.txt
# Speedup, but causes runtime incompatiblities
#- sh: AppDir/usr/bin/python -s -m pip install -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 -r ../requirements.txt
# Run scripts to prep pyfa data and build database
- sh: cd ../
# Prepare pyfa data
- sh: find locale/ -type f -name "*.po" -exec msgen "{}" -o "{}" \;
- sh: build/AppDir/usr/bin/python scripts/compile_lang.py
- sh: build/AppDir/usr/bin/python scripts/dump_crowdin_progress.py
- sh: build/AppDir/usr/bin/python db_update.py
- sh: export PYFA_VERSION="$(python3.7 scripts/dump_version.py)"
# Copy pyfa files to host
- sh: cp -r eos graphs gui imgs locale service utils eve.db config.py pyfa.py db_update.py README.md LICENSE version.yml ./build/$DEPLOY_DIR
- sh: find ./build/$DEPLOY_DIR | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf
# Copy static AppImage files
- sh: cd dist_assets/linux
- sh: chmod +x AppRun
- sh: cp AppRun pyfa.desktop ../../build/AppDir/
- sh: cp pyfa.desktop ../../build/AppDir/usr/share/applications/
- sh: cp pyfa.appdata.xml ../../build/AppDir/usr/share/metainfo/
- sh: chmod +x pyfa && cp pyfa ../../build/AppDir/usr/bin
- sh: cd ../../
# Package it all up
- sh: mkdir dist
- sh: ./build/$APPIMAGE_TOOL build/AppDir dist/pyfa-$PYFA_VERSION-linux.AppImage
- sh: pyenv global system
- sh: python3 -B scripts/compile_lang.py
- sh: python3 -B scripts/dump_crowdin_progress.py
- sh: python3 -B db_update.py
- sh: export PYFA_VERSION="$(python3 -B scripts/dump_version.py)"
- sh: mkdir build
# Download packaging tool
- sh: curl -o $APPIMAGE_TOOL -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
- sh: chmod +x $APPIMAGE_TOOL
build_script:
- sh: mkdir -p AppDir/opt/pyfa
- sh: cp -r eos graphs gui imgs locale service utils eve.db config.py pyfa.py db_update.py README.md LICENSE version.yml AppDir/opt/pyfa/
- sh: mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/
- sh: cp imgs/gui/pyfa64.png AppDir/usr/share/icons/hicolor/64x64/apps/pyfa.png
- sh: ./$APPIMAGE_TOOL --recipe dist_assets/linux/AppImageBuilder.yml
after_build:
- sh: ls -la build
- sh: ls -la
artifacts:
- path: dist/pyfa-$PYFA_VERSION-linux.AppImage
- path: pyfa-$PYFA_VERSION-linux.AppImage
deploy:
tag: $PYFA_VERSION
release: pyfa $PYFA_VERSION
@@ -85,17 +59,15 @@ for:
-
matrix:
only:
- image: Visual Studio 2019
- image: Visual Studio 2022
environment:
PYTHON: "C:\\Python37-x64"
PYTHON: "C:\\Python311-x64"
# Should be enabled only for build process debugging
# init:
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
cache:
- C:\users\appveyor\appdata\local\pip\cache\ -> requirements.txt
install:
- cmd: git fetch --prune --unshallow # to fix the version dump issues
- ps: echo("OS version:")
- ps: "[System.Environment]::OSVersion.Version"
@@ -131,10 +103,8 @@ for:
# pip will build them from source using the MSVC compiler matching the
# target Python version and architecture
- ps: echo("Install pip requirements:")
# This one is needed to build wxpython 4.0.6 on windows
- cmd: "python -m pip install pathlib2"
- cmd: "python -m pip install -r requirements.txt"
- cmd: "python -m pip install PyInstaller==3.6"
- cmd: "python -m pip install PyInstaller==6.0.0"
before_build:
# directory that will contain the built files
- ps: $env:PYFA_DIST_DIR = "c:\projects\$env:APPVEYOR_PROJECT_SLUG\dist"
@@ -150,7 +120,7 @@ for:
# Build gamedata DB
- cmd: "python db_update.py"
# Build command for PyInstaller
- cmd: "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
- cmd: "python -m PyInstaller --clean -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\
# InnoScript EXE building. This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
@@ -177,16 +147,15 @@ for:
-
matrix:
only:
- image: macos
- image: macos-catalina
environment:
APPVEYOR_SSH_KEY: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJDW/+oYNGOiPvwuwAL9tc/LQgg58aosIVpMYfepQZ20V+VZnHpZh8IRDA8Jo5xht19p2PksA+hFgqA0kpKtrSkuiWdE8rATQItfk4gf7yB0yGasJGGQZYazy9k/9XtmYkq2HHOOeEqdxvrICddJQ88MLCLT9lJENSUP/YS/yGcjZFXVxE11pTeIcqlCRU+3eYa1v7BeNvXIKNhZoK5orXWrtuH3cy8jrSns/u70aYfJ6B2jA8CnWnDbuvpeQtEY61SQqlKUsSArNa8NAsXj41wr3Ar9gAG9330w7EMTqlutk8HZO35uHI0q5qinUhaQYufPPrVkb2L/N+ZCfu0fnh appveyor"
cache:
- /Users/appveyor/Library/Caches/pip/ -> requirements.txt
init:
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
- sh: source ~/venv3.7/bin/activate
- sh: source ~/venv3.11/bin/activate
install:
- sh: git fetch --prune --unshallow # to fix the version dump issues
- sh: bash scripts/osx-setup.sh
build_script:
- sh: bash scripts/osx-translations.sh

View File

@@ -2,7 +2,7 @@
## Requirements
- Python 3.7
- Python 3.11
- Git CLI installed
- Python, pip and git are all available as command-line commands (add to the path if needed)

View File

@@ -1,6 +1,6 @@
# pyfa
[![Join us on Slack!](https://pyfainvite.azurewebsites.net/badge.svg)](https://pyfainvite.azurewebsites.net/) [![Build Status](https://travis-ci.org/pyfa-org/Pyfa.svg?branch=master)](https://travis-ci.org/pyfa-org/Pyfa)
[![Build Status](https://ci.appveyor.com/api/projects/status/github/pyfa-org/pyfa?branch=master&svg=true)]([https://travis-ci.org/pyfa-org/Pyfa](https://ci.appveyor.com/project/pyfa-org/pyfa))
![pyfa](https://user-images.githubusercontent.com/275209/66119992-864be080-e5e2-11e9-994a-3a4368c9fad7.png)

View File

@@ -9,6 +9,7 @@ import hashlib
from eos.const import FittingSlot
from cryptography.fernet import Fernet
from collections import namedtuple
pyfalog = Logger(__name__)
@@ -44,9 +45,16 @@ experimentalFeatures = None
version = None
language = None
API_CLIENT_ID = '095d8cd841ac40b581330919b49fe746'
ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback', 'supports_auto_login'])
supported_servers = {
"Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback', True),
# No point having SISI: https://developers.eveonline.com/blog/article/removing-datasource-singularity
# "Singularity": ApiServer("Singularity", "sisilogin.testeveonline.com", "esi.evetech.net", 'b9c3cc79448f449ab17f3aebd018842e', 'https://pyfa-org.github.io/Pyfa/callback'),
"Serenity": ApiServer("Serenity", "login.evepc.163.com", "ali-esi.evepc.163.com", 'bc90aa496a404724a93f41b4f4e97761', 'https://ali-esi.evepc.163.com/ui/oauth2-redirect.html', False)
}
SSO_LOGOFF_SERENITY='https://login.evepc.163.com/account/logoff'
ESI_CACHE = 'esi_cache'
SSO_CALLBACK = 'https://pyfa-org.github.io/Pyfa/callback'
LOGLEVEL_MAP = {
"critical": CRITICAL,
@@ -58,13 +66,22 @@ LOGLEVEL_MAP = {
CATALOG = 'lang'
slotColourMapDark = {
FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13
FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5
FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5
FittingSlot.RIG: '',
FittingSlot.SUBSYSTEM: ''}
errColorDark = wx.Colour(70, 20, 20)
slotColourMap = {
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
FittingSlot.RIG: '',
FittingSlot.SUBSYSTEM: ''
}
FittingSlot.SUBSYSTEM: ''}
errColor = wx.Colour(204, 51, 51)
def getClientSecret():
return clientHash

View File

@@ -141,14 +141,15 @@ def update_db():
(row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us'])
or row['typeName_en-us'] == 'Capsule'
or row['groupID'] == 4033 # destructible effect beacons
or re.match('AIR .+Booster.*', row['typeName_en-us'])
or row['typeID'] == 82941 # Metenox service
or re.match(r'AIR .+Booster.*', row['typeName_en-us'])
):
row['published'] = True
# Nearly useless and clutter search results too much
elif (
row['typeName_en-us'].startswith('Limited Synth ')
or row['typeName_en-us'].startswith('Expired ')
or re.match('Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
or re.match(r'Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
or row['typeName_en-us'].endswith(' Filament') and (
"'Needlejack'" not in row['typeName_en-us'] and
"'Devana'" not in row['typeName_en-us'] and
@@ -544,7 +545,7 @@ def update_db():
continue
typeName = row.get('typeName_en-us', '')
# Regular sets matching
m = re.match('(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
m = re.match(r'(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
if m:
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
# Special set matching
@@ -601,7 +602,7 @@ def update_db():
eos.gamedata.Item.name.like('%mutated%'),
eos.gamedata.Item.name.like('%_PLACEHOLDER%'),
# Drifter weapons are published for some reason
eos.gamedata.Item.name.in_(('Lux Kontos', 'Lux Xiphos'))
eos.gamedata.Item.name.in_(('Lux Kontos', 'Lux Xiphos', 'Lux Ballistra', 'Lux Kopis'))
)).all():
if 'Asteroid Mining Crystal' in item.name:
continue
@@ -617,6 +618,16 @@ def update_db():
eos.db.gamedata_session.delete(cat)
# Unused normally, can be useful for customizing items
def _copyItem(srcName, tgtTypeID, tgtName):
eveType = eos.db.gamedata_session.query(eos.gamedata.Item).filter(eos.gamedata.Item.name == srcName).one()
eos.db.gamedata_session.expunge(eveType)
sqlalchemy.orm.make_transient(eveType)
eveType.ID = tgtTypeID
for suffix in eos.config.translation_mapping.values():
setattr(eveType, f'typeName{suffix}', tgtName)
eos.db.gamedata_session.add(eveType)
eos.db.gamedata_session.flush()
def _hardcodeAttribs(typeID, attrMap):
for attrName, value in attrMap.items():
try:
@@ -625,7 +636,7 @@ def update_db():
except sqlalchemy.orm.exc.NoResultFound:
attrInfo = eos.db.gamedata_session.query(eos.gamedata.AttributeInfo).filter(eos.gamedata.AttributeInfo.name == attrName).one()
attr = eos.gamedata.Attribute()
attr.ID = attrInfo.ID
attr.attributeID = attrInfo.ID
attr.typeID = typeID
attr.value = value
eos.db.gamedata_session.add(attr)
@@ -641,138 +652,160 @@ def update_db():
effect.effectName = effectName
item.effects[effectName] = effect
def hardcodeGeri():
def hardcodeSuppressionTackleRange():
beaconTypeID = 79839
attrMap = {
'warfareBuff1ID': 2405,
'warfareBuff1Value': 10}
effectMap = {100000: 'pyfaCustomSuppressionTackleRange'}
_hardcodeAttribs(beaconTypeID, attrMap)
_hardcodeEffects(beaconTypeID, effectMap)
def hardcodeShapash():
shapashTypeID = 1000000
_copyItem(srcName='Utu', tgtTypeID=shapashTypeID, tgtName='Shapash')
attrMap = {
# Fitting
'powerOutput': 50,
'cpuOutput': 200,
'capacitorCapacity': 325,
'rechargeRate': 130000,
'cpuOutput': 225,
'capacitorCapacity': 420,
'rechargeRate': 187500,
# Slots
'hiSlots': 5,
'hiSlots': 3,
'medSlots': 4,
'lowSlots': 4,
'launcherSlotsLeft': 3,
'turretSlotsLeft': 2,
'launcherSlotsLeft': 0,
'turretSlotsLeft': 3,
# Rigs
'rigSlots': 2,
'rigSize': 1,
'upgradeCapacity': 400,
# Shield
'shieldCapacity': 1000,
'shieldEmDamageResonance': 1 - 0.75,
'shieldCapacity': 575,
'shieldRechargeRate': 625000,
'shieldEmDamageResonance': 1 - 0.0,
'shieldThermalDamageResonance': 1 - 0.6,
'shieldKineticDamageResonance': 1 - 0.4,
'shieldKineticDamageResonance': 1 - 0.85,
'shieldExplosiveDamageResonance': 1 - 0.5,
# Armor
'armorHP': 1000,
'armorEmDamageResonance': 1 - 0.9,
'armorHP': 1015,
'armorEmDamageResonance': 1 - 0.5,
'armorThermalDamageResonance': 1 - 0.675,
'armorKineticDamageResonance': 1 - 0.25,
'armorKineticDamageResonance': 1 - 0.8375,
'armorExplosiveDamageResonance': 1 - 0.1,
# Structure
'hp': 700,
'hp': 1274,
'emDamageResonance': 1 - 0.33,
'thermalDamageResonance': 1 - 0.33,
'kineticDamageResonance': 1 - 0.33,
'explosiveDamageResonance': 1 - 0.33,
'mass': 1309000,
'volume': 27289,
'capacity': 260,
'mass': 1215000,
'volume': 29500,
'capacity': 165,
# Navigation
'maxVelocity': 440,
'agility': 2.5,
'maxVelocity': 325,
'agility': 3.467,
'warpSpeedMultiplier': 5.5,
# Drones
'droneCapacity': 50,
'droneBandwidth': 10,
'droneCapacity': 75,
'droneBandwidth': 25,
# Targeting
'maxTargetRange': 42000,
'maxTargetRange': 49000,
'maxLockedTargets': 6,
'scanRadarStrength': 0,
'scanLadarStrength': 12,
'scanMagnetometricStrength': 0,
'scanLadarStrength': 0,
'scanMagnetometricStrength': 9,
'scanGravimetricStrength': 0,
'signatureRadius': 33,
'scanResolution': 770}
'signatureRadius': 39,
'scanResolution': 550,
# Misc
'energyWarfareResistance': 0,
'stasisWebifierResistance': 0,
'weaponDisruptionResistance': 0}
effectMap = {
100100: 'pyfaCustomGeriAfExploVel',
100101: 'pyfaCustomGeriAfRof',
100102: 'pyfaCustomGeriMfDmg',
100103: 'pyfaCustomGeriMfRep',
100104: 'pyfaCustomGeriRoleWebDroneStr',
100105: 'pyfaCustomGeriRoleWebDroneHP',
100106: 'pyfaCustomGeriRoleWebDroneSpeed',
100107: 'pyfaCustomGeriRoleMWDSigBloom'}
_hardcodeAttribs(74141, attrMap)
_hardcodeEffects(74141, effectMap)
100100: 'pyfaCustomShapashAfArAmount',
100101: 'pyfaCustomShapashAfShtTrackingOptimal',
100102: 'pyfaCustomShapashGfShtDamage',
100103: 'pyfaCustomShapashGfPointRange',
100104: 'pyfaCustomShapashGfPropOverheat',
100105: 'pyfaCustomShapashRolePlateMass',
100106: 'pyfaCustomShapashRoleHeat'}
_hardcodeAttribs(shapashTypeID, attrMap)
_hardcodeEffects(shapashTypeID, effectMap)
def hardcodeBestla():
def hardcodeCybele():
cybeleTypeID = 1000001
_copyItem(srcName='Adrestia', tgtTypeID=cybeleTypeID, tgtName='Cybele')
attrMap = {
# Fitting
'powerOutput': 1300,
'cpuOutput': 500,
'capacitorCapacity': 1500,
'rechargeRate': 200000,
'hiSlots': 6,
'medSlots': 5,
'lowSlots': 5,
'launcherSlotsLeft': 4,
'turretSlotsLeft': 2,
'powerOutput': 1284,
'cpuOutput': 400,
'capacitorCapacity': 2400,
'rechargeRate': 334000,
'hiSlots': 5,
'medSlots': 4,
'lowSlots': 6,
'launcherSlotsLeft': 0,
'turretSlotsLeft': 5,
# Rigs
'rigSlots': 2,
'rigSize': 2,
'upgradeCapacity': 400,
# Shield
'shieldCapacity': 3000,
'shieldEmDamageResonance': 1 - 0.75,
'shieldThermalDamageResonance': 1 - 0.6,
'shieldKineticDamageResonance': 1 - 0.4,
'shieldCapacity': 1200,
'shieldRechargeRate': 1250000,
'shieldEmDamageResonance': 1 - 0.0,
'shieldThermalDamageResonance': 1 - 0.5,
'shieldKineticDamageResonance': 1 - 0.9,
'shieldExplosiveDamageResonance': 1 - 0.5,
# Armor
'armorHP': 3000,
'armorEmDamageResonance': 1 - 0.9,
'armorThermalDamageResonance': 1 - 0.675,
'armorKineticDamageResonance': 1 - 0.25,
'armorHP': 1900,
'armorEmDamageResonance': 1 - 0.5,
'armorThermalDamageResonance': 1 - 0.69,
'armorKineticDamageResonance': 1 - 0.85,
'armorExplosiveDamageResonance': 1 - 0.1,
# Structure
'hp': 1600,
'hp': 2300,
'emDamageResonance': 1 - 0.33,
'thermalDamageResonance': 1 - 0.33,
'kineticDamageResonance': 1 - 0.33,
'explosiveDamageResonance': 1 - 0.33,
'mass': 11650000,
'volume': 96000,
'capacity': 660,
'mass': 11100000,
'volume': 112000,
'capacity': 450,
# Navigation
'maxVelocity': 300,
'agility': 0.47,
'maxVelocity': 235,
'agility': 0.457,
'warpSpeedMultiplier': 4.5,
# Drones
'droneCapacity': 125,
'droneBandwidth': 20,
'droneCapacity': 100,
'droneBandwidth': 50,
# Targeting
'maxTargetRange': 80000,
'maxLockedTargets': 7,
'maxTargetRange': 60000,
'maxLockedTargets': 6,
'scanRadarStrength': 0,
'scanLadarStrength': 22,
'scanMagnetometricStrength': 0,
'scanLadarStrength': 0,
'scanMagnetometricStrength': 15,
'scanGravimetricStrength': 0,
'signatureRadius': 120,
'scanResolution': 340}
'signatureRadius': 115,
'scanResolution': 330,
# Misc
'energyWarfareResistance': 0,
'stasisWebifierResistance': 0,
'weaponDisruptionResistance': 0}
effectMap = {
100200: 'pyfaCustomBestlaHacExploVel',
100201: 'pyfaCustomBestlaHacRof',
100202: 'pyfaCustomBestlaMcDmg',
100203: 'pyfaCustomBestlaMcRep',
100204: 'pyfaCustomBestlaRoleWebDroneStr',
100205: 'pyfaCustomBestlaRoleWebDroneHP',
100206: 'pyfaCustomBestlaRoleWebDroneSpeed'}
_hardcodeAttribs(74316, attrMap)
_hardcodeEffects(74316, effectMap)
100200: 'pyfaCustomCybeleHacMhtFalloff',
100201: 'pyfaCustomCybeleHacMhtTracking',
100202: 'pyfaCustomCybeleGcMhtDamage',
100203: 'pyfaCustomCybeleGcArAmount',
100204: 'pyfaCustomCybeleGcPointRange',
100205: 'pyfaCustomCybeleRoleVelocity',
100206: 'pyfaCustomCybeleRolePlateMass'}
_hardcodeAttribs(cybeleTypeID, attrMap)
_hardcodeEffects(cybeleTypeID, effectMap)
# hardcodeGeri()
# hardcodeBestla()
hardcodeSuppressionTackleRange()
eos.db.gamedata_session.commit()
eos.db.gamedata_engine.execute('VACUUM')

View File

@@ -0,0 +1,74 @@
version: 1
AppDir:
path: ./AppDir
app_info:
id: pyfa
name: pyfa
icon: pyfa
version: '{{PYFA_VERSION}}'
exec: usr/bin/python3.11
exec_args: "-s $APPDIR/opt/pyfa/pyfa.py $@"
apt:
arch: [ amd64 ]
sources:
- sourceline: 'deb http://us.archive.ubuntu.com/ubuntu jammy main restricted universe multiverse'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c'
- sourceline: 'deb http://us.archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c'
- sourceline: 'deb http://us.archive.ubuntu.com/ubuntu jammy-backports main restricted universe multiverse'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c'
- sourceline: 'deb http://us.archive.ubuntu.com/ubuntu jammy-security main restricted universe multiverse'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c'
- sourceline: 'deb https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy main'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xf23c5a6cf475977595c89f51ba6932366a755776'
include:
- python3.11
# wx dependencies
- libgtk-3-0
- librsvg2-common # GTK3 recommendation; without it, search in char editor crashes
- libwebkit2gtk-4.0-37 # Needed for wx's HTML lib
# Unknown
- libpcre2-32-0 # https://github.com/pyfa-org/Pyfa/issues/2572
- libnotify4 # https://github.com/pyfa-org/Pyfa/issues/2598
- libwayland-client0 # https://github.com/pyfa-org/Pyfa/issues/2600
exclude:
- hicolor-icon-theme
- humanity-icon-theme
- ubuntu-mono
after_bundle:
# Install python dependencies to bundled interpreter
- export PYTHONHOME="AppDir/usr"
- export PYTHONPATH="AppDir/usr/lib/python3.11/site-packages"
- curl -L https://bootstrap.pypa.io/get-pip.py -o get-pip.py
- AppDir/usr/bin/python3.11 get-pip.py
# Just to bundle certificates with AppImage
- AppDir/usr/bin/python3.11 -s -m pip install certifi
- AppDir/usr/bin/python3.11 -s -m pip install -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 -r requirements.txt
files:
exclude:
- usr/lib/x86_64-linux-gnu/gconv
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
- usr/include
runtime:
env:
PYTHONHOME: '${APPDIR}/usr'
PYTHONPATH: '${APPDIR}/usr/lib/python3.11/site-packages'
SSL_CERT_FILE: '${APPDIR}/usr/local/lib/python3.11/dist-packages/certifi/cacert.pem'
# Workaround for https://github.com/AppImageCrafters/appimage-builder/issues/336
XDG_DATA_DIRS: '${APPDIR}/usr/local/share:${APPDIR}/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS'
AppImage:
sign-key: None
arch: x86_64
file_name: 'pyfa-{{PYFA_VERSION}}-linux.AppImage'

View File

@@ -1,19 +0,0 @@
#! /bin/bash -i
# Export APPRUN if running from an extracted image
self="$(readlink -f -- $0)"
here="${self%/*}"
APPDIR="${APPDIR:-${here}}"
# Export TCl/Tk
export TCL_LIBRARY="${APPDIR}/usr/share/tcltk/tcl8.4"
export TK_LIBRARY="${APPDIR}/usr/share/tcltk/tk8.4"
export TKPATH="${TK_LIBRARY}"
# Export SSL certificate
export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
# Call the entry point
#! /bin/bash -i
${APPDIR}/usr/bin/pyfa "$@"

View File

@@ -1,3 +0,0 @@
#! /bin/bash
${APPDIR}/usr/bin/python3.7 -s "${APPDIR}/opt/pyfa/pyfa.py" "$@"

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>org.pyfa.pyfa</id>
<metadata_license>MIT</metadata_license>
<project_license>GPL-3</project_license>
<name>Pyfa</name>
<summary>Pyfa </summary>
<description>
<p> Python Fitting Assitant for EVE Online
</p>
</description>
<launchable type="desktop-id">pyfa.desktop</launchable>
<url type="homepage">https://github.com/pyfa-org/Pyfa</url>
<provides>
<binary>pyfa</binary>
</provides>
</component>

View File

@@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Pyfa
Exec=pyfa
Comment=Python Fitting Assistant for EVE: Online
Icon=python
Categories=Game;

View File

@@ -80,6 +80,7 @@ exe = EXE(pyz,
app = BUNDLE(
exe,
name='pyfa.app',
version=os.getenv('PYFA_VERSION'),
icon=icon,
bundle_identifier=None,
info_plist={
@@ -88,5 +89,7 @@ app = BUNDLE(
'CFBundleName': 'pyfa',
'CFBundleDisplayName': 'pyfa',
'CFBundleIdentifier': 'org.pyfaorg.pyfa',
'CFBundleVersion': os.getenv('PYFA_VERSION'),
'CFBundleShortVersionString': os.getenv('PYFA_VERSION'),
}
)

View File

@@ -14,7 +14,7 @@ with open("version.yml", 'r') as file:
os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
os.environ["PYFA_VERSION"] = version
iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
iscc = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")

View File

@@ -42,10 +42,10 @@ CloseApplications=yes
DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
LicenseFile={#MyAppDir}\LICENSE
LicenseFile={#MyAppDir}\app\LICENSE
OutputDir={#MyOutputDir}
OutputBaseFilename={#MyOutputFile}
SetupIconFile={#MyAppDir}\pyfa.ico
SetupIconFile={#MyAppDir}\app\pyfa.ico
SolidCompression=yes
[Languages]

View File

@@ -10,10 +10,15 @@
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>

View File

@@ -51,7 +51,7 @@ mapper(DynamicItemAttribute, dynamicAttributes_table,
properties={"info": relation(AttributeInfo, lazy=False)})
mapper(DynamicItemItem, dynamicApplicable_table, properties={
"mutaplasmid": relation(DynamicItem),
"mutaplasmid": relation(DynamicItem, viewonly=True),
})
DynamicItemAttribute.ID = association_proxy("info", "attributeID")

View File

@@ -69,7 +69,8 @@ props = {
primaryjoin=dynamicApplicable_table.c.applicableTypeID == items_table.c.typeID,
secondaryjoin=dynamicApplicable_table.c.typeID == DynamicItem.typeID,
secondary=dynamicApplicable_table,
backref="applicableItems"
backref="applicableItems",
viewonly=True
)
}

View File

@@ -21,7 +21,7 @@ for modName in iterNamespace(__name__, __path__):
# loop through python files, extracting update number and function, and
# adding it to a list
modname_tail = modName.rsplit('.', 1)[-1]
m = re.match("^upgrade(?P<index>\d+)$", modname_tail)
m = re.match(r"^upgrade(?P<index>\d+)$", modname_tail)
if not m:
continue
index = int(m.group("index"))

View File

@@ -7,7 +7,7 @@ Migration 1
loaded as they no longer exist in the database. We therefore replace these
modules with their new replacements
Based on http://community.eveonline.com/news/patch-notes/patch-notes-for-oceanus/
Based on https://www.eveonline.com/news/view/patch-notes-for-oceanus
and output of itemDiff.py
"""

View File

@@ -2,7 +2,7 @@
Migration 25
- Converts T3C fitting configurations based on the spreadsheet noted here:
https://community.eveonline.com/news/patch-notes/patch-notes-for-july-2017-release
https://www.eveonline.com/news/view/patch-notes-for-july-2017-release
(csv copies can be found on the pyfa repo in case the official documents are deleted)
@@ -4228,8 +4228,8 @@ def upgrade(saveddata_engine):
# We don't have a conversion for this. I don't think this will ever happen, but who knows
continue
# It doesn't actully matter which old module is replaced with which new module, so we don't have to worry
# about module position or anything like that. Just doe a straight up record UPDATE
# It doesn't actually matter which old module is replaced with which new module, so we don't have to worry
# about module position or anything like that. Just do a straight up record UPDATE
for i, old in enumerate(oldModules[:4]):
saveddata_engine.execute("UPDATE modules SET itemID = ? WHERE ID = ?", (newModules[i], old[0]))

View File

@@ -6,7 +6,7 @@ Migration 4
from database), which causes pyfa to crash. We therefore replace these
modules with their new replacements
Based on http://community.eveonline.com/news/patch-notes/patch-notes-for-proteus/
Based on https://www.eveonline.com/news/view/patch-notes-for-proteus
and output of itemDiff.py
"""

View File

@@ -0,0 +1,36 @@
"""
Migration 46
- add support for server selection for SSO characters
"""
import sqlalchemy
tmpTable = """
CREATE TABLE ssoCharacterTemp (
ID INTEGER NOT NULL,
client VARCHAR NOT NULL,
characterID INTEGER NOT NULL,
characterName VARCHAR NOT NULL,
refreshToken VARCHAR NOT NULL,
accessToken VARCHAR NOT NULL,
accessTokenExpires DATETIME NOT NULL,
created DATETIME,
modified DATETIME,
server VARCHAR,
PRIMARY KEY (ID),
CONSTRAINT "uix_client_server_characterID" UNIQUE (client, server, characterID),
CONSTRAINT "uix_client_server_characterName" UNIQUE (client, server, characterName)
)
"""
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT server FROM ssoCharacter LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute(tmpTable)
saveddata_engine.execute(
"INSERT INTO ssoCharacterTemp (ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, server) "
"SELECT ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, 'Tranquility' "
"FROM ssoCharacter")
saveddata_engine.execute("DROP TABLE ssoCharacter")
saveddata_engine.execute("ALTER TABLE ssoCharacterTemp RENAME TO ssoCharacter")

View File

@@ -0,0 +1,15 @@
"""
Migration 48
- added pilot security column (CONCORD ships)
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT pilotSecurity FROM fits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN pilotSecurity FLOAT")

View File

@@ -0,0 +1,15 @@
"""
Migration 49
- added hp column to targetResists table
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT hp FROM targetResists LIMIT 1;")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE targetResists ADD COLUMN hp FLOAT;")

View File

@@ -40,18 +40,18 @@ characters_table = Table("characters", saveddata_meta,
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now))
sso_table = Table("ssoCharacter", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("client", String, nullable=False),
Column("characterID", Integer, nullable=False),
Column("characterName", String, nullable=False),
Column("refreshToken", String, nullable=False),
Column("accessToken", String, nullable=False),
Column("accessTokenExpires", DateTime, nullable=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
UniqueConstraint('client', 'characterID', name='uix_client_characterID'),
UniqueConstraint('client', 'characterName', name='uix_client_characterName')
)
Column("ID", Integer, primary_key=True),
Column("client", String, nullable=False),
Column("characterID", Integer, nullable=False),
Column("characterName", String, nullable=False),
Column("server", String, nullable=False),
Column("refreshToken", String, nullable=False),
Column("accessToken", String, nullable=False),
Column("accessTokenExpires", DateTime, nullable=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
UniqueConstraint('client', 'server', 'characterID', name='uix_client_server_characterID'),
UniqueConstraint('client', 'server', 'characterName', name='uix_client_server_characterName'))
sso_character_map_table = Table("ssoCharacterMap", saveddata_meta,
Column("characterID", ForeignKey("characters.ID"), primary_key=True),

View File

@@ -63,7 +63,8 @@ fits_table = Table("fits", saveddata_meta,
Column("ignoreRestrictions", Boolean, default=0),
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now),
Column("systemSecurity", Integer, nullable=True)
Column("systemSecurity", Integer, nullable=True),
Column("pilotSecurity", Float, nullable=True),
)
projectedFits_table = Table("projectedFits", saveddata_meta,
@@ -175,12 +176,13 @@ mapper(es_Fit, fits_table,
collection_class=HandledModuleList,
primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), # noqa
order_by=modules_table.c.position,
overlaps='owner',
cascade='all, delete, delete-orphan'),
"_Fit__projectedModules": relation(
Module,
collection_class=HandledProjectedModList,
overlaps='owner, _Fit__modules',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), # noqa
"owner": relation(
User,
@@ -190,37 +192,37 @@ mapper(es_Fit, fits_table,
"_Fit__boosters": relation(
Booster,
collection_class=HandledBoosterList,
cascade='all, delete, delete-orphan',
single_parent=True),
overlaps='owner',
cascade='all, delete, delete-orphan'),
"_Fit__drones": relation(
Drone,
collection_class=HandledDroneCargoList,
overlaps='owner',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), # noqa
"_Fit__fighters": relation(
Fighter,
collection_class=HandledDroneCargoList,
overlaps='owner',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == False)), # noqa
"_Fit__cargo": relation(
Cargo,
collection_class=HandledDroneCargoList,
overlaps='owner',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(cargo_table.c.fitID == fits_table.c.ID)),
"_Fit__projectedDrones": relation(
Drone,
collection_class=HandledProjectedDroneList,
overlaps='owner, _Fit__drones',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), # noqa
"_Fit__projectedFighters": relation(
Fighter,
collection_class=HandledProjectedDroneList,
overlaps='owner, _Fit__fighters',
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == True)), # noqa
"_Fit__implants": relation(
Implant,

View File

@@ -493,9 +493,12 @@ def getSsoCharacters(clientHash, eager=None):
@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash")
def getSsoCharacter(lookfor, clientHash, eager=None):
def getSsoCharacter(lookfor, clientHash, server=None, eager=None):
filter = SsoCharacter.client == clientHash
if server is not None:
filter = and_(filter, SsoCharacter.server == server)
if isinstance(lookfor, int):
filter = and_(filter, SsoCharacter.ID == lookfor)
elif isinstance(lookfor, str):

View File

@@ -37,6 +37,7 @@ targetProfiles_table = Table(
Column('maxVelocity', Float, nullable=True),
Column('signatureRadius', Float, nullable=True),
Column('radius', Float, nullable=True),
Column('hp', Float, nullable=True),
Column('ownerID', ForeignKey('users.ID'), nullable=True),
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
@@ -48,4 +49,5 @@ mapper(
'rawName': targetProfiles_table.c.name,
'_maxVelocity': targetProfiles_table.c.maxVelocity,
'_signatureRadius': targetProfiles_table.c.signatureRadius,
'_radius': targetProfiles_table.c.radius})
'_radius': targetProfiles_table.c.radius,
'_hp': targetProfiles_table.c.hp})

File diff suppressed because it is too large Load Diff

View File

@@ -343,7 +343,9 @@ class Item(EqBase):
500018: "mordu",
500019: "sansha",
500020: "serpentis",
500026: "triglavian"
500026: "triglavian",
500027: "upwell",
500029: "deathless",
}
@property
@@ -351,11 +353,7 @@ class Item(EqBase):
if self.__race is None:
try:
if (
self.category.name == 'Structure' or
# Here until CCP puts their shit together
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
):
if self.category.name == 'Structure':
self.__race = "upwell"
else:
self.__race = self.factionMap[self.factionID]
@@ -377,7 +375,8 @@ class Item(EqBase):
16 : "jove",
32 : "sansha", # Incrusion Sansha
128: "ore",
135: "triglavian"
135: "triglavian",
168: "upwell",
}
# Race is None by default
race = None
@@ -571,13 +570,18 @@ class DynamicItem(EqBase):
@property
def shortName(self):
name = self.item.customName
keywords = ('Decayed', 'Gravid', 'Unstable', 'Radical')
keywords = (
'Decayed', 'Glorified Decayed',
'Gravid', 'Glorified Gravid',
'Unstable', 'Glorified Unstable',
'Radical', 'Glorified Radical')
for kw in keywords:
if name.startswith(f'{kw} '):
name = kw
m = re.match('(?P<mutagrade>\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
m = re.match(r'(?P<mutagrade>(Glorified )?\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
if m:
name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype'))
name = name.replace('Glorified ', 'Gl. ')
return name

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -161,7 +162,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getVolleyParameters(self, targetProfile=None):
if not self.dealsDamage or self.amountActive <= 0:
return {0: DmgTypes(0, 0, 0, 0)}
return {0: DmgTypes.default()}
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
@@ -170,11 +171,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes(
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
volley = deepcopy(self.__baseVolley)
volley.profile = targetProfile
return {0: volley}
def getVolley(self, targetProfile=None):
@@ -183,16 +181,12 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getDps(self, targetProfile=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
cycleParams = self.getCycleParameters()
if cycleParams is None:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
dpsFactor = 1 / (cycleParams.averageTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
dps = volley * dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):

View File

@@ -19,6 +19,7 @@
import math
from copy import deepcopy
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
@@ -198,16 +199,14 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities:
# Not passing resists here as we want to calculate and store base volley
self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
adjustedVolley = {}
adjustedVolleys = {}
for effectID, effectData in self.__baseVolley.items():
adjustedVolley[effectID] = {}
for volleyTime, volleyValue in effectData.items():
adjustedVolley[effectID][volleyTime] = DmgTypes(
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
adjustedVolleys[effectID] = {}
for volleyTime, baseVolley in effectData.items():
adjustedVolley = deepcopy(baseVolley)
adjustedVolley.profile = targetProfile
adjustedVolleys[effectID][volleyTime] = adjustedVolley
return adjustedVolleys
def getVolleyPerEffect(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
@@ -218,28 +217,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getVolley(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
em = 0
therm = 0
kin = 0
exp = 0
volley = DmgTypes.default()
for volleyData in volleyParams.values():
em += volleyData[0].em
therm += volleyData[0].thermal
kin += volleyData[0].kinetic
exp += volleyData[0].explosive
return DmgTypes(em, therm, kin, exp)
volley += volleyData[0]
return volley
def getDps(self, targetProfile=None):
em = 0
thermal = 0
kinetic = 0
explosive = 0
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
em += dps.em
thermal += dps.thermal
kinetic += dps.kinetic
explosive += dps.explosive
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
dps = DmgTypes.default()
for subdps in self.getDpsPerEffect(targetProfile=targetProfile).values():
dps += subdps
return dps
def getDpsPerEffect(self, targetProfile=None):
if not self.active or self.amount <= 0:

View File

@@ -116,7 +116,7 @@ class FighterAbility:
def getVolley(self, targetProfile=None):
if not self.dealsDamage or not self.active:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
if self.attrPrefix == "fighterAbilityLaunchBomb":
em = self.fighter.getModifiedChargeAttr("emDamage", 0)
therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0)
@@ -128,24 +128,17 @@ class FighterAbility:
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes(
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
volley = DmgTypes(em=em * dmgMult, thermal=therm * dmgMult, kinetic=kin * dmgMult, explosive=exp * dmgMult)
volley.profile = targetProfile
return volley
def getDps(self, targetProfile=None, cycleTimeOverride=None):
volley = self.getVolley(targetProfile=targetProfile)
if not volley:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes(
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
dps = volley * dpsFactor
return dps
def clear(self):

View File

@@ -159,6 +159,12 @@ class Fit:
self.gangBoosts = None
self.__ecmProjectedList = []
self.commandBonuses = {}
# Reps received, as a list of (amount, cycle time in seconds)
self._hullRr = []
self._armorRr = []
self._armorRrPreSpool = []
self._armorRrFullSpool = []
self._shieldRr = []
def clearFactorReloadDependentData(self):
# Here we clear all data known to rely on cycle parameters
@@ -550,6 +556,12 @@ class Fit:
if stuff is not None and stuff != self:
stuff.clear()
self._hullRr.clear()
self._armorRr.clear()
self._armorRrPreSpool.clear()
self._armorRrFullSpool.clear()
self._shieldRr.clear()
# If this is the active fit that we are clearing, not a projected fit,
# then this will run and clear the projected ships and flag the next
# iteration to skip this part to prevent recursion.
@@ -621,7 +633,7 @@ class Fit:
"duration", value)
if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP
self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True)
self.ship.boostItemAttr("shieldCapacity", value)
if warfareBuffID == 13: # Armor Burst: Armor Energizing: Armor Resistance
for damageType in ("Em", "Thermal", "Explosive", "Kinetic"):
@@ -640,7 +652,7 @@ class Fit:
"duration", value)
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
self.ship.boostItemAttr("armorHP", value)
if warfareBuffID == 16: # Information Burst: Sensor Optimization: Scan Resolution
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
@@ -734,7 +746,7 @@ class Fit:
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 42: # Erebus Effect Generator : Armor HP bonus
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
self.ship.boostItemAttr("armorHP", value)
if warfareBuffID == 43: # Erebus Effect Generator : Explosive resistance bonus
for attr in ("armorExplosiveDamageResonance", "shieldExplosiveDamageResonance", "explosiveDamageResonance"):
@@ -756,7 +768,7 @@ class Fit:
self.ship.boostItemAttr(attr, value, stackingPenalties=True)
if warfareBuffID == 48: # Leviathan Effect Generator : Shield HP bonus
self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True)
self.ship.boostItemAttr("shieldCapacity", value)
if warfareBuffID == 49: # Leviathan Effect Generator : EM resistance bonus
for attr in ("armorEmDamageResonance", "shieldEmDamageResonance", "emDamageResonance"):
@@ -870,6 +882,14 @@ class Fit:
if warfareBuffID == 100: # Weather_caustic_toxin_scan_resolution_bonus
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
if warfareBuffID == 2405: # Insurgency Suppression Bonus: Interdiction Range
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Navigation"),
"maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(
lambda mod: mod.item.group.name == "Stasis Web",
"maxRange", value, stackingPenalties=True)
del self.commandBonuses[warfareBuffID]
def __resetDependentCalcs(self):
@@ -1478,11 +1498,11 @@ class Fit:
def tank(self):
reps = {
"passiveShield": self.calculateShieldRecharge(),
"shieldRepair": self.extraAttributes["shieldRepair"],
"armorRepair": self.extraAttributes["armorRepair"],
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"],
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"],
"hullRepair": self.extraAttributes["hullRepair"]
"shieldRepair": self.extraAttributes["shieldRepair"] + self._getAppliedShieldRr(),
"armorRepair": self.extraAttributes["armorRepair"] + self._getAppliedArmorRr(),
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"] + self._getAppliedArmorPreSpoolRr(),
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"] + self._getAppliedArmorFullSpoolRr(),
"hullRepair": self.extraAttributes["hullRepair"] + self._getAppliedHullRr()
}
return reps
@@ -1519,11 +1539,11 @@ class Fit:
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"]
"shieldRepair": self.extraAttributes["shieldRepair"] + self._getAppliedShieldRr(),
"armorRepair": self.extraAttributes["armorRepair"] + self._getAppliedArmorRr(),
"armorRepairPreSpool": self.extraAttributes["armorRepairPreSpool"] + self._getAppliedArmorPreSpoolRr(),
"armorRepairFullSpool": self.extraAttributes["armorRepairFullSpool"] + self._getAppliedArmorFullSpoolRr(),
"hullRepair": self.extraAttributes["hullRepair"] + self._getAppliedHullRr()
}
if not self.capStable or self.factorReload:
# Map a local repairer type to the attribute it uses
@@ -1668,27 +1688,33 @@ class Fit:
self.__droneWaste = droneWaste
def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes(0, 0, 0, 0)
weaponDps = DmgTypes(0, 0, 0, 0)
weaponVolley = DmgTypes.default()
weaponDps = DmgTypes.default()
for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self):
droneVolley = DmgTypes(0, 0, 0, 0)
droneDps = DmgTypes(0, 0, 0, 0)
droneVolley = DmgTypes.default()
droneDps = DmgTypes.default()
for drone in self.drones:
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
droneDps += drone.getDps(targetProfile=self.targetProfile)
droneVolley += drone.getVolley()
droneDps += drone.getDps()
for fighter in self.fighters:
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
droneDps += fighter.getDps(targetProfile=self.targetProfile)
droneVolley += fighter.getVolley()
droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps
self.__droneVolley = droneVolley
@@ -1723,6 +1749,18 @@ class Fit:
secstatus = FitSystemSecurity.NULLSEC
return secstatus
def getPilotSecurity(self, low_limit=-10, high_limit=5):
secstatus = self.pilotSecurity
# Not defined -> use character SS, with 0.0 fallback if it fails
if secstatus is None:
try:
secstatus = self.character.secStatus
except (SystemExit, KeyboardInterrupt):
raise
except:
secstatus = 0
return max(low_limit, min(high_limit, secstatus))
def activeModulesIter(self):
for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE:
@@ -1760,6 +1798,38 @@ class Fit:
mults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
return calculateMultiplier(mults)
def _getAppliedHullRr(self):
return self.__getAppliedRr(self._hullRr)
def _getAppliedArmorRr(self):
return self.__getAppliedRr(self._armorRr)
def _getAppliedArmorPreSpoolRr(self):
return self.__getAppliedRr(self._armorRrPreSpool)
def _getAppliedArmorFullSpoolRr(self):
return self.__getAppliedRr(self._armorRrFullSpool)
def _getAppliedShieldRr(self):
return self.__getAppliedRr(self._shieldRr)
@staticmethod
def __getAppliedRr(rrList):
totalRaw = 0
for amount, cycleTime in rrList:
# That's right, for considerations of RR diminishing returns cycle time is rounded this way
totalRaw += amount / int(cycleTime)
RR_ADDITION = 7000
RR_MULTIPLIER = 10
appliedRr = 0
for amount, cycleTime in rrList:
rrps = amount / int(cycleTime)
modified_rrps = RR_ADDITION + (rrps * RR_MULTIPLIER)
rrps_mult = 1 - (((rrps + modified_rrps) / (totalRaw + modified_rrps)) - 1) ** 2
appliedRr += rrps_mult * amount / cycleTime
return appliedRr
def __deepcopy__(self, memo=None):
fitCopy = Fit()
# Character and owner are not copied
@@ -1772,6 +1842,7 @@ class Fit:
fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity
fitCopy.pilotSecurity = self.pilotSecurity
fitCopy.notes = self.notes
for i in self.modules:

View File

@@ -33,7 +33,7 @@ from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes, RRTypes
from eos.utils.stats import BreacherInfo, DmgTypes, RRTypes
pyfalog = Logger(__name__)
@@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
return True
return False
@property
def isBreacher(self):
return self.charge and 'dotMissileLaunching' in self.charge.effects
def canDealDamage(self, ignoreState=False):
if self.isEmpty:
return False
@@ -469,75 +473,77 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return {0: DmgTypes(0, 0, 0, 0)}
return {0: DmgTypes.default()}
if self.__baseVolley is None:
self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
if self.isBreacher:
dmgDelay = 1
subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000)
breacher_info = BreacherInfo(
absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
for i in range(subcycles):
volley = DmgTypes.default()
volley.add_breacher(dmgDelay + i, breacher_info)
self.__baseVolley[dmgDelay + i] = volley
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
# Reaper DD can damage each target only once
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
# Some delay attributes have non-0 default value, so we have to pick according to effects
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
else:
dmgDelay = 0
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
# Reaper DD can damage each target only once
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = 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
adjustedVolley = {}
for volleyTime, volleyValue in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes(
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
adjustedVolleys = {}
for volleyTime, baseVolley in self.__baseVolley.items():
adjustedVolley = baseVolley * spoolMultiplier
adjustedVolley.profile = targetProfile
adjustedVolleys[volleyTime] = adjustedVolley
return adjustedVolleys
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
if len(volleyParams) == 0:
return DmgTypes(0, 0, 0, 0)
return DmgTypes.default()
return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False):
dmgDuringCycle = DmgTypes(0, 0, 0, 0)
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dps = DmgTypes.default()
cycleParams = self.getCycleParameters()
if cycleParams is None:
return dmgDuringCycle
return dps
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0:
return dmgDuringCycle
for volleyValue in volleyParams.values():
dmgDuringCycle += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps = DmgTypes(
em=dmgDuringCycle.em * dpsFactor,
thermal=dmgDuringCycle.thermal * dpsFactor,
kinetic=dmgDuringCycle.kinetic * dpsFactor,
explosive=dmgDuringCycle.explosive * dpsFactor)
if not getSpreadDPS:
return dps
return {'em':dmgDuringCycle.em * dpsFactor,
'therm': dmgDuringCycle.thermal * dpsFactor,
'kin': dmgDuringCycle.kinetic * dpsFactor,
'exp': dmgDuringCycle.explosive * dpsFactor}
if self.isBreacher:
return volleyParams[min(volleyParams)]
for volleyValue in volleyParams.values():
dps += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps *= dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
@@ -949,6 +955,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParametersForDps(self, reloadOverride=None):
# Special hack for breachers, since those are DoT and work independently of gun cycle
if self.isBreacher:
return CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False)
else:
return self.getCycleParameters(reloadOverride=reloadOverride)
def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well"""
# Determine if we'll take into account reload time or not

View File

@@ -25,10 +25,11 @@ import time
class SsoCharacter:
def __init__(self, charID, name, client, accessToken=None, refreshToken=None):
def __init__(self, charID, name, client, server, accessToken=None, refreshToken=None):
self.characterID = charID
self.characterName = name
self.client = client
self.server = server
self.accessToken = accessToken
self.refreshToken = refreshToken
self.accessTokenExpires = None
@@ -37,6 +38,9 @@ class SsoCharacter:
def init(self):
pass
@property
def characterDisplay(self):
return "{} [{}]".format(self.characterName, self.server)
def is_token_expired(self):
if self.accessTokenExpires is None:
return True

View File

@@ -254,7 +254,7 @@ class TargetProfile:
def init(self):
self.builtin = False
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None, hp=None):
self.emAmount = emAmount
self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount
@@ -262,6 +262,7 @@ class TargetProfile:
self._maxVelocity = maxVelocity
self._signatureRadius = signatureRadius
self._radius = radius
self._hp = hp
@classmethod
def getBuiltinList(cls):
@@ -331,6 +332,18 @@ class TargetProfile:
def radius(self, val):
self._radius = val
@property
def hp(self):
if self._hp is None or self._hp == -1:
return math.inf
return self._hp
@hp.setter
def hp(self, val):
if val is not None and math.isinf(val):
val = None
self._hp = val
@classmethod
def importPatterns(cls, text):
lines = re.split('[\n\r]+', text)

View File

@@ -18,6 +18,9 @@
# ===============================================================================
import math
from collections import defaultdict
from eos.utils.float import floatUnerr
from utils.repr import makeReprStr
@@ -26,15 +29,133 @@ def _t(x):
return x
class BreacherInfo:
def __init__(self, absolute, relative):
self.absolute = absolute
self.relative = relative
def __mul__(self, mul):
return type(self)(absolute=self.absolute * mul, relative=self.relative * mul)
def __imul__(self, mul):
if mul == 1:
return self
self.absolute *= mul
self.relative *= mul
return self
def __truediv__(self, div):
return type(self)(absolute=self.absolute / div, relative=self.relative / div)
class DmgTypes:
"""Container for damage data stats."""
"""
Container for volley stats, which stores breacher pod data
in raw form, before application of it to target profile.
"""
def __init__(self, em, thermal, kinetic, explosive):
self.em = em
self.thermal = thermal
self.kinetic = kinetic
self.explosive = explosive
self._calcTotal()
self._em = em
self._thermal = thermal
self._kinetic = kinetic
self._explosive = explosive
self._breachers = defaultdict(lambda: [])
self.__profile = None
# Cached data
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
@classmethod
def default(cls):
return cls(0, 0, 0, 0)
def _clear_cached(self):
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
def add_breacher(self, key, data):
self._breachers[key].append(data)
@property
def profile(self):
return self.__profile
@profile.setter
def profile(self, profile):
self.__profile = profile
self._clear_cached()
@property
def em(self):
if self.__cached_em is not None:
return self.__cached_em
dmg = self._em
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "emAmount", 0)
self.__cached_em = dmg
return dmg
@property
def thermal(self):
if self.__cached_thermal is not None:
return self.__cached_thermal
dmg = self._thermal
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "thermalAmount", 0)
self.__cached_thermal = dmg
return dmg
@property
def kinetic(self):
if self.__cached_kinetic is not None:
return self.__cached_kinetic
dmg = self._kinetic
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "kineticAmount", 0)
self.__cached_kinetic = dmg
return dmg
@property
def explosive(self):
if self.__cached_explosive is not None:
return self.__cached_explosive
dmg = self._explosive
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "explosiveAmount", 0)
self.__cached_explosive = dmg
return dmg
@property
def pure(self):
if self.__cached_pure is not None:
return self.__cached_pure
if self.profile is None:
dmg = sum(
max((b.absolute for b in bs), default=0)
for bs in self._breachers.values())
else:
dmg = sum(
max((min(b.absolute, b.relative * getattr(self.profile, "hp", math.inf)) for b in bs), default=0)
for bs in self._breachers.values())
self.__cached_pure = dmg
return dmg
@property
def total(self):
if self.__cached_total is not None:
return self.__cached_total
dmg = self.em + self.thermal + self.kinetic + self.explosive + self.pure
self.__cached_total = dmg
return dmg
# Iterator is needed to support tuple-style unpacking
def __iter__(self):
@@ -42,6 +163,7 @@ class DmgTypes:
yield self.thermal
yield self.kinetic
yield self.explosive
yield self.pure
yield self.total
def __eq__(self, other):
@@ -50,77 +172,87 @@ class DmgTypes:
# Round for comparison's sake because often damage profiles are
# generated from data which includes float errors
return (
floatUnerr(self.em) == floatUnerr(other.em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
floatUnerr(self.total) == floatUnerr(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
floatUnerr(self._em) == floatUnerr(other._em) and
floatUnerr(self._thermal) == floatUnerr(other._thermal) and
floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
floatUnerr(self._explosive) == floatUnerr(other._explosive) and
sorted(self._breachers) == sorted(other._breachers) and
self.profile == other.profile)
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)
new = type(self)(
em=self._em + other._em,
thermal=self._thermal + other._thermal,
kinetic=self._kinetic + other._kinetic,
explosive=self._explosive + other._explosive)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k].extend(v)
for k, v in other._breachers.items():
new._breachers[k].extend(v)
return new
def __iadd__(self, other):
self.em += other.em
self.thermal += other.thermal
self.kinetic += other.kinetic
self.explosive += other.explosive
self._calcTotal()
self._em += other._em
self._thermal += other._thermal
self._kinetic += other._kinetic
self._explosive += other._explosive
for k, v in other._breachers.items():
self._breachers[k].extend(v)
self._clear_cached()
return self
def __mul__(self, mul):
return type(self)(
em=self.em * mul,
thermal=self.thermal * mul,
kinetic=self.kinetic * mul,
explosive=self.explosive * mul)
new = type(self)(
em=self._em * mul,
thermal=self._thermal * mul,
kinetic=self._kinetic * mul,
explosive=self._explosive * mul)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b * mul for b in v]
return new
def __imul__(self, mul):
if mul == 1:
return
self.em *= mul
self.thermal *= mul
self.kinetic *= mul
self.explosive *= mul
self._calcTotal()
return self
self._em *= mul
self._thermal *= mul
self._kinetic *= mul
self._explosive *= mul
for v in self._breachers.values():
for b in v:
b *= mul
self._clear_cached()
return self
def __truediv__(self, div):
return type(self)(
em=self.em / div,
thermal=self.thermal / div,
kinetic=self.kinetic / div,
explosive=self.explosive / div)
new = type(self)(
em=self._em / div,
thermal=self._thermal / div,
kinetic=self._kinetic / div,
explosive=self._explosive / div)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b / div for b in v]
return new
def __itruediv__(self, div):
if div == 1:
return
self.em /= div
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __bool__(self):
return any((
self._em, self._thermal, self._kinetic, self._explosive,
any(b.absolute or b.relative for b in self._breachers)))
def __repr__(self):
spec = DmgTypes.names()
spec.append('total')
return makeReprStr(self, spec)
class_name = type(self).__name__
return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
f'explosive={self._explosive}, breachers={len(self._breachers)})>')
@staticmethod
def names(short=None, postProcessor=None):
def names(short=None, postProcessor=None, includePure=False):
value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')]
if includePure:
value += [_t('pure')]
if postProcessor:
value = [postProcessor(x) for x in value]

View File

@@ -117,7 +117,7 @@ class TimeCache(FitDataCache):
pointData[timeStart] = (dps, volley)
# Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
pointData[prevTimeEnd] = (DmgTypes.default(), DmgTypes.default())
pointData[timeStart] = (dps, volley)
# Changed value
elif dps != prevDps or volley != prevVolley:
@@ -157,7 +157,7 @@ class TimeCache(FitDataCache):
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
if not addedVolleys:
return
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
volleySum = sum(addedVolleys, DmgTypes.default())
if volleySum.total > 0:
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
# We can take "just best" volley, no matter target resistances, because all
@@ -170,24 +170,38 @@ class TimeCache(FitDataCache):
def addDmg(ddKey, addedTime, addedDmg):
if addedDmg.total == 0:
return
addedDmg._breachers = {addedTime + k: v for k, v in addedDmg._breachers.items()}
addedDmg._clear_cached()
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParameters(reloadOverride=True)
cycleParams = mod.getCycleParametersForDps(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
isBreacher = mod.isBreacher
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleVolleys = []
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
time = currentTime + volleyTimeMs / 1000
if isBreacher:
time += 1
addDmg(mod, time, volley)
if isBreacher:
break
timeStart = currentTime
timeFinish = currentTime + cycleTimeMs / 1000
if isBreacher:
timeStart += 1
timeFinish += 1
addDpsVolley(mod, timeStart, timeFinish, cycleVolleys)
if inactiveTimeMs > 0:
nonstopCycles = 0
else:

View File

@@ -98,6 +98,8 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.isBreacher:
applicationMap[mod] = getBreacherMult(mod=mod, distance=distance) if inLockRange else 0
for drone in src.item.activeDronesIter():
if not drone.isDealingDamage():
continue
@@ -192,6 +194,21 @@ def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
return distanceFactor * applicationFactor
def getBreacherMult(mod, distance):
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0
# The ranges already consider ship radius
lowerRange, higherRange, higherChance = missileMaxRangeData
if distance is None or distance <= lowerRange:
distanceFactor = 1
elif lowerRange < distance <= higherRange:
distanceFactor = higherChance
else:
distanceFactor = 0
return distanceFactor
def getSmartbombMult(mod, distance):
modRange = mod.maxRange
if modRange is None:
@@ -211,7 +228,7 @@ def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
# Disallow only against subcaps, allow against caps and tgt profiles
if tgt.isFit and not tgt.item.ship.item.requiresSkill('Capital Ships'):
return 0
damageSig = mod.getModifiedItemAttr('doomsdayDamageRadius') or mod.getModifiedItemAttr('signatureRadius')
damageSig = mod.getModifiedItemAttr('signatureRadius')
if not damageSig:
return 1
return min(1, tgtSigRadius / damageSig)

View File

@@ -19,6 +19,7 @@
import eos.config
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter
@@ -27,17 +28,16 @@ from .calc.application import getApplicationPerKey
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists):
total = DmgTypes(em=0, thermal=0, kinetic=0, explosive=0)
def applyDamage(dmgMap, applicationMap, tgtResists, tgtFullHp):
total = DmgTypes.default()
for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0)
if not GraphSettings.getInstance().get('ignoreResists'):
emRes, thermRes, kinRes, exploRes = tgtResists
total = DmgTypes(
em=total.em * (1 - emRes),
thermal=total.thermal * (1 - thermRes),
kinetic=total.kinetic * (1 - kinRes),
explosive=total.explosive * (1 - exploRes))
else:
emRes = thermRes = kinRes = exploRes = 0
total.profile = TargetProfile(
emAmount=emRes, thermalAmount=thermRes, kineticAmount=kinRes, explosiveAmount=exploRes, hp=tgtFullHp)
return total
@@ -144,7 +144,8 @@ class XDistanceMixin(SmoothPointGetter):
'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x
@@ -186,7 +187,8 @@ class XDistanceMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y
@@ -241,14 +243,17 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
tgtResists = tgt.getResists()
# Custom iteration for time graph to show all data points
currentDmg = None
currentTime = None
for currentTime in sorted(timeCache):
prevDmg = currentDmg
currentDmgData = timeCache[currentTime]
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap, tgtResists=tgtResists).total
currentDmg = applyDamage(
dmgMap=currentDmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
if currentTime < minTime:
continue
# First set of data points
@@ -294,7 +299,11 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=time)
dmgData = self._getTimeCacheDataPoint(src=src, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap, tgtResists=tgt.getResists()).total
y = applyDamage(
dmgMap=dmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
return y
@@ -310,7 +319,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSpeed = x
@@ -353,7 +363,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y
@@ -398,7 +409,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()}
'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x
@@ -414,7 +426,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
y = applyDamage(
dmgMap=commonData['dmgMap'],
applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total
tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y

View File

@@ -89,7 +89,7 @@ class FitDamageStatsGraph(FitGraph):
cols = []
if not GraphSettings.getInstance().get('ignoreResists'):
cols.append('Target Resists')
cols.extend(('Speed', 'SigRadius', 'Radius'))
cols.extend(('Speed', 'SigRadius', 'Radius', 'FullHP'))
return cols
# Calculation stuff

View File

@@ -273,7 +273,7 @@ class GraphCanvasPanel(wx.Panel):
legendLines = []
for i, iData in enumerate(legendData):
color, lineStyle, label = iData
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', '\$')))
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', r'\$')))
if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
legend = self.subplot.legend(handles=legendLines)

View File

@@ -114,7 +114,7 @@ class GraphFrame(AuxiliaryFrame):
newW = max(curW, bestW)
newH = max(curH, bestH)
if newW > curW or newH > curH:
newSize = wx.Size(newW, newH)
newSize = wx.Size(round(newW), round(newH))
self.SetSize(newSize)
self.SetMinSize(newSize)

View File

@@ -39,7 +39,7 @@ class VectorPicker(wx.Window):
self._directionOnly = kwargs.pop('directionOnly', False)
super().__init__(*args, **kwargs)
self._fontsize = max(1, float(kwargs.pop('fontsize', 8 / self.GetContentScaleFactor())))
self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
self._font = wx.Font(round(self._fontsize), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
self._angle = 0
self.__length = 1
self._left = False
@@ -76,7 +76,7 @@ class VectorPicker(wx.Window):
self.__length = newLength
def DoGetBestSize(self):
return wx.Size(self._size, self._size)
return wx.Size(round(self._size), round(self._size))
def AcceptsFocusFromKeyboard(self):
return False
@@ -121,35 +121,37 @@ class VectorPicker(wx.Window):
radius = min(width, height) / 2 - 2
dc.SetBrush(wx.WHITE_BRUSH)
dc.DrawCircle(radius + 2, radius + 2, radius)
dc.DrawCircle(round(radius + 2), round(radius + 2), round(radius))
a = math.radians(self._angle + self._offset)
x = math.cos(a) * radius
y = math.sin(a) * radius
# See PR #2260 on why this is needed
pointRadius = 2 / self.GetContentScaleFactor() if 'wxGTK' in wx.PlatformInfo else 2
dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length)
dc.DrawLine(
round(radius + 2), round(radius + 2),
round(radius + 2 + x * self._length), round(radius + 2 - y * self._length))
dc.SetBrush(wx.BLACK_BRUSH)
dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, pointRadius)
dc.DrawCircle(round(radius + 2 + x * self._length), round(radius + 2 - y * self._length), round(pointRadius))
if self._label:
labelText = self._label
labelTextW, labelTextH = dc.GetTextExtent(labelText)
labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0
labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0
dc.DrawText(labelText, labelTextX, labelTextY)
dc.DrawText(labelText, round(labelTextX), round(labelTextY))
if not self._directionOnly:
lengthText = '%d%%' % (100 * self._length,)
lengthTextW, lengthTextH = dc.GetTextExtent(lengthText)
lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2
lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2
dc.DrawText(lengthText, lengthTextX, lengthTextY)
dc.DrawText(lengthText, round(lengthTextX), round(lengthTextY))
angleText = '%d\u00B0' % (self._angle,)
angleTextW, angleTextH = dc.GetTextExtent(angleText)
angleTextX = radius + 2 - x / 2 - angleTextW / 2
angleTextY = radius + 2 + y / 2 - angleTextH / 2
dc.DrawText(angleText, angleTextX, angleTextY)
dc.DrawText(angleText, round(angleTextX), round(angleTextY))
def OnEraseBackground(self, event):
pass

View File

@@ -145,6 +145,11 @@ class TargetWrapper(BaseWrapper):
else:
return em, therm, kin, explo
def getFullHp(self):
if self.isProfile:
return self.item.hp
if self.isFit:
return self.item.hp.get('shield', 0) + self.item.hp.get('armor', 0) + self.item.hp.get('hull', 0)
def _getShieldResists(ship):

View File

@@ -212,7 +212,7 @@ class AttributeGauge(wx.Window):
for x in range(1, 20):
dc.SetBrush(wx.Brush(wx.LIGHT_GREY))
dc.SetPen(wx.Pen(wx.LIGHT_GREY))
dc.DrawRectangle(x * 10, 1, 1, rect.height)
dc.DrawRectangle(round(x * 10), 1, 1, round(rect.height))
dc.SetBrush(wx.Brush(colour))
dc.SetPen(wx.Pen(colour))
@@ -222,19 +222,19 @@ class AttributeGauge(wx.Window):
if value >= 0:
padding = (half if is_even else math.ceil(half - 1)) + 1
dc.DrawRectangle(padding, 1, w, rect.height)
dc.DrawRectangle(round(padding), 1, round(w), round(rect.height))
else:
padding = half - w + 1 if is_even else math.ceil(half) - (w - 1)
dc.DrawRectangle(padding, 1, w, rect.height)
dc.DrawRectangle(round(padding), 1, round(w), round(rect.height))
if self.leading_edge and (self.edge_on_neutral or value != 0):
dc.SetPen(wx.Pen(wx.WHITE))
dc.SetBrush(wx.Brush(wx.WHITE))
if value > 0:
dc.DrawRectangle(min(padding + w, rect.width), 1, 1, rect.height)
dc.DrawRectangle(round(min(padding + w, rect.width)), 1, 1, round(rect.height))
else:
dc.DrawRectangle(max(padding - 1, 1), 1, 1, rect.height)
dc.DrawRectangle(round(max(padding - 1, 1)), 1, 1, round(rect.height))
def OnTimer(self, event):
old_value = self._old_percentage

View File

@@ -103,10 +103,9 @@ class BitmapLoader:
pyfalog.warning("Missing icon file: {0}/{1}".format(location, filename))
return None
bmp: wx.Bitmap = img.ConvertToBitmap()
if scale > 1:
bmp.SetSize((bmp.GetWidth() // scale, bmp.GetHeight() // scale))
return bmp
return img.Scale(round(img.GetWidth() // scale), round(img.GetHeight() // scale)).ConvertToBitmap()
return img.ConvertToBitmap()
@classmethod
def loadScaledBitmap(cls, name, location, scale=0):

View File

@@ -43,6 +43,10 @@ class BoosterViewDrop(wx.DropTarget):
if self.GetData():
dragged_data = DragDropHelper.data
data = dragged_data.split(':')
if dragged_data is None:
return t
self.dropFn(x, y, data)
return t

View File

@@ -41,6 +41,10 @@ class CargoViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -56,6 +56,10 @@ class CommandViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -52,6 +52,10 @@ class DroneViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t
@@ -195,7 +199,11 @@ class DroneView(Display):
@staticmethod
def droneKey(drone):
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName
if drone.isMutated:
item = drone.baseItem
else:
item = drone.item
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
def fitChanged(self, event):

View File

@@ -34,7 +34,10 @@ from service.fit import Fit
from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
FIGHTER_ORDER = (
'Light Fighter', 'Structure Light Fighter',
'Heavy Fighter', 'Structure Heavy Fighter',
'Support Fighter', 'Structure Support Fighter')
_t = wx.GetTranslation
@@ -49,6 +52,10 @@ class FighterViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -46,6 +46,10 @@ class ImplantViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -65,6 +65,10 @@ class ProjectedViewDrop(wx.DropTarget):
def OnData(self, x, y, t):
if self.GetData():
dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t

View File

@@ -20,6 +20,7 @@ from gui.builtinContextMenus.targetProfile import editor
from gui.builtinContextMenus import itemStats
from gui.builtinContextMenus import itemMarketJump
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import shipJump
# Generic item manipulations
from gui.builtinContextMenus import itemRemove
@@ -35,6 +36,7 @@ from gui.builtinContextMenus import skillAffectors
from gui.builtinContextMenus import itemFill
from gui.builtinContextMenus import droneAddStack
from gui.builtinContextMenus import cargoAdd
from gui.builtinContextMenus import cargoFill
from gui.builtinContextMenus import cargoAddAmmo
from gui.builtinContextMenus import itemProject
from gui.builtinContextMenus import ammoToDmgPattern

View File

@@ -25,7 +25,7 @@ class AddToCargoAmmo(ContextMenuSingle):
return True
def getText(self, callingWindow, itmContext, mainItem):
if mainItem.marketGroup.name == "Scan Probes":
if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
return _t("Add {0} to Cargo (x8)").format(itmContext)
return _t("Add {0} to Cargo (x1000)").format(itmContext)
@@ -34,7 +34,7 @@ class AddToCargoAmmo(ContextMenuSingle):
fitID = self.mainFrame.getActiveFit()
typeID = int(mainItem.ID)
if mainItem.marketGroup.name == "Scan Probes":
if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=8)
else:
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000)

View File

@@ -0,0 +1,68 @@
import wx
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from eos.saveddata.cargo import Cargo
_t = wx.GetTranslation
class FillCargoWithItem(ContextMenuSingle):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if srcContext not in ("marketItemGroup", "marketItemMisc", "cargoItem"):
return False
if mainItem is None:
return False
if self.mainFrame.getActiveFit() is None:
return False
if srcContext in ("marketItemGroup", "marketItemMisc"):
if not (mainItem.isCharge or mainItem.isCommodity):
return False
return True
def getText(self, callingWindow, itmContext, mainItem):
return _t("Fill Cargo With {0}").format(itmContext)
def activate(self, callingWindow, fullContext, mainItem, i):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if isinstance(mainItem, Cargo):
itemVolume = mainItem.item.attributes['volume'].value
itemID = mainItem.itemID
else:
itemVolume = mainItem.attributes['volume'].value
itemID = int(mainItem.ID)
if itemVolume is None or itemVolume <= 0:
return
# Calculate how many items can fit in the cargo
cargoCapacity = fit.ship.getModifiedItemAttr("capacity")
currentCargoVolume = fit.cargoBayUsed
availableVolume = cargoCapacity - currentCargoVolume
if availableVolume <= 0:
return
# Calculate maximum amount that can fit
maxAmount = int(availableVolume / itemVolume)
if maxAmount <= 0:
return
# Add the items to cargo
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=itemID, amount=maxAmount)
if self.mainFrame.command.Submit(command):
self.mainFrame.additionsPane.select("Cargo", focus=False)
FillCargoWithItem.register()

View File

@@ -65,7 +65,6 @@ class DroneStackSplit(wx.Dialog):
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
self.input.SetValue(str(value))
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -75,12 +74,13 @@ class DroneStackSplit(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -123,7 +123,11 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
data.groups[_t('Abyssal Weather')] = self.getAbyssalWeather()
data.groups[_t('Sansha Incursion')] = self.getEffectBeacons(
_t('ContextMenu|ProjectedEffectManipulation|Sansha Incursion'))
data.groups[_t('Drifter Incursion')] = self.getDrifterIncursion()
data.groups[_t('Triglavian Invasion')] = self.getInvasionBeacons()
data.groups[_t('Pirate Insurgency')] = self.getEffectBeacons(
_t('ContextMenu|ProjectedEffectManipulation|Insurgency'),
extra_garbage=(_t('ContextMenu|ProjectedEffectManipulation|Beacon'),))
return data
def getEffectBeacons(self, *groups, extra_garbage=()):
@@ -174,7 +178,6 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
container.append(Entry(beacon.ID, beaconname, shortname))
# Break loop on 1st result
break
data.sort()
return data
def getAbyssalWeather(self):
@@ -231,12 +234,31 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
data.sort()
return data
def getDrifterIncursion(self):
data = self.getEffectBeacons(_t('ContextMenu|ProjectedEffectManipulation|Drifter Incursion'))
# Drifter Crisis
item = Market.getInstance().getItem(87294)
data.items.append(Entry(item.ID, item.name, item.name))
return data
def getInvasionBeacons(self):
data = self.getDestructibleBeacons()
# Trig Minor Victory
item = Market.getInstance().getItem(87177)
data.items.append(Entry(item.ID, item.name, item.name))
# Trig Final Liminality
item = Market.getInstance().getItem(87164)
data.items.append(Entry(item.ID, item.name, item.name))
# Turnur weather
item = Market.getInstance().getItem(74002)
data.items.append(Entry(item.ID, item.name, item.name))
return data
def getInsurgencyBeacons(self):
data = self.getDestructibleBeacons()
# Suppression Interdiction Range Beacon
item = Market.getInstance().getItem(79839)
data.items.append(Entry(item.ID, item.name, item.name))
return data
AddEnvironmentEffect.register()

View File

@@ -0,0 +1,157 @@
import re
import wx
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.fit import Fit
_t = wx.GetTranslation
class FitPilotSecurityMenu(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if srcContext != "fittingShip":
return False
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if fit.ship.name not in ('Pacifier', 'Enforcer', 'Marshal', 'Sidewinder', 'Cobra', 'Python'):
return
return True
def getText(self, callingWindow, itmContext):
return _t("Pilot Security Status")
def addOption(self, menu, optionLabel, optionValue):
id = ContextMenuUnconditional.nextID()
self.optionIds[id] = optionValue
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleMode, menuItem)
return menuItem
def addOptionCustom(self, menu, optionLabel):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleModeCustom, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
msw = True if "wxMSW" in wx.PlatformInfo else False
self.optionIds = {}
sub = wx.Menu()
presets = (-10, -8, -6, -4, -2, 0, 1, 2, 3, 4, 5)
# Inherit
char_sec_status = round(fit.character.secStatus, 2)
menuItem = self.addOption(rootMenu if msw else sub, _t('Character') + f' ({char_sec_status})', None)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity is None)
# Custom
label = _t('Custom')
is_checked = False
if fit.pilotSecurity is not None and fit.pilotSecurity not in presets:
sec_status = round(fit.getPilotSecurity(), 2)
label += f' ({sec_status})'
is_checked = True
menuItem = self.addOptionCustom(rootMenu if msw else sub, label)
sub.Append(menuItem)
menuItem.Check(is_checked)
sub.AppendSeparator()
# Predefined options
for sec_status in presets:
menuItem = self.addOption(rootMenu if msw else sub, str(sec_status), sec_status)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity == sec_status)
return sub
def handleMode(self, event):
optionValue = self.optionIds[event.Id]
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=self.mainFrame.getActiveFit(),
secStatus=optionValue))
def handleModeCustom(self, event):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
sec_status = fit.getPilotSecurity()
with SecStatusChanger(self.mainFrame, value=sec_status) as dlg:
if dlg.ShowModal() == wx.ID_OK:
cleanInput = re.sub(r'[^0-9.\-+]', '', dlg.input.GetLineText(0).strip())
if cleanInput:
try:
cleanInputFloat = float(cleanInput)
except ValueError:
return
else:
return
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=fitID, secStatus=max(-10.0, min(5.0, cleanInputFloat))))
FitPilotSecurityMenu.register()
class SecStatusChanger(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title=_t('Change Security Status'), style=wx.DEFAULT_DIALOG_STYLE)
self.SetMinSize((346, 156))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
bSizer2 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, _t('Security Status (min -10.0, max 5.0):'))
bSizer2.Add(text, 0)
bSizer1.Add(bSizer2, 0, wx.ALL, 10)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
if value is None:
value = '0.0'
else:
if value == int(value):
value = int(value)
value = str(value)
self.input.SetValue(value)
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 15)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)
# checks to make sure it's valid number
@staticmethod
def onChar(event):
key = event.GetKeyCode()
acceptable_characters = '1234567890.-+'
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -59,7 +59,6 @@ class NameDialog(wx.Dialog):
else:
value = str(value)
self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -69,11 +68,12 @@ class NameDialog(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -108,7 +108,6 @@ class AmountChanger(wx.Dialog):
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
self.input.SetValue(str(value))
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -118,12 +117,13 @@ class AmountChanger(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -13,6 +13,16 @@ from service.fit import Fit
_t = wx.GetTranslation
GLORIFIED_PREFIX = 'Gl. '
def nameSorter(mutaplasmid):
name = mutaplasmid.shortName
if name.startswith(GLORIFIED_PREFIX):
return name[len(GLORIFIED_PREFIX):], True
return name, False
class ChangeItemMutation(ContextMenuSingle):
def __init__(self):
@@ -45,7 +55,7 @@ class ChangeItemMutation(ContextMenuSingle):
menu = rootMenu if msw else sub
for mutaplasmid in mainItem.item.mutaplasmids:
for mutaplasmid in sorted(mainItem.item.mutaplasmids, key=nameSorter):
id = ContextMenuSingle.nextID()
self.eventIDs[id] = (mutaplasmid, mainItem)
mItem = wx.MenuItem(menu, id, mutaplasmid.shortName)

View File

@@ -94,7 +94,6 @@ class RangeChanger(wx.Dialog):
value = int(value)
value = str(value)
self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -104,12 +103,13 @@ class RangeChanger(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -72,6 +72,7 @@ AttrGroupDict = {
"specialAmmoHoldCapacity",
"specialCommandCenterHoldCapacity",
"specialPlanetaryCommoditiesHoldCapacity",
"specialColonyResourcesHoldCapacity",
"structureDamageLimit",
"specialSubsystemHoldCapacity",
"emDamageResonance",

View File

@@ -4,13 +4,18 @@ import wx
# noinspection PyPackageRequirements
import wx.lib.mixins.listctrl as listmix
from gui.utils.dark import isDark
class AutoListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ListRowHighlighter):
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
listmix.ListCtrlAutoWidthMixin.__init__(self)
listmix.ListRowHighlighter.__init__(self)
if isDark():
listcol = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)
highlight = listcol.ChangeLightness(110)
listmix.ListRowHighlighter.SetHighlightColor(self, highlight)
class AutoListCtrlNoHighlight(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ListRowHighlighter):
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):

View File

@@ -22,9 +22,9 @@ class ItemDescription(wx.Panel):
desc = item.description.replace("\n", "<br>")
# Strip font tags
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", "\g<inside>", desc)
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", r"\g<inside>", desc)
# Strip URLs
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", "\g<inside>", desc)
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", r"\g<inside>", desc)
desc = "<body bgcolor='{}' text='{}'>{}</body>".format(
bgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
fgcolor.GetAsString(wx.C2S_HTML_SYNTAX),

View File

@@ -5,3 +5,5 @@ import wx.lib.newevent
ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent()
RECENTLY_USED_MODULES = -2
CHARGES_FOR_FIT = -3

View File

@@ -2,14 +2,17 @@ import wx
from logbook import Logger
import gui.builtinMarketBrowser.pfSearchBox as SBox
from config import slotColourMap
import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark
from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from gui.contextMenu import ContextMenu
from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper
from gui.utils.dark import isDark
from service.fit import Fit
from service.market import Market
from service.ammo import Ammo
pyfalog = Logger(__name__)
@@ -31,6 +34,7 @@ class ItemView(Display):
self.filteredStore = set()
self.sMkt = marketBrowser.sMkt
self.sFit = Fit.getInstance()
self.sAmmo = Ammo.getInstance()
self.marketBrowser = marketBrowser
self.marketView = marketBrowser.marketView
@@ -50,6 +54,9 @@ class ItemView(Display):
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
# the "charges for active fitting" needs to listen to fitting changes
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.active = []
def delaySearch(self, evt):
@@ -90,7 +97,11 @@ class ItemView(Display):
if sel.IsOk():
# Get data field of the selected item (which is a marketGroup ID if anything was selected)
seldata = self.marketView.GetItemData(sel)
if seldata is not None and seldata != RECENTLY_USED_MODULES:
if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
elif seldata == CHARGES_FOR_FIT:
items = self.getChargesForActiveFit()
elif seldata is not None:
# If market group treeview item doesn't have children (other market groups or dummies),
# then it should have items in it and we want to request them
if self.marketView.ItemHasChildren(sel) is False:
@@ -102,11 +113,7 @@ class ItemView(Display):
else:
items = set()
else:
# If method was called but selection wasn't actually made or we have a hit on recently used modules
if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
else:
items = set()
items = set()
# Fill store
self.updateItemStore(items)
@@ -114,6 +121,9 @@ class ItemView(Display):
# Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered)
if seldata == RECENTLY_USED_MODULES:
self.marketBrowser.mode = 'recent'
if seldata == CHARGES_FOR_FIT:
self.marketBrowser.mode = 'charges'
self.setToggles()
if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1:
@@ -122,6 +132,41 @@ class ItemView(Display):
btn.setUserSelection(True)
self.filterItemStore()
def getChargesForActiveFit(self):
fitId = self.mainFrame.getActiveFit()
# no active fit => no charges
if fitId is None:
return set()
fit = self.sFit.getFit(fitId)
# use a set so we only add one entry for each charge
items = set()
for mod in fit.modules:
charges = self.sAmmo.getModuleFlatAmmo(mod)
for charge in charges:
items.add(charge)
return items
def fitChanged(self, event):
# skip the event so the other handlers also get called
event.Skip()
if self.marketBrowser.mode != 'charges':
return
activeFitID = self.mainFrame.getActiveFit()
# if it was not the active fitting that was changed, do not do anything
if activeFitID is not None and activeFitID not in event.fitIDs:
return
items = self.getChargesForActiveFit()
# update the UI
self.updateItemStore(items)
self.filterItemStore()
def updateItemStore(self, items):
self.unfilteredStore = items
@@ -243,6 +288,7 @@ class ItemView(Display):
def columnBackground(self, colItem, item):
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
colorMap = slotColourMapDark if isDark() else slotColourMap
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
else:
return self.GetBackgroundColour()

View File

@@ -1,7 +1,7 @@
import wx
from gui.cachingImageList import CachingImageList
from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES
from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from logbook import Logger
@@ -35,6 +35,9 @@ class MarketTree(wx.TreeCtrl):
# Add recently used modules node
rumIconId = self.addImage("market_small", "gui")
self.AppendItem(self.root, _t("Recently Used Items"), rumIconId, data=RECENTLY_USED_MODULES)
# Add charges for active fitting node
cffIconId = self.addImage("damagePattern_small", "gui")
self.AppendItem(self.root, _t("Charges For Active Fit"), cffIconId, data=CHARGES_FOR_FIT)
# Bind our lookup method to when the tree gets expanded
self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup)

View File

@@ -253,8 +253,8 @@ class PFSearchBox(wx.Window):
else:
spad = 0
dc.DrawBitmap(self.searchBitmapShadow, self.searchButtonX + 1, self.searchButtonY + 1)
dc.DrawBitmap(self.searchBitmap, self.searchButtonX + spad, self.searchButtonY + spad)
dc.DrawBitmap(self.searchBitmapShadow, round(self.searchButtonX + 1), round(self.searchButtonY + 1))
dc.DrawBitmap(self.searchBitmap, round(self.searchButtonX + spad), round(self.searchButtonY + spad))
if self.isCancelButtonVisible:
if self.cancelBitmap:
@@ -262,8 +262,8 @@ class PFSearchBox(wx.Window):
cpad = 1
else:
cpad = 0
dc.DrawBitmap(self.cancelBitmapShadow, self.cancelButtonX + 1, self.cancelButtonY + 1)
dc.DrawBitmap(self.cancelBitmap, self.cancelButtonX + cpad, self.cancelButtonY + cpad)
dc.DrawBitmap(self.cancelBitmapShadow, round(self.cancelButtonX + 1), round(self.cancelButtonY + 1))
dc.DrawBitmap(self.cancelBitmap, round(self.cancelButtonX + cpad), round(self.cancelButtonY + cpad))
dc.SetPen(wx.Pen(sepColor, 1))
dc.DrawLine(0, rect.height - 1, rect.width, rect.height - 1)

View File

@@ -1,9 +1,11 @@
# noinspection PyPackageRequirements
import wx
import config
import gui.mainFrame
from gui.bitmap_loader import BitmapLoader
from gui.preferenceView import PreferenceView
from service.esi import Esi
from service.settings import EsiSettings
# noinspection PyPackageRequirements
@@ -41,38 +43,68 @@ class PFEsiPref(PreferenceView):
"due to 'Signature has expired' error")))
mainSizer.Add(self.enforceJwtExpiration, 0, wx.ALL | wx.EXPAND, 5)
self.ssoServer = wx.CheckBox(panel, wx.ID_ANY, _t("Auto-login (starts local server)"), wx.DefaultPosition,
wx.DefaultSize,
0)
self.ssoServer.SetToolTip(wx.ToolTip(_t("This allows the EVE SSO to callback to your local pyfa instance and complete the authentication process without manual intervention.")))
mainSizer.Add(self.ssoServer, 0, wx.ALL | wx.EXPAND, 5)
rbSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rbMode = wx.RadioBox(panel, -1, _t("Login Authentication Method"), wx.DefaultPosition, wx.DefaultSize,
[_t('Local Server'), _t('Manual')], 1, wx.RA_SPECIFY_COLS)
self.rbMode.SetItemToolTip(0, _t("This option starts a local webserver that EVE SSO Server will call back to"
" with information about the character login."))
self.rbMode.SetItemToolTip(1, _t("This option prompts users to copy and paste information to allow for"
" character login. Use this if having issues with the local server."))
self.rbMode.SetSelection(self.settings.get('loginMode'))
self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration" or True))
self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration") or True)
self.ssoServer.SetValue(True if self.settings.get("loginMode") == 0 else False)
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5)
mainSizer.Add(rbSizer, 0, wx.ALL | wx.EXPAND, 0)
self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange)
esiSizer = wx.BoxSizer(wx.HORIZONTAL)
self.esiServer = wx.StaticText(panel, wx.ID_ANY, _t("Default SSO Server:"), wx.DefaultPosition, wx.DefaultSize, 0)
self.esiServer.Wrap(-1)
esiSizer.Add(self.esiServer, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
self.esiServer.SetToolTip(wx.ToolTip(_t('The source you choose will be used on connection.')))
self.chESIserver = wx.Choice(panel, choices=list(self.settings.keys()))
self.chESIserver.SetStringSelection(self.settings.get("server"))
esiSizer.Add(self.chESIserver, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
mainSizer.Add(esiSizer, 0, wx.TOP | wx.RIGHT, 10)
self.chESIserver.Bind(wx.EVT_CHOICE, self.OnServerChange)
self.enforceJwtExpiration.Bind(wx.EVT_CHECKBOX, self.OnEnforceChange)
mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0)
self.ssoServer.Bind(wx.EVT_CHECKBOX, self.OnModeChange)
panel.SetSizer(mainSizer)
panel.Layout()
def OnTimeoutChange(self, event):
self.settings.set('timeout', event.GetEventObject().GetValue())
event.Skip()
def OnModeChange(self, event):
self.settings.set('loginMode', event.GetInt())
self.settings.set('loginMode', 0 if self.ssoServer.GetValue() else 1)
event.Skip()
def OnEnforceChange(self, event):
self.settings.set('enforceJwtExpiration', self.enforceJwtExpiration.GetValue())
event.Skip()
def OnServerChange(self, event):
# pass
source = self.chESIserver.GetString(self.chESIserver.GetSelection())
esiService = Esi.getInstance()
# init servers
esiService.init(config.supported_servers[source])
self.settings.set("server", source)
event.Skip()
def getImage(self):
return BitmapLoader.getBitmap("eve", "gui")
PFEsiPref.register()
PFEsiPref.register()

View File

@@ -40,7 +40,7 @@ class PFGeneralPref(PreferenceView):
langSizer = wx.BoxSizer(wx.HORIZONTAL)
self.langChoices = sorted([langInfo for lang, langInfo in LocaleSettings.supported_langauges().items()], key=lambda x: x.Description)
self.langChoices = sorted([langInfo for lang, langInfo in LocaleSettings.supported_languages().items()], key=lambda x: x.Description)
pyfaLangsEnabled = bool(self.langChoices)
if pyfaLangsEnabled:
@@ -64,7 +64,7 @@ class PFGeneralPref(PreferenceView):
langBox.Add(hl.HyperLinkCtrl(panel, -1,
_t("Interested in helping with translations?"),
URL="https://github.com/pyfa-org/Pyfa/blob/master/locale/README.md"
), 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 15)
), 0, wx.LEFT, 15)
else:
self.stLangLabel = wx.StaticText(panel, wx.ID_ANY, _t("Pyfa language selection disabled. Please check if .mo files have been generated.\nRefer to locale/README.md for info."), wx.DefaultPosition, wx.DefaultSize, 0)
self.stLangLabel.Wrap(-1)
@@ -93,7 +93,7 @@ class PFGeneralPref(PreferenceView):
langBox.Add(wx.StaticText(panel, wx.ID_ANY,
_t("Auto will use the same language pyfa uses if available, otherwise English"),
wx.DefaultPosition,
wx.DefaultSize, 0), 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 15)
wx.DefaultSize, 0), 0, wx.LEFT, 15)
self.cbGlobalChar = wx.CheckBox(panel, wx.ID_ANY, _t("Use global character"), wx.DefaultPosition, wx.DefaultSize,
0)

View File

@@ -104,14 +104,14 @@ class CategoryItem(SFBrowserItem):
textColor = colorUtils.GetSuitable(windowColor, 1)
mdc.SetTextForeground(textColor)
mdc.DrawBitmap(self.dropShadowBitmap, self.shipBmpx + 1, self.shipBmpy + 1)
mdc.DrawBitmap(self.shipBmp, self.shipBmpx, self.shipBmpy, 0)
mdc.DrawBitmap(self.dropShadowBitmap, round(self.shipBmpx + 1), round(self.shipBmpy + 1))
mdc.DrawBitmap(self.shipBmp, round(self.shipBmpx), round(self.shipBmpy), 0)
mdc.SetFont(self.fontBig)
categoryName, fittings = self.fittingInfo
mdc.DrawText(categoryName, self.catx, self.caty)
mdc.DrawText(categoryName, round(self.catx), round(self.caty))
# =============================================================================

View File

@@ -416,9 +416,18 @@ class FitItem(SFItem.SFBrowserItem):
if self.dragging:
if not self.dragged:
if self.dragMotionTrigger < 0:
if not self.dragTLFBmp:
tdc = wx.MemoryDC()
bmpWidth = self.toolbarx if self.toolbarx < 200 else 200
self.dragTLFBmp = wx.Bitmap(round(bmpWidth), round(self.GetRect().height))
tdc.SelectObject(self.dragTLFBmp)
tdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)))
tdc.DrawRectangle(0, 0, bmpWidth, self.GetRect().height)
self.DrawItem(tdc)
tdc.SelectObject(wx.NullBitmap)
if not self.HasCapture():
self.CaptureMouse()
self.dragWindow = PFBitmapFrame(self, pos, self.dragTLFBmp)
self.dragWindow = PFBitmapFrame(self, pos, self.dragTLFBmp)
self.dragWindow.Show()
self.dragged = True
self.dragMotionTrigger = self.dragMotionTrail
@@ -493,9 +502,9 @@ class FitItem(SFItem.SFBrowserItem):
else:
shipEffBk = self.shipEffBk
mdc.DrawBitmap(shipEffBk, self.shipEffx, self.shipEffy, 0)
mdc.DrawBitmap(shipEffBk, round(self.shipEffx), round(self.shipEffy), 0)
mdc.DrawBitmap(self.shipBmp, self.shipBmpx, self.shipBmpy, 0)
mdc.DrawBitmap(self.shipBmp, round(self.shipBmpx), round(self.shipBmpy), 0)
mdc.SetFont(self.fontNormal)
@@ -504,26 +513,21 @@ class FitItem(SFItem.SFBrowserItem):
pfdate = drawUtils.GetPartialText(mdc, fitLocalDate,
self.toolbarx - self.textStartx - self.padding * 2 - self.thoverw)
mdc.DrawText(pfdate, self.textStartx, self.timestampy)
mdc.DrawText(pfdate, round(self.textStartx), round(self.timestampy))
mdc.SetFont(self.fontSmall)
mdc.DrawText(self.toolbar.hoverLabel, self.thoverx, self.thovery)
mdc.DrawText(self.toolbar.hoverLabel, round(self.thoverx), round(self.thovery))
mdc.SetFont(self.fontBig)
psname = drawUtils.GetPartialText(mdc, self.fitName,
self.toolbarx - self.textStartx - self.padding * 2 - self.thoverw)
mdc.DrawText(psname, self.textStartx, self.fitNamey)
mdc.DrawText(psname, round(self.textStartx), round(self.fitNamey))
if self.tcFitName.IsShown():
self.AdjustControlSizePos(self.tcFitName, self.textStartx, self.toolbarx - self.editWidth - self.padding)
tdc = wx.MemoryDC()
self.dragTLFBmp = wx.Bitmap((self.toolbarx if self.toolbarx < 200 else 200), rect.height, 24)
tdc.SelectObject(self.dragTLFBmp)
tdc.Blit(0, 0, (self.toolbarx if self.toolbarx < 200 else 200), rect.height, mdc, 0, 0, wx.COPY)
tdc.SelectObject(wx.NullBitmap)
def AdjustControlSizePos(self, editCtl, start, end):
fnEditSize = editCtl.GetSize()

View File

@@ -231,7 +231,7 @@ class NavigationPanel(SFItem.SFBrowserItem):
self.toolbar.SetPosition((self.toolbarx, self.toolbary))
mdc.SetFont(self.fontSmall)
mdc.DrawText(self.toolbar.hoverLabel, self.thoverx, self.thovery)
mdc.DrawText(self.toolbar.hoverLabel, round(self.thoverx), round(self.thovery))
mdc.SetPen(wx.Pen(sepColor, 1))
mdc.DrawLine(0, rect.height - 1, rect.width, rect.height - 1)

View File

@@ -55,7 +55,7 @@ class PFBitmapFrame(wx.Frame):
# todo: evaluate wx.DragImage, might make this class obsolete, however might also lose our customizations
# (like the sexy fade-in animation)
rect = self.GetRect()
canvas = wx.Bitmap(rect.width, rect.height)
canvas = wx.Bitmap(round(rect.width), round(rect.height))
# todo: convert to context manager after updating to wxPython >v4.0.1 (4.0.1 has a bug, see #1421)
# See #1418 for discussion
mdc = wx.BufferedPaintDC(self)
@@ -63,4 +63,4 @@ class PFBitmapFrame(wx.Frame):
mdc.DrawBitmap(self.bitmap, 0, 0)
mdc.SetPen(wx.Pen("#000000", width=1))
mdc.SetBrush(wx.TRANSPARENT_BRUSH)
mdc.DrawRectangle(0, 0, rect.width, rect.height)
mdc.DrawRectangle(0, 0, round(rect.width), round(rect.height))

View File

@@ -57,7 +57,7 @@ class PFListPane(wx.ScrolledWindow):
posy = self.GetScrollPos(wx.VERTICAL)
posy -= self.itemsHeight
self.Scroll(0, posy)
self.Scroll(0, round(posy))
event.Skip()
@@ -65,7 +65,7 @@ class PFListPane(wx.ScrolledWindow):
posy = self.GetScrollPos(wx.VERTICAL)
posy += self.itemsHeight
self.Scroll(0, posy)
self.Scroll(0, round(posy))
event.Skip()
@@ -109,7 +109,7 @@ class PFListPane(wx.ScrolledWindow):
# if we need to adjust
if new_vs_x != -1 or new_vs_y != -1:
self.Scroll(new_vs_x, new_vs_y)
self.Scroll(round(new_vs_x), round(new_vs_y))
def AddWidget(self, widget):
widget.Reparent(self)
@@ -163,6 +163,10 @@ class PFListPane(wx.ScrolledWindow):
def RemoveAllChildren(self):
for widget in self._wList:
widget.Destroy()
# this forces the garbage collector to work properly by removing dangling references to objects which are still alive, otherwise widget cannot be gc-ed eventually causing GDI id exhaustion and crash
for i in widget.__dict__.keys():
widget.__dict__[i] =None
del widget
self.Scroll(0, 0)
self._wList = []

View File

@@ -68,7 +68,7 @@ class RaceSelector(wx.Window):
img = img.Rotate90(False)
img.Replace(0, 0, 0, sysTextColour[0], sysTextColour[1], sysTextColour[2])
if layout == wx.VERTICAL:
img = img.Scale(self.minWidth, 8, wx.IMAGE_QUALITY_HIGH)
img = img.Scale(round(self.minWidth), 8, wx.IMAGE_QUALITY_HIGH)
self.bmpArrow = wx.Bitmap(img)
@@ -194,25 +194,25 @@ class RaceSelector(wx.Window):
bmp = wx.Bitmap(img)
if self.layout == wx.VERTICAL:
mdc.DrawBitmap(dropShadow, rect.width - self.buttonsPadding - bmp.GetWidth() + 1, y + 1)
mdc.DrawBitmap(bmp, rect.width - self.buttonsPadding - bmp.GetWidth(), y)
mdc.DrawBitmap(dropShadow, round(rect.width - self.buttonsPadding - bmp.GetWidth() + 1), round(y + 1))
mdc.DrawBitmap(bmp, round(rect.width - self.buttonsPadding - bmp.GetWidth()), round(y))
y += raceBmp.GetHeight() + self.buttonsPadding
mdc.SetPen(wx.Pen(sepColor, 1))
mdc.DrawLine(rect.width - 1, 0, rect.width - 1, rect.height)
else:
mdc.DrawBitmap(dropShadow, x + 1, self.buttonsPadding + 1)
mdc.DrawBitmap(bmp, x, self.buttonsPadding)
mdc.DrawBitmap(dropShadow, round(x + 1), round(self.buttonsPadding + 1))
mdc.DrawBitmap(bmp, round(x), round(self.buttonsPadding))
x += raceBmp.GetWidth() + self.buttonsPadding
mdc.SetPen(wx.Pen(sepColor, 1))
mdc.DrawLine(0, 0, rect.width, 0)
if self.direction < 1:
if self.layout == wx.VERTICAL:
mdc.DrawBitmap(self.bmpArrow, -2, (rect.height - self.bmpArrow.GetHeight()) / 2)
mdc.DrawBitmap(self.bmpArrow, -2, round((rect.height - self.bmpArrow.GetHeight()) / 2))
else:
mdc.SetPen(wx.Pen(sepColor, 1))
mdc.DrawLine(0, 0, rect.width, 0)
mdc.DrawBitmap(self.bmpArrow, (rect.width - self.bmpArrow.GetWidth()) / 2, -2)
mdc.DrawBitmap(self.bmpArrow, round((rect.width - self.bmpArrow.GetWidth()) / 2), -2)
def OnTimer(self, event):
if event.GetId() == self.animTimerID:

View File

@@ -233,8 +233,8 @@ class PFToolbar:
bmpWidth = bmp.GetWidth()
pdc.DrawBitmap(dropShadowBmp, bx + self.padding / 2, self.toolbarY + self.padding / 2)
pdc.DrawBitmap(bmp, tbx, by)
pdc.DrawBitmap(dropShadowBmp, round(bx + self.padding / 2), round(self.toolbarY + self.padding / 2))
pdc.DrawBitmap(bmp, round(tbx), round(by))
bx += bmpWidth + self.padding

View File

@@ -247,12 +247,12 @@ class ShipItem(SFItem.SFBrowserItem):
else:
shipEffBk = self.shipEffBk
mdc.DrawBitmap(shipEffBk, self.shipEffx, self.shipEffy, 0)
mdc.DrawBitmap(shipEffBk, round(self.shipEffx), round(self.shipEffy), 0)
mdc.DrawBitmap(self.shipBmp, self.shipBmpx, self.shipBmpy, 0)
mdc.DrawBitmap(self.shipBmp, round(self.shipBmpx), round(self.shipBmpy), 0)
mdc.DrawBitmap(self.raceDropShadowBmp, self.raceBmpx + 1, self.raceBmpy + 1)
mdc.DrawBitmap(self.raceBmp, self.raceBmpx, self.raceBmpy)
mdc.DrawBitmap(self.raceDropShadowBmp, round(self.raceBmpx + 1), round(self.raceBmpy + 1))
mdc.DrawBitmap(self.raceBmp, round(self.raceBmpx), round(self.raceBmpy))
shipName, shipTrait, fittings = self.shipFittingInfo
@@ -264,17 +264,17 @@ class ShipItem(SFItem.SFBrowserItem):
fformat = "%d fits"
mdc.SetFont(self.fontNormal)
mdc.DrawText(fformat % fittings if fittings > 0 else fformat, self.textStartx, self.fittingsy)
mdc.DrawText(fformat % fittings if fittings > 0 else fformat, round(self.textStartx), round(self.fittingsy))
mdc.SetFont(self.fontSmall)
mdc.DrawText(self.toolbar.hoverLabel, self.thoverx, self.thovery)
mdc.DrawText(self.toolbar.hoverLabel, round(self.thoverx), round(self.thovery))
mdc.SetFont(self.fontBig)
psname = drawUtils.GetPartialText(mdc, shipName,
self.toolbarx - self.textStartx - self.padding * 2 - self.thoverw)
mdc.DrawText(psname, self.textStartx, self.shipNamey)
mdc.DrawText(psname, round(self.textStartx), round(self.shipNamey))
if self.tcFitName.IsShown():
self.AdjustControlSizePos(self.tcFitName, self.textStartx, self.toolbarx - self.editWidth - self.padding)

View File

@@ -173,7 +173,7 @@ class FirepowerViewFull(StatsView):
if hasSpool:
lines.append("")
lines.append(_t("Current") + ": {}".format(formatAmount(normal.total, prec, lowest, highest)))
for dmgType in normal.names():
for dmgType in normal.names(includePure=True):
val = getattr(normal, dmgType, None)
if val:
lines.append("{}{}: {}%".format(
@@ -215,13 +215,13 @@ class FirepowerViewFull(StatsView):
val = val() if fit is not None else None
preSpoolVal = preSpoolVal() if fit is not None else None
fullSpoolVal = fullSpoolVal() if fit is not None else None
if self._cachedValues[counter] != val:
if self._cachedValues[counter] != getattr(val, 'total', None):
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format(
formatAmount(0 if val is None else val.total, prec, lowest, highest),
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val
self._cachedValues[counter] = getattr(val, 'total', None)
counter += 1
self.panel.Layout()

View File

@@ -146,8 +146,8 @@ class ResistancesViewFull(StatsView):
lbl = PyGauge(contentPanel, font, 100)
lbl.SetMinSize((48, 16))
lbl.SetBackgroundColour(wx.Colour(bc[0], bc[1], bc[2]))
lbl.SetBarColour(wx.Colour(fc[0], fc[1], fc[2]))
lbl.SetBackgroundColour(wx.Colour(round(bc[0]), round(bc[1]), round(bc[2])))
lbl.SetBarColour(wx.Colour(round(fc[0]), round(fc[1]), round(fc[2])))
lbl.SetBarGradient()
lbl.SetFractionDigits(1)

View File

@@ -112,6 +112,7 @@ class TargetingMiscViewMinimal(StatsView):
cargoNamesOrder = OrderedDict((
("fleetHangarCapacity", _t("Fleet hangar")),
("shipMaintenanceBayCapacity", _t("Maintenance bay")),
("specialColonyResourcesHoldCapacity", _t("Infrastructure hold")),
("specialAmmoHoldCapacity", _t("Ammo hold")),
("specialFuelBayCapacity", _t("Fuel bay")),
("specialShipHoldCapacity", _t("Ship hold")),
@@ -134,6 +135,7 @@ class TargetingMiscViewMinimal(StatsView):
cargoValues = {
"main": lambda: fit.ship.getModifiedItemAttr("capacity"),
"fleetHangarCapacity": lambda: fit.ship.getModifiedItemAttr("fleetHangarCapacity"),
"specialColonyResourcesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialColonyResourcesHoldCapacity"),
"shipMaintenanceBayCapacity": lambda: fit.ship.getModifiedItemAttr("shipMaintenanceBayCapacity"),
"specialAmmoHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialAmmoHoldCapacity"),
"specialFuelBayCapacity": lambda: fit.ship.getModifiedItemAttr("specialFuelBayCapacity"),

View File

@@ -197,6 +197,30 @@ class SignatureRadiusColumn(GraphColumn):
SignatureRadiusColumn.register()
class FullHpColumn(GraphColumn):
name = 'FullHP'
stickPrefixToValue = True
def __init__(self, fittingView, params):
super().__init__(fittingView, 68)
def _getValue(self, stuff):
if isinstance(stuff, Fit):
full_hp = stuff.hp.get('shield', 0) + stuff.hp.get('armor', 0) + stuff.hp.get('hull', 0)
elif isinstance(stuff, TargetProfile):
full_hp = stuff.hp
else:
full_hp = 0
return full_hp, 'hp'
def _getFitTooltip(self):
return 'Total raw HP'
FullHpColumn.register()
class ShieldAmountColumn(GraphColumn):
name = 'ShieldAmount'

View File

@@ -93,8 +93,6 @@ class Miscellanea(ViewColumn):
text = "{} dmg".format(formatAmount(dmg, 3, 0, 6))
tooltip = "Raw damage done"
return text, tooltip
pass
elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"):
trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed")
optimalSig = stuff.getModifiedItemAttr("optimalSigRadius")
@@ -167,7 +165,7 @@ class Miscellanea(ViewColumn):
text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3))
tooltip = "Energy neutralization per second"
return text, tooltip
elif itemGroup == "Salvager":
elif itemGroup in ("Salvager", "Salvage Drone"):
chance = stuff.getModifiedItemAttr("accessDifficultyBonus")
if not chance:
return "", None
@@ -590,7 +588,7 @@ class Miscellanea(ViewColumn):
text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3))
tooltip = "Energy neutralization per second"
return text, tooltip
elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators"):
elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators", "Capital Mobility Modules"):
cycleTime = stuff.getModifiedItemAttr("duration") / 1000
text = "{0}s".format(formatAmount(cycleTime, 3, 0, 3))
tooltip = "Spoolup time"
@@ -810,6 +808,19 @@ class Miscellanea(ViewColumn):
text = "{}".format(formatAmount(scanStr, 4, 0, 3))
tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0))
return text, tooltip
elif chargeGroup in ("SCARAB Breacher Pods",):
duration = stuff.getModifiedChargeAttr("dotDuration") / 1000
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick")
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick")
text = "{}/{}% over {}s".format(
formatAmount(dmgAbs * duration, 3, 0, 6),
formatAmount(dmgRel * duration, 3, 0, 6),
formatAmount(duration, 0, 0, 0))
fullDmgHp = dmgAbs / (dmgRel / 100)
tooltip = (
'Pure damage inflicted over time, minimum of absolute / relative\n'
'Full DPS from {} target HP').format(formatAmount(fullDmgHp, 3, 0, 6))
return text, tooltip
else:
return "", None
else:

View File

@@ -39,9 +39,10 @@ from gui.builtinViewColumns.state import State
from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED
from gui.contextMenu import ContextMenu
from gui.utils.staticHelpers import DragDropHelper
from gui.utils.dark import isDark
from service.fit import Fit
from service.market import Market
from config import slotColourMap
from config import slotColourMap, slotColourMapDark, errColor, errColorDark
from gui.fitCommands.helpers import getSimilarModPositions
pyfalog = Logger(__name__)
@@ -126,6 +127,10 @@ class FittingViewDrop(wx.DropTarget):
if self.GetData():
dragged_data = DragDropHelper.data
# pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText())
if dragged_data is None:
return t
data = dragged_data.split(':')
self.dropFn(x, y, data)
return t
@@ -729,7 +734,10 @@ class FittingView(d.Display):
event.Skip()
def slotColour(self, slot):
return slotColourMap.get(slot) or self.GetBackgroundColour()
if isDark():
return slotColourMapDark.get(slot) or self.GetBackgroundColour()
else:
return slotColourMap.get(slot) or self.GetBackgroundColour()
def refresh(self, stuff):
"""
@@ -774,7 +782,7 @@ class FittingView(d.Display):
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51))
self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor)
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
self.SetItemBackgroundColour(i, self.slotColour(mod.slot))
@@ -895,7 +903,7 @@ class FittingView(d.Display):
opts.m_labelText = name
if imgId != -1:
opts.m_labelBitmap = wx.Bitmap(isize, isize)
opts.m_labelBitmap = wx.Bitmap(round(isize), round(isize))
width = render.DrawHeaderButton(self, tdc, (0, 0, 16, 16), sortArrow=wx.HDR_SORT_ICON_NONE, params=opts)
@@ -911,7 +919,7 @@ class FittingView(d.Display):
maxWidth += columnsWidths[i]
mdc = wx.MemoryDC()
mbmp = wx.Bitmap(maxWidth, maxRowHeight * rows + padding * 4 + headerSize)
mbmp = wx.Bitmap(round(maxWidth), round(maxRowHeight * rows + padding * 4 + headerSize))
mdc.SelectObject(mbmp)
@@ -956,7 +964,7 @@ class FittingView(d.Display):
cx = padding
if slotMap[st.slot]:
mdc.DrawRectangle(cx, cy, maxWidth - cx, maxRowHeight)
mdc.DrawRectangle(round(cx), round(cy), round(maxWidth - cx), round(maxRowHeight))
for i, col in enumerate(self.activeColumns):
if i > maxColumns:

View File

@@ -13,8 +13,8 @@ from service.market import Market
def stripHtml(text):
text = re.sub('<\s*br\s*/?\s*>', '\n', text)
text = re.sub('</?[^/]+?(/\s*)?>', '', text)
text = re.sub(r'<\s*br\s*/?\s*>', '\n', text)
text = re.sub(r'</?[^/]+?(/\s*)?>', '', text)
return text

View File

@@ -115,7 +115,7 @@ class CharacterEntityEditor(EntityEditor):
sChar = Character.getInstance()
if entity.alphaCloneID:
trimmed_name = re.sub('[ \(\u03B1\)]+$', '', name)
trimmed_name = re.sub('[ \\(\u03B1\\)]+$', '', name)
sChar.rename(entity, trimmed_name)
else:
sChar.rename(entity, name)
@@ -371,7 +371,7 @@ class SkillTreeView(wx.Panel):
bSizerButtons.AddStretchSpacer()
importExport = ((_t("Import skills from clipboard"), wx.ART_FILE_OPEN, "import"),
(_t("Export skills from clipboard"), wx.ART_FILE_SAVE_AS, "export"))
(_t("Export skills to clipboard"), wx.ART_FILE_SAVE_AS, "export"))
for tooltip, art, attr in importExport:
bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON)
@@ -446,6 +446,7 @@ class SkillTreeView(wx.Panel):
text = fromClipboard().strip()
if text:
sCharacter = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
try:
lines = text.splitlines()
@@ -455,7 +456,7 @@ class SkillTreeView(wx.Panel):
skill, level = s.rsplit(None, 1)[0], arabicOrRomanToInt(s.rsplit(None, 1)[1])
skill = char.getSkill(skill)
if skill:
skill.setLevel(level, ignoreRestrict=True)
sCharacter.changeLevel(char.ID, skill.item.ID, level)
except (KeyboardInterrupt, SystemExit):
raise
@@ -480,6 +481,35 @@ class SkillTreeView(wx.Panel):
toClipboard(list)
def exportSkillsSuperCondensed(self, evt):
char = self.charEditor.entityEditor.getActiveEntity()
skills = {}
explicit_levels = {}
implicit_levels = {}
for s in char.__class__.getSkillNameMap().keys():
skill = char.getSkill(s)
if skill.level < 1:
continue
skills[skill.item.ID] = skill
explicit_levels[skill.item.ID] = skill.level
for skill in skills.values():
for req_skill, level in skill.item.requiredSkills.items():
if req_skill.ID not in implicit_levels or implicit_levels[req_skill.ID] < level:
implicit_levels[req_skill.ID] = level
condensed = {}
for typeID, level in explicit_levels.items():
if typeID not in implicit_levels or implicit_levels[typeID] < level:
condensed[skills[typeID].item.name] = level
lines = []
for skill in sorted(condensed):
lines.append(f'{skill}\t{condensed[skill]}')
toClipboard('\n'.join(lines))
def onSecStatus(self, event):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
@@ -516,7 +546,10 @@ class SkillTreeView(wx.Panel):
def populateSkillTreeSkillSearch(self, event=None):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
search = self.searchInput.GetLineText(0)
try:
search = self.searchInput.GetLineText(0)
except AttributeError:
search = self.searchInput.GetValue()
root = self.root
tree = self.skillTreeListCtrl
@@ -530,7 +563,7 @@ class SkillTreeView(wx.Panel):
iconId = self.skillBookDirtyImageId
childId = tree.AppendItem(root, name, iconId, data=('skill', id))
tree.SetItemText(childId, 1, _t("Level {}d").format(int(level)) if isinstance(level, float) else level)
tree.SetItemText(childId, 1, _t("Level {}").format(int(level)) if isinstance(level, float) else level)
def populateSkillTree(self, event=None):
sChar = Character.getInstance()
@@ -588,7 +621,6 @@ class SkillTreeView(wx.Panel):
iconId = self.skillBookDirtyImageId
childId = tree.AppendItem(root, name, iconId, data=('skill', id))
tree.SetItemText(childId, 1, _t("Level {}").format(int(level)) if isinstance(level, float) else level)
def spawnMenu(self, event):
@@ -804,7 +836,12 @@ class APIView(wx.Panel):
self.SetSizer(pmainSizer)
self.Layout()
self.ssoListChanged(None)
try:
self.ssoListChanged(None)
except (KeyboardInterrupt, SystemExit):
raise
except:
pass
def ssoCharChanged(self, event):
sChar = Character.getInstance()
@@ -856,7 +893,7 @@ class APIView(wx.Panel):
noneID = self.charChoice.Append(_t("None"), None)
for char in ssoChars:
currId = self.charChoice.Append(char.characterName, char.ID)
currId = self.charChoice.Append(char.characterDisplay, char.ID)
if sso is not None and char.ID == sso.ID:
self.charChoice.SetSelection(currId)
@@ -910,7 +947,7 @@ class SecStatusDialog(wx.Dialog):
self.m_staticText1.Wrap(-1)
bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5)
self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
self.floatSpin = FloatSpin(self, value=sec, min_val=-10.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5)
btnOk = wx.Button(self, wx.ID_OK)

View File

@@ -514,7 +514,7 @@ class _TabRenderer:
Creates the tab background bitmap based upon calculated dimension values
and modified bitmaps via InitBitmaps()
"""
bk_bmp = wx.Bitmap(self.tab_width, self.tab_height)
bk_bmp = wx.Bitmap(round(self.tab_width), round(self.tab_height))
mdc = wx.MemoryDC()
mdc.SelectObject(bk_bmp)
@@ -525,16 +525,16 @@ class _TabRenderer:
# convert middle bitmap and scale to tab width
cm = self.ctab_middle_bmp.ConvertToImage()
mimg = cm.Scale(self.content_width, self.ctab_middle.GetHeight(),
mimg = cm.Scale(round(self.content_width), round(self.ctab_middle.GetHeight()),
wx.IMAGE_QUALITY_NORMAL)
mbmp = wx.Bitmap(mimg)
# draw middle bitmap, offset by left
mdc.DrawBitmap(mbmp, self.left_width, 0)
mdc.DrawBitmap(mbmp, round(self.left_width), 0)
# draw right bitmap offset by left + middle
mdc.DrawBitmap(self.ctab_right_bmp,
self.content_width + self.left_width, 0)
round(self.content_width + self.left_width), 0)
mdc.SelectObject(wx.NullBitmap)
@@ -555,7 +555,7 @@ class _TabRenderer:
+ self.left_width \
- self.ctab_close_bmp.GetWidth() / 2
y_offset = (self.tab_height - self.ctab_close_bmp.GetHeight()) / 2
self.close_region.Offset(x_offset, y_offset)
self.close_region.Offset(round(x_offset), round(y_offset))
def InitColors(self):
"""Determines colors used for tab, based on system settings"""
@@ -573,7 +573,7 @@ class _TabRenderer:
height = self.tab_height
canvas = wx.Bitmap(self.tab_width, self.tab_height, 24)
canvas = wx.Bitmap(round(self.tab_width), round(self.tab_height), 24)
mdc = wx.MemoryDC()
@@ -590,8 +590,8 @@ class _TabRenderer:
# Draw tab icon
mdc.DrawBitmap(
bmp,
self.left_width + self.padding - bmp.GetWidth() / 2,
(height - bmp.GetHeight()) / 2)
round(self.left_width + self.padding - bmp.GetWidth() / 2),
round((height - bmp.GetHeight()) / 2))
# draw close button
if self.closeable:
@@ -604,8 +604,8 @@ class _TabRenderer:
mdc.DrawBitmap(
cbmp,
self.content_width + self.left_width - cbmp.GetWidth() / 2,
(height - cbmp.GetHeight()) / 2)
round(self.content_width + self.left_width - cbmp.GetWidth() / 2),
round((height - cbmp.GetHeight()) / 2))
mdc.SelectObject(wx.NullBitmap)
@@ -640,7 +640,7 @@ class _TabRenderer:
# draw text (with no ellipses)
text = draw.GetPartialText(dc, self.text, maxsize, "")
tx, ty = dc.GetTextExtent(text)
dc.DrawText(text, text_start + self.padding, height / 2 - ty / 2)
dc.DrawText(text, round(text_start + self.padding), round(height / 2 - ty / 2))
def __repr__(self):
return "_TabRenderer(text={}, disabled={}) at {}".format(
@@ -1005,7 +1005,7 @@ class _TabsContainer(wx.Panel):
region = tab.GetCloseButtonRegion()
posx, posy = tab.GetPosition()
region.Offset(posx, posy)
region.Offset(round(posx), round(posy))
if region.Contains(x, y):
index = self.tabs.index(tab)
@@ -1036,7 +1036,7 @@ class _TabsContainer(wx.Panel):
region = self.add_button.GetRegion()
ax, ay = self.add_button.GetPosition()
region.Offset(ax, ay)
region.Offset(round(ax), round(ay))
if region.Contains(x, y):
ev = PageAdding()
@@ -1058,7 +1058,7 @@ class _TabsContainer(wx.Panel):
for tab in self.tabs:
region = tab.GetCloseButtonRegion()
posx, posy = tab.GetPosition()
region.Offset(posx, posy)
region.Offset(round(posx), round(posy))
if region.Contains(x, y):
if not tab.GetCloseButtonHoverStatus():
@@ -1093,7 +1093,7 @@ class _TabsContainer(wx.Panel):
tabRegion = tab.GetTabRegion()
tabPos = tab.GetPosition()
tabPosX, tabPosY = tabPos
tabRegion.Offset(tabPosX, tabPosY)
tabRegion.Offset(round(tabPosX), round(tabPosY))
if tabRegion.Contains(x, y):
return True
@@ -1166,7 +1166,7 @@ class _TabsContainer(wx.Panel):
region = self.add_button.GetRegion()
ax, ay = self.add_button.GetPosition()
region.Offset(ax, ay)
region.Offset(round(ax), round(ay))
if region.Contains(x, y):
if not self.add_button.IsHighlighted():
@@ -1198,7 +1198,7 @@ class _TabsContainer(wx.Panel):
if self.show_add_button:
ax, ay = self.add_button.GetPosition()
mdc.DrawBitmap(self.add_button.Render(), ax, ay, True)
mdc.DrawBitmap(self.add_button.Render(), round(ax), round(ay), True)
for i in range(len(self.tabs) - 1, -1, -1):
tab = self.tabs[i]
@@ -1206,14 +1206,14 @@ class _TabsContainer(wx.Panel):
if not tab.IsSelected():
# drop shadow first
mdc.DrawBitmap(self.fxBmps[tab], posx, posy, True)
mdc.DrawBitmap(self.fxBmps[tab], round(posx), (posy), True)
bmp = tab.Render()
img = bmp.ConvertToImage()
img = img.AdjustChannels(1, 1, 1, 0.85)
bmp = wx.Bitmap(img)
mdc.DrawBitmap(bmp, posx, posy, True)
mdc.DrawBitmap(bmp, round(posx), (posy), True)
mdc.SetDeviceOrigin(posx, posy)
mdc.SetDeviceOrigin(round(posx), round(posy))
tab.DrawText(mdc)
mdc.SetDeviceOrigin(0, 0)
else:
@@ -1224,7 +1224,7 @@ class _TabsContainer(wx.Panel):
if selected:
posx, posy = selected.GetPosition()
# drop shadow first
mdc.DrawBitmap(self.fxBmps[selected], posx, posy, True)
mdc.DrawBitmap(self.fxBmps[selected], round(posx), round(posy), True)
bmp = selected.Render()
@@ -1233,9 +1233,9 @@ class _TabsContainer(wx.Panel):
img = img.AdjustChannels(1.2, 1.2, 1.2, 0.7)
bmp = wx.Bitmap(img)
mdc.DrawBitmap(bmp, posx, posy, True)
mdc.DrawBitmap(bmp, round(posx), round(posy), True)
mdc.SetDeviceOrigin(posx, posy)
mdc.SetDeviceOrigin(round(posx), round(posy))
selected.DrawText(mdc)
mdc.SetDeviceOrigin(0, 0)
@@ -1501,7 +1501,7 @@ class PFNotebookPagePreview(wx.Frame):
def OnWindowPaint(self, event):
rect = self.GetRect()
canvas = wx.Bitmap(rect.width, rect.height)
canvas = wx.Bitmap(round(rect.width), round(rect.height))
mdc = wx.BufferedPaintDC(self)
mdc.SelectObject(canvas)
color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
@@ -1514,7 +1514,7 @@ class PFNotebookPagePreview(wx.Frame):
x, y = mdc.GetTextExtent(self.title)
mdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)))
mdc.DrawRectangle(0, 0, rect.width, 16)
mdc.DrawRectangle(0, 0, round(rect.width), 16)
mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
@@ -1523,4 +1523,4 @@ class PFNotebookPagePreview(wx.Frame):
mdc.SetPen(wx.Pen("#000000", width=1))
mdc.SetBrush(wx.TRANSPARENT_BRUSH)
mdc.DrawRectangle(0, 16, rect.width, rect.height - 16)
mdc.DrawRectangle(0, 16, round(rect.width), round(rect.height - 16))

View File

@@ -193,7 +193,7 @@ class CopySelectDialog(wx.Dialog):
def exportEsi(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportESI(fit, True, callback)
Port.exportESI(fit, False, False, False, callback)
def exportXml(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())

View File

@@ -29,8 +29,9 @@ class Display(wx.ListCtrl):
DEFAULT_COLS = None
def __init__(self, parent, size=wx.DefaultSize, style=0):
wx.ListCtrl.__init__(self, parent, size=size, style=wx.LC_REPORT | style)
wx.ListCtrl.__init__(self)
self.EnableSystemTheme(False)
self.Create(parent, size=size, style=wx.LC_REPORT | style)
self.imageList = CachingImageList(16, 16)
self.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL)
self.activeColumns = []

View File

@@ -96,7 +96,7 @@ class EveFittings(AuxiliaryFrame):
self.charChoice.Clear()
for char in chars:
self.charChoice.Append(char.characterName, char.ID)
self.charChoice.Append(char.characterDisplay, char.ID)
if len(chars) > 0:
self.charChoice.SetSelection(0)
@@ -227,21 +227,6 @@ class EveFittings(AuxiliaryFrame):
self.fitView.update([])
class ESIServerExceptionHandler:
def __init__(self, parentWindow, ex):
pyfalog.error(ex)
with wx.MessageDialog(
parentWindow,
_t("There was an issue starting up the localized server, try setting "
"Login Authentication Method to Manual by going to Preferences -> EVE SS0 -> "
"Login Authentication Method. If this doesn't fix the problem please file an "
"issue on Github."),
_t("Add Character Error"),
wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
class ESIExceptionHandler:
# todo: make this a generate excetpion handler for all calls
def __init__(self, ex):
@@ -283,7 +268,7 @@ class ExportToEve(AuxiliaryFrame):
def __init__(self, parent):
super().__init__(
parent, id=wx.ID_ANY, title=_t("Export fit to EVE"), pos=wx.DefaultPosition,
size=wx.Size(400, 140) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 115), resizeable=True)
size=wx.Size(400, 175) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 145), resizeable=True)
self.mainFrame = parent
@@ -305,6 +290,16 @@ class ExportToEve(AuxiliaryFrame):
self.exportChargesCb.Bind(wx.EVT_CHECKBOX, self.OnChargeExportChange)
mainSizer.Add(self.exportChargesCb, 0, 0, 5)
self.exportImplantsCb = wx.CheckBox(self, wx.ID_ANY, _t('Export Implants'), wx.DefaultPosition, wx.DefaultSize, 0)
self.exportImplantsCb.SetValue(EsiSettings.getInstance().get('exportImplants'))
self.exportImplantsCb.Bind(wx.EVT_CHECKBOX, self.OnImplantsExportChange)
mainSizer.Add(self.exportImplantsCb, 0, 0, 5)
self.exportBoostersCb = wx.CheckBox(self, wx.ID_ANY, _t('Export Boosters'), wx.DefaultPosition, wx.DefaultSize, 0)
self.exportBoostersCb.SetValue(EsiSettings.getInstance().get('exportBoosters'))
self.exportBoostersCb.Bind(wx.EVT_CHECKBOX, self.OnBoostersExportChange)
mainSizer.Add(self.exportBoostersCb, 0, 0, 5)
self.exportBtn.Bind(wx.EVT_BUTTON, self.exportFitting)
self.statusbar = wx.StatusBar(self)
@@ -324,13 +319,21 @@ class ExportToEve(AuxiliaryFrame):
EsiSettings.getInstance().set('exportCharges', self.exportChargesCb.GetValue())
event.Skip()
def OnImplantsExportChange(self, event):
EsiSettings.getInstance().set('exportImplants', self.exportImplantsCb.GetValue())
event.Skip()
def OnBoostersExportChange(self, event):
EsiSettings.getInstance().set('exportBoosters', self.exportBoostersCb.GetValue())
event.Skip()
def updateCharList(self):
sEsi = Esi.getInstance()
chars = sEsi.getSsoCharacters()
self.charChoice.Clear()
for char in chars:
self.charChoice.Append(char.characterName, char.ID)
self.charChoice.Append(char.characterDisplay, char.ID)
if len(chars) > 0:
self.charChoice.SetSelection(0)
@@ -360,8 +363,10 @@ class ExportToEve(AuxiliaryFrame):
sFit = Fit.getInstance()
exportCharges = self.exportChargesCb.GetValue()
exportImplants = self.exportImplantsCb.GetValue()
exportBoosters = self.exportBoostersCb.GetValue()
try:
data = sPort.exportESI(sFit.getFit(fitID), exportCharges)
data = sPort.exportESI(sFit.getFit(fitID), exportCharges, exportImplants, exportBoosters)
except ESIExportException as e:
msg = str(e)
if not msg:
@@ -414,6 +419,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.lcCharacters.InsertColumn(0, heading=_t('Character'))
self.lcCharacters.InsertColumn(1, heading=_t('Character ID'))
self.lcCharacters.InsertColumn(2, heading=_t('Server'))
self.popCharList()
@@ -476,9 +482,11 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.lcCharacters.InsertItem(index, char.characterName)
self.lcCharacters.SetItem(index, 1, str(char.characterID))
self.lcCharacters.SetItemData(index, char.ID)
self.lcCharacters.SetItem(index, 2, char.server or "<unknown>")
self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(2, wx.LIST_AUTOSIZE)
def addChar(self, event):
try:
@@ -486,8 +494,6 @@ class SsoCharacterMgmt(AuxiliaryFrame):
sEsi.login()
except (KeyboardInterrupt, SystemExit):
raise
except Exception as ex:
ESIServerExceptionHandler(self, ex)
def delChar(self, event):
item = self.lcCharacters.GetFirstSelected()

View File

@@ -112,7 +112,7 @@ class FitBrowserLiteDialog(wx.Dialog):
return True
matches = []
searchTokens = [t.lower() for t in re.split('\s+', searchPattern)]
searchTokens = [t.lower() for t in re.split(r'\s+', searchPattern)]
for fit in self.allFits:
if isMatch(fit, searchTokens):
matches.append(fit)

View File

@@ -12,6 +12,7 @@ from .gui.cargo.remove import GuiRemoveCargosCommand
from .gui.commandFit.add import GuiAddCommandFitsCommand
from .gui.commandFit.remove import GuiRemoveCommandFitsCommand
from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand
from .gui.fitPilotSecurity import GuiChangeFitPilotSecurityCommand
from .gui.fitRename import GuiRenameFitCommand
from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand
from .gui.fitSystemSecurity import GuiChangeFitSystemSecurityCommand

View File

@@ -0,0 +1,32 @@
import wx
from logbook import Logger
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.fitID = fitID
self.secStatus = secStatus
self.savedSecStatus = None
def Do(self):
pyfalog.debug('Doing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
fit = Fit.getInstance().getFit(self.fitID, basic=True)
# Fetching status via getter and then saving 'raw' security status
# is intentional, to restore pre-change state properly
if fit.pilotSecurity == self.secStatus:
return False
self.savedSecStatus = fit.pilotSecurity
fit.pilotSecurity = self.secStatus
return True
def Undo(self):
pyfalog.debug('Undoing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.savedSecStatus)
return cmd.Do()

View File

@@ -0,0 +1,36 @@
import wx
from service.fit import Fit
import eos.db
import gui.mainFrame
from gui import globalEvents as GE
from gui.fitCommands.helpers import InternalCommandHistory
from gui.fitCommands.calc.fitPilotSecurity import CalcChangeFitPilotSecurityCommand
class GuiChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.internalHistory = InternalCommandHistory()
self.fitID = fitID
self.secStatus = secStatus
def Do(self):
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.secStatus)
success = self.internalHistory.submit(cmd)
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success
def Undo(self):
success = self.internalHistory.undoAll()
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success

View File

@@ -61,10 +61,11 @@ from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor
from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard
from gui.utils.progressHelper import ProgressHelper
from service.character import Character
from service.esi import Esi
from service.fit import Fit
from service.port import IPortUser, Port
from service.port import Port
from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update
@@ -130,7 +131,6 @@ class OpenFitsThread(threading.Thread):
self.running = False
# todo: include IPortUser again
class MainFrame(wx.Frame):
__instance = None
@@ -845,14 +845,15 @@ class MainFrame(wx.Frame):
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
self.progressDialog = wx.ProgressDialog(
_t("Importing fits"),
" " * 100, # set some arbitrary spacing to create width in window
parent=self,
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
)
Port.importFitsThreaded(dlg.GetPaths(), self)
self.progressDialog.ShowModal()
# set some arbitrary spacing to create width in window
progress = ProgressHelper(message=" " * 100, callback=self._openAfterImport)
call = (Port.importFitsThreaded, [dlg.GetPaths(), progress], {})
self.handleProgress(
title=_t("Importing fits"),
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
call=call,
progress=progress,
errMsgLbl=_t("Import Error"))
def backupToXml(self, event):
""" Back up all fits to EVE XML file """
@@ -863,32 +864,30 @@ class MainFrame(wx.Frame):
_t("Save Backup As..."),
wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
defaultFile=defaultFile,
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
filePath = dlg.GetPath()
defaultFile=defaultFile) as fileDlg:
if fileDlg.ShowModal() == wx.ID_OK:
filePath = fileDlg.GetPath()
if '.' not in os.path.basename(filePath):
filePath += ".xml"
sFit = Fit.getInstance()
max_ = sFit.countAllFits()
self.progressDialog = wx.ProgressDialog(
_t("Backup fits"),
_t("Backing up {} fits to: {}").format(max_, filePath),
maximum=max_,
parent=self,
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL
)
Port.backupFits(filePath, self)
self.progressDialog.ShowModal()
fitAmount = Fit.getInstance().countAllFits()
progress = ProgressHelper(
message=_t("Backing up {} fits to: {}").format(fitAmount, filePath),
maximum=fitAmount + 1)
call = (Port.backupFits, [filePath, progress], {})
self.handleProgress(
title=_t("Backup fits"),
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
call=call,
progress=progress,
errMsgLbl=_t("Export Error"))
def exportHtml(self, event):
from gui.utils.exportHtml import exportHtml
sFit = Fit.getInstance()
settings = HTMLExportSettings.getInstance()
max_ = sFit.countAllFits()
path = settings.getPath()
if not os.path.isdir(os.path.dirname(path)):
@@ -903,82 +902,44 @@ class MainFrame(wx.Frame):
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
return
progress = ProgressHelper(
message=_t("Generating HTML file at: {}").format(path),
maximum=sFit.countAllFits() + 1)
call = (exportHtml.getInstance().refreshFittingHtml, [True, progress], {})
self.handleProgress(
title=_t("Backup fits"),
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME,
call=call,
progress=progress)
self.progressDialog = wx.ProgressDialog(
_t("Backup fits"),
_t("Generating HTML file at: {}").format(path),
maximum=max_, parent=self,
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME)
exportHtml.getInstance().refreshFittingHtml(True, self.backupCallback)
self.progressDialog.ShowModal()
def backupCallback(self, info):
if info == -1:
self.closeProgressDialog()
else:
self.progressDialog.Update(info)
def on_port_process_start(self):
# flag for progress dialog.
self.__progress_flag = True
def on_port_processing(self, action, data=None):
# 2017/03/29 NOTE: implementation like interface
wx.CallAfter(
self._on_port_processing, action, data
)
return self.__progress_flag
def _on_port_processing(self, action, data):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
_message = None
if action & IPortUser.ID_ERROR:
self.closeProgressDialog()
_message = _t("Import Error") if action & IPortUser.PROCESS_IMPORT else _t("Export Error")
def handleProgress(self, title, style, call, progress, errMsgLbl=None):
extraArgs = {}
if progress.maximum is not None:
extraArgs['maximum'] = progress.maximum
with wx.ProgressDialog(
parent=self,
title=title,
message=progress.message,
style=style,
**extraArgs
) as dlg:
func, args, kwargs = call
func(*args, **kwargs)
while progress.working:
wx.MilliSleep(250)
wx.Yield()
(progress.dlgWorking, skip) = dlg.Update(progress.current, progress.message)
if progress.error and errMsgLbl:
with wx.MessageDialog(
self,
_t("The following error was generated") +
f"\n\n{data}\n\n" +
f"\n\n{progress.error}\n\n" +
_t("Be aware that already processed fits were not saved"),
_message, wx.OK | wx.ICON_ERROR
errMsgLbl, wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
return
# data is str
if action & IPortUser.PROCESS_IMPORT:
if action & IPortUser.ID_PULSE:
_message = ()
# update message
elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message:
_message = data
if _message is not None:
self.__progress_flag, _unuse = self.progressDialog.Pulse(_message)
else:
self.closeProgressDialog()
if action & IPortUser.ID_DONE:
self._openAfterImport(data)
# data is tuple(int, str)
elif action & IPortUser.PROCESS_EXPORT:
if action & IPortUser.ID_DONE:
self.closeProgressDialog()
else:
self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1])
elif progress.callback:
progress.callback(*progress.cbArgs)
def _openAfterImport(self, fits):
if len(fits) > 0:
@@ -988,6 +949,8 @@ class MainFrame(wx.Frame):
wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True))
else:
fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name))
# Show 100 fits max
fits = fits[:100]
results = []
for fit in fits:
results.append((
@@ -999,15 +962,6 @@ class MainFrame(wx.Frame):
))
wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True))
def closeProgressDialog(self):
# Windows apparently handles ProgressDialogs differently. We can
# simply Destroy it here, but for other platforms we must Close it
if 'wxMSW' in wx.PlatformInfo:
self.progressDialog.Destroy()
else:
self.progressDialog.EndModal(wx.ID_OK)
self.progressDialog.Close()
def importCharacter(self, event):
""" Imports character XML file from EVE API """
with wx.FileDialog(

View File

@@ -146,7 +146,7 @@ class MarketBrowser(wx.Panel):
setting = self.settings.get('marketMGSearchMode')
# We turn on all meta buttons for the duration of search/recents
if setting == 1:
if newMode in ('search', 'recent'):
if newMode in ('search', 'recent', 'charges'):
for btn in self.metaButtons:
btn.setUserSelection(True)
if newMode == 'normal':

View File

@@ -42,7 +42,7 @@ class DmgPatternNameValidator(BaseValidator):
return DmgPatternNameValidator()
def Validate(self, win):
entityEditor = win.parent
entityEditor = win.Parent.parent
textCtrl = self.GetWindow()
text = textCtrl.GetValue().strip()

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