Files
gods-eye-view/src/scenes/nepalEvidencePack.js
T
German KhokhlovandClaude Opus 5.5 20f4cf3763 i18n: fix the LGTM review findings and tidy the branch texts
- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 21:00:28 +04:00

362 lines
10 KiB
JavaScript

import { SOURCE_PATH_BEAT_IDS } from '../data/bhoteKoshiShotPaths.js';
/**
* Nepal evidence expansion. Coordinates and camera framing come from
* the source event pack; existing authored cameras remain owned by the Scene.
*/
const ADDITIONAL_SHOTS = [
{
id: 'second-landslide',
// i18n-ignore: stored shot title, matched verbatim
title: 'Second landslide',
lat: 28.399234,
lon: 85.535806,
alt: 10145,
heading: 224,
pitch: -38,
hold: 11,
},
{
id: 'dhunche',
// i18n-ignore: stored shot title, matched verbatim
title: 'Dhunche',
lat: 28.161431,
lon: 85.324278,
alt: 6212,
heading: 208,
pitch: -39,
hold: 16,
},
{
id: 'mailung-upper-trishuli',
// i18n-ignore: stored shot title, matched verbatim
title: 'Mailung / Upper Trishuli bridge',
lat: 28.140642,
lon: 85.265996,
alt: 6353,
heading: 211,
pitch: -40,
hold: 13,
},
{
id: 'mailung-bazzar',
// i18n-ignore: stored shot title, matched verbatim
title: 'Mailung Bazzar, Dandaguan',
lat: 28.126526,
lon: 85.248027,
alt: 6331,
heading: 212,
pitch: -40,
hold: 15,
},
{
id: 'dandagaun',
// i18n-ignore: stored shot title, matched verbatim
title: 'Dandagaun, Rasuwa',
lat: 28.124424,
lon: 85.245154,
alt: 6311,
heading: 214,
pitch: -40,
hold: 15,
},
{
id: 'dandagaun-viewpoint',
// i18n-ignore: stored shot title, matched verbatim
title: 'Dandagaun viewpoint restaurant',
lat: 28.086663,
lon: 85.229914,
alt: 6740,
heading: 215,
pitch: -41,
hold: 13,
},
{
id: 'betrawati-bazaar',
// i18n-ignore: stored shot title, matched verbatim
title: 'Betrawati Bazaar',
lat: 28.029827,
lon: 85.227694,
alt: 7034,
heading: 216,
pitch: -41,
hold: 12,
},
{
id: 'bhainse',
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhainse',
lat: 28.023087,
lon: 85.229449,
alt: 8051,
heading: 219,
pitch: -41,
hold: 13,
},
{
id: 'devighat-taadi-khola-bridge',
// i18n-ignore: stored shot title, matched verbatim
title: 'Devighat / Taadi Khola Bridge',
lat: 27.921132,
lon: 85.177113,
alt: 9056,
heading: 224,
pitch: -42,
hold: 13,
},
{
id: 'charaudi',
// i18n-ignore: stored shot title, matched verbatim
title: 'Charaudi',
lat: 27.865095,
lon: 84.838424,
alt: 10347,
heading: 232,
pitch: -42,
hold: 11,
},
{
id: 'final-view',
// i18n-ignore: stored shot title, matched verbatim
title: 'Final view',
lat: 28.0834,
lon: 85.206,
alt: 113520,
heading: 64,
pitch: -83,
roll: 174,
duration: 4,
hold: 11,
locatorPresentation: 'bhote-koshi-incident-places',
},
];
const EVIDENCE_ORDER = [
'immediate-collapse-viewpoint',
'debris-dammed-lake',
'second-landslide',
'gyirong-border-gate',
'timure-cluster',
'syabru-besi',
'dhunche',
'mailung-upper-trishuli',
'mailung-bazzar',
'dandagaun',
'dandagaun-viewpoint',
'betrawati-bazaar',
'bhainse',
'bidur-trishuli-bridge',
'devighat-taadi-khola-bridge',
'charaudi',
'final-view',
];
// Per-reach motion timing keeps the schematic front ahead of the fixed 4.2s
// camera flights without pretending short urban reaches and long valley runs
// should advance at one global speed.
const PATH_DURATION_BY_BEAT = Object.freeze({
'debris-dammed-lake': 4.8,
'second-landslide': 6.5,
'timure-cluster': 2.1,
'syabru-besi': 2.8,
dhunche: 2.5,
'mailung-upper-trishuli': 2.9,
'mailung-bazzar': 2.1,
dandagaun: 1.2,
'dandagaun-viewpoint': 2.5,
'betrawati-bazaar': 2.9,
bhainse: 1.5,
'bidur-trishuli-bridge': 2.6,
});
/** Extend the six-beat Nepal pack without replacing existing authored shots. */
export function expandNepalEvidencePack(base) {
const layerId = base.requiredSourcePackLayerId;
const extraShots = ADDITIONAL_SHOTS.map(
({ id, hold, duration = 4.2, locatorPresentation, ...camera }) => ({
...camera,
roll: Number.isFinite(Number(camera.roll)) ? Number(camera.roll) : 0,
duration,
hold,
mapStack: 'photoreal',
layers: {
[layerId]: {
enabled: true,
params: {
presentation: 'scene-beat',
beatId: id,
beatReveal: 0.36,
split: 0.5,
},
},
...(locatorPresentation
? {
'bhote-koshi-locator': {
enabled: true,
params: { presentation: locatorPresentation },
},
}
: {}),
},
}),
);
const byBeat = new Map(
[...base.cameraPath, ...extraShots].map((shot) => [
shot.layers[layerId].params.beatId,
shot,
]),
);
const cameraPath = EVIDENCE_ORDER.map((id) => byBeat.get(id));
const runtimeControlsByBeat = { ...base.runtimeControlsByBeat };
const cameraLedPathBeats = new Set([
'timure-cluster',
'syabru-besi',
...SOURCE_PATH_BEAT_IDS.slice(2),
]);
const mediaLedPathBeats = new Set(['debris-dammed-lake', 'second-landslide']);
const finalSourcedPathBeatId = SOURCE_PATH_BEAT_IDS.at(-1);
const postSourceHistoryBeats = new Set([
'devighat-taadi-khola-bridge',
'charaudi',
'final-view',
]);
for (const shot of cameraPath) {
const id = shot.layers[layerId].params.beatId;
const isFinalView = id === 'final-view';
const authoredPathDurationSec = PATH_DURATION_BY_BEAT[id];
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
evidenceMediaAutoplay: !isFinalView,
...(isFinalView
? {}
: {
mediaPlaybackHoldSec: id === 'mailung-bazzar' ? 7 : 6,
mediaExitDurationSec: 0.65,
}),
deferEvidenceUntilCameraSettled: !isFinalView,
evidenceRevealDurationSec: 0.9,
evidencePath: SOURCE_PATH_BEAT_IDS.includes(id)
? 'source'
: ['gyirong-border-gate', 'timure-cluster', 'syabru-besi'].includes(id)
? 'comparison'
: 'none',
...(cameraLedPathBeats.has(id)
? {
evidencePathDuringCamera: true,
evidencePathPersistent: true,
...(authoredPathDurationSec
? { evidencePathTravelDurationSec: authoredPathDurationSec }
: {}),
}
: {}),
...(mediaLedPathBeats.has(id)
? {
evidencePathDuringMedia: true,
evidencePathPersistent: true,
evidencePathStartDelaySec: 0,
}
: {}),
};
if (SOURCE_PATH_BEAT_IDS.includes(id)) {
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
evidenceHoldCard: true,
evidencePathHistory: true,
evidencePathDurationSec:
authoredPathDurationSec || Math.max(4, shot.hold),
minimumHoldSec: Math.max(4, shot.hold),
};
}
if (SOURCE_PATH_BEAT_IDS.slice(2).includes(id)) {
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
evidencePathHistory: true,
evidencePathElevation: 'source',
};
}
if (postSourceHistoryBeats.has(id)) {
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
evidencePath: 'history',
evidencePathHistory: true,
evidencePathHistoryBeatId: finalSourcedPathBeatId,
evidencePathPersistent: true,
evidencePathElevation: 'source',
};
}
const pathDurationSec =
Number(runtimeControlsByBeat[id].evidencePathDurationSec) ||
(!cameraLedPathBeats.has(id) &&
runtimeControlsByBeat[id].evidenceSequence === true
? Number(runtimeControlsByBeat[id].evidenceSequenceDurationSec) || 0
: 0);
const mediaPresentationSec = isFinalView
? 0
: (id === 'mailung-bazzar' ? 7 : 6) + 0.65;
const mediaAndPathHoldSec =
pathDurationSec > 0 && !cameraLedPathBeats.has(id)
? mediaLedPathBeats.has(id)
? Math.max(mediaPresentationSec, pathDurationSec)
: mediaPresentationSec + pathDurationSec
: mediaPresentationSec;
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
minimumHoldSec:
id === 'mailung-bazzar'
? mediaAndPathHoldSec
: Math.max(
Number(runtimeControlsByBeat[id].minimumHoldSec) || 0,
mediaAndPathHoldSec,
),
};
}
// Keep the downstream sequence on its sourced river profile. Coarse Google
// mesh samples can contain cliff-height spikes that reverse apparent motion.
// Preserve each authored readable hold after the media-first timing floor.
for (const [id, seconds, sequenceSeconds = seconds] of [
['immediate-collapse-viewpoint', 10],
['gyirong-border-gate', 8, 2.5],
['timure-cluster', 12],
['syabru-besi', 11],
]) {
runtimeControlsByBeat[id] = {
...runtimeControlsByBeat[id],
evidenceHoldCard: true,
evidenceSequenceDurationSec: sequenceSeconds,
minimumHoldSec: Math.max(
Number(runtimeControlsByBeat[id].minimumHoldSec) || 0,
seconds,
),
};
}
const finalView = byBeat.get('final-view');
return Object.freeze({
...base,
version: 18,
cameraPath,
runtimeControlsByBeat,
// Clip trims must also replace older saved holds, not just lower a floor.
runtimeHoldSecByBeat: { 'mailung-bazzar': 7 + 0.65 },
expansionFromVersion: 17,
previousRequiredSourcePackBeatIds: EVIDENCE_ORDER.slice(0, -1),
previousRequiredSourcePackBeatIdVariants: [
base.requiredSourcePackBeatIds,
EVIDENCE_ORDER.slice(0, -1),
],
requiredSourcePackBeatIds: EVIDENCE_ORDER,
requiredShotTitles: [
...base.requiredShotTitles.slice(0, -base.cameraPath.length),
...cameraPath.map(({ title }) => title),
],
adoptExistingShotTitles: ['Final view'],
shotPatches: [
...(base.shotPatches || []),
{
// i18n-ignore: stored shot title, matched verbatim
title: 'Final view',
layers: finalView.layers,
},
],
photorealSurfaceBeatIds: EVIDENCE_ORDER,
});
}