-
Notifications
You must be signed in to change notification settings - Fork 0
Document.Build metrics
@elite-dangerous-almanac/core / Build metrics
How this library arrives at the numbers an outfitting screen shows — power, shields, armour, resistances, weapon output, ammunition, heat and jump range. Each metric has its own page in the API reference; this is the argument that runs through all of them, which no single symbol owns.
Every metric exists twice, and the difference is where the numbers come from rather than what the maths does.
The calculation modules — ships/power, ships/shields, ships/armour,
ships/resistances, ships/weapons, ships/ammunition, ships/heat, ships/jump-range — are
data-free. You hand them the constants and they hand back a figure. They bundle to almost
nothing, and they are the right layer for a what-if tool that is not modelling a real
build.
import { powerBudget } from '@elite-dangerous-almanac/core/ships/power';
powerBudget(20.4, [
{ draw: 0.45, priority: 1 }, // life support
{ draw: 5.72, priority: 1 }, // thrusters
{ draw: 2.48, priority: 3, deployedOnly: true }, // a beam laser
]).deployed; // -> 8.65The ShipLoadout methods of the same name gather those constants out of a real build — the fitted modules, the hull, and whatever engineering each module carries — and call the function for you. This is what an outfitting screen wants.
import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout; // a Federal Corvette
build.powerBudget().deployed; // -> 46.8597
build.shieldMetrics()?.strength; // -> 3940.4
build.armourMetrics().hitPoints; // -> 5062.6
build.weaponMetrics().total.damagePerSecond; // -> 137.04The rest of this page is about the second layer, because the first is documented where it lives: each function's own page states its formula, its units and its reference implementation.
Every ShipLoadout metric reads post-engineering stats. A build's modifiers are
folded onto the module's catalogue values before any metric sees them, so there is no
step where a caller applies engineering themselves — and no way to ask for the stock
figure through these methods.
Two consequences are worth knowing before you read a number.
A journal's own modifiers are never recomputed. When a build comes from
fromLoadout or fromSlef, the Engineering.Modifiers block the game wrote is taken as
stated. That is deliberate: the game is the
authority on a build it exported, and a recomputation that disagreed would silently
replace a fact with a model. The library recomputes only what it rolled itself, through
applyBlueprint.
Four stats are percentages of a multiplier, not of the stat. Hull boost, shield boost
and the four damage resistances compound on 1 + v and 1 − v respectively, whichever
apply method the recipe names. A +80% bulkhead engineered by a +32% blueprint reads
137.6%, not 105.6%, because 1.8 × 1.32 − 1 = 1.376; a −20% kinetic resistance with
+5% becomes −14%, because the multipliers 1.2 × 0.95 multiply. This is Frontier's
own convention and it is why those four stats look wrong if you read them as ordinary
percentages. ships/engineering states the rule; every metric below inherits it.
The plant's capacity against what the build draws, split two ways because weapons and most utility fittings only draw while the hardpoints are deployed.
import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout;
const power = build.powerBudget();
power.available; // MW the plant makes
power.retracted; // MW drawn with hardpoints in
power.deployed; // MW drawn with hardpoints out
power.withinBudget; // does it fit?
power.bands; // the five priority groupsbands is what drives a priority-group table. A group is powered when its running
total — its own draw plus every higher-priority group's — fits in available; the game
shuts off the first group that would go over and everything below it, rather than the
individual module that broke the budget.
A module whose draw the library cannot determine is named in unknownDraws rather than counted as zero, so a budget is never quietly optimistic about a module absent from the catalogue. While that list is non-empty every other figure is a lower bound, so show it alongside them.
Strength and hit points are separate calculations, but the four resistances that decide
what they are worth stack by one shared rule, in ships/resistances.
Sources stack multiplicatively on the damage multiplier, not additively on the
resistance: two 20% resisters leave 0.8 × 0.8 = 0.64, which is 36% resisted, not 40%.
The game then bends the result so stacking cannot run away — past a threshold, the
remaining gain is halved. The threshold differs between shields and hull, and both are
stated on stackShieldResistance and
stackArmourResistance.
import { stackShieldResistance } from '@elite-dangerous-almanac/core/ships/resistances';
// A stock generator at 40% kinetic, under four 20% resistance-augmented boosters.
stackShieldResistance(0.4, [0.2, 0.2, 0.2, 0.2]); // -> 0.667…, not 0.4 + 4 × 0.2What a resistance is worth is the effective hit points it buys — the pool divided by what still gets through. Both metrics report that per damage type, and effectiveHitPoints is the same function they use, so a pool of your own converts the same way:
import { effectiveHitPoints } from '@elite-dangerous-almanac/core/ships/resistances';
// 945 hull points behind lightweight alloy, which is weak to kinetic (-20%) and
// explosive (-40%) damage alike.
effectiveHitPoints(945, { kinetic: -0.2, thermal: 0, explosive: -0.4, caustic: 0 }).kinetic;
// -> 787.5, fewer than the hull holdsTwo things about shields catch people out. A generator's strength multiplier is read off a
curve against the bare hull mass, not the loaded ship — so fitting more modules never
weakens your shields. And past the generator's maxMass it will not raise a shield at
all, which the curve reports as 0 rather than as a small number.
shieldMetrics() returns null when the build has no generator; armourMetrics() always
returns a figure, because every hull has armour.
weaponMetrics() reports per-weapon figures and a total. The distinction that matters is
damage per second against sustained damage per second: the first is the rate while
firing, the second folds in the clip and the reload, because a weapon that stops to reload
is not firing.
import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout;
const weapons = build.weaponMetrics();
weapons.total.damagePerSecond; // while firing
weapons.total.sustainedDamagePerSecond; // with reloads
weapons.total.energyPerSecond; // weapons capacitor draw, MW
weapons.total.heatPerSecond;
weapons.weapons.length; // per-hardpoint breakdown
const capacitor = build.weaponsCapacitorMetrics({ weaponsPips: 2 });
capacitor.rechargeRate; // actual WEP recharge at two pips, MJ/s
capacitor.netDrainRate; // sustained draw minus recharge, floored at zero
capacitor.timeToDrain; // seconds from full, or Infinity when recharge keeps paceBeam and mining lasers are continuous: they carry no rate of fire, and their damage, distributor draw and thermal load are already per second, so the per-shot arithmetic collapses to the raw stats.
A distributor's catalogue recharge is the four-WEP-pip maximum. The capacitor result
scales it by (weaponsPips / 4) ^ 1.1, then compares it with sustained energy per
second: a magazine's reload is time for the capacitor to recover, so burst draw would
understate endurance. Fractional allocations from zero through four are accepted.
The build facade applies the deployed power budget. A distributor or weapon shed by its
priority group contributes nothing; unresolved power draws keep the power budget's
optimistic assumption, so inspect build.powerBudget().unknownDraws when present.
One asymmetry is deliberate. Frontier's Rapid Fire and High Capacity recipes shorten the
fire interval rather than raising the rate of fire, so that is the label those
blueprints carry; a weapon's combined rate of fire follows from the interval and the burst
pattern. ShipLoadout recomputes it for you, so you only meet this if you call
computeModifiers directly.
ammunitionCapacity reports what a module can hold when
fully rearmed — the magazine, the reserve behind it, and the two together. A journal's
AmmoInClip and AmmoInHopper report what is loaded at the moment of capture, which is a
different question: a reading is a lower bound on a capacity and never a reading of
one, so the library never infers a catalogue figure from a rearm state.
Three answers are distinct, and a consumer should not collapse them:
- a module with a magazine and a reserve reports both, and their sum;
- a module with a magazine but no reserve figure is reported as unlimited;
- a reserve of zero is a real answer, not an unlimited one — the Mk II Plasma Shock Accelerator has nothing behind its magazine.
Engineered ammunition is reported in whole rounds, because a ship cannot load a tenth of a round and both stats are multiplicative under engineering. A clip rounds up to a whole burst; a reserve rounds to the nearest round. The rounding runs after every blueprint and experimental contribution has compounded, and it applies only to a value the library computed — a clip a journal states passes through untouched, a recipe leg that overwrites the clip is a published figure rather than a product of one, and a roll that leaves the clip where it was leaves it there.
There is one more wrinkle, and it exists because registries state a recipe's multiplier to
three or four decimals. A leg meant to add two thirds is written 0.667, which computes
10.002 rounds on a 6-round rack — and left alone that thousandth becomes a whole extra
round once the clip rounds up, or a whole extra burst on a burst weapon. A clip within half
a unit in the multiplier's third decimal is therefore taken as the whole number it means. A
quality roll between two published legs gets no such treatment: it is a real number with no
whole magazine behind it.
What the build runs at, and whether firing everything cooks it. Heat is the one metric
here that no stated Frontier figure underwrites: the game publishes no formula and shows
no dissipation figure, so the model — and the per-hull heatDissipation it reads — is
community measurement of the game, ported from EDSY and credited in ATTRIBUTIONS.md.
import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout;
const heat = build.heatMetrics();
heat?.idle.gauge; // hardpoints stowed, as the cockpit gauge reads it: 1 is 100%
heat?.fsdCharging.gauge; // spooling a jump, the hottest thing most ships do
heat?.firingSustained.overheats; // holding the trigger with the WEP capacitor keeping up
heat?.firingDrained.secondsToOverheat; // and the alpha strike, on an empty capacitorTwo numbers decide everything, and they are not interchangeable. Dissipation is a
ceiling: a build whose thermal load stays under the hull's heatDissipation settles below
heat level 1 and never overheats, however long it fires; one that goes over never settles
at all. Capacity is only inertia — it sets how long the climb takes, which is why a
build that cooks itself in eight seconds and one that cooks itself in two are the same
kind of broken.
Heat follows what the plant actually feeds. A module switched off makes no heat, and
neither does one in a priority group the plant cannot keep lit — including the thrusters
and the guns. That check is state-dependent, so a build whose thrusters survive with the
hardpoints stowed but get shed once they are out reports thruster heat in thrusters and
none in the firing scenarios.
Each scenario is cumulative, and each reports both a settled level and a countdown:
gauge is the level as a fraction of the in-game readout, overheats says whether it
settles at all, and secondsToOverheat fills in when it does not. A load beyond
dissipation reports Infinity for the level rather than a settling point it never
reaches.
Weapons are the part worth reading twice. firingSustained and firingDrained differ
only in the state of the weapons capacitor, and the gap is large: a shot the capacitor
cannot pay for makes five times its thermal load. A build that never overheats in a
duel can cook itself in a wing fight with the same guns.
jumpRangeSummary() returns the loads a screen actually shows, so you do not have to
assemble them:
import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout;
const jumps = build.jumpRangeSummary();
jumps.max; // best single jump: one jump's fuel, empty hold
jumps.unladen; // full tank, empty hold
jumps.laden; // full tank, full hold
jumps.totalMax.range; // the same best jump as a one-jump total
jumps.totalMax.jumps; // one jump when the build carries fuel
jumps.totalUnladen.range; // every jump on one tank, empty
jumps.totalUnladen.jumps; // number of jumps, including the final partial one
jumps.totalLaden.range; // every jump on one tank, full
const tank = build.totalRange();
tank.range; // summed distance as the tank drains
tank.jumps; // full and final-partial jumps before the tank is empty
build.totalRange({ fuel: 8, cargo: 32 }); // total for a chosen partial load
build.frameShiftDriveMassFactor(); // optMass / loadedMass, dimensionlessThe model is the community-standard hyperspace one, and ships/jump-range holds it as
pure functions if you want a single jump rather than a summary. Guardian FSD boosters and
the drive's own engineering are already folded in by the time ShipLoadout calls them.
An FSD has no thruster-style three-point mass curve: its mass term is the direct
optMass / (mass + fuel) ratio, while a Guardian boost is added after that base equation.
A build can contain a module the catalogue cannot classify. Each metric handles that differently, so do not assume a figure is load-bearing:
-
powerBudget()names it, in theunknownDrawsthe Power section above describes. -
unladenMass,fuelCapacityandcargoCapacityare the three that come in nullable/…Resultpairs: the property isnulland the result names what was missing. The failure model covers that split, and how it differs from the errors a malformed input raises. -
shieldMetrics()andarmourMetrics()take an unresolved module's contribution as zero and report a figure anyway, andweaponMetrics()omits a hardpoint it cannot resolve fromweaponsand from the totals. None of the three carries a diagnostic. -
jumpRangeSummary()and the other jump methods throwTypeErrorrather than answer, because the mass they need is unknown. -
heatMetrics()returnsnulloutright when the build has no powered plant or its hull is unknown. When it does answer, it names unresolved modules in its ownunknownDraws, mirroring the power budget: a module the catalogue cannot resolve draws power the model cannot see and makes heat it cannot count — and, because an unknown draw is left out of the priority-group totals, it also leaves the groups below it reading as powered when the real plant would shed them. The two errors pull opposite ways, so while that list is non-empty the profile is a projection over the modules that did resolve rather than a bound:overheatscan be wrong in either direction, and a settled level with it.
Check build.validation.issues for unknownModule before trusting any of the above on a
build you did not assemble yourself.
- Engineering — what a recipe may go on, and what it does to the stats above.
- Building an outfitting screen — the screen these metrics feed.
- The failure model
- Complete API reference
Guides
astro
Classes (1)
ProceduralSystem
Properties
Accessors
Methods
Interfaces (16)
Type Aliases (3)
Variables (11)
Functions (37)
- absoluteBoxelToBoxelCode
- boxelCodeToAbsoluteBoxel
- boxelCodeToLetters
- boxelEdgeLy
- boxelInternalSize
- canonicalizeSectorName
- canonicalizeSystemName
- decodeModSystemAddress
- decodeSystemAddress
- encodeModSystemAddress
- encodeSystemAddress
- findHandAuthoredRegionAt
- formatSystemName
- getCodexRegion
- getCodexRegionByName
- getHandAuthoredRegionOrigin
- getNebulaByName
- isPermitLockedRegionName
- isPermitLockedSystemName
- isProceduralSystemName
- lettersToBoxelCode
- massCodeToSizeClass
- nearestNebulae
- nebulaeWithin
- parseSystemName
- permitLockedRegionForSystemName
- permitLockedSystemForAddress
- permitLockedSystemForName
- permitLockForSystemName
- resolveNamingRegionOrigin
- sectorGridPositionFromGalacticPosition
- sectorGridPositionFromName
- sectorNameFromGalacticPosition
- sectorNameFromGridPosition
- sizeClassToMassCode
- toSystemAddress
- tryToSystemAddress
commodities
Interfaces (1)
Type Aliases (1)
Variables (3)
Functions (3)
equipment
Interfaces (6)
Type Aliases (8)
Variables (3)
Functions (10)
Subpath modules (2)
i18n
Interfaces (1)
Type Aliases (1)
Functions (17)
- getBlueprintName
- getCalculationIssueMessage
- getEngineeringGroupName
- getExperimentalEffectDescription
- getExperimentalEffectName
- getLoadoutEditErrorMessage
- getLoadoutIssueMessage
- getLoadoutSlotName
- getMaterialName
- getMicroResourceName
- getModuleName
- getOutfittingFamilyName
- getPreEngineeredVariantName
- getShipManufacturer
- getShipName
- getSlefDiagnosticMessage
- getSlotRestrictionLabel
materials
Enumerations (2)
Interfaces (2)
Type Aliases (2)
Variables (9)
ships
Classes (3)
BuildMetrics
Methods
- armourMetrics()
- buildCost()
- buildMass()
- cellBanks()
- distributorMetrics()
- distributorMetricsResult()
- frameShiftDrive()
- frameShiftDriveMassFactor()
- fuelPerJump()
- heatMetrics()
- heatMetricsResult()
- jumpRange()
- jumpRangeSummary()
- ladenJumpRange()
- loadout()
- maxJumpRange()
- mobilityCapacitorMetrics()
- mobilityCapacitorMetricsResult()
- mobilityMetrics()
- mobilityMetricsResult()
- powerBudget()
- shieldCapacitorMetrics()
- shieldCapacitorMetricsResult()
- shieldMetrics()
- shieldMetricsResult()
- shieldRecovery()
- shieldRecoveryResult()
- standardLoad()
- standardLoadResult()
- thrusters()
- totalRange()
- weaponMetrics()
- weaponsCapacitorMetrics()
- of()
LoadoutEditError
Constructors
Properties
Methods
ShipLoadout
Accessors
- cargoCapacity
- fuelCapacity
- hullValue
- importOutcomes
- modulesValue
- rebuy
- shipIdent
- shipName
- shipSymbol
- sourcePurchase
- unladenMass
Methods
- applyBlueprint()
- availableBlueprints()
- availableExperimentalEffects()
- clearEngineering()
- completeEngineeringGrade()
- fittedModuleAt()
- fittedModules()
- modulesForSlot()
- removeModule()
- repairFixedMount()
- setExperimentalEffect()
- setModule()
- setModuleEnabled()
- setModulePriority()
- setPreEngineeredVariant()
- slots()
- toLoadoutEvent()
- toSlef()
- toSlefString()
- validation()
- default()
- empty()
- fromLoadout()
- fromSlef()
Interfaces (125)
- AmmunitionCapacity
- AmmunitionStats
- ApplyBlueprintOptions
- ArmourInput
- ArmourMetrics
- AvailableBlueprint
- Blueprint
- BlueprintFeature
- BlueprintGrade
- BlueprintModuleEngineering
- BuildCost
- BuildCredits
- BuildMass
- BuildSlotBase
- BuildWeaponMetrics
- BulkheadParams
- CalculationIssue
- CellBankInput
- CellBankMetrics
- CellBankSummary
- CoreBuildSlot
- CoreSlots
- DamageComponents
- DamageDistribution
- DamageResistanceParams
- DamageSplit
- DamageTypeValues
- DistributorCapacitorMetrics
- DistributorInput
- DistributorMetrics
- DistributorOptions
- DistributorPips
- EngineeringMaterial
- EngineeringModifier
- EngineeringNormalizationUnchanged
- EngineeringNormalizationUnsupported
- EngineeringNormalized
- EngineeringOptionGroup
- ExperimentalContribution
- ExperimentalEffect
- ExperimentalEffectUnchanged
- ExperimentalEffectUnsupported
- ExperimentalEffectUpdated
- FittedModule
- FittedWeaponMetrics
- FrameShiftDriveJumpStats
- FrameShiftDriveParams
- FuelCapacity
- GunsightPoint
- HardpointBuildSlot
- HardpointSlotSpec
- HeatInput
- HeatMetrics
- HeatState
- HeatWeapon
- HullReinforcementParams
- JumpOptions
- JumpRangeSummary
- LoadoutCalculationModule
- LoadoutEvent
- LoadoutExportOptions
- LoadoutIssue
- LoadoutMass
- LoadoutModule
- LoadoutValidation
- LoadoutValidationInput
- MassCurveStats
- MobilityCapacitorInput
- MobilityCapacitorMetrics
- MobilityCapacitorOptions
- MobilityInput
- MobilityMetrics
- ModuleLimitEntry
- ModuleLimitIncrease
- ModuleLimitUsage
- ModuleReinforcementParams
- OptionalBuildSlot
- OptionalSlotSpec
- OutfittingModule
- OutfittingModuleIdentity
- OutfittingModuleStats
- ParsedSlot
- PowerBand
- PowerBudget
- PowerConsumer
- PowerConsumerResult
- PowerDistributorStats
- PowerGenerationStats
- PreEngineeredModifier
- PreEngineeredVariant
- ProjectileRangeBoundaries
- ShieldBoosterParams
- ShieldCapacitorInput
- ShieldCapacitorMetrics
- ShieldCapacitorOptions
- ShieldGeneratorParams
- ShieldInput
- ShieldMetrics
- ShieldRecovery
- ShieldRecoveryInput
- ShieldRecoveryOptions
- ShieldRegenerationStats
- Ship
- ShipSlots
- SimpleBuildSlot
- SlefDiagnostic
- SlefEntry
- SlefExportOptions
- SlefHeader
- SlefInspection
- SlefStringifyOptions
- SourceModuleValue
- SourcePurchaseRecord
- StandardLoadInputs
- ThrusterCurveParams
- ThrusterParams
- TotalRangeDetails
- ValidationModule
- WeaponDamageStats
- WeaponMetrics
- WeaponsCapacitorInput
- WeaponsCapacitorMetrics
- WeaponsOptions
- WeaponStats
- WeaponTotals
Type Aliases (44)
- BlueprintGrades
- BuildSlot
- CalculationIssueReason
- CalculationResult
- CoreSlotType
- DamageResistances
- DamageType
- EngineeringGroupId
- EngineeringNormalizationCode
- EngineeringNormalizationResult
- ExperimentalEffectMutationCode
- ExperimentalEffectMutationResult
- FixedMountRepairResult
- GunsightOffset
- HardpointRestriction
- ImmovableReason
- LoadoutEditErrorCode
- LoadoutImportOutcome
- LoadoutIssueCode
- LoadoutIssueParam
- LoadoutIssueParams
- LoadoutSlot
- ModifierMethod
- ModuleCategory
- ModuleEngineering
- ModuleExclusionGroup
- ModuleFitConstraint
- ModuleGuidance
- ModuleLimitGroup
- ModuleMount
- ModuleRating
- ModuleSlot
- OptionalRestriction
- OutfittingFamilyId
- PreEngineeredAcquisition
- ShipGunsight
- ShipGunsightCatalogue
- Slef
- SlefConstraint
- SlefDiagnosticCode
- SlotKind
- SlotRestriction
- StandardLoad
- ThrusterLoad
Variables (10)
Functions (85)
- ammunitionCapacity
- armourMetrics
- armourPiercingFactor
- calculateCargoCapacity
- calculateFuelCapacity
- calculateModuleLimits
- calculateUnladenMass
- cellBankSummary
- combinedRateOfFire
- computeModifiers
- damageFalloff
- damagePerSecond
- distributorMetrics
- effectiveHitPoints
- effectiveWeaponThermalLoad
- energyPerSecond
- enumerateSlots
- equilibriumHeatLevel
- frameShiftDriveMassFactor
- fuelPerJump
- getBlueprint
- getBlueprintGrade
- getBlueprintsForModule
- getBulkheadsForShip
- getEngineeringGroup
- getExperimentalEffect
- getExperimentalsForBlueprint
- getExperimentalsForModule
- getLoadoutModifier
- getModuleBySymbol
- getModulesByName
- getPreEngineeredJournalModifiers
- getPreEngineeredModifiers
- getPreEngineeredStats
- getPreEngineeredVariants
- getShipByName
- getShipBySymbol
- getShipGunsight
- getShipSlots
- getSourceModuleValue
- hasFrameShiftDriveJumpStats
- hasMassCurveStats
- hasPowerDistributorStats
- hasPowerGenerationStats
- hasShieldRegenerationStats
- hasWeaponDamageStats
- heatLevelAtTime
- heatMetrics
- heatPerSecond
- identifyPreEngineeredVariant
- inspectSlef
- isPreEngineered
- mapDamageTypes
- mobilityCapacitorMetrics
- mobilityMetrics
- parseSlef
- parseSlotName
- powerBudget
- projectGunsight
- resolveBlueprintForModule
- secondsToHeatLevel
- shieldCapacitorMetrics
- shieldMassCurveMultiplier
- shieldMetrics
- shieldRecovery
- shieldStrength
- singleJumpRange
- sourcePurchaseFromLoadout
- splitDamage
- stackArmourResistance
- stackShieldResistance
- stringifySlef
- sumMaterials
- sumSourceModuleValues
- sumWeaponMetrics
- sustainedDamagePerSecond
- sustainedFireFactor
- systemsResistance
- thrusterMassCurveMultiplier
- toSlef
- totalRange
- unresolvedModifiers
- validateLoadout
- weaponMetrics
- weaponsCapacitorMetrics