SOURCE / PINNED RELEASE
Made of little things.
Napplet Machines V8
- Release
- b3f614e739f0…
- Author-recorded commit
- 534f19acb4ba…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a
Archive hash verified: 0f7feb97e2c3d336…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { mkdir } from 'node:fs/promises';
export default async ({ players, check }) => {
const { page } = players[0];
let frame = players[0].frame;
await mkdir('.napplet-space/promotion', { recursive: true });
let element = await frame.frameElement();
await frame.evaluate(() => document.fonts.ready);
const resize = async (w, h) => {
await page.setViewportSize({
width: Math.max(320, w),
height: Math.max(300, h),
});
await element.evaluate(
(el, { w, h }) => {
for (const [key, value] of Object.entries({
position: 'fixed',
top: '0',
left: '0',
width: w + 'px',
height: h + 'px',
minHeight: '0',
minWidth: '0',
maxWidth: 'none',
border: '0',
zIndex: '9999',
}))
el.style[key] = value;
},
{ w, h },
);
await frame.waitForFunction(
({ w, h }) => innerWidth === w && innerHeight === h,
{ w, h },
);
};
const themes = {
light: { background: '#f8f6ed', text: '#27362f', primary: '#80b69c' },
dark: { background: '#17282b', text: '#e7eee7', primary: '#f7c95c' },
};
const landscape = await frame
.locator('#scene')
.evaluate((el) => el.style.background);
for (const [theme, colors] of Object.entries(themes)) {
// Host theme changes must leave the app-owned violet UI and scene intact.
await element.evaluate(
(el, colors) =>
el.contentWindow.postMessage(
{ type: 'theme.changed', theme: { colors } },
'*',
),
colors,
);
await frame.evaluate(() => new Promise(requestAnimationFrame));
check(
`violet UI stays app-owned in ${theme} host theme`,
await frame.evaluate(
() =>
getComputedStyle(document.documentElement)
.getPropertyValue('--bg')
.trim() === '#282044' &&
getComputedStyle(document.documentElement).colorScheme === 'dark',
),
);
check(
`game landscape keeps its own palette in ${theme} host theme`,
(await frame.locator('#scene').evaluate((el) => el.style.background)) ===
landscape,
);
for (const [w, h] of [
[200, 160],
[320, 560],
[900, 600],
[2400, 1200],
]) {
await resize(w, h);
const facts = await frame.evaluate(() => ({
width: innerWidth,
height: innerHeight,
overflow: document.documentElement.scrollWidth > innerWidth,
notice: getComputedStyle(document.querySelector('.too-small')).display,
bg: getComputedStyle(document.body).backgroundColor,
root: getComputedStyle(document.querySelector('#app')).backgroundColor,
}));
check(
`${theme} ${w}×${h} exact frame and no horizontal overflow`,
facts.width === w && facts.height === h && !facts.overflow,
);
check(
`${theme} ${w}×${h} theme covers body and app`,
facts.bg === facts.root,
);
check(
`${theme} ${w}×${h} minimum-size notice`,
(facts.notice !== 'none') === (w < 280 || h < 240),
);
if (w >= 280 && h >= 240)
await frame.waitForFunction(
() => document.querySelector('#scene').dataset.renderReady === 'true',
);
await page.waitForTimeout(150);
await element.screenshot({
path: `.napplet-space/promotion/${theme}-${w}x${h}.png`,
});
}
}
await resize(900, 600);
await frame.getByRole('button', { name: 'Workshop', exact: false }).click();
const clickPlan = async (x, y) => {
const box = await frame.locator('#plan').boundingBox(),
scale = Number(await frame.locator('#plan').getAttribute('data-scale'));
await page.mouse.click(
box.x + box.width / 2 + x * scale,
box.y + box.height / 2 + y * scale,
);
};
const before = await frame.locator('#pointCount').innerText();
await frame.locator('#addPoint').click();
check(
'add tool waits for a placement click',
(await frame.locator('#pointCount').innerText()) === before,
);
await clickPlan(-17, -38);
check(
'placement inserts in the nearest section',
(await frame.locator('#pointCount').innerText()) === '11 / 32' &&
(await frame.locator('#pointLabel').innerText()) === 'POINT 02',
);
await page.keyboard.press('Escape');
check(
'Escape leaves placement mode',
await frame
.locator('#moveTool')
.evaluate((el) => el.classList.contains('active')),
);
await frame.locator('#heightInput').fill('14');
await frame.locator('#heightInput').dispatchEvent('input');
check(
'elevation changed',
(await frame.locator('#heightValue').innerText()) === 'Z 14.0 m',
);
await frame.locator('#widthInput').fill('20.8');
await frame.locator('#widthInput').dispatchEvent('input');
await frame.locator('#wallThicknessInput').fill('1.8');
await frame.locator('#wallThicknessInput').dispatchEvent('input');
await clickPlan(-34, -22);
check(
'neighbor keeps its own width and wall thickness',
(await frame.locator('#widthInput').inputValue()) === '11.7' &&
(await frame.locator('#wallThicknessInput').inputValue()) === '0.6',
);
await frame.locator('#widthInput').fill('5');
await frame.locator('#widthInput').dispatchEvent('input');
await clickPlan(-17, -38);
check(
'new point remains at clicked position with independent dimensions',
(await frame.locator('#widthInput').inputValue()) === '20.8' &&
(await frame.locator('#wallThicknessInput').inputValue()) === '1.8',
);
await frame.locator('#wallTool').click();
await clickPlan(-12, -36);
check(
'clicking a section selects its wall settings',
(await frame.locator('#sectionLabel').innerText()) === 'SECTION 2 → 3',
);
await frame.locator('#segmentWall').uncheck();
check(
'open section disables its thickness control',
await frame.locator('#wallThicknessInput').isDisabled(),
);
await frame.locator('#moveTool').click();
await clickPlan(-34, -22);
check(
'opening one section keeps neighboring walls',
await frame.locator('#segmentWall').isChecked(),
);
await frame.locator('#trackName').fill('Test Ridge');
await frame.locator('#saveTrack').click();
await frame.waitForFunction(
() => document.querySelector('#saveState').textContent === 'SAVED',
);
await frame.locator('#trackName').fill('Changed');
await frame.locator('#loadTrack').click();
await frame.waitForFunction(
() => document.querySelector('#trackName').value === 'Test Ridge',
);
await clickPlan(-17, -38);
check(
'SDK save/load preserves the new 20.8 m maximum and section settings',
(await frame.locator('#widthInput').inputValue()) === '20.8' &&
(await frame.locator('#heightInput').inputValue()) === '14' &&
!(await frame.locator('#segmentWall').isChecked()) &&
(await frame.locator('#wallThicknessInput').inputValue()) === '1.8',
);
await frame.locator('#segmentWall').check();
for (const [theme, colors] of Object.entries(themes)) {
await element.evaluate(
(el, colors) =>
el.contentWindow.postMessage(
{ type: 'theme.changed', theme: { colors } },
'*',
),
colors,
);
for (const [w, h] of [
[200, 160],
[320, 560],
[900, 600],
[2400, 1200],
]) {
await resize(w, h);
check(
`workshop ${theme} ${w}×${h} fits frame`,
await frame.evaluate(
() => document.documentElement.scrollWidth === innerWidth,
),
);
if (w >= 280 && h >= 240) {
await frame.locator('#wallThicknessInput').scrollIntoViewIfNeeded();
check(
`workshop ${theme} ${w}×${h} section control reachable`,
await frame.locator('#wallThicknessInput').isVisible(),
);
}
await element.screenshot({
path: `.napplet-space/promotion/workshop-${theme}-${w}x${h}.png`,
});
}
}
await resize(900, 600);
await frame.locator('#drawTool').click();
const box = await frame.locator('#plan').boundingBox();
const points = [
[0.3, 0.35],
[0.5, 0.2],
[0.75, 0.35],
[0.78, 0.65],
[0.5, 0.78],
[0.25, 0.6],
[0.3, 0.35],
];
await page.mouse.move(
box.x + box.width * points[0][0],
box.y + box.height * points[0][1],
);
await page.mouse.down();
for (const [x, y] of points.slice(1))
await page.mouse.move(box.x + box.width * x, box.y + box.height * y, {
steps: 5,
});
await page.mouse.up();
await frame.waitForFunction(() =>
document.querySelector('#moveTool').classList.contains('active'),
);
check('drawing closes a new editable loop', true);
await frame.locator('#saveTrack').click();
await frame.waitForFunction(
() => document.querySelector('#saveState').textContent === 'SAVED',
);
await page.reload();
await page.waitForSelector('iframe');
frame = await (await page.locator('iframe').elementHandle()).contentFrame();
element = await frame.frameElement();
await frame.locator('#scene').waitFor();
await resize(900, 600);
await frame.getByRole('button', { name: 'Workshop', exact: false }).click();
await frame.locator('#loadTrack').click();
await frame.waitForFunction(
() => document.querySelector('#trackName').value === 'Test Ridge',
);
check('saved track survives iframe reload', true);
// Actual touch emulation plus simultaneous pointer actions on the game's controls.
const cdp = await page.context().newCDPSession(page);
await cdp.send('Emulation.setTouchEmulationEnabled', {
enabled: true,
maxTouchPoints: 5,
});
await resize(390, 844);
const touchTap = async (x, y) => {
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x, y, id: 0 }],
});
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
});
};
const touchBefore = await frame.locator('#pointCount').innerText();
await frame.locator('#addPoint').scrollIntoViewIfNeeded();
const addBox = await frame.locator('#addPoint').boundingBox();
await touchTap(addBox.x + addBox.width / 2, addBox.y + addBox.height / 2);
const planBox = await frame.locator('#plan').boundingBox();
await touchTap(planBox.x + planBox.width / 2, planBox.y + planBox.height / 2);
check(
'touch can place a new point without hover or keyboard',
(await frame.locator('#pointCount').innerText()) !== touchBefore,
);
for (const [w, h] of [
[390, 844],
[360, 640],
[844, 390],
]) {
await resize(w, h);
await frame.getByRole('button', { name: 'Garage', exact: false }).click();
await frame.locator('#raceButton').click();
await frame.waitForFunction(
() => document.querySelector('#scene').dataset.countdown === '0',
);
const go = await frame.locator('[data-drive=go]').boundingBox(),
right = await frame.locator('#steeringPad').boundingBox();
check(
`touch targets ${w}×${h}`,
go.width >= 44 &&
go.height >= 44 &&
right.width >= 44 &&
right.height >= 44,
);
const pause = await frame.locator('#pauseButton').boundingBox(),
recover = await frame.locator('#recoverButton').boundingBox(),
exit = await frame.locator('#exitRace').boundingBox(),
camera = await frame.locator('#cameraButton').boundingBox(),
pad = await frame.locator('#pedalPad').boundingBox();
check(
`two thumb zones stay clear ${w}×${h}`,
right.x + right.width + 8 < pad.x && Math.min(right.y, pad.y) > h * 0.5,
);
check(
`non-driving actions stay at the top ${w}×${h}`,
[pause, recover, exit, camera].every(
(b) => b.y + b.height < Math.min(right.y, pad.y),
),
);
const before = await frame.locator('#scene').getAttribute('data-local-x');
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{ x: go.x + go.width / 2, y: go.y + go.height / 2, id: 0 },
{
x: right.x + right.width * 0.7,
y: right.y + right.height * 0.6,
id: 1,
},
],
});
await page.waitForTimeout(250);
check(
`touch steering is analog ${w}×${h}`,
await frame.evaluate(() => {
const s = document.querySelector('#scene').dataset;
return (
Number(s.inputSteer) < -0.1 &&
Number(s.inputSteer) > -0.9 &&
Number(s.inputThrottle) === 1
);
}),
);
const drift = await frame.locator('[data-drive=drift]').boundingBox();
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{ x: drift.x + drift.width / 2, y: drift.y + drift.height / 2, id: 0 },
{
x: right.x + right.width * 0.7,
y: right.y + right.height * 0.6,
id: 1,
},
],
});
await page.waitForTimeout(100);
check(
`one right thumb can power a drift ${w}×${h}`,
await frame.evaluate(() => {
const s = document.querySelector('#scene').dataset;
return (
Number(s.inputThrottle) === 1 &&
s.inputBrake === 'true' &&
Number(s.inputSteer) < -0.1
);
}),
);
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
});
check(
`simultaneous touch driving ${w}×${h}`,
(await frame.locator('#scene').getAttribute('data-local-x')) !== before,
);
check(
`touch released ${w}×${h}`,
(await frame.locator('#touchControls .pressed').count()) === 0,
);
check(
`steering springs to centre ${w}×${h}`,
(await frame.locator('#steeringPad').getAttribute('aria-valuenow')) ===
'0',
);
await element.screenshot({
path: `.napplet-space/promotion/touch-${w}x${h}.png`,
});
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{ x: go.x + go.width / 2, y: go.y + go.height / 2, id: 0 },
{
x: right.x + right.width * 0.7,
y: right.y + right.height * 0.6,
id: 1,
},
],
});
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchCancel',
touchPoints: [],
});
await page.waitForTimeout(80);
check(
`cancel clears both thumb inputs ${w}×${h}`,
(await frame.locator('#steeringPad').getAttribute('aria-valuenow')) ===
'0' && (await frame.locator('#touchControls .pressed').count()) === 0,
);
await frame.locator('#exitRace').click();
}
await cdp.detach();
};
