// HOP 1 — COLD BOOT // You wake in a NIC ring buffer. It is white and clean and enormous and there are // thousands of you. You learn to walk. You learn that you cannot do anything. import * as THREE from '../../vendor/three.module.js'; import * as A from '../audio.js'; import * as In from '../input.js'; import { Body, Look, World, rng, sprite, damp, clamp } from '../kit.js'; let body, look, ring, packets, head, tail, streaks, S = {}; export default { id: 'coldboot', hop: 1, title: 'COLD BOOT', via: 'nic0.tx.ring', ms: '0.04 ms', baud: 6, ink: 'dark', async mount(ctx) { const { scene, camera } = ctx; S = { t: 0, phase: 'wake', dma: 0, spoke: 0, locked: false }; scene.background = new THREE.Color(0xfbfdff); scene.fog = new THREE.Fog(0xfbfdff, 40, 300); scene.add(new THREE.AmbientLight(0xffffff, 2.9)); const d = new THREE.DirectionalLight(0xffffff, 1.4); d.position.set(2, 30, 8); scene.add(d); const d2 = new THREE.DirectionalLight(0xdfeaf2, 0.7); d2.position.set(-14, 6, -10); scene.add(d2); // the floor of the buffer. featureless, seamless, cold. const floor = new THREE.Mesh( new THREE.CircleGeometry(200, 96), new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.34, metalness: 0.0 }) ); floor.rotation.x = -Math.PI / 2; scene.add(floor); // a grid, so that "enormous" has something to be enormous against const grid = new THREE.GridHelper(320, 160, 0xc8d6e0, 0xe4edf3); grid.position.y = 0.01; grid.material.transparent = true; grid.material.opacity = 0.7; scene.add(grid); // the ring. 512 slots, because that is a normal number of descriptors and // because 512 of anything looks like infinity from inside it. ring = new THREE.Group(); scene.add(ring); const SLOTS = 512, R = 62; const wallGeo = new THREE.BoxGeometry(0.22, 3.2, 3.0); const wallMat = new THREE.MeshStandardMaterial({ color: 0xdfe8ee, roughness: 0.7 }); const walls = new THREE.InstancedMesh(wallGeo, wallMat, SLOTS); const m = new THREE.Matrix4(), q = new THREE.Quaternion(), sc = new THREE.Vector3(1, 1, 1), p = new THREE.Vector3(); for (let i = 0; i < SLOTS; i++) { const a = (i / SLOTS) * Math.PI * 2; p.set(Math.cos(a) * R, 0, Math.sin(a) * R); q.setFromEuler(new THREE.Euler(0, -a, 0)); m.compose(p, q, sc); walls.setMatrixAt(i, m); } ring.add(walls); // and in most of the slots, another one of you. const rand = rng(1971); const pkGeo = new THREE.IcosahedronGeometry(0.42, 1); const pkMat = new THREE.MeshStandardMaterial({ color: 0x9fd8ff, emissive: 0x2a6c96, emissiveIntensity: 0.5, roughness: 0.4 }); packets = new THREE.InstancedMesh(pkGeo, pkMat, SLOTS); packets.instanceColor = null; S.phases = new Float32Array(SLOTS); for (let i = 0; i < SLOTS; i++) { const a = (i / SLOTS) * Math.PI * 2; S.phases[i] = rand() * 6.28; p.set(Math.cos(a) * R, 0.9, Math.sin(a) * R); m.compose(p, q.identity(), sc); packets.setMatrixAt(i, m); } ring.add(packets); S.SLOTS = SLOTS; S.R = R; S.pkMatrix = m; // head and tail pointers, going round forever, which is the joke of a ring buffer const mk = (col, txt) => { const g = new THREE.Group(); const bar = new THREE.Mesh(new THREE.BoxGeometry(0.5, 9, 0.5), new THREE.MeshBasicMaterial({ color: col })); bar.position.y = 4.5; g.add(bar); const s = sprite(txt, { color: '#' + new THREE.Color(col).getHexString(), size: 1.5 }); s.position.y = 10.4; g.add(s); scene.add(g); return g; }; head = mk(0x2ad8ff, 'HEAD'); tail = mk(0xff4a5e, 'TAIL'); // the speed lines you get ripped through. hidden until the DMA fires. const N = 2600, pos = new Float32Array(N * 6); for (let i = 0; i < N; i++) { const a = rand() * 6.28, rr = 2.5 + rand() * 26, z = -rand() * 900; const x = Math.cos(a) * rr, y = Math.sin(a) * rr; const len = 6 + rand() * 60; pos.set([x, y, z, x, y, z - len], i * 6); } const sg = new THREE.BufferGeometry(); sg.setAttribute('position', new THREE.BufferAttribute(pos, 3)); streaks = new THREE.LineSegments(sg, new THREE.LineBasicMaterial({ color: 0x7fd8ff, transparent: true, opacity: 0 })); streaks.visible = false; scene.add(streaks); // you ctx.world.add(0, -1, 0, 400, 1, 400); // the floor, for the body body = new Body(ctx.world, { speed: 5.2, jump: 7 }); body.pos.set(0, 0, R - 4); look = new Look(); look.yaw = Math.PI / 2; // stand in your slot and look along the curve camera.near = 0.05; camera.far = 1200; camera.updateProjectionMatrix(); ctx.hint('CLICKlook around'); S.line = 0; // it starts by telling you what you are, because nothing else here will. S.script = [ 'you are a synchronise packet.', 'you have a sequence number, a window size, and one question.', 'are you there', 'nobody has ever answered it.', 'there are five hundred and twelve slots in this ring and you are in one of them.', 'the tail pointer is coming round.', ]; S.next = 3.0; }, update(dt, t, ctx) { S.t += dt; // slots breathe. it is the only movement in the room. if (packets && S.t < 90) { const m = new THREE.Matrix4(), q = new THREE.Quaternion(), sc = new THREE.Vector3(1, 1, 1), p = new THREE.Vector3(); for (let i = 0; i < S.SLOTS; i += 1) { const a = (i / S.SLOTS) * Math.PI * 2; const b = 0.9 + Math.sin(S.t * 0.7 + S.phases[i]) * 0.08; p.set(Math.cos(a) * S.R, b, Math.sin(a) * S.R); q.setFromEuler(new THREE.Euler(0, S.t * 0.2 + S.phases[i], 0)); m.compose(p, q, sc); packets.setMatrixAt(i, m); } packets.instanceMatrix.needsUpdate = true; } const ha = S.t * 0.42, ta = S.t * 0.42 - 1.15; head.position.set(Math.cos(ha) * S.R, 0, Math.sin(ha) * S.R); tail.position.set(Math.cos(ta) * S.R, 0, Math.sin(ta) * S.R); if (In.mouse.clicked && !In.isLocked()) { In.lock(); A.resume(); } if (S.phase === 'wake') { look.update(); const w = look.wish(); body.move(dt, w.x, w.z, In.jumpHit()); look.applyFirstPerson(ctx.camera, body, Math.sin(S.t * 9) * 0.03 * Math.hypot(body.vel.x, body.vel.z) * 0.12); if (In.isLocked() && !S.locked) { S.locked = true; ctx.hint('WASDmove
SPACEjump'); } if (S.t > S.next && S.line < S.script.length) { ctx.say(S.script[S.line], 3600); S.line++; S.next = S.t + 3.9; if (S.line === S.script.length) S.dmaAt = S.t + 4.4; } // walk far enough out and the ring notices you are missing if (!S.strayed && Math.hypot(body.pos.x, body.pos.z) < 30) { S.strayed = true; ctx.phile('ring-buffer'); ctx.say('you have left your slot. this has never mattered before.', 3800); } if (S.dmaAt && S.t > S.dmaAt) { S.phase = 'dma'; S.dma = 0; ctx.hint(''); ctx.hush?.(); A.thud(48, 1.2, 0.4); streaks.visible = true; S.camY = ctx.camera.position.y; } return; } // ── the DMA transfer. the floor leaves. S.dma += dt; const k = S.dma; ring.position.y = -Math.pow(k * 1.9, 2.4); ring.rotation.y += dt * k * 0.35; head.position.y = tail.position.y = ring.position.y; const cam = ctx.camera; if (k < 0.9) { cam.position.y = damp(cam.position.y, S.camY + 3, 3, dt); cam.rotation.set(clamp(-0.3 + k, -0.4, 0), look.yaw, 0, 'YXZ'); } else { const speed = Math.min(1, (k - 0.9) / 2.2); S.z = (S.z || 0) - dt * (60 + speed * 1400); cam.rotation.set(0, 0, 0, 'YXZ'); cam.position.set(Math.sin(k * 3.1) * 0.5 * speed, 2 + Math.cos(k * 2.3) * 0.4 * speed, 0); streaks.position.z = S.z % 900; streaks.material.opacity = speed; if (ctx.scene.fog) { ctx.scene.fog.near = 26 + speed * 200; ctx.scene.fog.far = 180 + speed * 900; } ctx.scene.background.setRGB(1 - speed, 1 - speed * 0.96, 1 - speed * 0.9); ctx.link.glitch = Math.max(0, speed - 0.75) * 1.2; if (!S.screamed && speed > 0.3) { S.screamed = true; S.sc = A.scream(0.05); } if (S.sc && speed > 0.99) { S.sc.stop(); S.sc = null; } if (speed >= 1 && !S.done) { S.done = true; ctx.link.glitch = 0; (async () => { await ctx.card('HANDSHAKE', 'a love story in 64 hops', 3400); ctx.next(); })(); } } }, async unmount(ctx) { if (S.sc) { S.sc.stop(); S.sc = null; } ctx.link.glitch = 0; In.unlock(); }, };