e9b904332e
- pkg/calc: DriveForSpeed treats restMass==0 as a valid ceiling-only case (every positive drive solves it), so locking the displayed speed of a D=1, W=A=S=C=0 ship is no longer a phantom "infeasible". - ship-design-area: drive/weapons/shields/cargo inputs use a JS-driven smart step on ArrowUp/ArrowDown (0↔1 jump, otherwise ±0.1) and hide the native spinner so it cannot produce invalid (0, 1) values; armament keeps its native step 1. - Tech and planet MAT cells follow the same lock idiom as goal-seek locks: open padlock (🔓) over the inherited value → click to open an input with a closed padlock (🔒). The padlock slot is always reserved, so the column width is stable. - Tech overrides (design area and modernization target) are floored at the player's current tech on this turn — a lower value is flagged as invalid.
100 lines
3.8 KiB
Go
100 lines
3.8 KiB
Go
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].
|
|
// With a positive restMass the speed approaches but never reaches the
|
|
// stripped-hull ceiling 20*driveTech, so a target at or above the
|
|
// ceiling is infeasible. With restMass==0 the drive block carries no
|
|
// other mass: every positive drive yields exactly the ceiling speed, so
|
|
// the ceiling target is the only feasible one and any positive drive
|
|
// (canonically 1) solves it. Non-positive targetSpeed or driveTech are
|
|
// always infeasible.
|
|
func DriveForSpeed(targetSpeed, driveTech, restMass float64) (float64, bool) {
|
|
if driveTech <= 0 || targetSpeed <= 0 {
|
|
return 0, false
|
|
}
|
|
ceiling := 20 * driveTech
|
|
if restMass <= 0 {
|
|
if targetSpeed != ceiling {
|
|
return 0, false
|
|
}
|
|
return 1, true
|
|
}
|
|
if 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
|
|
}
|