ui/phase-18: ship-class calc bridge with live designer preview
Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.
CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// Package calc is the WASM-side bridge over `galaxy/calc`'s ship math.
|
||||
// Each function is a one-line passthrough: signatures match the
|
||||
// underlying `pkg/calc/ship.go` exactly so the bridge contains zero
|
||||
// math beyond the call. Wrapping `pkg/calc` here keeps the JS/Go
|
||||
// surface in one file and lets the canonical math live in a single
|
||||
// upstream package shared with the engine.
|
||||
package calc
|
||||
|
||||
import "galaxy/calc"
|
||||
|
||||
// DriveEffective wraps `calc.DriveEffective` (`pkg/calc/ship.go`):
|
||||
// effective drive power equals the ship's drive block multiplied by
|
||||
// the player's drive tech level.
|
||||
func DriveEffective(drive, driveTech float64) float64 {
|
||||
return calc.DriveEffective(drive, driveTech)
|
||||
}
|
||||
|
||||
// EmptyMass wraps `calc.EmptyMass` (`pkg/calc/ship.go`): mass of the
|
||||
// ship without cargo. Returns ok == false when the weapons/armament
|
||||
// pair is invalid (one zero, the other non-zero).
|
||||
func EmptyMass(drive, weapons float64, armament uint, shields, cargo float64) (float64, bool) {
|
||||
return calc.EmptyMass(drive, weapons, armament, shields, cargo)
|
||||
}
|
||||
|
||||
// WeaponsBlockMass wraps `calc.WeaponsBlockMass` (`pkg/calc/ship.go`):
|
||||
// mass of the weapons sub-block. Returns ok == false on the same
|
||||
// invalid pairing as EmptyMass.
|
||||
func WeaponsBlockMass(weapons float64, armament uint) (float64, bool) {
|
||||
return calc.WeaponsBlockMass(weapons, armament)
|
||||
}
|
||||
|
||||
// FullMass wraps `calc.FullMass` (`pkg/calc/ship.go`): empty mass plus
|
||||
// the mass of the carried cargo.
|
||||
func FullMass(emptyMass, carryingMass float64) float64 {
|
||||
return calc.FullMass(emptyMass, carryingMass)
|
||||
}
|
||||
|
||||
// Speed wraps `calc.Speed` (`pkg/calc/ship.go`): light-years per turn,
|
||||
// equal to effective drive times 20 divided by the ship's full mass.
|
||||
// Zero when fullMass is non-positive.
|
||||
func Speed(driveEffective, fullMass float64) float64 {
|
||||
return calc.Speed(driveEffective, fullMass)
|
||||
}
|
||||
|
||||
// CargoCapacity wraps `calc.CargoCapacity` (`pkg/calc/ship.go`):
|
||||
// hold capacity of one ship in cargo units, scaled by the player's
|
||||
// cargo tech.
|
||||
func CargoCapacity(cargo, cargoTech float64) float64 {
|
||||
return calc.CargoCapacity(cargo, cargoTech)
|
||||
}
|
||||
|
||||
// CarryingMass wraps `calc.CarryingMass` (`pkg/calc/ship.go`): mass of
|
||||
// a payload of `load` cargo units at the player's cargo tech. Used by
|
||||
// the designer preview to derive full-load mass from CargoCapacity.
|
||||
func CarryingMass(load, cargoTech float64) float64 {
|
||||
return calc.CarryingMass(load, cargoTech)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package calc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
source "galaxy/calc"
|
||||
bridge "galaxy/core/calc"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// shipFixture is the input set passed to every parity check. Values
|
||||
// are picked to exercise both the typical mid-tech ship and the
|
||||
// invalid weapons/armament pairing path of EmptyMass /
|
||||
// WeaponsBlockMass.
|
||||
type shipFixture struct {
|
||||
name string
|
||||
drive float64
|
||||
armament uint
|
||||
weapons float64
|
||||
shields float64
|
||||
cargo float64
|
||||
driveTech float64
|
||||
cargoTech float64
|
||||
}
|
||||
|
||||
func fixtures() []shipFixture {
|
||||
return []shipFixture{
|
||||
{
|
||||
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,
|
||||
driveTech: 1.5, cargoTech: 1.2,
|
||||
},
|
||||
{
|
||||
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,
|
||||
driveTech: 1, cargoTech: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid_armament_no_weapons",
|
||||
drive: 5, armament: 3, weapons: 0, shields: 1, cargo: 2,
|
||||
driveTech: 1, cargoTech: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveEffectiveParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, f := range fixtures() {
|
||||
t.Run(f.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.DriveEffective(f.drive, f.driveTech)
|
||||
got := bridge.DriveEffective(f.drive, f.driveTech)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyMassParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, f := range fixtures() {
|
||||
t.Run(f.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
wantMass, wantOk := source.EmptyMass(f.drive, f.weapons, f.armament, f.shields, f.cargo)
|
||||
gotMass, gotOk := bridge.EmptyMass(f.drive, f.weapons, f.armament, f.shields, f.cargo)
|
||||
assert.Equal(t, wantOk, gotOk)
|
||||
assert.Equal(t, wantMass, gotMass)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeaponsBlockMassParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, f := range fixtures() {
|
||||
t.Run(f.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
wantMass, wantOk := source.WeaponsBlockMass(f.weapons, f.armament)
|
||||
gotMass, gotOk := bridge.WeaponsBlockMass(f.weapons, f.armament)
|
||||
assert.Equal(t, wantOk, gotOk)
|
||||
assert.Equal(t, wantMass, gotMass)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullMassParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
emptyMass float64
|
||||
carrying float64
|
||||
}{
|
||||
{"zero", 0, 0},
|
||||
{"empty_only", 25, 0},
|
||||
{"loaded", 25, 12.5},
|
||||
{"negative_carrying_clamped_by_caller", 25, -3},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.FullMass(c.emptyMass, c.carrying)
|
||||
got := bridge.FullMass(c.emptyMass, c.carrying)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpeedParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
driveEffective float64
|
||||
fullMass float64
|
||||
}{
|
||||
{"zero_mass_returns_zero", 12, 0},
|
||||
{"negative_mass_returns_zero", 12, -1},
|
||||
{"typical", 12, 30},
|
||||
{"fast_light_ship", 50, 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.Speed(c.driveEffective, c.fullMass)
|
||||
got := bridge.Speed(c.driveEffective, c.fullMass)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCargoCapacityParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, f := range fixtures() {
|
||||
t.Run(f.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.CargoCapacity(f.cargo, f.cargoTech)
|
||||
got := bridge.CargoCapacity(f.cargo, f.cargoTech)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarryingMassParity(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
load float64
|
||||
cargoTech float64
|
||||
}{
|
||||
{"zero_load", 0, 1},
|
||||
{"negative_load_returns_zero", -5, 1},
|
||||
{"typical_high_tech", 24, 2},
|
||||
{"low_tech_amplifies", 24, 0.5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := source.CarryingMass(c.load, c.cargoTech)
|
||||
got := bridge.CarryingMass(c.load, c.cargoTech)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDesignerPreviewComposition exercises the exact composition the
|
||||
// ship-class designer performs: empty mass, full-load mass via
|
||||
// CarryingMass(CargoCapacity), max speed at empty, and range at full
|
||||
// load. Catches regressions if a future bridge tweak silently changes
|
||||
// the composition shape.
|
||||
func TestDesignerPreviewComposition(t *testing.T) {
|
||||
t.Parallel()
|
||||
const (
|
||||
drive = 8.0
|
||||
armament = uint(2)
|
||||
weapons = 5.0
|
||||
shields = 3.0
|
||||
cargo = 4.0
|
||||
driveTech = 1.5
|
||||
cargoTech = 1.2
|
||||
)
|
||||
emptyMass, ok := bridge.EmptyMass(drive, weapons, armament, shields, cargo)
|
||||
require.True(t, ok)
|
||||
cargoCap := bridge.CargoCapacity(cargo, cargoTech)
|
||||
carryAtFull := bridge.CarryingMass(cargoCap, cargoTech)
|
||||
fullLoadMass := bridge.FullMass(emptyMass, carryAtFull)
|
||||
driveEff := bridge.DriveEffective(drive, driveTech)
|
||||
maxSpeed := bridge.Speed(driveEff, emptyMass)
|
||||
rangePerTurn := bridge.Speed(driveEff, fullLoadMass)
|
||||
|
||||
wantEmpty, _ := source.EmptyMass(drive, weapons, armament, shields, cargo)
|
||||
wantCap := source.CargoCapacity(cargo, cargoTech)
|
||||
wantCarry := source.CarryingMass(wantCap, cargoTech)
|
||||
wantFull := source.FullMass(wantEmpty, wantCarry)
|
||||
wantDE := source.DriveEffective(drive, driveTech)
|
||||
wantMaxSpeed := source.Speed(wantDE, wantEmpty)
|
||||
wantRange := source.Speed(wantDE, wantFull)
|
||||
|
||||
assert.Equal(t, wantEmpty, emptyMass)
|
||||
assert.Equal(t, wantCap, cargoCap)
|
||||
assert.Equal(t, wantCarry, carryAtFull)
|
||||
assert.Equal(t, wantFull, fullLoadMass)
|
||||
assert.Equal(t, wantMaxSpeed, maxSpeed)
|
||||
assert.Equal(t, wantRange, rangePerTurn)
|
||||
}
|
||||
Reference in New Issue
Block a user