214 lines
4.9 KiB
JavaScript
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.`
|
|
);
|
|
}
|