Files
heros_web/scripts/verify-dialogue-portrait-layout.mjs

214 lines
4.9 KiB
JavaScript

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.`
);
}