Back to Napplet Machines V8
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.

tests/ui.mjs
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();
};