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:
+115
-3
@@ -14,14 +14,26 @@
|
||||
// - verifyEvent(publicKey, signature, fields) -> boolean
|
||||
// - verifyPayloadHash(payloadBytes, payloadHash) -> boolean
|
||||
//
|
||||
// Phase 18 adds the ship-math bridge over `pkg/calc/ship.go`. Each
|
||||
// function is a thin wrapper around the same-named upstream calc
|
||||
// function (zero math here, the bridge only marshals JS objects):
|
||||
//
|
||||
// - driveEffective(fields) -> number
|
||||
// - emptyMass(fields) -> number | null (null when invalid)
|
||||
// - weaponsBlockMass(fields) -> number | null (null when invalid)
|
||||
// - fullMass(fields) -> number
|
||||
// - speed(fields) -> number
|
||||
// - cargoCapacity(fields) -> number
|
||||
// - carryingMass(fields) -> number
|
||||
//
|
||||
// Field objects are plain JS objects with camelCase keys matching the
|
||||
// TypeScript `Core` interface, and bytes fields are Uint8Array.
|
||||
// Timestamps are JS Number (Unix milliseconds fit in 53 bits well past
|
||||
// year 2200).
|
||||
//
|
||||
// All functions return either a Uint8Array, a boolean, or fail closed.
|
||||
// They never throw — callers may inspect the boolean result or rely on
|
||||
// the canon-byte length to detect malformed input.
|
||||
// All functions return either a Uint8Array, a number, a boolean, null,
|
||||
// or fail closed. They never throw — callers may inspect the result
|
||||
// or rely on the canon-byte length to detect malformed input.
|
||||
|
||||
//go:build js && wasm
|
||||
|
||||
@@ -30,6 +42,7 @@ package main
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
"galaxy/core/calc"
|
||||
"galaxy/core/canon"
|
||||
)
|
||||
|
||||
@@ -39,6 +52,13 @@ func main() {
|
||||
"verifyResponse": js.FuncOf(verifyResponse),
|
||||
"verifyEvent": js.FuncOf(verifyEvent),
|
||||
"verifyPayloadHash": js.FuncOf(verifyPayloadHash),
|
||||
"driveEffective": js.FuncOf(driveEffective),
|
||||
"emptyMass": js.FuncOf(emptyMass),
|
||||
"weaponsBlockMass": js.FuncOf(weaponsBlockMass),
|
||||
"fullMass": js.FuncOf(fullMass),
|
||||
"speed": js.FuncOf(speed),
|
||||
"cargoCapacity": js.FuncOf(cargoCapacity),
|
||||
"carryingMass": js.FuncOf(carryingMass),
|
||||
}))
|
||||
|
||||
// Block forever so the Go runtime stays alive while JS keeps calling
|
||||
@@ -112,6 +132,98 @@ func verifyPayloadHash(_ js.Value, args []js.Value) any {
|
||||
return js.ValueOf(true)
|
||||
}
|
||||
|
||||
// driveEffective bridges `calc.DriveEffective`. Input
|
||||
// `{ drive, driveTech }`, output a JS number.
|
||||
func driveEffective(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
drive := args[0].Get("drive").Float()
|
||||
driveTech := args[0].Get("driveTech").Float()
|
||||
return js.ValueOf(calc.DriveEffective(drive, driveTech))
|
||||
}
|
||||
|
||||
// emptyMass bridges `calc.EmptyMass`. Input
|
||||
// `{ drive, weapons, armament, shields, cargo }`, output a JS number
|
||||
// or null when the upstream validator rejects the weapons/armament
|
||||
// pairing.
|
||||
func emptyMass(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
drive := args[0].Get("drive").Float()
|
||||
weapons := args[0].Get("weapons").Float()
|
||||
armament := uint(args[0].Get("armament").Int())
|
||||
shields := args[0].Get("shields").Float()
|
||||
cargo := args[0].Get("cargo").Float()
|
||||
mass, ok := calc.EmptyMass(drive, weapons, armament, shields, cargo)
|
||||
if !ok {
|
||||
return js.Null()
|
||||
}
|
||||
return js.ValueOf(mass)
|
||||
}
|
||||
|
||||
// weaponsBlockMass bridges `calc.WeaponsBlockMass`. Input
|
||||
// `{ weapons, armament }`, output a JS number or null on the same
|
||||
// invalid pairing as emptyMass.
|
||||
func weaponsBlockMass(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
weapons := args[0].Get("weapons").Float()
|
||||
armament := uint(args[0].Get("armament").Int())
|
||||
mass, ok := calc.WeaponsBlockMass(weapons, armament)
|
||||
if !ok {
|
||||
return js.Null()
|
||||
}
|
||||
return js.ValueOf(mass)
|
||||
}
|
||||
|
||||
// fullMass bridges `calc.FullMass`. Input
|
||||
// `{ emptyMass, carryingMass }`, output a JS number.
|
||||
func fullMass(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
em := args[0].Get("emptyMass").Float()
|
||||
cm := args[0].Get("carryingMass").Float()
|
||||
return js.ValueOf(calc.FullMass(em, cm))
|
||||
}
|
||||
|
||||
// speed bridges `calc.Speed`. Input `{ driveEffective, fullMass }`,
|
||||
// output a JS number (zero when fullMass is non-positive).
|
||||
func speed(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
de := args[0].Get("driveEffective").Float()
|
||||
fm := args[0].Get("fullMass").Float()
|
||||
return js.ValueOf(calc.Speed(de, fm))
|
||||
}
|
||||
|
||||
// cargoCapacity bridges `calc.CargoCapacity`. Input
|
||||
// `{ cargo, cargoTech }`, output a JS number (cargo units of hold).
|
||||
func cargoCapacity(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
cargo := args[0].Get("cargo").Float()
|
||||
cargoTech := args[0].Get("cargoTech").Float()
|
||||
return js.ValueOf(calc.CargoCapacity(cargo, cargoTech))
|
||||
}
|
||||
|
||||
// carryingMass bridges `calc.CarryingMass`. Input
|
||||
// `{ load, cargoTech }`, output a JS number (mass of `load` cargo
|
||||
// units at the player's cargo tech).
|
||||
func carryingMass(_ js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
return js.Null()
|
||||
}
|
||||
load := args[0].Get("load").Float()
|
||||
cargoTech := args[0].Get("cargoTech").Float()
|
||||
return js.ValueOf(calc.CarryingMass(load, cargoTech))
|
||||
}
|
||||
|
||||
// copyBytesFromJS materialises a JS Uint8Array (or any indexable
|
||||
// byte-shaped value) into a Go byte slice. We avoid `js.CopyBytesToGo`
|
||||
// because TinyGo's implementation panics on values it does not
|
||||
|
||||
Reference in New Issue
Block a user