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.
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
// makeFakeCore builds a complete `Core` whose calc methods mirror
|
|
// `pkg/calc` exactly, for component and unit tests that must not boot the
|
|
// real WASM module. The committed `core.wasm` is rebuilt out-of-band
|
|
// (`make wasm`, needs TinyGo), so tests that exercise calculator math
|
|
// inject this fake instead of depending on a freshly built binary. The
|
|
// Go parity tests in `ui/core/calc` guarantee the real bridge agrees with
|
|
// `pkg/calc`, so a fake that also mirrors `pkg/calc` stays faithful.
|
|
//
|
|
// Pass `overrides` to replace individual methods — e.g. `vi.fn()` spies
|
|
// when a test wants to assert how the calc-model orchestrates the bridge.
|
|
|
|
import type { Core } from "../src/platform/core/index";
|
|
|
|
function weaponsBlockMass(weapons: number, armament: number): number | null {
|
|
if ((armament === 0 && weapons !== 0) || (armament !== 0 && weapons === 0)) {
|
|
return null;
|
|
}
|
|
return (armament + 1) * (weapons / 2);
|
|
}
|
|
|
|
export function makeFakeCore(overrides: Partial<Core> = {}): Core {
|
|
const base: Core = {
|
|
signRequest: () => new Uint8Array(),
|
|
verifyResponse: () => true,
|
|
verifyEvent: () => true,
|
|
verifyPayloadHash: () => true,
|
|
driveEffective: ({ drive, driveTech }) => drive * driveTech,
|
|
emptyMass: ({ drive, weapons, armament, shields, cargo }) => {
|
|
const wb = weaponsBlockMass(weapons, armament);
|
|
if (wb === null) return null;
|
|
return drive + shields + cargo + wb;
|
|
},
|
|
weaponsBlockMass: ({ weapons, armament }) =>
|
|
weaponsBlockMass(weapons, armament),
|
|
fullMass: ({ emptyMass, carryingMass }) => emptyMass + carryingMass,
|
|
speed: ({ driveEffective, fullMass }) =>
|
|
fullMass <= 0 ? 0 : (driveEffective * 20) / fullMass,
|
|
cargoCapacity: ({ cargo, cargoTech }) =>
|
|
cargoTech * (cargo + (cargo * cargo) / 20),
|
|
carryingMass: ({ load, cargoTech }) => (load <= 0 ? 0 : load / cargoTech),
|
|
blockUpgradeCost: ({ blockMass, currentTech, targetTech }) =>
|
|
blockMass === 0 || targetTech <= currentTech
|
|
? 0
|
|
: (1 - currentTech / targetTech) * 10 * blockMass,
|
|
effectiveAttack: ({ weapons, weaponsTech }) => weapons * weaponsTech,
|
|
effectiveDefence: ({ shields, shieldsTech, fullMass }) =>
|
|
fullMass <= 0
|
|
? 0
|
|
: ((shields * shieldsTech) / Math.cbrt(fullMass)) * Math.cbrt(30),
|
|
bombingPower: ({ weapons, weaponsTech, armament, number }) =>
|
|
(Math.sqrt(weapons * weaponsTech) / 10 + 1) *
|
|
weapons *
|
|
weaponsTech *
|
|
armament *
|
|
number,
|
|
shipBuildCost: ({ shipMass, material, resources }) => {
|
|
const matNeed = Math.max(0, shipMass - material);
|
|
const matFarm = resources > 0 ? matNeed / resources : 0;
|
|
return shipMass * 10 + matFarm;
|
|
},
|
|
produceShipsInTurn: ({
|
|
productionAvailable,
|
|
material,
|
|
resources,
|
|
shipMass,
|
|
}) => {
|
|
if (productionAvailable <= 0 || shipMass <= 0) {
|
|
return {
|
|
ships: 0,
|
|
materialLeft: material,
|
|
productionUsed: 0,
|
|
progress: 0,
|
|
};
|
|
}
|
|
let pa = productionAvailable;
|
|
let mat = material;
|
|
let ships = 0;
|
|
for (;;) {
|
|
const matNeed = Math.max(0, shipMass - mat);
|
|
const cost = shipMass * 10 + (resources > 0 ? matNeed / resources : 0);
|
|
if (pa < cost) {
|
|
return {
|
|
ships,
|
|
materialLeft: mat,
|
|
productionUsed: pa,
|
|
progress: pa / cost,
|
|
};
|
|
}
|
|
pa -= cost;
|
|
mat = mat - shipMass + matNeed;
|
|
ships += 1;
|
|
}
|
|
},
|
|
weaponsForAttack: ({ targetAttack, weaponsTech }) =>
|
|
weaponsTech <= 0 || targetAttack < 0 ? null : targetAttack / weaponsTech,
|
|
driveForSpeed: ({ targetSpeed, driveTech, restMass }) => {
|
|
if (driveTech <= 0 || targetSpeed <= 0) return null;
|
|
const ceiling = 20 * driveTech;
|
|
if (restMass <= 0) {
|
|
if (targetSpeed !== ceiling) return null;
|
|
return 1;
|
|
}
|
|
if (targetSpeed >= ceiling) return null;
|
|
return (targetSpeed * restMass) / (ceiling - targetSpeed);
|
|
},
|
|
shieldsForDefence: ({ targetDefence, shieldsTech, restMass }) => {
|
|
if (targetDefence <= 0 || shieldsTech <= 0) return null;
|
|
const def = (s: number) =>
|
|
((s * shieldsTech) / Math.cbrt(s + restMass)) * Math.cbrt(30);
|
|
let lo = 0;
|
|
let hi = 1;
|
|
while (def(hi) < targetDefence) {
|
|
hi *= 2;
|
|
if (hi > 1e12) return null;
|
|
}
|
|
for (let i = 0; i < 100; i++) {
|
|
const mid = (lo + hi) / 2;
|
|
if (def(mid) < targetDefence) lo = mid;
|
|
else hi = mid;
|
|
}
|
|
return (lo + hi) / 2;
|
|
},
|
|
cargoForEmptyMass: ({ targetEmptyMass, restMass }) =>
|
|
targetEmptyMass - restMass < 0 ? null : targetEmptyMass - restMass,
|
|
loadForFullMass: ({ targetFullMass, emptyMass, cargoTech }) =>
|
|
cargoTech <= 0 || targetFullMass < emptyMass
|
|
? null
|
|
: (targetFullMass - emptyMass) * cargoTech,
|
|
ceil3: ({ value }) => Math.ceil(Math.round(value * 1e9) / 1e6) / 1000,
|
|
};
|
|
return { ...base, ...overrides };
|
|
}
|