Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291// fiapup under a finger: what strokes, flicks, taps and holds become.// node --test xbox/fiapup/tests/touch.test.mjs
import { test } from "node:test";import assert from "node:assert/strict";import { load, countingHost } from "./load.mjs";
const tick = 1 / 60;function fresh(width = 1920) { let us = 0; const { host } = countingHost({ runtime: () => ({ width, height: 1080, monotonicUs: (us += 16667) }) }); const game = load(host); game.boot(); game.fiapup.step(); // aims the camera return game.fiapup;}// One tick with these fingers down: [{ id, x, y }].function frame(f, fingers = [], down = []) { globalThis.__fiapupScript = { down, touches: fingers }; try { f.step(); } finally { globalThis.__fiapupScript = null; }}const idle = (f, seconds) => { for (let t = 0; t < seconds; t += tick) frame(f); };const pupOnScreen = (f) => { const p = f.world.pup; return f.screenOf(p.x, f.groundY(p.x, p.z) + 21 + p.pose.bob, p.z); };// A finger stroking back and forth across the pup for `seconds`.function stroke(f, seconds, id = 1, extra = () => []) { let t = 0; for (; t < seconds; t += tick) { const at = pupOnScreen(f); frame(f, [{ id, x: at.x + Math.sin(t * 9) * 40, y: at.y }, ...extra(t)]); }}
test("a stroke across the pup is petting, and keeps on into a rollover", () => { const f = fresh(); idle(f, .2); stroke(f, .6); assert.equal(f.world.pup.state, "petted"); assert.ok(f.world.hand.petting); assert.ok(f.world.buzzes.includes("soft"), "a soft tap while petting"); stroke(f, 1.8); assert.equal(f.world.pup.state, "rollover"); idle(f, 2); assert.equal(f.world.pup.state, "idle");});
test("a moving stroke counts for more than a resting finger", () => { const joyAfter = (moving) => { const f = fresh(); idle(f, .2); const start = f.world.pup.joy; for (let t = 0; t < 1.2; t += tick) { const at = pupOnScreen(f); frame(f, [{ id: 1, x: at.x + (moving ? Math.sin(t * 9) * 40 : 0), y: at.y }]); } return f.world.pup.joy - start; }; const still = joyAfter(false), moving = joyAfter(true); assert.ok(still > 0, "a resting finger still pets"); assert.ok(moving > still * 1.3, `stroke ${moving.toFixed(3)} vs still ${still.toFixed(3)}`);});
test("a flick throws the ball at the flick's velocity; a gentle lift sets it down", () => { const f = fresh(); const b = f.world.ball, at = f.screenOf(b.x, 5, b.z); frame(f, [{ id: 1, x: at.x, y: at.y }]); assert.equal(f.world.hand.holding, null, "a finger on the ball is a tap until it moves"); // Up the screen, fast: into the yard. // Where the finger is on the lawn, read against the camera each frame sees. const path = []; for (let i = 1; i <= 6; i++) { path.push(f.ground(at.x, at.y - i * 30)); frame(f, [{ id: 1, x: at.x, y: at.y - i * 30 }]); } const before = f.world.ball.heldBy; const from = path[1], to = path[5]; frame(f, []); assert.equal(before, "hand"); assert.equal(f.world.ball.thrown, true); const b2 = f.world.ball; assert.ok(b2.vz < -140, `thrown into the yard: vz ${b2.vz.toFixed(0)}`); const off = Math.atan2(b2.vz, b2.vx) - Math.atan2(to.z - from.z, to.x - from.x); assert.ok(Math.abs(Math.atan2(Math.sin(off), Math.cos(off))) < .05, "along the flick, on the lawn"); assert.ok(f.world.buzzes.includes("light")); idle(f, .1); assert.equal(f.world.pup.state, "fetch");
// Faster flicks throw harder. const speedOf = (pixels) => { const g = fresh(), a = g.screenOf(g.world.ball.x, 5, g.world.ball.z); frame(g, [{ id: 1, x: a.x, y: a.y }]); for (let i = 1; i <= 6; i++) frame(g, [{ id: 1, x: a.x + i * pixels, y: a.y }]); frame(g, []); return Math.hypot(g.world.ball.vx, g.world.ball.vz); }; assert.ok(speedOf(24) > speedOf(10) * 1.5);
const g = fresh(), a = g.screenOf(g.world.ball.x, 5, g.world.ball.z); frame(g, [{ id: 1, x: a.x, y: a.y }]); for (let i = 1; i <= 30; i++) frame(g, [{ id: 1, x: a.x + i, y: a.y }]); for (let i = 0; i < 10; i++) frame(g, [{ id: 1, x: a.x + 30, y: a.y }]); frame(g, []); assert.equal(g.world.ball.thrown, false, "set down, not thrown"); assert.equal(g.world.ball.heldBy, null);});
// Tap once on the screen at a world point, `y` over the ground there.function tapAt(f, x, y, z) { const at = f.screenOf(x, f.groundY(x, z) + y, z); frame(f, [{ id: 9, x: at.x, y: at.y }]); frame(f, []); return at;}
test("tap the grass: the pup trots to that spot, with a marker, and settles", () => { const f = fresh(); const spot = f.screenOf(-210, f.groundY(-210, -60), -60); // off the blanket, on open grass frame(f, [{ id: 1, x: spot.x, y: spot.y }]); const target = f.ground(spot.x, spot.y); // the camera the lift is read against frame(f, []); assert.equal(f.world.pup.state, "go"); assert.ok(Math.hypot(f.world.goSpot.x - target.x, f.world.goSpot.z - target.z) < 1); assert.ok(Math.hypot(target.x + 210, target.z + 60) < 8, "near where it was aimed"); assert.ok(f.world.ripples.length > 0, "a ripple marks it"); idle(f, 4); const p = f.world.pup; assert.ok(Math.hypot(p.x - target.x, p.z - target.z) < 12, `arrived: ${p.x.toFixed(0)},${p.z.toFixed(0)}`); assert.ok(f.world.log.includes("settle"));});
test("the pup turns its head the moment a finger lands", () => { const f = fresh(); idle(f, 1); const p = f.world.pup, before = p.pose.yaw; const at = f.screenOf(p.x - 150, 0, p.z + 100); frame(f, [{ id: 1, x: at.x, y: at.y }]); for (let i = 0; i < 5; i++) frame(f, [{ id: 1, x: at.x, y: at.y }]); assert.ok(Math.abs(p.pose.yaw - before) > .1, "head came round within 6 frames"); assert.ok(p.pose.perk > .1, "ears up");});
test("two taps on the grass start play", () => { const g = fresh(); g.world.pup.energy = .9; const s2 = g.screenOf(60, 0, 100); frame(g, [{ id: 1, x: s2.x, y: s2.y }]); frame(g, []); idle(g, .1); frame(g, [{ id: 2, x: s2.x + 5, y: s2.y }]); frame(g, []); assert.equal(g.world.pup.state, "playbow");});
test("tap the ball: it fetches it and brings it toward you", () => { const f = fresh(); const b = f.world.ball; tapAt(f, b.x, 5, b.z); assert.equal(f.world.pup.state, "fetch"); let carried = false; for (let t = 0; t < 8; t += tick) { frame(f); carried ||= b.heldBy === "pup"; } assert.ok(carried, "picked it up"); assert.equal(b.heldBy, null, "and set it down"); assert.ok(b.z > 30, `brought toward you: z ${b.z.toFixed(0)}, from 30`);});
test("tap the bowl: it goes and drinks", () => { const f = fresh(); const bowl = f.home.bowl; tapAt(f, bowl.x, 5, bowl.z); assert.equal(f.world.pup.state, "drink"); let lapped = false; for (let t = 0; t < 6; t += tick) { frame(f); lapped ||= f.world.pup.phase === "lap"; } assert.ok(lapped); assert.ok(Math.hypot(f.world.pup.x - bowl.x, f.world.pup.z - bowl.z) < 40, "at the bowl");});
test("tap the bed: it goes to bed and naps", () => { const f = fresh(); const bed = f.home.bed; tapAt(f, bed.x, 6, bed.z); assert.equal(f.world.pup.state, "sleepy"); let napped = null; for (let t = 0; t < 7 && !napped; t += tick) { frame(f); if (f.world.pup.state === "nap") napped = { ...f.world.pup }; } assert.ok(napped, "napped"); assert.ok(Math.hypot(napped.x - bed.x, napped.z - bed.z) < 20, "in the bed");});
test("tap the rope: it snatches it up, shakes it, and parades it", () => { const f = fresh(); const r = f.world.rope; tapAt(f, r.x + Math.cos(r.angle) * 15, 3.4, r.z + Math.sin(r.angle) * 15); assert.equal(f.world.pup.state, "shake"); let shook = false; for (let t = 0; t < 5; t += tick) { frame(f); shook ||= f.world.pup.phase === "shake" && r.heldBy === "pup"; } assert.ok(shook); assert.ok(f.world.log.includes("parade"));});
test("tap the pup: a happy wiggle", () => { const f = fresh(); idle(f, .2); const p = f.world.pup; tapAt(f, p.x, 21, p.z); assert.equal(p.state, "wiggle"); assert.ok(f.world.heard.length > 0, "a yip"); idle(f, 1.2); assert.equal(p.state, "idle");});
test("a tap redirects: mid-nap it wakes and comes; mid-fetch it goes where you tap", () => { const f = fresh(); f.world.pup.energy = .15; idle(f, 9); assert.equal(f.world.pup.state, "nap"); tapAt(f, -210, 0, 70); assert.equal(f.world.pup.state, "stretch"); idle(f, 5); const p = f.world.pup; assert.ok(f.world.log.includes("go")); assert.ok(Math.hypot(p.x + 210, p.z - 70) < 14, `came to the tap: ${p.x.toFixed(0)},${p.z.toFixed(0)}`);
const g = fresh(); tapAt(g, g.world.ball.x, 5, g.world.ball.z); for (let t = 0; t < 8 && g.world.ball.heldBy !== "pup"; t += tick) frame(g); assert.equal(g.world.ball.heldBy, "pup"); tapAt(g, -230, 0, 50); assert.equal(g.world.pup.state, "go"); idle(g, 5); assert.equal(g.world.ball.heldBy, null, "dropped it at the spot"); assert.ok(Math.hypot(g.world.ball.x + 230, g.world.ball.z - 50) < 45);});
test("holding still on the grass shows a treat; letting go drops it", () => { const f = fresh(); const spot = f.screenOf(40, 0, 60); for (let t = 0; t < .5; t += tick) frame(f, [{ id: 1, x: spot.x, y: spot.y }]); assert.equal(f.world.hand.holding, "treat"); for (let t = 0; t < 3; t += tick) frame(f, [{ id: 1, x: spot.x, y: spot.y }]); assert.equal(f.world.pup.state, "beg"); frame(f, []); assert.equal(f.world.treat.onFloor, true); idle(f, 3); assert.ok(f.world.log.includes("eat")); assert.equal(f.world.treat.onFloor, false, "eaten");});
test("dragging the rope starts a tug", () => { const f = fresh(); const r = f.world.rope; Object.assign(r, { x: 0, z: 60 }); Object.assign(f.world.pup, { x: 20, z: -20 }); idle(f, .1); const at = f.screenOf(r.x + Math.cos(r.angle) * 15, 3.4, r.z + Math.sin(r.angle) * 15); frame(f, [{ id: 1, x: at.x, y: at.y }]); for (let t = 0; t < 3; t += tick) frame(f, [{ id: 1, x: at.x, y: at.y + 20 + t * 20 }]); assert.equal(f.world.hand.holding, "rope"); assert.equal(f.world.pup.state, "tug"); assert.equal(r.heldBy, "both"); frame(f, []); assert.equal(f.world.pup.state, "parade");});
test("a second finger doesn't break a stroke", () => { const f = fresh(); idle(f, .2); const far = f.screenOf(200, 0, 120); stroke(f, 1.2, 1, (t) => (t > .4 && t < .5 ? [{ id: 2, x: far.x, y: far.y }] : [])); assert.equal(f.world.pup.state, "petted"); assert.ok(f.world.hand.petting);});
test("no play button: a double tap plays; a pad press takes the game back from touch", () => { const f = fresh(); f.world.pup.energy = .9; assert.equal(f.playButton, undefined); assert.equal(f.world.touch.mode, false); f.stageTap("play"); assert.equal(f.world.pup.state, "playbow"); frame(f, [], ["ArrowLeft"]); assert.equal(f.world.touch.mode, false);});
test("portrait frames the pup bigger across the screen than landscape", () => { const share = (width) => { const f = fresh(width); const p = f.world.pup, c = Math.cos(p.heading), s = Math.sin(p.heading); const nose = f.screenOf(p.x + c * 26, 25, p.z + s * 26), tail = f.screenOf(p.x - c * 20, 25, p.z - s * 20); return Math.hypot(nose.x - tail.x, nose.y - tail.y) / width; }; const portrait = share(500), landscape = share(1920); assert.ok(portrait > landscape * 2, `portrait ${portrait.toFixed(2)} of the width, landscape ${landscape.toFixed(2)}`); assert.ok(portrait > .15 && portrait < .7, `portrait share ${portrait.toFixed(2)}`);});