feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
Fuse the standalone ship-class designer (Phases 17/18) into a sidebar calculator: live mass/speed/attack/defence/bombing results, a planet build-rate readout, single-target goal-seek, a modernization-cost mode, and auto reach circles on the map for the selected planet. pkg/calc becomes the single source for the new math (no mirroring): extract BombingPower from the engine model and the per-turn ship-production loop from controller.ProduceShip into pkg/calc (engine now delegates), and add inverse goal-seek solvers in pkg/calc/solve.go. Thin-bridge the combat, planet-build, and solver functions through ui/core/calc + ui/wasm and rebuild core.wasm. Remove the standalone designer view/route; the ship-classes table and the view/bottom menus open the calculator via a shared request store. Docs: rewrite ui/PLAN.md Phase 30, adjust Phase 34 (realistic forecast + CAP/COL ownership), add ui/docs/calculator-ux.md, extend calc-bridge.md, fix navigation.md; remove ui/CALCULATOR.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+73
-10
@@ -28,28 +28,28 @@ type shipFixture struct {
|
||||
func fixtures() []shipFixture {
|
||||
return []shipFixture{
|
||||
{
|
||||
name: "all_zero",
|
||||
drive: 0, armament: 0, weapons: 0, shields: 0, cargo: 0,
|
||||
name: "all_zero",
|
||||
drive: 0, armament: 0, weapons: 0, shields: 0, cargo: 0,
|
||||
driveTech: 1, cargoTech: 1,
|
||||
},
|
||||
{
|
||||
name: "typical_mid_tech",
|
||||
drive: 8, armament: 2, weapons: 5, shields: 3, cargo: 4,
|
||||
name: "typical_mid_tech",
|
||||
drive: 8, armament: 2, weapons: 5, shields: 3, cargo: 4,
|
||||
driveTech: 1.5, cargoTech: 1.2,
|
||||
},
|
||||
{
|
||||
name: "heavy_armoured",
|
||||
drive: 3, armament: 5, weapons: 12, shields: 20, cargo: 1,
|
||||
name: "heavy_armoured",
|
||||
drive: 3, armament: 5, weapons: 12, shields: 20, cargo: 1,
|
||||
driveTech: 0.8, cargoTech: 0.5,
|
||||
},
|
||||
{
|
||||
name: "invalid_weapons_no_armament",
|
||||
drive: 5, armament: 0, weapons: 4, shields: 1, cargo: 2,
|
||||
name: "invalid_weapons_no_armament",
|
||||
drive: 5, armament: 0, weapons: 4, shields: 1, cargo: 2,
|
||||
driveTech: 1, cargoTech: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid_armament_no_weapons",
|
||||
drive: 5, armament: 3, weapons: 0, shields: 1, cargo: 2,
|
||||
name: "invalid_armament_no_weapons",
|
||||
drive: 5, armament: 3, weapons: 0, shields: 1, cargo: 2,
|
||||
driveTech: 1, cargoTech: 1,
|
||||
},
|
||||
}
|
||||
@@ -236,3 +236,66 @@ func TestDesignerPreviewComposition(t *testing.T) {
|
||||
assert.Equal(t, wantMaxSpeed, maxSpeed)
|
||||
assert.Equal(t, wantRange, rangePerTurn)
|
||||
}
|
||||
|
||||
func TestEffectiveAttackParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
weapons float64
|
||||
weaponsTech float64
|
||||
}{
|
||||
{"zero", 0, 1},
|
||||
{"typical", 15, 1.5},
|
||||
{"high_tech", 8, 3.2},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.EffectiveAttack(c.weapons, c.weaponsTech)
|
||||
got := bridge.EffectiveAttack(c.weapons, c.weaponsTech)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveDefenceParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
shields float64
|
||||
shieldsTech float64
|
||||
fullMass float64
|
||||
}{
|
||||
{"zero_mass_returns_zero", 10, 1, 0},
|
||||
{"typical", 20, 1.2, 45},
|
||||
{"heavy_hull", 100, 1, 600},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.EffectiveDefence(c.shields, c.shieldsTech, c.fullMass)
|
||||
got := bridge.EffectiveDefence(c.shields, c.shieldsTech, c.fullMass)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBombingPowerParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
weapons, weaponsTech, armament, number float64
|
||||
}{
|
||||
{"no_armament", 30, 1, 0, 1},
|
||||
{"battle_station", 30, 1, 3, 1},
|
||||
{"fleet", 30, 1.5, 3, 4},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.BombingPower(c.weapons, c.weaponsTech, c.armament, c.number)
|
||||
got := bridge.BombingPower(c.weapons, c.weaponsTech, c.armament, c.number)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user