Added vertical marker line to show exact values on graphs
This commit is contained in:
@@ -31,62 +31,61 @@ def getWebbedSpeed(src, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighte
|
||||
if tgt.isFit and tgt.item.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
|
||||
return currentUnwebbedSpeed
|
||||
maxUnwebbedSpeed = tgt.getMaxVelocity()
|
||||
try:
|
||||
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
|
||||
except ZeroDivisionError:
|
||||
currentWebbedSpeed = 0
|
||||
else:
|
||||
appliedMultipliers = {}
|
||||
# Modules first, they are applied always the same way
|
||||
for wData in webMods:
|
||||
appliedBoost = wData.boost * calculateRangeFactor(
|
||||
srcOptimalRange=wData.optimal,
|
||||
srcFalloffRange=wData.falloff,
|
||||
distance=distance)
|
||||
if appliedBoost:
|
||||
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
|
||||
# What's immobile cannot be slowed
|
||||
if maxUnwebbedSpeed == 0:
|
||||
return maxUnwebbedSpeed
|
||||
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
|
||||
appliedMultipliers = {}
|
||||
# Modules first, they are applied always the same way
|
||||
for wData in webMods:
|
||||
appliedBoost = wData.boost * calculateRangeFactor(
|
||||
srcOptimalRange=wData.optimal,
|
||||
srcFalloffRange=wData.falloff,
|
||||
distance=distance)
|
||||
if appliedBoost:
|
||||
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
|
||||
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Drones and fighters
|
||||
mobileWebs = []
|
||||
mobileWebs.extend(webFighters)
|
||||
# Drones have range limit
|
||||
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
|
||||
mobileWebs.extend(webDrones)
|
||||
atkRadius = src.getRadius()
|
||||
# As mobile webs either follow the target or stick to the attacking ship,
|
||||
# if target is within mobile web optimal - it can be applied unconditionally
|
||||
longEnoughMws = [mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius]
|
||||
if longEnoughMws:
|
||||
for mwData in longEnoughMws:
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Drones and fighters
|
||||
mobileWebs = []
|
||||
mobileWebs.extend(webFighters)
|
||||
# Drones have range limit
|
||||
if distance is None or distance <= src.item.extraAttributes['droneControlRange']:
|
||||
mobileWebs.extend(webDrones)
|
||||
atkRadius = src.getRadius()
|
||||
# As mobile webs either follow the target or stick to the attacking ship,
|
||||
# if target is within mobile web optimal - it can be applied unconditionally
|
||||
longEnoughMws = [mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius]
|
||||
if longEnoughMws:
|
||||
for mwData in longEnoughMws:
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Apply remaining webs, from fastest to slowest
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
while mobileWebs:
|
||||
# Process in batches unified by speed to save up resources
|
||||
fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed
|
||||
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
|
||||
for mwData in fastestMws:
|
||||
# Faster than target or set to follow it - apply full slowdown
|
||||
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
|
||||
appliedMwBoost = mwData.boost
|
||||
# Otherwise project from the center of the ship
|
||||
# Apply remaining webs, from fastest to slowest
|
||||
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
|
||||
while mobileWebs:
|
||||
# Process in batches unified by speed to save up resources
|
||||
fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed
|
||||
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
|
||||
for mwData in fastestMws:
|
||||
# Faster than target or set to follow it - apply full slowdown
|
||||
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
|
||||
appliedMwBoost = mwData.boost
|
||||
# Otherwise project from the center of the ship
|
||||
else:
|
||||
if distance is None:
|
||||
rangeFactorDistance = None
|
||||
else:
|
||||
if distance is None:
|
||||
rangeFactorDistance = None
|
||||
else:
|
||||
rangeFactorDistance = distance + atkRadius - mwData.radius
|
||||
appliedMwBoost = mwData.boost * calculateRangeFactor(
|
||||
srcOptimalRange=mwData.optimal,
|
||||
srcFalloffRange=mwData.falloff,
|
||||
distance=rangeFactorDistance)
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
rangeFactorDistance = distance + atkRadius - mwData.radius
|
||||
appliedMwBoost = mwData.boost * calculateRangeFactor(
|
||||
srcOptimalRange=mwData.optimal,
|
||||
srcFalloffRange=mwData.falloff,
|
||||
distance=rangeFactorDistance)
|
||||
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
|
||||
mobileWebs.remove(mwData)
|
||||
maxWebbedSpeed = tgt.getMaxVelocity(extraMultipliers=appliedMultipliers)
|
||||
currentWebbedSpeed = maxWebbedSpeed * speedRatio
|
||||
# Ensure consistent results - round off a little to avoid float errors
|
||||
return floatUnerr(currentWebbedSpeed)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import itertools
|
||||
import os
|
||||
import traceback
|
||||
from bisect import bisect
|
||||
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
@@ -29,6 +30,7 @@ from logbook import Logger
|
||||
|
||||
|
||||
from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES, hsl_to_hsv
|
||||
from gui.utils.numberFormatter import roundToPrec
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -99,6 +101,7 @@ class GraphCanvasPanel(wx.Panel):
|
||||
self.subplot.grid(True)
|
||||
allXs = set()
|
||||
allYs = set()
|
||||
plotData = {}
|
||||
legendData = []
|
||||
chosenX = self.graphFrame.ctrlPanel.xType
|
||||
chosenY = self.graphFrame.ctrlPanel.yType
|
||||
@@ -146,6 +149,7 @@ class GraphCanvasPanel(wx.Panel):
|
||||
ySpec=chosenY,
|
||||
src=source,
|
||||
tgt=target)
|
||||
plotData[(source, target)] = (xs, ys)
|
||||
allXs.update(xs)
|
||||
allYs.update(ys)
|
||||
# If we have single data point, show marker - otherwise line won't be shown
|
||||
@@ -159,7 +163,7 @@ class GraphCanvasPanel(wx.Panel):
|
||||
else:
|
||||
legendData.append((color, lineStyle, '{} vs {}'.format(source.shortName, target.shortName)))
|
||||
except Exception:
|
||||
pyfalog.warning('Invalid values in "{0}"', source.name)
|
||||
pyfalog.warning('Failed to plot "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
|
||||
self.canvas.draw()
|
||||
self.Refresh()
|
||||
return
|
||||
@@ -171,17 +175,17 @@ class GraphCanvasPanel(wx.Panel):
|
||||
maxY = max(allYs, default=0)
|
||||
# Extend range a little for some visual space
|
||||
yRange = maxY - minY
|
||||
minY -= yRange * 0.05
|
||||
maxY += yRange * 0.05
|
||||
canvasMinY = minY - yRange * 0.05
|
||||
canvasMaxY = maxY + yRange * 0.1 # Extra space for "X mark"
|
||||
# Extend by % of value if we show function of a constant
|
||||
if minY == maxY:
|
||||
minY -= minY * 0.05
|
||||
maxY += minY * 0.05
|
||||
if canvasMinY == canvasMaxY:
|
||||
canvasMinY -= canvasMinY * 0.05
|
||||
canvasMaxY += canvasMinY * 0.05
|
||||
# If still equal, function is 0, spread out visual space as special case
|
||||
if minY == maxY:
|
||||
minY -= 5
|
||||
maxY += 5
|
||||
self.subplot.set_ylim(bottom=minY, top=maxY)
|
||||
if canvasMinY == canvasMaxY:
|
||||
canvasMinY -= 5
|
||||
canvasMaxY += 5
|
||||
self.subplot.set_ylim(bottom=canvasMinY, top=canvasMaxY)
|
||||
|
||||
# Process X marks line
|
||||
if self.xMark is not None:
|
||||
@@ -189,8 +193,62 @@ class GraphCanvasPanel(wx.Panel):
|
||||
maxX = max(allXs, default=None)
|
||||
if minX is not None and maxX is not None:
|
||||
xMark = max(min(self.xMark, maxX), minX)
|
||||
# Draw line
|
||||
self.subplot.axvline(x=xMark, linestyle='dotted', linewidth=1, color=(0, 0, 0))
|
||||
# Draw its X position
|
||||
if chosenX.unit is None:
|
||||
xLabel = ' {}'.format(roundToPrec(xMark, 4))
|
||||
else:
|
||||
xLabel = ' {} {}'.format(roundToPrec(xMark, 4), chosenX.unit)
|
||||
self.subplot.annotate(
|
||||
xLabel, xy=(xMark, maxY + 0.66 * (canvasMaxY - maxY)), xytext=(-1, -1),
|
||||
textcoords='offset pixels', ha='left', va='center', fontsize='small')
|
||||
# Get Y values
|
||||
yMarks = set()
|
||||
|
||||
def addYMark(val):
|
||||
yMarks.add(roundToPrec(val, 4))
|
||||
|
||||
for source, target in iterList:
|
||||
xs, ys = plotData[(source, target)]
|
||||
if not xs or xMark < min(xs) or xMark > max(xs):
|
||||
continue
|
||||
# Fetch values from graphs when we're asked to provide accurate data
|
||||
if accurateMarks:
|
||||
try:
|
||||
y = view.getPoint(
|
||||
x=xMark,
|
||||
miscInputs=miscInputs,
|
||||
xSpec=chosenX,
|
||||
ySpec=chosenY,
|
||||
src=source,
|
||||
tgt=target)
|
||||
addYMark(y)
|
||||
except Exception:
|
||||
pyfalog.warning('Failed to get X mark for "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
|
||||
# Silently skip this mark, otherwise other marks and legend display will fail
|
||||
continue
|
||||
# Otherwise just do linear interpolation between two points
|
||||
else:
|
||||
if xMark in xs:
|
||||
# We might have multiples of the same value in our sequence, pick value for the last one
|
||||
idx = len(xs) - xs[::-1].index(xMark) - 1
|
||||
addYMark(ys[idx])
|
||||
continue
|
||||
idx = bisect(xs, xMark)
|
||||
xLeft = xs[idx - 1]
|
||||
xRight = xs[idx]
|
||||
yLeft = ys[idx - 1]
|
||||
yRight = ys[idx]
|
||||
pos = (xMark - xLeft) / (xRight - xLeft)
|
||||
yMark = yLeft + pos * (yRight - yLeft)
|
||||
addYMark(yMark)
|
||||
|
||||
# Draw Y values
|
||||
for yMark in yMarks:
|
||||
self.subplot.annotate(
|
||||
' {}'.format(yMark), xy=(xMark, yMark), xytext=(-1, -1),
|
||||
textcoords='offset pixels', ha='left', va='center', fontsize='small')
|
||||
|
||||
legendLines = []
|
||||
for i, iData in enumerate(legendData):
|
||||
@@ -212,11 +270,6 @@ class GraphCanvasPanel(wx.Panel):
|
||||
self.xMark = x
|
||||
self.draw(accurateMarks=False)
|
||||
|
||||
def markXAccurate(self, x):
|
||||
if x is not None:
|
||||
self.xMark = x
|
||||
self.draw()
|
||||
|
||||
def unmarkX(self):
|
||||
self.xMark = None
|
||||
self.draw()
|
||||
@@ -243,4 +296,8 @@ class GraphCanvasPanel(wx.Panel):
|
||||
if self.mplOnReleaseHandler:
|
||||
self.canvas.mpl_disconnect(self.mplOnReleaseHandler)
|
||||
self.mplOnReleaseHandler = None
|
||||
self.markXAccurate(event.xdata)
|
||||
# Do not write markX here because of strange mouse behavior: when dragging,
|
||||
# sometimes when you release button, x coordinate changes. To avoid that,
|
||||
# we just re-use coordinates set on click/drag and just request to redraw
|
||||
# using accurate data
|
||||
self.draw(accurateMarks=True)
|
||||
|
||||
Reference in New Issue
Block a user