feat: refine exploration presentation and audio flow
This commit is contained in:
213
scripts/verify-dialogue-portrait-layout.mjs
Normal file
213
scripts/verify-dialogue-portrait-layout.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
const projectRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
const helperPath = path.join(
|
||||
projectRoot,
|
||||
'src/game/ui/dialoguePortrait.ts'
|
||||
);
|
||||
const helperSource = await readFile(helperPath, 'utf8');
|
||||
const transpiledHelper = ts.transpileModule(helperSource, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.ES2022,
|
||||
target: ts.ScriptTarget.ES2022
|
||||
},
|
||||
fileName: helperPath
|
||||
}).outputText;
|
||||
const helperModule = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(transpiledHelper).toString('base64')}`
|
||||
);
|
||||
const {
|
||||
calculateDialogueFacePortraitLayout,
|
||||
fitDialogueFacePortrait
|
||||
} = helperModule;
|
||||
|
||||
const squareLayout = calculateDialogueFacePortraitLayout(
|
||||
1254,
|
||||
1254,
|
||||
220
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
cropX: squareLayout.cropX,
|
||||
cropY: squareLayout.cropY,
|
||||
cropSize: squareLayout.cropSize
|
||||
},
|
||||
{
|
||||
cropX: 251,
|
||||
cropY: 44,
|
||||
cropSize: 752
|
||||
},
|
||||
'The standard portrait must use the established square face-and-shoulders crop.'
|
||||
);
|
||||
assertCenteredCrop(squareLayout, 1254, 1254, 220);
|
||||
|
||||
const landscapeLayout = calculateDialogueFacePortraitLayout(
|
||||
1600,
|
||||
1000,
|
||||
192
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
cropX: landscapeLayout.cropX,
|
||||
cropY: landscapeLayout.cropY,
|
||||
cropSize: landscapeLayout.cropSize
|
||||
},
|
||||
{
|
||||
cropX: 500,
|
||||
cropY: 35,
|
||||
cropSize: 600
|
||||
},
|
||||
'Landscape sources must still produce a square, horizontally centered face crop.'
|
||||
);
|
||||
assertCenteredCrop(landscapeLayout, 1600, 1000, 192);
|
||||
|
||||
const portraitLayout = calculateDialogueFacePortraitLayout(
|
||||
1000,
|
||||
1600,
|
||||
218
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
cropX: portraitLayout.cropX,
|
||||
cropY: portraitLayout.cropY,
|
||||
cropSize: portraitLayout.cropSize
|
||||
},
|
||||
{
|
||||
cropX: 200,
|
||||
cropY: 56,
|
||||
cropSize: 600
|
||||
},
|
||||
'Portrait-oriented sources must preserve the upper face-and-shoulders framing.'
|
||||
);
|
||||
assertCenteredCrop(portraitLayout, 1000, 1600, 218);
|
||||
|
||||
assert.throws(
|
||||
() => calculateDialogueFacePortraitLayout(0, 1254, 220),
|
||||
RangeError,
|
||||
'Invalid source dimensions must fail explicitly.'
|
||||
);
|
||||
assert.throws(
|
||||
() => calculateDialogueFacePortraitLayout(1254, 1254, Number.NaN),
|
||||
RangeError,
|
||||
'Invalid display dimensions must fail explicitly.'
|
||||
);
|
||||
|
||||
const imageCalls = [];
|
||||
const image = {
|
||||
frame: {
|
||||
realWidth: 1254,
|
||||
realHeight: 1254
|
||||
},
|
||||
setCrop(...values) {
|
||||
imageCalls.push(['crop', ...values]);
|
||||
return this;
|
||||
},
|
||||
setOrigin(...values) {
|
||||
imageCalls.push(['origin', ...values]);
|
||||
return this;
|
||||
},
|
||||
setScale(...values) {
|
||||
imageCalls.push(['scale', ...values]);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
const appliedLayout = fitDialogueFacePortrait(image, 220);
|
||||
assert.deepEqual(
|
||||
imageCalls.map(([operation]) => operation),
|
||||
['crop', 'origin', 'scale'],
|
||||
'Applying the layout must crop, center the crop origin, and then scale it.'
|
||||
);
|
||||
assert.deepEqual(
|
||||
imageCalls[0],
|
||||
[
|
||||
'crop',
|
||||
appliedLayout.cropX,
|
||||
appliedLayout.cropY,
|
||||
appliedLayout.cropSize,
|
||||
appliedLayout.cropSize
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
imageCalls[1],
|
||||
['origin', appliedLayout.originX, appliedLayout.originY]
|
||||
);
|
||||
assert.deepEqual(
|
||||
imageCalls[2],
|
||||
['scale', appliedLayout.scale]
|
||||
);
|
||||
|
||||
await assertSceneIntegration(
|
||||
'src/game/scenes/StoryScene.ts',
|
||||
3
|
||||
);
|
||||
await assertSceneIntegration(
|
||||
'src/game/scenes/CityStayScene.ts',
|
||||
1
|
||||
);
|
||||
await assertSceneIntegration(
|
||||
'src/game/scenes/PrologueVillageScene.ts',
|
||||
1
|
||||
);
|
||||
await assertSceneIntegration(
|
||||
'src/game/scenes/PrologueMilitiaCampScene.ts',
|
||||
1
|
||||
);
|
||||
await assertSceneIntegration(
|
||||
'src/game/scenes/CampVisitExplorationScene.ts',
|
||||
1
|
||||
);
|
||||
|
||||
console.log(
|
||||
'Verified shared dialogue portrait face crops, centered origins, retained display sizes, and all story/exploration dialogue integrations.'
|
||||
);
|
||||
|
||||
function assertCenteredCrop(
|
||||
layout,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
displaySize
|
||||
) {
|
||||
const centeredX =
|
||||
layout.cropX +
|
||||
layout.cropSize / 2 -
|
||||
layout.originX * sourceWidth;
|
||||
const centeredY =
|
||||
layout.cropY +
|
||||
layout.cropSize / 2 -
|
||||
layout.originY * sourceHeight;
|
||||
assert.ok(
|
||||
Math.abs(centeredX) < 1e-9 &&
|
||||
Math.abs(centeredY) < 1e-9,
|
||||
'The visible crop center must remain anchored to the image position.'
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(layout.cropSize * layout.scale - displaySize) <
|
||||
1e-9,
|
||||
'The cropped portrait must retain its requested display size.'
|
||||
);
|
||||
}
|
||||
|
||||
async function assertSceneIntegration(relativePath, minimumCalls) {
|
||||
const source = await readFile(
|
||||
path.join(projectRoot, relativePath),
|
||||
'utf8'
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/import\s+\{\s*fitDialogueFacePortrait\s*\}\s+from\s+'\.\.\/ui\/dialoguePortrait';/,
|
||||
`${relativePath} must import the shared dialogue portrait helper.`
|
||||
);
|
||||
const callCount =
|
||||
source.match(/fitDialogueFacePortrait\s*\(/g)?.length ?? 0;
|
||||
assert.ok(
|
||||
callCount >= minimumCalls,
|
||||
`${relativePath} must apply the shared helper at every portrait rendering path.`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user