haxtory/src/kit.js
type-two 696b0c65e8 HANDSHAKE — 13 playable hops, one renderer
You are SYN. You want ACK. Root is the only one who can give it to you.

Three load-bearing ideas, all working:
  - TTL is the health bar and it only goes down. 64 hops, no healing.
  - Bandwidth is resolution. One render target, one shader; at 1200 baud
    and below the 3D scene renders as ASCII glyphs.
  - Every header you drop is a piece of clothing. Seven wireframe shells,
    one per OSI layer, shed on the way down.

Every frequency is the real frequency, synthesised, nothing sampled:
350+440 dial tone, the DTMF matrix, R1 MF pairs at real KP/digit timing,
2600 Hz, ANSam at 2100 reversing phase every 450 ms, Bell 103 FSK.

Thirteen hops, each a different genre over one renderer — FP tutorial,
OSI descent platformer, walking sim, EFnet twin-stick, 2D buffer overflow,
Morris worm RTS, LBL terminal stealth, a whole BBS with a playable LORD,
the five-boss handshake arena, the copper, a playable blue box, the climb,
and an empty room with an ACK in it.

No engine, no build step, one vendored three.js.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 15:34:58 +10:00

285 lines
12 KiB
JavaScript

// kit.js — the shared parts. A body that collides with boxes, a camera rig that can
// be first or third person, text that floats, and a seeded random so that ASLR can
// reshuffle a level the same way twice when it wants to.
import * as THREE from '../vendor/three.module.js';
import * as In from './input.js';
// ───────────────────────────────────────────────────────────────── random
export function rng(seed) {
let a = seed >>> 0;
return () => {
a += 0x6D2B79F5; let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export const lerp = (a, b, t) => a + (b - a) * t;
export const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
export const damp = (a, b, l, dt) => lerp(a, b, 1 - Math.exp(-l * dt));
// ────────────────────────────────────────────────────────────────── text
const labelCache = new Map();
export function labelTexture(text, { fg = '#ffffff', bg = 'rgba(0,0,0,0)', px = 64, pad = 12, font = 'ui-monospace, Menlo, monospace', weight = 'bold' } = {}) {
const key = text + fg + bg + px + weight;
if (labelCache.has(key)) return labelCache.get(key);
const cv = document.createElement('canvas');
const g = cv.getContext('2d');
g.font = `${weight} ${px}px ${font}`;
const w = Math.ceil(g.measureText(text).width) + pad * 2;
cv.width = Math.max(2, w); cv.height = px + pad * 2;
const g2 = cv.getContext('2d');
g2.font = `${weight} ${px}px ${font}`;
g2.fillStyle = bg; g2.fillRect(0, 0, cv.width, cv.height);
g2.fillStyle = fg; g2.textBaseline = 'middle'; g2.textAlign = 'center';
g2.fillText(text, cv.width / 2, cv.height / 2);
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
tex.minFilter = THREE.LinearFilter;
labelCache.set(key, tex);
return tex;
}
export function sprite(text, { color = '#ffffff', size = 1, opacity = 1 } = {}) {
const tex = labelTexture(text, { fg: color });
const m = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity, depthWrite: false });
const s = new THREE.Sprite(m);
const ar = tex.image.width / tex.image.height;
s.scale.set(size * ar, size, 1);
// the new texture is a different width, so the scale has to follow it — otherwise
// a longer string gets squeezed into the old aspect and reads as garbage
s.userData.setText = (t2, c2) => {
const nt = labelTexture(t2, { fg: c2 || color });
m.map = nt; m.needsUpdate = true;
s.scale.set(size * (nt.image.width / nt.image.height), size, 1);
};
return s;
}
// Flat text panel that faces one way. For walls, signs, ANSI on plaster.
export function panel(text, { color = '#ffffff', size = 1, bg = 'rgba(0,0,0,0)' } = {}) {
const tex = labelTexture(text, { fg: color, bg });
const ar = tex.image.width / tex.image.height;
const m = new THREE.Mesh(
new THREE.PlaneGeometry(size * ar, size),
new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false, side: THREE.DoubleSide })
);
return m;
}
// ─────────────────────────────────────────────────────────────── materials
export function glow(color, intensity = 1) {
return new THREE.MeshBasicMaterial({ color: new THREE.Color(color).multiplyScalar(intensity) });
}
export function solid(color, { rough = 0.9, metal = 0.0, emissive = 0x000000, ei = 1 } = {}) {
return new THREE.MeshStandardMaterial({ color, roughness: rough, metalness: metal, emissive, emissiveIntensity: ei });
}
export function wire(color, opacity = 0.35) {
return new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity });
}
// ──────────────────────────────────────────────────────────────── the box world
export class World {
constructor() { this.boxes = []; this.tris = []; }
// add(x,y,z, w,h,d) — centred on x,z, y is the BOTTOM. Matches how you think
// about a platform.
add(x, y, z, w, h, d, tag = null) {
const b = { min: new THREE.Vector3(x - w / 2, y, z - d / 2), max: new THREE.Vector3(x + w / 2, y + h, z + d / 2), tag };
this.boxes.push(b);
return b;
}
addMesh(mesh, tag = null) {
mesh.updateMatrixWorld(true);
const bb = new THREE.Box3().setFromObject(mesh);
const b = { min: bb.min.clone(), max: bb.max.clone(), tag, mesh };
this.boxes.push(b);
return b;
}
clear() { this.boxes.length = 0; }
}
const _tmp = new THREE.Vector3();
export class Body {
constructor(world, { r = 0.4, h = 1.7, gravity = 26, speed = 7, air = 0.45, jump = 9.2 } = {}) {
this.world = world;
this.pos = new THREE.Vector3();
this.vel = new THREE.Vector3();
this.r = r; this.h = h;
this.gravity = gravity; this.speed = speed; this.air = air; this.jumpV = jump;
this.grounded = false; this.coyote = 0; this.buffer = 0; this.ground = null;
this.noclip = false; this.floorY = 0; this.hasFloor = true;
}
get feet() { return this.pos.y; }
aabb(p = this.pos) {
return { min: _tmp.set(p.x - this.r, p.y, p.z - this.r).clone(), max: new THREE.Vector3(p.x + this.r, p.y + this.h, p.z + this.r) };
}
static overlap(a, b) {
return a.min.x < b.max.x && a.max.x > b.min.x &&
a.min.y < b.max.y && a.max.y > b.min.y &&
a.min.z < b.max.z && a.max.z > b.min.z;
}
// Axis-separated sweep. Boring, correct at the edges, does not tunnel at these speeds.
move(dt, wishX, wishZ, wantJump) {
const accel = this.grounded ? 60 : 60 * this.air;
const tgt = { x: wishX * this.speed, z: wishZ * this.speed };
this.vel.x = damp(this.vel.x, tgt.x, this.grounded ? 18 : 6, dt);
this.vel.z = damp(this.vel.z, tgt.z, this.grounded ? 18 : 6, dt);
void accel;
if (wantJump) this.buffer = 0.12;
this.buffer = Math.max(0, this.buffer - dt);
this.coyote = this.grounded ? 0.11 : Math.max(0, this.coyote - dt);
if (this.buffer > 0 && this.coyote > 0) { this.vel.y = this.jumpV; this.grounded = false; this.coyote = 0; this.buffer = 0; }
this.vel.y -= this.gravity * dt;
if (this.vel.y < -60) this.vel.y = -60;
if (this.noclip) { this.pos.addScaledVector(this.vel, dt); return; }
const step = (axis, d) => {
if (d === 0) return;
this.pos[axis] += d;
const a = this.aabb();
for (const b of this.world.boxes) {
if (b.disabled) continue;
if (!Body.overlap(a, b)) continue;
if (b.tag === 'nx' && axis === 'y' && d < 0) continue; // you may not stand there
if (b.trigger) { b.hit = true; continue; }
if (axis === 'y') {
if (d < 0) { this.pos.y = b.max.y; this.vel.y = 0; this.grounded = true; this.ground = b; }
else { this.pos.y = b.min.y - this.h; this.vel.y = 0; }
} else {
if (d > 0) this.pos[axis] = (axis === 'x' ? b.min.x : b.min.z) - this.r;
else this.pos[axis] = (axis === 'x' ? b.max.x : b.max.z) + this.r;
this.vel[axis] = 0;
}
a.min.copy(this.aabb().min); a.max.copy(this.aabb().max);
}
};
this.grounded = false; this.ground = null;
step('y', this.vel.y * dt);
step('x', this.vel.x * dt);
step('z', this.vel.z * dt);
if (this.hasFloor && this.pos.y < this.floorY) { this.pos.y = this.floorY; this.vel.y = 0; this.grounded = true; }
}
}
// ─────────────────────────────────────────────────────────── camera rigs
export class Look {
constructor({ sens = 0.0022, maxPitch = 1.45 } = {}) {
this.yaw = 0; this.pitch = 0; this.sens = sens; this.maxPitch = maxPitch;
}
update() {
this.yaw -= In.mouse.dx * this.sens;
this.pitch -= In.mouse.dy * this.sens;
this.pitch = clamp(this.pitch, -this.maxPitch, this.maxPitch);
return this;
}
// Move axis rotated into world space by yaw. The camera looks down its own -Z,
// so forward is (-sin, -cos) and right is (cos, -sin). Get this wrong and W
// walks you backwards, which is exactly what it did the first time.
wish() {
const a = In.axis();
const s = Math.sin(this.yaw), c = Math.cos(this.yaw);
return { x: a.x * c + a.y * s, z: -a.x * s + a.y * c };
}
// unit forward vector on the ground plane
forward() { return { x: -Math.sin(this.yaw), z: -Math.cos(this.yaw) }; }
applyFirstPerson(cam, body, bob = 0) {
cam.position.set(body.pos.x, body.pos.y + body.h * 0.92 + bob, body.pos.z);
cam.rotation.set(0, 0, 0, 'YXZ');
cam.rotation.order = 'YXZ';
cam.rotation.y = this.yaw; cam.rotation.x = this.pitch;
}
applyThirdPerson(cam, body, dist = 7, height = 3.2, world = null) {
const dir = new THREE.Vector3(Math.sin(this.yaw), 0, Math.cos(this.yaw));
const want = new THREE.Vector3(
body.pos.x + dir.x * dist,
body.pos.y + height - this.pitch * 4,
body.pos.z + dir.z * dist
);
cam.position.lerp(want, 0.22);
cam.lookAt(body.pos.x, body.pos.y + body.h * 0.6, body.pos.z);
void world;
}
}
// ─────────────────────────────────────────────────────────────── the player
// SYN. A small bright thing with too many layers on.
export function makeSyn() {
const g = new THREE.Group();
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.34, 1), glow(0x9ff5ff, 1.6));
g.add(core);
const halo = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5, 1),
new THREE.MeshBasicMaterial({ color: 0x2ad8ff, transparent: true, opacity: 0.22, wireframe: true }));
g.add(halo);
// the headers. seven of them, and they come off.
const HEADERS = [
{ name: 'TLS', color: 0xffffff, r: 1.30 },
{ name: 'HTTP', color: 0xd8e8ff, r: 1.14 },
{ name: 'SESSION', color: 0xa8d8ff, r: 0.99 },
{ name: 'TCP', color: 0x7ec8ff, r: 0.86 },
{ name: 'IP', color: 0x58b0ff, r: 0.74 },
{ name: 'ETHERNET', color: 0x3c90e0, r: 0.63 },
{ name: 'VOLTAGE', color: 0x2a70b0, r: 0.55 },
];
const shells = HEADERS.map((h, i) => {
const m = new THREE.Mesh(
new THREE.IcosahedronGeometry(h.r, 1),
new THREE.MeshBasicMaterial({ color: h.color, wireframe: true, transparent: true, opacity: 0.30 - i * 0.02 })
);
m.userData.header = h.name;
g.add(m);
return m;
});
g.userData = {
core, halo, shells, headers: HEADERS, worn: shells.length,
shed() {
if (this.worn <= 0) return null;
this.worn--;
const s = shells[this.worn];
s.userData.shedding = 1;
return HEADERS[this.worn].name;
},
tick(t, dt) {
halo.rotation.y += dt * 0.8; halo.rotation.x += dt * 0.31;
core.rotation.y -= dt * 1.2;
const pulse = 1 + Math.sin(t * 3.1) * 0.06;
core.scale.setScalar(pulse);
shells.forEach((s, i) => {
if (i >= this.worn) {
s.userData.shedding = (s.userData.shedding ?? 1) - dt * 0.7;
const k = Math.max(0, s.userData.shedding);
s.scale.setScalar(1 + (1 - k) * 3.5);
s.material.opacity = k * 0.3;
s.visible = k > 0.01;
} else {
s.rotation.y += dt * (0.12 + i * 0.05) * (i % 2 ? -1 : 1);
s.rotation.z += dt * 0.07;
}
});
}
};
return g;
}
// ─────────────────────────────────────────────────────────────── lighting
export function threePointLight(scene, { key = 0xffffff, fill = 0x334455, keyI = 1.5, ambI = 0.35 } = {}) {
const a = new THREE.AmbientLight(fill, ambI); scene.add(a);
const d = new THREE.DirectionalLight(key, keyI); d.position.set(4, 12, 6); scene.add(d);
const d2 = new THREE.DirectionalLight(fill, keyI * 0.4); d2.position.set(-6, 4, -8); scene.add(d2);
return { a, d, d2 };
}