fix(ui): calculator polish — smart input steps, unified tech/MAT lock idiom, tech floor, speed-lock ceiling fix
- 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.
This commit is contained in:
+17
-4
@@ -22,12 +22,25 @@ func WeaponsForAttack(targetAttack, weaponsTech float64) (float64, bool) {
|
||||
// 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.
|
||||
// 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 driveTech <= 0 || targetSpeed <= 0 || targetSpeed >= ceiling {
|
||||
if restMass <= 0 {
|
||||
if targetSpeed != ceiling {
|
||||
return 0, false
|
||||
}
|
||||
return 1, true
|
||||
}
|
||||
if targetSpeed >= ceiling {
|
||||
return 0, false
|
||||
}
|
||||
return targetSpeed * restMass / (ceiling - targetSpeed), true
|
||||
|
||||
+19
-1
@@ -24,12 +24,30 @@ func TestDriveForSpeed(t *testing.T) {
|
||||
if !ok || math.Abs(got-drive) > 1e-9 {
|
||||
t.Errorf("DriveForSpeed round-trip = %v (ok=%v), want %v", got, ok, drive)
|
||||
}
|
||||
// Speed can never reach the stripped-hull ceiling 20*driveTech.
|
||||
// With a positive restMass speed can never reach 20*driveTech.
|
||||
if _, ok := calc.DriveForSpeed(20*driveTech, driveTech, restMass); ok {
|
||||
t.Error("DriveForSpeed at the speed ceiling should be infeasible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveForSpeedZeroRest(t *testing.T) {
|
||||
// With restMass==0 the only achievable speed is the stripped-hull
|
||||
// ceiling 20*driveTech; any positive drive reaches it. Off-ceiling
|
||||
// targets are infeasible.
|
||||
const driveTech = 1.5
|
||||
ceiling := 20 * driveTech
|
||||
got, ok := calc.DriveForSpeed(ceiling, driveTech, 0)
|
||||
if !ok || got <= 0 {
|
||||
t.Errorf("DriveForSpeed(ceiling, _, 0) = %v (ok=%v), want positive", got, ok)
|
||||
}
|
||||
if _, ok := calc.DriveForSpeed(ceiling/2, driveTech, 0); ok {
|
||||
t.Error("DriveForSpeed(below ceiling, _, 0) should be infeasible")
|
||||
}
|
||||
if _, ok := calc.DriveForSpeed(ceiling+1, driveTech, 0); ok {
|
||||
t.Error("DriveForSpeed(above ceiling, _, 0) should be infeasible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShieldsForDefence(t *testing.T) {
|
||||
const shields, shieldsTech, restMass = 5.75, 1.0, 40.0
|
||||
defence := calc.EffectiveDefence(shields, shieldsTech, shields+restMass)
|
||||
|
||||
Reference in New Issue
Block a user