Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
5c68a1b
feat(naval-drone): add the ids, run, hull and jet model, and assembly…
veelenga Oct 3, 2026
f2cad59
feat(naval-drone): add the playback, speed trial, link, horizon and f…
veelenga Oct 3, 2026
d0cd473
feat(naval-drone): add the timeline and the store with the speed tria…
veelenga Oct 3, 2026
7a145b8
feat(naval-drone): add the dock, gauge, chapter widgets and horizon view
veelenga Oct 3, 2026
68a462d
feat(naval-drone): add the camera views, controller and scene bindings
veelenga Oct 3, 2026
4a93c9e
feat(naval-drone): add the entry, styles, manifest and placeholder im…
veelenga Oct 3, 2026
06b13a6
fix(naval-drone): keep the horizon labels clear of the line of sight,…
veelenga Oct 3, 2026
01102d2
fix(naval-drone): lower the satellites so the sky view fits them
veelenga Oct 3, 2026
0bf01c8
feat(naval-drone): write the English chapters and locale
veelenga Oct 3, 2026
78ae183
feat(naval-drone): add the Portuguese copy
veelenga Oct 3, 2026
8d36d1e
feat(naval-drone): add the Chinese copy
veelenga Oct 3, 2026
ba43a70
feat(naval-drone): add the Japanese copy
veelenga Oct 3, 2026
dc5e1d0
feat(naval-drone): add the Spanish copy
veelenga Oct 3, 2026
26cb564
feat(naval-drone): add the German copy
veelenga Oct 3, 2026
cbd4435
fix(naval-drone): say the boat gives off little heat, as the commande…
veelenga Oct 3, 2026
a3dd0e8
feat(naval-drone): add the Ukrainian copy
veelenga Oct 3, 2026
2ceba98
feat(naval-drone): add the French copy
veelenga Oct 3, 2026
bf4dc4f
feat(naval-drone): use the writer's link preview description
veelenga Oct 3, 2026
4491b2e
fix(naval-drone): keep the jet boat speed in step with the gauge
veelenga Oct 3, 2026
36aace5
fix(naval-drone): redraw the link widget only when the shown speed ch…
veelenga Oct 3, 2026
8370e31
fix(naval-drone): show the hull slider speed the way the gauge does
veelenga Oct 3, 2026
ad095d5
fix(naval-drone): keep one step rounding helper in the model
veelenga Oct 3, 2026
884d8d7
fix(naval-drone): define the percent and unit constants once in the m…
veelenga Oct 3, 2026
9cba9bd
fix(naval-drone): convert the jet flow to litres with litres per cubi…
veelenga Oct 3, 2026
0e6ea91
fix(naval-drone): start the hull and jet trials on the hump and cruis…
veelenga Oct 3, 2026
3558eec
fix(naval-drone): take the rounding decimals from each range step
veelenga Oct 3, 2026
3966dc2
fix(naval-drone): never let go of the boat in a chapter change
veelenga Oct 3, 2026
f14866d
fix(naval-drone): drop the repeated empty check in the run memo
veelenga Oct 3, 2026
5d172a6
fix(naval-drone): name the straight nozzle limit in degrees
veelenga Oct 3, 2026
dbd9646
fix(naval-drone): apply the review to the English copy
veelenga Oct 3, 2026
8cb5056
fix(naval-drone): apply the review to the Portuguese copy
veelenga Oct 3, 2026
ba210dd
fix(naval-drone): apply the review to the Spanish copy
veelenga Oct 3, 2026
6529a7c
fix(naval-drone): apply the review to the German copy
veelenga Oct 3, 2026
ebed107
fix(naval-drone): apply the review to the Japanese copy
veelenga Oct 3, 2026
b2e206b
fix(naval-drone): apply the review to the French copy
veelenga Oct 3, 2026
3fa4f30
fix(naval-drone): apply the review to the Ukrainian copy
veelenga Oct 3, 2026
d926a95
fix(naval-drone): apply the review to the Chinese copy
veelenga Oct 3, 2026
3b68568
feat(naval-drone): loft the hull with its deck items and waterjet
veelenga Oct 3, 2026
489753b
feat(naval-drone): fit the engine in its tub, the tanks, payload bay …
veelenga Oct 3, 2026
725455e
feat(naval-drone): add the dawn sky, the shader sea and the water sec…
veelenga Oct 3, 2026
fa4b9ef
feat(naval-drone): add the wake, bow wave, spray, jet stream and pump…
veelenga Oct 3, 2026
7c12a33
feat(naval-drone): add the missile fit, companions, ghost and radio l…
veelenga Oct 3, 2026
cf895e8
test(naval-drone): cover the dawn light, the scene options and the re…
veelenga Oct 3, 2026
62fc5dc
feat(naval-drone): add the coast, the slipway and the ground station
veelenga Oct 3, 2026
e969cc3
feat(naval-drone): tune the spray, the wake foam and the wave motion
veelenga Oct 3, 2026
23682a7
test(naval-drone): cover the fitted boxes and the hull plan mask
veelenga Oct 3, 2026
c5dc759
fix(naval-drone): keep the camera windows clear of z-fighting and sof…
veelenga Oct 3, 2026
a71526d
fix(naval-drone): soften the reverse stream and the spray turned down…
veelenga Oct 3, 2026
287de2a
feat(naval-drone): add the generic ship at anchor and the two satellites
veelenga Oct 3, 2026
67883b9
feat(naval-drone): assemble the boat, sea, effects and world with a d…
veelenga Oct 3, 2026
4373197
refactor(naval-drone): slim the shore to fit the page budget
veelenga Oct 3, 2026
57c1687
fix(naval-drone): reach the sea grid past the fog so the horizon has …
veelenga Oct 3, 2026
b4bce0d
feat(naval-drone): switch to the real scene with its lighting and opt…
veelenga Oct 3, 2026
83eee56
feat(naval-drone): open on the whole boat and cut it open in the jet …
veelenga Oct 3, 2026
0fdaa1c
fix(naval-drone): describe the closed boat in the overview and follow…
veelenga Oct 3, 2026
03b5fbb
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
2d738b2
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
b0c2b95
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
971d06e
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
b2e6c16
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
a1e231b
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
ccb0b35
fix(naval-drone): describe the closed boat and the power path in the …
veelenga Oct 3, 2026
0231380
refactor(naval-drone): rebuild the ship, shore and satellites as comp…
veelenga Oct 3, 2026
b8b7c2a
fix(naval-drone): show the water section only in the hull and jet cha…
veelenga Oct 3, 2026
e9c82cc
fix(naval-drone): fade the water section by view angle, clip spray at…
veelenga Oct 3, 2026
c55d59a
perf(naval-drone): trim the scene to fit the page budget
veelenga Oct 3, 2026
50728b1
fix(naval-drone): start the chine spray at the hump as white spray an…
veelenga Oct 3, 2026
7ce5486
fix(naval-drone): draw the wake as broken, feathered foam that thins …
veelenga Oct 3, 2026
f5c4961
fix(naval-drone): show the hump bow wave as white water against the hull
veelenga Oct 3, 2026
d2d5c43
fix(naval-drone): shape the ship with a raked flared bow, sheer strak…
veelenga Oct 3, 2026
772ffa6
perf(naval-drone): tighten the English copy to fit the page budget
veelenga Oct 3, 2026
2e2bb77
perf(naval-drone): tighten the Chinese copy to fit the page budget
veelenga Oct 3, 2026
2cae4f5
perf(naval-drone): tighten the Portuguese copy to fit the page budget
veelenga Oct 3, 2026
9179148
perf(naval-drone): tighten the Japanese copy to fit the page budget
veelenga Oct 3, 2026
b70c418
perf(naval-drone): tighten the Spanish copy to fit the page budget
veelenga Oct 3, 2026
544a1e2
perf(naval-drone): tighten the Ukrainian copy to fit the page budget
veelenga Oct 3, 2026
cfff813
perf(naval-drone): tighten the German copy to fit the page budget
veelenga Oct 3, 2026
ed8af5a
perf(naval-drone): tighten the French copy to fit the page budget
veelenga Oct 3, 2026
fc5c9ec
feat(naval-drone): add the cover and the link preview card
veelenga Oct 3, 2026
e4d384a
fix(naval-drone): follow the boat with the core's position follow mode
veelenga Oct 3, 2026
ff36918
fix(naval-drone): end the run a few metres short of the ship's side
veelenga Oct 3, 2026
285ea62
fix(naval-drone): look from just forward of the dome in the eye view
veelenga Oct 3, 2026
089e3c4
fix(naval-drone): frame the whole stern on the phone and thin the jet…
veelenga Oct 3, 2026
5dd922e
fix(naval-drone): set the boat off centre on the phone so the hull la…
veelenga Oct 3, 2026
db8c67c
fix(naval-drone): wrap long words in the slider readouts so the phone…
veelenga Oct 3, 2026
e2ffafd
fix(naval-drone): rock the boats only on the long swell, smoothed, an…
veelenga Oct 3, 2026
b8a58aa
fix(naval-drone): keep the water window open from the jet camera and …
veelenga Oct 3, 2026
d6e37bc
fix(naval-drone): stow the reverse bucket on its pivot and start soft…
veelenga Oct 3, 2026
2e51b4b
fix(naval-drone): keep the fast spray low off the chine and the wette…
veelenga Oct 3, 2026
bfb71dc
fix(naval-drone): show the satellites only with the links and draw th…
veelenga Oct 3, 2026
e68012a
fix(naval-drone): give the shore an irregular waterline, wet sand and…
veelenga Oct 3, 2026
e5f0ebf
fix(naval-drone): draw the clouds as soft, thin, high streaks
veelenga Oct 3, 2026
35985d8
fix(naval-drone): orbit the sky and group views around the boats inst…
veelenga Oct 3, 2026
712894b
fix(naval-drone): keep the link chapter's antennas lit but unlabelled…
veelenga Oct 3, 2026
117195a
fix(naval-drone): frame the fleet from behind a companion so the deck…
veelenga Oct 3, 2026
11872a1
fix(naval-drone): keep chapter controls on their own chapter and the …
veelenga Oct 3, 2026
d1480b4
fix(naval-drone): label the satellites only while the links draw them
veelenga Oct 3, 2026
1e1d8c2
fix(naval-drone): set the overview boat left of centre on the phone s…
veelenga Oct 3, 2026
dc7e537
fix(naval-drone): set the pump right of centre on the phone so the je…
veelenga Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
600 changes: 600 additions & 0 deletions explainers/naval-drone/chapters.html

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions explainers/naval-drone/explainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"slug": "naval-drone",
"tags": ["vehicles", "physics", "weapons"],
"cover": "cover.webp",
"entry": "src/index.ts",
"chapters": "chapters.html",
"locales": ["en", "zh", "es", "uk", "pt", "fr", "de", "ja"],
"social": {
"image": "social/og-image.png",
"alt": "A grey uncrewed boat skimming over the sea at dawn with spray flying from its chines, beside the title How a naval drone works"
}
}
322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/de.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/en.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/es.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/fr.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/ja.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/pt.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/uk.json

Large diffs are not rendered by default.

322 changes: 322 additions & 0 deletions explainers/naval-drone/locales/zh.json

Large diffs are not rendered by default.

Binary file added explainers/naval-drone/public/cover.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions explainers/naval-drone/src/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
export const PART_IDS = [
'hull',
'chines',
'sprayRails',
'payloadBay',
'fuelTanks',
'engine',
'electronicsBay',
'starlinkPanels',
'backupPanel',
'cameraDome',
'bowCamera',
'intake',
'duct',
'driveShaft',
'impeller',
'waterjet',
'stator',
'nozzle',
'steeringNozzle',
'reverseBucket',
'missileRails',
'jetStream',
'bowWave',
'spray',
'wake',
'wettedLength',
'videoGhost',
'satLink',
'backupLink',
'satellite',
'backupSatellite',
'groundStation',
'ship',
'shipRadar',
'companions',
] as const;

export type PartId = (typeof PART_IDS)[number];

export const PHASE_IDS = ['launch', 'hump', 'cruise', 'sprint', 'arrival'] as const;

export type PhaseId = (typeof PHASE_IDS)[number];

export const MOMENT_IDS = [
'hullSpeed',
'humpPeak',
'onPlane',
'cruiseSpeed',
'throttleUp',
'topSpeed',
'alongside',
] as const;

export type MomentId = (typeof MOMENT_IDS)[number];

export const PRESET_IDS = ['overview', 'hull', 'jet', 'link', 'horizon', 'fleet'] as const;

export type PresetId = (typeof PRESET_IDS)[number];

export const FIT_IDS = ['standard', 'missile'] as const;

export type FitId = (typeof FIT_IDS)[number];

export const HELM_IDS = ['left', 'straight', 'right', 'reverse'] as const;

export type HelmId = (typeof HELM_IDS)[number];

export const LINK_MODES = ['satellite', 'backup', 'lost'] as const;

export type LinkMode = (typeof LINK_MODES)[number];

export const SEA_STATE_IDS = ['smooth', 'slight', 'moderate', 'rough'] as const;

export type SeaStateId = (typeof SEA_STATE_IDS)[number];

export const SPEED_MARK_IDS = ['hullSpeed', 'hump', 'planing', 'cruise', 'top'] as const;

export type SpeedMarkId = (typeof SPEED_MARK_IDS)[number];

export const HULL_MODES = ['floating', 'hump', 'planing'] as const;

export type HullMode = (typeof HULL_MODES)[number];

export type CameraView = 'chase' | 'waterline' | 'stern' | 'sky' | 'eye' | 'group';

export type RegionId = 'scene' | 'boat' | 'ship' | 'shore';

export type AnchorId =
'boat' | 'dome' | 'stern' | 'ship' | 'satellite' | 'backupSatellite' | 'groundStation';

export type Point = readonly [x: number, y: number, z: number];

export interface RoutePose {
position: Point;
heading: number;
}

export type CompanionReading = RoutePose;

export interface BoatReading extends RoutePose {
knots: number;
distance: number;
held: boolean;
}

export interface PlaningReading {
knots: number;
mode: HullMode;
trim: number;
heave: number;
transomDepth: number;
wettedLength: number;
keelWettedLength: number;
chineWettedLength: number;
liftShare: number;
speedLengthRatio: number;
}

export interface JetReading {
throttle: number;
flow: number;
jetSpeed: number;
thrust: number;
efficiency: number;
impellerShare: number;
nozzleAngle: number;
bucket: number;
}

export interface SeaReading {
state: SeaStateId;
waveHeight: number;
}

export interface LinkReading {
mode: LinkMode;
ghost: RoutePose | null;
}

export interface ViewOptions {
cutaway: boolean;
flow: boolean;
links: boolean;
labels: boolean;
}

export interface AssemblyState {
phase: number;
playing: boolean;
boat: BoatReading;
planing: PlaningReading;
jet: JetReading;
companions: readonly CompanionReading[];
sea: SeaReading;
link: LinkReading;
fit: FitId;
view: ViewOptions;
waterSection: boolean;
wettedBar: boolean;
}
28 changes: 28 additions & 0 deletions explainers/naval-drone/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import './style.css';
import { defineExplainer } from '@core/explainer';
import { SCENE_OPTIONS, mountNavalDroneScene } from './scene';
import { PART_INFO } from './scene/partInfo';
import { PRESETS, createNavalDroneStore } from './state';
import type { NavalDroneStoreState } from './state';
import { NAVAL_DRONE_TIMELINE } from './timeline';
import {
CHAPTER_ACTIONS,
NAVAL_DRONE_CHOICES,
NAVAL_DRONE_READOUTS,
VIEW_TOGGLES,
mountNavalDroneUi,
} from './ui';

export default defineExplainer<NavalDroneStoreState>({
id: 'naval-drone',
timeline: NAVAL_DRONE_TIMELINE,
presets: PRESETS,
parts: PART_INFO,
createStore: () => createNavalDroneStore(),
dock: { choices: NAVAL_DRONE_CHOICES, toggles: VIEW_TOGGLES },
readouts: NAVAL_DRONE_READOUTS,
actions: CHAPTER_ACTIONS,
scene: SCENE_OPTIONS,
mountScene: mountNavalDroneScene,
mountUi: mountNavalDroneUi,
});
13 changes: 13 additions & 0 deletions explainers/naval-drone/src/model/fleet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { BOAT_COST_USD, KOTOV_VALUE_MUSD, PAYLOAD_MAX_KG } from './fleet';

describe('fleet figures', () => {
it('costs about a quarter of a million dollars and carries up to 320 kg', () => {
expect(BOAT_COST_USD).toEqual([250000, 273000]);
expect(PAYLOAD_MAX_KG).toBe(320);
});

it('quotes the patrol ship at the value GUR gave', () => {
expect(KOTOV_VALUE_MUSD).toBe(65);
});
});
3 changes: 3 additions & 0 deletions explainers/naval-drone/src/model/fleet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const BOAT_COST_USD = [250000, 273000] as const;
export const PAYLOAD_MAX_KG = 320;
export const KOTOV_VALUE_MUSD = 65;
49 changes: 49 additions & 0 deletions explainers/naval-drone/src/model/horizon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import {
DETECTION_KM,
LENS_HEIGHT_M,
RADAR_HEIGHT_M,
detectionCovered,
hiddenUpToHalf,
minutesAtTopSpeed,
radarLineOfSightKm,
surfaceDropM,
visualHorizonKm,
} from './horizon';

describe('horizon', () => {
it('sees a 0.5 m boat from a 20 m radar out to about 21 km', () => {
expect(radarLineOfSightKm(20)).toBeCloseTo(21.3, 1);
expect(RADAR_HEIGHT_M).toEqual({ min: 5, max: 50, step: 1, default: 20 });
});

it('puts the horizon about 3 km from the 0.7 m camera', () => {
expect(LENS_HEIGHT_M).toBe(0.7);
expect(visualHorizonKm(LENS_HEIGHT_M)).toBeCloseTo(3.2, 1);
});

it('crosses 21 km at 42 kn in about 16 minutes and the 9 km detection range in about 7', () => {
expect(minutesAtTopSpeed(radarLineOfSightKm(20))).toBeCloseTo(16.5, 1);
expect(DETECTION_KM).toBe(9.26);
expect(minutesAtTopSpeed(DETECTION_KM)).toBeCloseTo(7.1, 1);
});

it('bends the sea so the line of sight grazes it at the radar horizon', () => {
const radarHeight = 20;
const horizonKm = radarLineOfSightKm(radarHeight) - radarLineOfSightKm(0);
expect(surfaceDropM(horizonKm)).toBeCloseTo(radarHeight, 9);
expect(surfaceDropM(0)).toBe(0);
});

it('expects detection only up to a moderate sea, sea state 4', () => {
expect(detectionCovered('smooth')).toBe(true);
expect(detectionCovered('moderate')).toBe(true);
expect(detectionCovered('rough')).toBe(false);
});

it('hides the boat behind waves up to half the time from a slight sea up', () => {
expect(hiddenUpToHalf('smooth')).toBe(false);
expect(hiddenUpToHalf('slight')).toBe(true);
expect(hiddenUpToHalf('rough')).toBe(true);
});
});
45 changes: 45 additions & 0 deletions explainers/naval-drone/src/model/horizon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SEA_STATE_IDS } from '../ids';
import type { SeaStateId } from '../ids';
import { BOAT } from './layout';
import { knotsToMs } from './scale';

export const RADAR_K = 4.12;
export const VISUAL_K = 3.86;
export const TARGET_HEIGHT_M = 0.5;
export const LENS_HEIGHT_M = BOAT.lensHeight;
export const RADAR_HEIGHT_M = { min: 5, max: 50, step: 1, default: 20 } as const;
export const DETECTION_KM = 9.26;
export const METRES_PER_KM = 1000;
export const SECONDS_PER_MINUTE = 60;

const DETECTION_COVERED_UP_TO: SeaStateId = 'moderate';
const HIDDEN_FROM: SeaStateId = 'slight';

export function radarLineOfSightKm(radarHeight: number): number {
return RADAR_K * (Math.sqrt(radarHeight) + Math.sqrt(TARGET_HEIGHT_M));
}

export function visualHorizonKm(eyeHeight: number): number {
return VISUAL_K * Math.sqrt(eyeHeight);
}

export function minutesAtTopSpeed(km: number): number {
const seconds = (km * METRES_PER_KM) / knotsToMs(BOAT.topKnots);
return seconds / SECONDS_PER_MINUTE;
}

export function surfaceDropM(km: number): number {
return (km / RADAR_K) ** 2;
}

function seaRank(state: SeaStateId): number {
return SEA_STATE_IDS.indexOf(state);
}

export function detectionCovered(state: SeaStateId): boolean {
return seaRank(state) <= seaRank(DETECTION_COVERED_UP_TO);
}

export function hiddenUpToHalf(state: SeaStateId): boolean {
return seaRank(state) >= seaRank(HIDDEN_FROM);
}
Loading
Loading