package calc // This file holds the inverse ("goal-seek") counterparts of the forward // ship formulas. The ship-class calculator lets a player pin one derived // result and back-solve the single input it claims; each solver inverts // exactly one forward function so the math stays in this package rather // than leaking into the UI bridge. Every solver reports ok == false when // the request is infeasible (e.g. an unreachable target or a division by // a non-positive tech level), leaving the returned value undefined. // WeaponsForAttack returns the weapons block that yields targetAttack at // weapons tech level weaponsTech, inverting [EffectiveAttack]. It is // infeasible when weaponsTech is non-positive or targetAttack is // negative. func WeaponsForAttack(targetAttack, weaponsTech float64) (float64, bool) { if weaponsTech <= 0 || targetAttack < 0 { return 0, false } return targetAttack / weaponsTech, true } // DriveForSpeed returns the drive block that yields targetSpeed for a // ship whose mass excluding the drive block is restMass, at drive tech // level driveTech, inverting [Speed] composed with [DriveEffective]. // Speed approaches but never reaches the stripped-hull ceiling // 20*driveTech, so a target at or above the ceiling (or a non-positive // target or tech level) is infeasible. func DriveForSpeed(targetSpeed, driveTech, restMass float64) (float64, bool) { ceiling := 20 * driveTech if driveTech <= 0 || targetSpeed <= 0 || targetSpeed >= ceiling { return 0, false } return targetSpeed * restMass / (ceiling - targetSpeed), true } // ShieldsForDefence returns the shields block that yields targetDefence // for a ship whose mass excluding the shields block is restMass, at // shields tech level shieldsTech, inverting [EffectiveDefence]. Defence // rises monotonically with shields (the block adds mass to its own // denominator), so the block is found by bisection. It is infeasible when // targetDefence or shieldsTech is non-positive. func ShieldsForDefence(targetDefence, shieldsTech, restMass float64) (float64, bool) { if targetDefence <= 0 || shieldsTech <= 0 { return 0, false } lo, hi := 0.0, 1.0 for EffectiveDefence(hi, shieldsTech, hi+restMass) < targetDefence { hi *= 2 if hi > 1e12 { return 0, false } } for range 100 { mid := (lo + hi) / 2 if EffectiveDefence(mid, shieldsTech, mid+restMass) < targetDefence { lo = mid } else { hi = mid } } return (lo + hi) / 2, true } // CargoForEmptyMass returns the cargo block that brings a ship's empty // mass to targetEmptyMass, given restMass — the combined mass of the // other blocks (drive, shields, and the weapons block) — inverting the // cargo term of [EmptyMass]. It is infeasible when targetEmptyMass is // below restMass, which would require a negative cargo block. func CargoForEmptyMass(targetEmptyMass, restMass float64) (float64, bool) { cargo := targetEmptyMass - restMass if cargo < 0 { return 0, false } return cargo, true } // LoadForFullMass returns the cargo load that brings a ship's full mass // to targetFullMass, given its empty mass and cargo tech level, inverting // [CarryingMass] inside [FullMass]. It is infeasible when targetFullMass // is below emptyMass or cargoTech is non-positive. func LoadForFullMass(targetFullMass, emptyMass, cargoTech float64) (float64, bool) { if cargoTech <= 0 || targetFullMass < emptyMass { return 0, false } return (targetFullMass - emptyMass) * cargoTech, true }