diff --git a/src/scenes/recipes.js b/src/scenes/recipes.js index 348579744..aa10648e4 100644 --- a/src/scenes/recipes.js +++ b/src/scenes/recipes.js @@ -1070,7 +1070,7 @@ export function displaySceneTitle(scene) { export function displayShotTitle(title, shot = undefined, scene = undefined) { const text = typeof title === 'string' ? title : ''; const fromRecipe = shot - ? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, text) + ? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, text, shot) : true; if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text); const numbered = /^Shot ([1-9]\d*)$/.exec(text); @@ -1081,8 +1081,10 @@ export function displayShotTitle(title, shot = undefined, scene = undefined) { * Whether the built-in recipe behind a scene declares a shot with this title: * its own keyframes, or the legacy base shots an applied append pack wrote * (those carry no sourcePackId, see SceneDirector._bootstrapLegacyShotPacks). + * A pack marker with shotBindings vouches only for the shot bound to the + * title, so a later shot typed with the same title stays as typed. */ -function recipeDeclaresShotTitle(scene, title) { +function recipeDeclaresShotTitle(scene, title, shot) { const declares = (cameraPath) => Boolean(cameraPath?.some((keyframe) => keyframe?.title === title)); const recipe = scene?.id ? getSceneRecipeById(scene.id) : null; @@ -1090,9 +1092,11 @@ function recipeDeclaresShotTitle(scene, title) { const packs = Array.isArray(scene?.appliedShotPacks) ? scene.appliedShotPacks : []; - return packs.some((entry) => - declares( - getSceneAppendRecipeById(entry?.id)?.legacySceneBootstrap?.cameraPath, - ), + return packs.some( + (entry) => + declares( + getSceneAppendRecipeById(entry?.id)?.legacySceneBootstrap?.cameraPath, + ) && + (!entry.shotBindings || entry.shotBindings[title] === shot?.id), ); } diff --git a/src/scenes/sceneTitleDisplay.test.mjs b/src/scenes/sceneTitleDisplay.test.mjs index 2a9aad9f9..972ac9702 100644 --- a/src/scenes/sceneTitleDisplay.test.mjs +++ b/src/scenes/sceneTitleDisplay.test.mjs @@ -163,4 +163,20 @@ test('legacy base shots of an applied append pack show their built-in names', as assert.notEqual(displayShotTitle(title), title); // Without the pack marker the same title in another scene stays as typed. assert.equal(displayShotTitle(title, shot, { id: 'scene-user' }), title); + // A marker with shotBindings still localizes the shot bound to the title + // (a typed one stays as typed: shotTitleProvenance.test.mjs). + const boundScene = { + id: 'bhote-koshi-nepal-scene', + appliedShotPacks: [ + { + id: pack.id, + version: pack.version, + shotBindings: { [title]: shot.id }, + }, + ], + }; + assert.equal( + displayShotTitle(title, shot, boundScene), + displayShotTitle(title), + ); }); diff --git a/src/scenes/shotTitleProvenance.test.mjs b/src/scenes/shotTitleProvenance.test.mjs new file mode 100644 index 000000000..c75108a96 --- /dev/null +++ b/src/scenes/shotTitleProvenance.test.mjs @@ -0,0 +1,30 @@ +// A user-made shot (no sourcePackId) typed as a legacy bootstrap title keeps +// its text even though its scene carries the evidence-pack marker. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { SCENE_APPEND_RECIPES, displayShotTitle } from './recipes.js'; +import { setLocale } from '../i18n/index.js'; + +test.afterEach(() => setLocale('en')); + +test('a new user shot typed as a bootstrap title keeps its text in a pack scene', () => { + setLocale('ru'); + const pack = SCENE_APPEND_RECIPES.find( + (recipe) => recipe.legacySceneBootstrap?.cameraPath?.length, + ); + assert.ok(pack); + const title = 'Global Incident Context'; + const scene = { + id: 'bhote-koshi-nepal-scene', + appliedShotPacks: [ + { + id: pack.id, + version: pack.version, + // The bootstrap binds this title to the original base shot. + shotBindings: { [title]: 'legacy-base-1' }, + }, + ], + }; + const userShot = { id: 'shot-user-new', title }; // no sourcePackId + assert.equal(displayShotTitle(title, userShot, scene), title); +});