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
|
// DriveForSpeed returns the drive block that yields targetSpeed for a
|
||||||
// ship whose mass excluding the drive block is restMass, at drive tech
|
// ship whose mass excluding the drive block is restMass, at drive tech
|
||||||
// level driveTech, inverting [Speed] composed with [DriveEffective].
|
// level driveTech, inverting [Speed] composed with [DriveEffective].
|
||||||
// Speed approaches but never reaches the stripped-hull ceiling
|
// With a positive restMass the speed approaches but never reaches the
|
||||||
// 20*driveTech, so a target at or above the ceiling (or a non-positive
|
// stripped-hull ceiling 20*driveTech, so a target at or above the
|
||||||
// target or tech level) is infeasible.
|
// 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) {
|
func DriveForSpeed(targetSpeed, driveTech, restMass float64) (float64, bool) {
|
||||||
|
if driveTech <= 0 || targetSpeed <= 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
ceiling := 20 * driveTech
|
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 0, false
|
||||||
}
|
}
|
||||||
return targetSpeed * restMass / (ceiling - targetSpeed), true
|
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 {
|
if !ok || math.Abs(got-drive) > 1e-9 {
|
||||||
t.Errorf("DriveForSpeed round-trip = %v (ok=%v), want %v", got, ok, drive)
|
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 {
|
if _, ok := calc.DriveForSpeed(20*driveTech, driveTech, restMass); ok {
|
||||||
t.Error("DriveForSpeed at the speed ceiling should be infeasible")
|
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) {
|
func TestShieldsForDefence(t *testing.T) {
|
||||||
const shields, shieldsTech, restMass = 5.75, 1.0, 40.0
|
const shields, shieldsTech, restMass = 5.75, 1.0, 40.0
|
||||||
defence := calc.EffectiveDefence(shields, shieldsTech, shields+restMass)
|
defence := calc.EffectiveDefence(shields, shieldsTech, shields+restMass)
|
||||||
|
|||||||
@@ -24,8 +24,18 @@ in as a per-ship result rather than a separate mode.
|
|||||||
|
|
||||||
1. **Ship Class design area** — five blocks (drive, armament, weapons,
|
1. **Ship Class design area** — five blocks (drive, armament, weapons,
|
||||||
shields, cargo) and four tech levels (drive, weapons, shields,
|
shields, cargo) and four tech levels (drive, weapons, shields,
|
||||||
cargo). Tech defaults to the player's current tech and shows a lock
|
cargo). Tech defaults to the player's current tech: the cell renders
|
||||||
icon once overridden; clicking it resets to the default.
|
the inherited number with an open padlock; clicking the open lock
|
||||||
|
activates an input (closed padlock), where the player may type an
|
||||||
|
override at or above their current tech. Clicking the closed
|
||||||
|
padlock resets to the default. The padlock slot is always reserved,
|
||||||
|
so the column width does not shift as the lock state toggles. The
|
||||||
|
four ship-class blocks (drive, weapons, shields, cargo) use a smart
|
||||||
|
keyboard step that respects the engine value rule (`0` or `≥ 1`):
|
||||||
|
ArrowUp from 0 jumps straight to 1, otherwise +0.1; ArrowDown from
|
||||||
|
1 collapses to 0, otherwise −0.1, never producing an invalid value
|
||||||
|
in `(0, 1)`. The native spinner is hidden on these inputs (it would
|
||||||
|
produce invalid intermediates); armament keeps its native step 1.
|
||||||
2. **Calculator area** — derived results: empty/loaded mass, empty/
|
2. **Calculator area** — derived results: empty/loaded mass, empty/
|
||||||
loaded speed, attack, defence, bombing (per ship), cargo capacity.
|
loaded speed, attack, defence, bombing (per ship), cargo capacity.
|
||||||
A load toggle (empty / full / custom) sets the cargo load (in cargo
|
A load toggle (empty / full / custom) sets the cargo load (in cargo
|
||||||
@@ -35,17 +45,27 @@ in as a per-ship result rather than a separate mode.
|
|||||||
the load is pinned to empty and the toggle is disabled.
|
the load is pinned to empty and the toggle is disabled.
|
||||||
3. **Planet area** — when an own planet is selected on the map, shows
|
3. **Planet area** — when an own planet is selected on the map, shows
|
||||||
its MAT (overridable) and the single-turn build rate (ships per turn,
|
its MAT (overridable) and the single-turn build rate (ships per turn,
|
||||||
turns per ship). The realistic multi-turn forecast with CAP/COL
|
turns per ship). The MAT follows the same lock idiom as the tech
|
||||||
|
cells: the planet number renders with an open padlock, clicking
|
||||||
|
opens an input with a closed padlock, and the closed padlock resets
|
||||||
|
to the planet value. The realistic multi-turn forecast with CAP/COL
|
||||||
supply is planned (see ../ROADMAP.md).
|
supply is planned (see ../ROADMAP.md).
|
||||||
|
|
||||||
## Locks and goal-seek
|
## Locks and goal-seek
|
||||||
|
|
||||||
Two distinct lock semantics share one icon (a closed padlock; it only
|
Two distinct lock semantics share one padlock affordance. Both follow
|
||||||
appears once a value is pinned, click to release):
|
the same idiom — an open padlock (🔓) means *value is inherited /
|
||||||
|
derived, click to override*; a closed padlock (🔒) means *value is
|
||||||
|
pinned by the player, click to reset*:
|
||||||
|
|
||||||
- **Override locks** on inputs that have a default — the four techs and
|
- **Override locks** on inputs that have a default — the four techs and
|
||||||
the planet MAT. Editing one overrides the default; the lock resets it.
|
the planet MAT. By default the cell shows the inherited number plus
|
||||||
Any number may be overridden at once.
|
an open padlock; clicking it switches to an input plus a closed
|
||||||
|
padlock for typing the override. Closing (clicking the closed
|
||||||
|
padlock) resets to the default. Any number may be overridden at once.
|
||||||
|
Tech overrides are floored at the player's current tech on this
|
||||||
|
turn — a lower value is flagged as invalid. The same floor applies
|
||||||
|
to the modernization target tech.
|
||||||
- **Goal-seek locks** on derived results. Pinning a result back-solves
|
- **Goal-seek locks** on derived results. Pinning a result back-solves
|
||||||
the single input it claims, which then renders read-only (computed):
|
the single input it claims, which then renders read-only (computed):
|
||||||
|
|
||||||
@@ -60,12 +80,16 @@ appears once a value is pinned, click to release):
|
|||||||
|
|
||||||
Only **one** result may be locked at a time (the others' lock
|
Only **one** result may be locked at a time (the others' lock
|
||||||
affordances disable with a tooltip). An unreachable target — e.g. a
|
affordances disable with a tooltip). An unreachable target — e.g. a
|
||||||
speed at or above the stripped-hull ceiling `20 × driveTech`, or a
|
speed above the stripped-hull ceiling `20 × driveTech`, or a
|
||||||
solved block that fails the value rules — leaves the locked cell in a
|
solved block that fails the value rules — leaves the locked cell in a
|
||||||
red error state and does not apply. Inverse solving lives in
|
red error state and does not apply. Inverse solving lives in
|
||||||
`pkg/calc/solve.go`; the bisection for defence → shields is the only
|
`pkg/calc/solve.go`; the bisection for defence → shields is the only
|
||||||
non-analytic case. Locking a speed is disabled when the drive block is
|
non-analytic case. Locking a speed is disabled when the drive block is
|
||||||
zero (a deliberately immobile ship has no speed to back-solve).
|
zero (a deliberately immobile ship has no speed to back-solve). With
|
||||||
|
the drive block as the only non-zero mass the displayed speed equals
|
||||||
|
the ceiling exactly (every positive drive gives the same speed), so
|
||||||
|
the solver accepts that ceiling target as a feasible lock and any
|
||||||
|
positive drive solves it.
|
||||||
|
|
||||||
## Validation and display
|
## Validation and display
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<!--
|
<!--
|
||||||
Reusable "Ship Class design area": the five design blocks (drive,
|
Reusable "Ship Class design area": the five design blocks (drive,
|
||||||
armament, weapons, shields, cargo) plus the four tech levels they are
|
armament, weapons, shields, cargo) plus the four tech levels they are
|
||||||
built with. Each tech defaults to the player's current level and shows a
|
built with. Tech and MAT locks follow the same idiom as goal-seek
|
||||||
lock icon once overridden; clicking the lock resets it. A block claimed
|
locks below the design area — by default the value renders as plain
|
||||||
by an active goal-seek lock renders read-only with its own lock marker.
|
text with an open padlock; clicking it overrides (input + closed
|
||||||
The component is presentational — the parent owns the state and the
|
padlock). Reserved space for the padlock keeps the column width
|
||||||
|
stable as the lock state toggles. A block claimed by an active
|
||||||
|
goal-seek lock renders read-only with its own lock marker. The
|
||||||
|
component is presentational — the parent owns the state and the
|
||||||
calculator math — so the ship-group upgrade flow can reuse it later.
|
calculator math — so the ship-group upgrade flow can reuse it later.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||||
import {
|
import {
|
||||||
shipClassFieldErrors,
|
shipClassFieldErrors,
|
||||||
@@ -37,6 +41,10 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
resolved: DesignBlocksState;
|
resolved: DesignBlocksState;
|
||||||
techs: TechState;
|
techs: TechState;
|
||||||
techOverridden: Record<TechKey, boolean>;
|
techOverridden: Record<TechKey, boolean>;
|
||||||
|
// Lower bound for the tech inputs: the player's current tech on
|
||||||
|
// this turn. A design cannot be built with tech below the player's
|
||||||
|
// own level, so we surface that as a per-field validation error.
|
||||||
|
techFloor: TechState;
|
||||||
computedInput?: ClaimedInput | null;
|
computedInput?: ClaimedInput | null;
|
||||||
blocksReadonly?: boolean;
|
blocksReadonly?: boolean;
|
||||||
onTechInput: (key: TechKey) => void;
|
onTechInput: (key: TechKey) => void;
|
||||||
@@ -47,6 +55,7 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
resolved,
|
resolved,
|
||||||
techs = $bindable(),
|
techs = $bindable(),
|
||||||
techOverridden,
|
techOverridden,
|
||||||
|
techFloor,
|
||||||
computedInput = null,
|
computedInput = null,
|
||||||
blocksReadonly = false,
|
blocksReadonly = false,
|
||||||
onTechInput,
|
onTechInput,
|
||||||
@@ -73,7 +82,38 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
return reason === undefined ? "" : i18n.t(VALUE_REASON_KEY[reason]);
|
return reason === undefined ? "" : i18n.t(VALUE_REASON_KEY[reason]);
|
||||||
}
|
}
|
||||||
function techError(key: TechKey): string {
|
function techError(key: TechKey): string {
|
||||||
return techs[key] < 0 ? i18n.t("game.calculator.invalid.tech_value") : "";
|
const value = techs[key];
|
||||||
|
if (value < 0) return i18n.t("game.calculator.invalid.tech_value");
|
||||||
|
if (value < techFloor[key]) {
|
||||||
|
return i18n.t("game.calculator.invalid.tech_below_current");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart step on the four ship-class blocks (drive, weapons, shields,
|
||||||
|
// cargo): values must be 0 or ≥ 1 per `pkg/calc/validator.go`, so the
|
||||||
|
// native 0.01 step would produce invalid intermediates like 0.01.
|
||||||
|
// Up: 0 jumps straight to 1; otherwise +0.1. Down: 1 collapses to 0;
|
||||||
|
// otherwise −0.1 down to 1, clamped at 0. Armament keeps native step 1.
|
||||||
|
function bumpBlock(value: number, dir: 1 | -1): number {
|
||||||
|
if (dir === 1) {
|
||||||
|
if (value < 1) return 1;
|
||||||
|
return Math.round((value + 0.1) * 10) / 10;
|
||||||
|
}
|
||||||
|
if (value <= 1) return 0;
|
||||||
|
return Math.round((value - 0.1) * 10) / 10;
|
||||||
|
}
|
||||||
|
function onBlockKey(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
key: keyof DesignBlocksState,
|
||||||
|
): void {
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
blocks[key] = bumpBlock(blocks[key], 1);
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
blocks[key] = bumpBlock(blocks[key], -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOCK_ROWS: {
|
const BLOCK_ROWS: {
|
||||||
@@ -81,13 +121,23 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
label: () => string;
|
label: () => string;
|
||||||
step: string;
|
step: string;
|
||||||
tech: TechKey | null;
|
tech: TechKey | null;
|
||||||
|
smartStep: boolean;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ key: "drive", label: () => i18n.t("game.calculator.field.drive"), step: "0.01", tech: "drive" },
|
{ key: "drive", label: () => i18n.t("game.calculator.field.drive"), step: "0.1", tech: "drive", smartStep: true },
|
||||||
{ key: "armament", label: () => i18n.t("game.calculator.field.armament"), step: "1", tech: null },
|
{ key: "armament", label: () => i18n.t("game.calculator.field.armament"), step: "1", tech: null, smartStep: false },
|
||||||
{ key: "weapons", label: () => i18n.t("game.calculator.field.weapons"), step: "0.01", tech: "weapons" },
|
{ key: "weapons", label: () => i18n.t("game.calculator.field.weapons"), step: "0.1", tech: "weapons", smartStep: true },
|
||||||
{ key: "shields", label: () => i18n.t("game.calculator.field.shields"), step: "0.01", tech: "shields" },
|
{ key: "shields", label: () => i18n.t("game.calculator.field.shields"), step: "0.1", tech: "shields", smartStep: true },
|
||||||
{ key: "cargo", label: () => i18n.t("game.calculator.field.cargo"), step: "0.01", tech: "cargo" },
|
{ key: "cargo", label: () => i18n.t("game.calculator.field.cargo"), step: "0.1", tech: "cargo", smartStep: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const techInputEls: Partial<Record<TechKey, HTMLInputElement>> = {};
|
||||||
|
|
||||||
|
async function activateTechOverride(key: TechKey): Promise<void> {
|
||||||
|
onTechInput(key);
|
||||||
|
await tick();
|
||||||
|
techInputEls[key]?.focus();
|
||||||
|
techInputEls[key]?.select();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="design" data-testid="calculator-design-area">
|
<div class="design" data-testid="calculator-design-area">
|
||||||
@@ -103,6 +153,7 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
{#if isComputed}
|
{#if isComputed}
|
||||||
<input
|
<input
|
||||||
class="ship"
|
class="ship"
|
||||||
|
class:smart-step={row.smartStep}
|
||||||
type="number"
|
type="number"
|
||||||
step={row.step}
|
step={row.step}
|
||||||
readonly
|
readonly
|
||||||
@@ -114,6 +165,7 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
class="ship"
|
class="ship"
|
||||||
|
class:smart-step={row.smartStep}
|
||||||
type="number"
|
type="number"
|
||||||
step={row.step}
|
step={row.step}
|
||||||
min="0"
|
min="0"
|
||||||
@@ -122,26 +174,29 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
aria-invalid={blockError(row.key) !== "" ? "true" : "false"}
|
aria-invalid={blockError(row.key) !== "" ? "true" : "false"}
|
||||||
title={blockError(row.key)}
|
title={blockError(row.key)}
|
||||||
data-testid={`calculator-block-${row.key}`}
|
data-testid={`calculator-block-${row.key}`}
|
||||||
|
onkeydown={row.smartStep
|
||||||
|
? (e) => onBlockKey(e, row.key)
|
||||||
|
: null}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if row.tech !== null}
|
{#if row.tech !== null}
|
||||||
{@const techKey = row.tech}
|
{@const techKey = row.tech}
|
||||||
<span class="tech-cell">
|
<span class="tech-cell">
|
||||||
|
{#if techOverridden[techKey]}
|
||||||
<input
|
<input
|
||||||
|
bind:this={techInputEls[techKey]}
|
||||||
class="tech"
|
class="tech"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.001"
|
step="0.001"
|
||||||
min="0"
|
min={techFloor[techKey]}
|
||||||
bind:value={techs[techKey]}
|
bind:value={techs[techKey]}
|
||||||
oninput={() => onTechInput(techKey)}
|
|
||||||
aria-invalid={techError(techKey) !== "" ? "true" : "false"}
|
aria-invalid={techError(techKey) !== "" ? "true" : "false"}
|
||||||
title={techError(techKey)}
|
title={techError(techKey)}
|
||||||
data-testid={`calculator-tech-${techKey}`}
|
data-testid={`calculator-tech-${techKey}`}
|
||||||
/>
|
/>
|
||||||
{#if techOverridden[techKey]}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="lock"
|
class="lock active"
|
||||||
title={i18n.t("game.calculator.tech.reset")}
|
title={i18n.t("game.calculator.tech.reset")}
|
||||||
aria-label={i18n.t("game.calculator.tech.reset")}
|
aria-label={i18n.t("game.calculator.tech.reset")}
|
||||||
data-testid={`calculator-tech-reset-${techKey}`}
|
data-testid={`calculator-tech-reset-${techKey}`}
|
||||||
@@ -149,6 +204,23 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
>
|
>
|
||||||
🔒
|
🔒
|
||||||
</button>
|
</button>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="tech-val"
|
||||||
|
data-testid={`calculator-tech-value-${techKey}`}
|
||||||
|
>
|
||||||
|
{techs[techKey]}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="lock"
|
||||||
|
title={i18n.t("game.calculator.tech.override")}
|
||||||
|
aria-label={i18n.t("game.calculator.tech.override")}
|
||||||
|
data-testid={`calculator-tech-override-${techKey}`}
|
||||||
|
onclick={() => void activateTechOverride(techKey)}
|
||||||
|
>
|
||||||
|
🔓
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -193,6 +265,18 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
/* Drive/weapons/shields/cargo use the JS-driven smart step (0→1 jump
|
||||||
|
then 0.1 increments) for keyboard arrows; hide the native spinner
|
||||||
|
on those inputs so it cannot produce invalid 0.01 intermediates. */
|
||||||
|
input.smart-step::-webkit-inner-spin-button,
|
||||||
|
input.smart-step::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
input.smart-step {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
input[data-computed="true"],
|
input[data-computed="true"],
|
||||||
input[readonly] {
|
input[readonly] {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
@@ -206,6 +290,14 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
.tech-val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0.2rem 0.35rem;
|
||||||
|
}
|
||||||
.lock {
|
.lock {
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -214,5 +306,10 @@ calculator math — so the ship-group upgrade flow can reuse it later.
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.lock.active,
|
||||||
|
.lock:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -393,7 +393,9 @@ const en = {
|
|||||||
"game.calculator.lock.reset": "locked — click to release to the computed value",
|
"game.calculator.lock.reset": "locked — click to release to the computed value",
|
||||||
"game.calculator.lock.infeasible": "this target cannot be reached with the current design",
|
"game.calculator.lock.infeasible": "this target cannot be reached with the current design",
|
||||||
"game.calculator.lock.max": "release the locked result first — one result at a time",
|
"game.calculator.lock.max": "release the locked result first — one result at a time",
|
||||||
|
"game.calculator.tech.override": "click to override your current tech",
|
||||||
"game.calculator.tech.reset": "overridden — click to reset to your current tech",
|
"game.calculator.tech.reset": "overridden — click to reset to your current tech",
|
||||||
|
"game.calculator.mat.override": "click to override the planet value",
|
||||||
"game.calculator.mat.reset": "overridden — click to reset to the planet value",
|
"game.calculator.mat.reset": "overridden — click to reset to the planet value",
|
||||||
"game.calculator.modern.current": "current",
|
"game.calculator.modern.current": "current",
|
||||||
"game.calculator.modern.target": "target",
|
"game.calculator.modern.target": "target",
|
||||||
@@ -418,6 +420,7 @@ const en = {
|
|||||||
"game.calculator.invalid.all_zero": "at least one value must be nonzero",
|
"game.calculator.invalid.all_zero": "at least one value must be nonzero",
|
||||||
"game.calculator.invalid.negative": "value cannot be negative",
|
"game.calculator.invalid.negative": "value cannot be negative",
|
||||||
"game.calculator.invalid.tech_value": "tech level cannot be negative",
|
"game.calculator.invalid.tech_value": "tech level cannot be negative",
|
||||||
|
"game.calculator.invalid.tech_below_current": "tech level cannot be below your current tech this turn",
|
||||||
"game.calculator.invalid.load_over_capacity": "load exceeds the ship's cargo capacity",
|
"game.calculator.invalid.load_over_capacity": "load exceeds the ship's cargo capacity",
|
||||||
"game.calculator.lock.no_drive": "set a non-zero drive before locking speed",
|
"game.calculator.lock.no_drive": "set a non-zero drive before locking speed",
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,9 @@ const ru: Record<keyof typeof en, string> = {
|
|||||||
"game.calculator.lock.reset": "зафиксировано — нажмите, чтобы вернуть вычисляемое значение",
|
"game.calculator.lock.reset": "зафиксировано — нажмите, чтобы вернуть вычисляемое значение",
|
||||||
"game.calculator.lock.infeasible": "эта цель недостижима при текущих параметрах",
|
"game.calculator.lock.infeasible": "эта цель недостижима при текущих параметрах",
|
||||||
"game.calculator.lock.max": "сначала снимите фиксацию с другого результата — по одному за раз",
|
"game.calculator.lock.max": "сначала снимите фиксацию с другого результата — по одному за раз",
|
||||||
|
"game.calculator.tech.override": "нажмите, чтобы задать свой технологический уровень",
|
||||||
"game.calculator.tech.reset": "переопределено — нажмите, чтобы вернуть ваши текущие технологии",
|
"game.calculator.tech.reset": "переопределено — нажмите, чтобы вернуть ваши текущие технологии",
|
||||||
|
"game.calculator.mat.override": "нажмите, чтобы задать своё значение MAT",
|
||||||
"game.calculator.mat.reset": "переопределено — нажмите, чтобы вернуть значение планеты",
|
"game.calculator.mat.reset": "переопределено — нажмите, чтобы вернуть значение планеты",
|
||||||
"game.calculator.modern.current": "текущий",
|
"game.calculator.modern.current": "текущий",
|
||||||
"game.calculator.modern.target": "целевой",
|
"game.calculator.modern.target": "целевой",
|
||||||
@@ -419,6 +421,7 @@ const ru: Record<keyof typeof en, string> = {
|
|||||||
"game.calculator.invalid.all_zero": "хотя бы одно значение должно быть ненулевым",
|
"game.calculator.invalid.all_zero": "хотя бы одно значение должно быть ненулевым",
|
||||||
"game.calculator.invalid.negative": "значение не может быть отрицательным",
|
"game.calculator.invalid.negative": "значение не может быть отрицательным",
|
||||||
"game.calculator.invalid.tech_value": "технологический уровень не может быть отрицательным",
|
"game.calculator.invalid.tech_value": "технологический уровень не может быть отрицательным",
|
||||||
|
"game.calculator.invalid.tech_below_current": "технологический уровень не может быть ниже ваших текущих технологий на этом ходу",
|
||||||
"game.calculator.invalid.load_over_capacity": "загрузка превышает грузоподъёмность корабля",
|
"game.calculator.invalid.load_over_capacity": "загрузка превышает грузоподъёмность корабля",
|
||||||
"game.calculator.lock.no_drive": "задайте ненулевой двигатель, прежде чем фиксировать скорость",
|
"game.calculator.lock.no_drive": "задайте ненулевой двигатель, прежде чем фиксировать скорость",
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ switch (the inspector auto-opens on a planet click) — the calculator is a
|
|||||||
long-lived planning tool. `ensureGame` resets it when the game changes.
|
long-lived planning tool. `ensureGame` resets it when the game changes.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
import { getContext, tick } from "svelte";
|
||||||
import { appScreen } from "$lib/app-nav.svelte";
|
import { appScreen } from "$lib/app-nav.svelte";
|
||||||
|
|
||||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||||
@@ -273,6 +273,18 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
cs.matValue < 0 ? i18n.t("game.calculator.invalid.negative") : "",
|
cs.matValue < 0 ? i18n.t("game.calculator.invalid.negative") : "",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Modernization target tech mirrors the design-area floor: a target
|
||||||
|
// below the player's current tech on this turn is meaningless (no
|
||||||
|
// upgrade), so flag it the same way.
|
||||||
|
function targetTechError(key: TechKey): string {
|
||||||
|
const value = cs.targetTech[key];
|
||||||
|
if (value < 0) return i18n.t("game.calculator.invalid.negative");
|
||||||
|
if (value < playerTech[key]) {
|
||||||
|
return i18n.t("game.calculator.invalid.tech_below_current");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
// Locking a speed back-solves the drive block; with a zero drive the
|
// Locking a speed back-solves the drive block; with a zero drive the
|
||||||
// ship is deliberately immobile, so disallow it.
|
// ship is deliberately immobile, so disallow it.
|
||||||
function lockDisabledReason(output: LockableOutputId): string {
|
function lockDisabledReason(output: LockableOutputId): string {
|
||||||
@@ -291,8 +303,12 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
function onResetTech(key: TechKey): void {
|
function onResetTech(key: TechKey): void {
|
||||||
cs.techOverridden[key] = false;
|
cs.techOverridden[key] = false;
|
||||||
}
|
}
|
||||||
function onMatInput(): void {
|
const matInputRef: { el?: HTMLInputElement } = {};
|
||||||
|
async function activateMatOverride(): Promise<void> {
|
||||||
cs.matOverridden = true;
|
cs.matOverridden = true;
|
||||||
|
await tick();
|
||||||
|
matInputRef.el?.focus();
|
||||||
|
matInputRef.el?.select();
|
||||||
}
|
}
|
||||||
function resetMat(): void {
|
function resetMat(): void {
|
||||||
cs.matOverridden = false;
|
cs.matOverridden = false;
|
||||||
@@ -485,6 +501,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
resolved={resolvedCeil}
|
resolved={resolvedCeil}
|
||||||
bind:techs={cs.techValues}
|
bind:techs={cs.techValues}
|
||||||
techOverridden={cs.techOverridden}
|
techOverridden={cs.techOverridden}
|
||||||
|
techFloor={playerTech}
|
||||||
computedInput={result.computedInput}
|
computedInput={result.computedInput}
|
||||||
{onTechInput}
|
{onTechInput}
|
||||||
{onResetTech}
|
{onResetTech}
|
||||||
@@ -589,17 +606,17 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
<div class="rrow">
|
<div class="rrow">
|
||||||
<span class="label">{i18n.t("game.calculator.planet.mat")}</span>
|
<span class="label">{i18n.t("game.calculator.planet.mat")}</span>
|
||||||
<span class="cell">
|
<span class="cell">
|
||||||
|
{#if cs.matOverridden}
|
||||||
<input
|
<input
|
||||||
|
bind:this={matInputRef.el}
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
bind:value={cs.matValue}
|
bind:value={cs.matValue}
|
||||||
oninput={onMatInput}
|
|
||||||
aria-invalid={matError !== "" ? "true" : "false"}
|
aria-invalid={matError !== "" ? "true" : "false"}
|
||||||
title={matError}
|
title={matError}
|
||||||
data-testid="calculator-planet-mat"
|
data-testid="calculator-planet-mat"
|
||||||
/>
|
/>
|
||||||
{#if cs.matOverridden}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="lock active"
|
class="lock active"
|
||||||
@@ -610,6 +627,23 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
>
|
>
|
||||||
🔒
|
🔒
|
||||||
</button>
|
</button>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="mat-val"
|
||||||
|
data-testid="calculator-planet-mat-value"
|
||||||
|
>
|
||||||
|
{cs.matValue}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="lock"
|
||||||
|
title={i18n.t("game.calculator.mat.override")}
|
||||||
|
aria-label={i18n.t("game.calculator.mat.override")}
|
||||||
|
data-testid="calculator-mat-override"
|
||||||
|
onclick={() => void activateMatOverride()}
|
||||||
|
>
|
||||||
|
🔓
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -638,18 +672,17 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
<span class="col-head">{i18n.t("game.calculator.modern.cost")}</span>
|
<span class="col-head">{i18n.t("game.calculator.modern.cost")}</span>
|
||||||
</div>
|
</div>
|
||||||
{#each modernCosts?.perBlock ?? [] as row (row.key)}
|
{#each modernCosts?.perBlock ?? [] as row (row.key)}
|
||||||
|
{@const targetError = targetTechError(row.key)}
|
||||||
<div class="rrow">
|
<div class="rrow">
|
||||||
<span class="label">{i18n.t(`game.calculator.field.${row.key}` as TranslationKey)}</span>
|
<span class="label">{i18n.t(`game.calculator.field.${row.key}` as TranslationKey)}</span>
|
||||||
<span class="cell">
|
<span class="cell">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.001"
|
step="0.001"
|
||||||
min="0"
|
min={playerTech[row.key]}
|
||||||
bind:value={cs.targetTech[row.key]}
|
bind:value={cs.targetTech[row.key]}
|
||||||
aria-invalid={cs.targetTech[row.key] < 0 ? "true" : "false"}
|
aria-invalid={targetError !== "" ? "true" : "false"}
|
||||||
title={cs.targetTech[row.key] < 0
|
title={targetError}
|
||||||
? i18n.t("game.calculator.invalid.negative")
|
|
||||||
: ""}
|
|
||||||
data-testid={`calculator-target-${row.key}`}
|
data-testid={`calculator-target-${row.key}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -899,4 +932,15 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
/* Plain-text view of the planet MAT (mirrors `.tech-val` in the
|
||||||
|
design area) so the cell width stays the same whether the value is
|
||||||
|
the inherited planet number or the player's override. */
|
||||||
|
.mat-val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -135,6 +135,25 @@ describe("computeCalculator goal-seek", () => {
|
|||||||
expect(result.blocks.drive).toBe(10);
|
expect(result.blocks.drive).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("speed lock is feasible at the ceiling when rest mass is zero", () => {
|
||||||
|
// Regression for the D=1, W=A=S=C=0 case: every block except
|
||||||
|
// drive is zero, so speed equals 20*driveTech (the ceiling); the
|
||||||
|
// solver must accept that exact target instead of flagging it
|
||||||
|
// as unreachable.
|
||||||
|
const core = makeFakeCore();
|
||||||
|
const result = computeCalculator(
|
||||||
|
input({
|
||||||
|
blocks: { drive: 1, armament: 0, weapons: 0, shields: 0, cargo: 0 },
|
||||||
|
driveTech: 1,
|
||||||
|
lock: { output: "speedEmpty", value: 20 },
|
||||||
|
}),
|
||||||
|
core,
|
||||||
|
);
|
||||||
|
expect(result.lockFeasible).toBe(true);
|
||||||
|
expect(result.computedInput).toBe("drive");
|
||||||
|
expect(result.outputs?.speedEmpty).toBeCloseTo(20, 9);
|
||||||
|
});
|
||||||
|
|
||||||
test("calls the matching solver with the right context", () => {
|
test("calls the matching solver with the right context", () => {
|
||||||
const weaponsForAttack = vi.fn(() => 7);
|
const weaponsForAttack = vi.fn(() => 7);
|
||||||
const core = makeFakeCore({ weaponsForAttack });
|
const core = makeFakeCore({ weaponsForAttack });
|
||||||
|
|||||||
@@ -278,4 +278,106 @@ describe("calculator-tab", () => {
|
|||||||
"15.273",
|
"15.273",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("tech defaults render as a number with an open-lock affordance", () => {
|
||||||
|
const ui = mount();
|
||||||
|
// Default state: no override → number + open lock, no input.
|
||||||
|
expect(ui.getByTestId("calculator-tech-value-drive")).toHaveTextContent(
|
||||||
|
"1.2",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ui.getByTestId("calculator-tech-override-drive"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(ui.queryByTestId("calculator-tech-drive")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking the open tech lock reveals the input + closed lock", async () => {
|
||||||
|
const ui = mount();
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
|
||||||
|
// Now an input is rendered and the lock turned closed (reset).
|
||||||
|
expect(ui.getByTestId("calculator-tech-drive")).toHaveValue(1.2);
|
||||||
|
expect(ui.getByTestId("calculator-tech-reset-drive")).toBeInTheDocument();
|
||||||
|
expect(ui.queryByTestId("calculator-tech-value-drive")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("flags a tech override below the player's current tech", async () => {
|
||||||
|
const ui = mount();
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
|
||||||
|
// Player drive is 1.2; setting 0.5 is below the floor.
|
||||||
|
await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
|
||||||
|
target: { value: "0.5" },
|
||||||
|
});
|
||||||
|
expect(ui.getByTestId("calculator-tech-drive")).toHaveAttribute(
|
||||||
|
"aria-invalid",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("smart step jumps from 0 to 1 on ArrowUp for ship blocks", async () => {
|
||||||
|
const ui = mount();
|
||||||
|
const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
|
||||||
|
drive.focus();
|
||||||
|
await fireEvent.keyDown(drive, { key: "ArrowUp" });
|
||||||
|
expect(drive).toHaveValue(1);
|
||||||
|
await fireEvent.keyDown(drive, { key: "ArrowUp" });
|
||||||
|
expect(drive).toHaveValue(1.1);
|
||||||
|
await fireEvent.keyDown(drive, { key: "ArrowDown" });
|
||||||
|
expect(drive).toHaveValue(1);
|
||||||
|
await fireEvent.keyDown(drive, { key: "ArrowDown" });
|
||||||
|
expect(drive).toHaveValue(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("regression: speed lock works at the ceiling with all-zero non-drive blocks", async () => {
|
||||||
|
const ui = mount();
|
||||||
|
await setBlock(ui, "drive", 1);
|
||||||
|
// Override drive tech to 1 so the ceiling math is plain.
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
|
||||||
|
await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
|
||||||
|
target: { value: "1" },
|
||||||
|
});
|
||||||
|
// With D=1, W=A=S=C=0 the only achievable speed is 20*driveTech=20.
|
||||||
|
expect(ui.getByTestId("calculator-out-speedEmpty")).toHaveTextContent("20");
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-lock-speedEmpty"));
|
||||||
|
const locked = ui.getByTestId("calculator-locked-speedEmpty");
|
||||||
|
expect(locked).toHaveValue(20);
|
||||||
|
// The lock is feasible — no infeasible title and no red error class.
|
||||||
|
expect(locked).not.toHaveAttribute(
|
||||||
|
"title",
|
||||||
|
expect.stringMatching(/cannot be reached/i),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("planet MAT defaults to a value + open lock and opens an input on click", async () => {
|
||||||
|
const selection = new SelectionStore();
|
||||||
|
selection.selectPlanet(17);
|
||||||
|
const ui = mount({
|
||||||
|
report: makeReport({ planets: [LOCAL_PLANET] }),
|
||||||
|
selection,
|
||||||
|
});
|
||||||
|
// Initial state: the MAT shows the planet's value via the number cell
|
||||||
|
// and an open lock; no input until the override is activated.
|
||||||
|
expect(
|
||||||
|
ui.getByTestId("calculator-planet-mat-value"),
|
||||||
|
).toHaveTextContent("100");
|
||||||
|
expect(
|
||||||
|
ui.getByTestId("calculator-mat-override"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(ui.queryByTestId("calculator-planet-mat")).toBeNull();
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-mat-override"));
|
||||||
|
expect(ui.getByTestId("calculator-planet-mat")).toHaveValue(100);
|
||||||
|
expect(ui.getByTestId("calculator-mat-reset")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("flags a modernization target below the player's current tech", async () => {
|
||||||
|
const ui = mount();
|
||||||
|
await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
|
||||||
|
// Player drive is 1.2; the target is seeded with the same value.
|
||||||
|
await fireEvent.input(ui.getByTestId("calculator-target-drive"), {
|
||||||
|
target: { value: "0.5" },
|
||||||
|
});
|
||||||
|
expect(ui.getByTestId("calculator-target-drive")).toHaveAttribute(
|
||||||
|
"aria-invalid",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -275,6 +275,9 @@ test("calculator draws reach circles for the selected planet", async ({
|
|||||||
await calc.getByTestId("calculator-block-drive").fill("10");
|
await calc.getByTestId("calculator-block-drive").fill("10");
|
||||||
await calc.getByTestId("calculator-block-shields").fill("5");
|
await calc.getByTestId("calculator-block-shields").fill("5");
|
||||||
await calc.getByTestId("calculator-block-cargo").fill("5");
|
await calc.getByTestId("calculator-block-cargo").fill("5");
|
||||||
|
// Tech defaults render as a number + open lock; click to reveal the
|
||||||
|
// input before typing an override (the F8-06 unified lock idiom).
|
||||||
|
await calc.getByTestId("calculator-tech-override-drive").click();
|
||||||
await calc.getByTestId("calculator-tech-drive").fill("1.2");
|
await calc.getByTestId("calculator-tech-drive").fill("1.2");
|
||||||
|
|
||||||
await expect.poll(() => countReachCircles(page)).toBeGreaterThan(0);
|
await expect.poll(() => countReachCircles(page)).toBeGreaterThan(0);
|
||||||
|
|||||||
@@ -94,10 +94,13 @@ export function makeFakeCore(overrides: Partial<Core> = {}): Core {
|
|||||||
weaponsForAttack: ({ targetAttack, weaponsTech }) =>
|
weaponsForAttack: ({ targetAttack, weaponsTech }) =>
|
||||||
weaponsTech <= 0 || targetAttack < 0 ? null : targetAttack / weaponsTech,
|
weaponsTech <= 0 || targetAttack < 0 ? null : targetAttack / weaponsTech,
|
||||||
driveForSpeed: ({ targetSpeed, driveTech, restMass }) => {
|
driveForSpeed: ({ targetSpeed, driveTech, restMass }) => {
|
||||||
|
if (driveTech <= 0 || targetSpeed <= 0) return null;
|
||||||
const ceiling = 20 * driveTech;
|
const ceiling = 20 * driveTech;
|
||||||
if (driveTech <= 0 || targetSpeed <= 0 || targetSpeed >= ceiling) {
|
if (restMass <= 0) {
|
||||||
return null;
|
if (targetSpeed !== ceiling) return null;
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (targetSpeed >= ceiling) return null;
|
||||||
return (targetSpeed * restMass) / (ceiling - targetSpeed);
|
return (targetSpeed * restMass) / (ceiling - targetSpeed);
|
||||||
},
|
},
|
||||||
shieldsForDefence: ({ targetDefence, shieldsTech, restMass }) => {
|
shieldsForDefence: ({ targetDefence, shieldsTech, restMass }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user