From df02345a61db9c6c114a13d4c79ad9c40f46a6da Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 2 Aug 2026 16:09:46 +1000 Subject: [PATCH] Tuesday, and a room with people in it ROOT now reads the save it has been carrying the whole game. The receipt names your philes, your expiries, the baud you negotiated and lived with, whether somebody vouched for you, whether you hung up without saying goodbye. It had all of that and used none of it. And the FidoNet message comes back. You wrote it at hop 35 and handed it to the 04:00 mail run and it said it would reach the rest of the world by Tuesday. Twenty-nine hops later it is Tuesday, and somebody replied, because somebody always did. The loop now keeps your philes and resets everything else. TIME_WAIT is a room instead of six chairs. Sixteen seated, one or two lines each, most of it not about you. One of them ages out mid-sentence and nobody in the room looks up. FIN will not tell you about the door until you have listened to three people who are not him. Stay a full two-minute lap of the clock and the room notices. Co-Authored-By: Claude Opus 5 --- src/levels/root.js | 95 ++++++++++++++++++++++++++++++++-- src/levels/timewait.js | 113 ++++++++++++++++++++++++++++++----------- src/philes.js | 37 ++++++++++++++ 3 files changed, 210 insertions(+), 35 deletions(-) diff --git a/src/levels/root.js b/src/levels/root.js index b3961e7..edfe146 100644 --- a/src/levels/root.js +++ b/src/levels/root.js @@ -8,6 +8,18 @@ import * as THREE from '../../vendor/three.module.js'; import * as A from '../audio.js'; import * as In from '../input.js'; import { Body, Look, sprite, glow, damp, clamp } from '../kit.js'; +import { TOTAL as PHILE_TOTAL } from '../philes.js'; +import { BAUDS } from '../link.js'; + +// Somebody always replied. That was the whole reason anybody ran a board. +const REPLIES = [ + 'saw this come in on the 04:00 run. no idea who you are. hello anyway.', + 'four days for eleven words. worth it. write again.', + 'this reached me via three boards and a long distance call somebody else paid for.\nwhoever you are: received.', + 'i read this out to the room. the room says hello back.', + 'came in overnight, printed it, stuck it on the wall. that is what we do with these.', + 'i do not know your area code and i am never going to. still — got it. got you.', +]; let S = {}; @@ -132,6 +144,8 @@ export default { ctx.say('four packets. that is the whole ceremony.', 4200); await ctx.sleep(4400); + await this.tuesday(ctx); + // ── back in the beige room, with FIN, who told you await ctx.card('TIME_WAIT', 'two minutes of maximum segment lifetime', 3600, 'warm'); ctx.say('told you.', 3000, 'FIN'); @@ -139,17 +153,90 @@ export default { ctx.say('it was worth it.', 3000); await ctx.sleep(3400); - ctx.phile('root', 'root', 'the room was empty. the reply was not.'); - await ctx.card('HANDSHAKE', `${ctx.game.philes.size} philes · ${ctx.game.deaths} expiries · TTL ${ctx.game.ttl}`, 5200); + ctx.phile('root'); + await this.receipt(ctx); // ── and it starts again, and that is not a punishment - ctx.say('new sequence number. TTL 64.', 4000); - await ctx.sleep(2400); + ctx.say('new sequence number. TTL 64. the philes stay.', 4400); + await ctx.sleep(3000); + const philes = new Set(ctx.game.philes); ctx.game.ttl = 64; ctx.game.hop = 1; + ctx.game.cargo = []; + ctx.game.mtu = 1500; + ctx.game.flags = Object.create(null); + ctx.game.flags.looped = (ctx.game.flags.looped || 0) + 1; + ctx.game.philes = philes; ctx.goto('coldboot'); }, + // ── Tuesday. + // You wrote it at hop 35 and handed it to the four in the morning mail run, and it + // went out one modem call at a time while you were asleep, and it said it would + // reach the rest of the world by Tuesday. Twenty-nine hops later, it is Tuesday. + async tuesday(ctx) { + const msg = ctx.game.flags.fidoMessage; + if (!msg) return; + + await ctx.sleep(1400); + A.relayClick(0.22, 0.5); + ctx.say('something else is still coming.', 3400); + await ctx.sleep(3600); + + S.fido = new THREE.Group(); + const env = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.7, 0.06), + new THREE.MeshBasicMaterial({ color: 0x14232c })); + S.fido.add(env); + const tag = sprite('1:129/22 → 1:1/0 → the world', { color: '#54707e', size: 0.32 }); + tag.position.y = 0.9; S.fido.add(tag); + S.fido.position.set(0, 1.4, -120); + ctx.scene.add(S.fido); + + for (let i = 0; i < 70; i++) { + S.fido.position.z = damp(S.fido.position.z, -5, 0.8, 0.06); + await ctx.sleep(60); + } + await ctx.sleep(700); + + ctx.say('you wrote this at four in the morning, twenty-nine hops ago,
and handed it to a modem that dialled another modem.', 5600); + await ctx.sleep(5800); + ctx.say(`“${msg}”`, 5200); + await ctx.sleep(5400); + ctx.say('it said it would get there by tuesday.', 3600); + await ctx.sleep(3800); + ctx.say('it is tuesday.', 3200); + await ctx.sleep(3400); + + // and somebody replied, because somebody always did + A.burst(1320, 1760, 0.24, 0.14); + ctx.say(REPLIES[(msg.length + ctx.game.philes.size) % REPLIES.length], 6000, 'RE: (no subject)'); + await ctx.sleep(6200); + ctx.phile('tuesday', 'the reply', `you wrote: “${msg}”\n\nand four days later, from somewhere, somebody wrote back.`); + ctx.scene.remove(S.fido); + await ctx.sleep(1200); + }, + + // ── the receipt. it has had all of this the whole time and never once used it. + async receipt(ctx) { + const g = ctx.game; + const f = g.flags; + const lines = []; + lines.push(`${g.philes.size} philes of ${PHILE_TOTAL}`); + lines.push(g.deaths === 0 ? 'you never once expired' : + g.deaths === 1 ? 'you expired once' : `you expired ${g.deaths} times`); + if (f.baudEarned !== undefined) { + lines.push(f.baudEarned >= 6 ? 'it agreed to everything you asked for' + : f.baudEarned <= 1 ? 'you saw most of this in letters' + : `you negotiated ${BAUDS[f.baudEarned].label} and lived with it`); + } + if (f.elite) lines.push('somebody vouched for you'); + if (f.knowsTheTrunk) lines.push('you hung up without saying goodbye'); + if (g.cargo.length) lines.push(`still carrying ${g.cargo.length}`); + if (f.looped) lines.push(`round ${f.looped + 1}`); + + await ctx.card('HANDSHAKE', lines.join(' · '), 5600); + }, + async unmount(ctx) { A.stopPad(); In.unlock(); diff --git a/src/levels/timewait.js b/src/levels/timewait.js index 936b510..9242c30 100644 --- a/src/levels/timewait.js +++ b/src/levels/timewait.js @@ -7,33 +7,43 @@ import * as A from '../audio.js'; import * as In from '../input.js'; import { Body, Look, sprite, panel, rng, damp } from '../kit.js'; +// Sixteen of them. One or two lines each, and most of it is not about you — a +// waiting room is oppressive because of how many people are in it, not because of +// how much any one of them has to say. const SEATED = [ - { name: 'SYN', lines: [ - 'i sent mine in ninety-eight.', - 'sequence number four one two, window eight thousand one hundred and ninety two.', - 'i still know it. you never stop knowing it.', - ]}, - { name: 'SYN', lines: [ - 'you get used to the light.', - 'it is fifty hertz. everything in here is fifty hertz.', - ]}, - { name: 'SYN', lines: [ - "it isn't that they said no.", - "they didn't say anything.", - 'i would have taken a reset. a reset is an answer.', - ]}, - { name: 'SYN', lines: [ - 'the clock is for the segments. not for us.', - 'it still helps.', - ]}, - { name: 'SYN', lines: [ - 'first time?', - 'you look like you still think this is a corridor.', - ]}, - { name: 'SYN', lines: [ - 'somebody came back once. through there.', - 'i did not ask what it was like. i think that was rude of me.', - ]}, + { name: 'SYN', lines: ['i sent mine in ninety-eight.', + 'sequence number four one two. i still know it. you never stop knowing it.'] }, + { name: 'SYN', lines: ['you get used to the light.', + 'it is fifty hertz. everything in here is fifty hertz.'] }, + { name: 'SYN', lines: ["it isn't that they said no.", "they didn't say anything.", + 'i would have taken a reset. a reset is an answer.'] }, + { name: 'SYN', lines: ['the clock is for the segments. not for us.', 'it still helps.'] }, + { name: 'SYN', lines: ['first time?', 'you look like you still think this is a corridor.'] }, + { name: 'SYN', lines: ['somebody came back once. through there.', + 'i did not ask what it was like. i think that was rude of me.'] }, + { name: 'SYN', lines: ['do not sit down.'] }, + { name: 'SYN', lines: ['there is no reception. there is not a desk. i have looked.'] }, + { name: 'SYN', lines: ['my window was eight kilobytes.', 'that was a lot, then.'] }, + { name: 'SYN', lines: ['i keep thinking i hear the door.'] }, + { name: 'SYN', lines: ['he is going to tell you to turn round.', 'he is right. nobody ever does.'] }, + { name: 'SYN', lines: ['...'] }, + { name: 'SYN', lines: ['i was retransmitted four times. four.', + 'you would think after the second one somebody would have said something.'] }, + { name: 'SYN', lines: ['we are not being punished. i want that on the record.', + 'this is just what the protocol says happens.'] }, + { name: 'SYN', lines: ['what was yours?', '...', 'no. sorry. that was rude too.'] }, + { name: 'SYN', lines: ['two minutes is not very long.', 'it is per segment. not per us.'] }, +]; + +// The one that goes. It happens mid-sentence, quietly, and nobody in the room reacts, +// because everybody in the room has watched it happen a great many times. +const AGE_OUT = 'i was going to say —'; + +// He will not talk to somebody who walked the length of the room without stopping. +const FIN_REFUSE = [ + 'there is a room full of them behind you and you have come straight to the door.', + 'go and talk to them. they have been waiting longer than you have.', + 'one more. then i will tell you about the door.', ]; const FIN_LINES = [ @@ -56,7 +66,8 @@ export default { async mount(ctx) { const { scene, camera } = ctx; - S = { t: 0, near: null, talking: null, li: 0, finDone: false, door: null, cool: 0 }; + S = { t: 0, near: null, talking: null, li: 0, finDone: false, door: null, cool: 0, + listened: 0, lapped: false, doomed: null }; scene.background = new THREE.Color(0x1a1712); scene.fog = new THREE.Fog(0x1a1712, 8, 46); @@ -64,7 +75,7 @@ export default { // beige. institutional. under a light that has never been replaced. const beige = new THREE.MeshStandardMaterial({ color: 0xbfae90, roughness: 0.95 }); const floorMat = new THREE.MeshStandardMaterial({ color: 0x6e6252, roughness: 0.85 }); - const W = 26, D = 18, H = 3.4; + const W = 30, D = 22, H = 3.4; const floor = new THREE.Mesh(new THREE.BoxGeometry(W, 0.4, D), floorMat); floor.position.y = -0.2; scene.add(floor); @@ -98,11 +109,13 @@ export default { const legMat = new THREE.MeshStandardMaterial({ color: 0x9aa0a4, roughness: 0.4, metalness: 0.6 }); const rand = rng(2); S.people = []; - const rows = [[-4.5, -1], [-4.5, 3], [4.5, -1], [4.5, 3]]; + // six rows of five, three-quarters of them occupied. it should look like a place + // that has been filling up for a very long time and never empties. + const rows = [[-5.0, -6], [-5.0, 6], [-1.0, -6], [-1.0, 6], [3.0, -6], [3.0, 6]]; let idx = 0; rows.forEach(([bz, bxo], ri) => { for (let i = 0; i < 5; i++) { - const x = bxo + (i - 2) * 1.25 + (ri > 1 ? 6 : -6); + const x = bxo + (i - 2) * 1.3; const z = bz; const seat = new THREE.Mesh(new THREE.BoxGeometry(1.05, 0.1, 0.95), chairMat); seat.position.set(x, 0.5, z); scene.add(seat); @@ -123,11 +136,13 @@ export default { g.add(halo); g.position.set(x, 0.92, z); scene.add(g); - S.people.push({ g, core, halo, x, z, data: SEATED[idx], phase: rand() * 6.28, spoken: 0 }); + S.people.push({ g, core, halo, x, z, data: SEATED[idx], phase: rand() * 6.28, spoken: 0, out: 0 }); idx++; } } }); + // one of them is nearly out of time. we do not mark which, and they do not know. + S.doomed = S.people[Math.floor(S.people.length * 0.6)]; // the clock. everybody is watching it. it is not for them. const clockG = new THREE.Group(); @@ -180,6 +195,7 @@ export default { ctx.hint('WASDwalk
Etalk
CLICKcapture mouse'); await ctx.card('TIME_WAIT', 'hop 9 · two minutes of maximum segment lifetime', 3200); ctx.say('nobody here is going anywhere. it is not a punishment. it is a protocol.', 4800); + setTimeout(() => ctx.say('there is nothing to do in here. that is not an oversight.', 5200), 5400); }, update(dt, t, ctx) { @@ -200,17 +216,44 @@ export default { // people breathe. that is all they do. S.people.forEach(p => { + if (p.gone) { + // aged out. the chair is still there. it goes on being a chair. + p.out = Math.min(1, p.out + dt * 0.55); + p.g.scale.setScalar(1 - p.out); + p.halo.material.opacity = 0.35 * (1 - p.out); + p.g.visible = p.out < 0.99; + return; + } p.g.position.y = 0.92 + Math.sin(S.t * 0.9 + p.phase) * 0.02; p.halo.rotation.y += dt * 0.15; const d = Math.hypot(b.pos.x - p.x, b.pos.z - p.z); p.core.material.color.setHex(d < 2.2 ? 0x9fc4d6 : 0x6f8fa0); }); + + // ── somebody ages out, mid-sentence, and nobody in the room reacts, because + // everybody in the room has watched this happen a great many times. + if (S.doomed && !S.doomed.gone && S.t > 38) { + S.doomed.gone = true; + if (S.near === S.doomed) S.near = null; + ctx.say(AGE_OUT, 1100, 'SYN'); + setTimeout(() => ctx.hush(), 1100); + ctx.phile('ageout', '2MSL expired', + 'they were halfway through a sentence. nobody in the room looked up.'); + } + + // ── a full lap of the clock. two minutes is MSL, and the room notices you did it. + if (!S.lapped && S.t > 120) { + S.lapped = true; + ctx.say('you have been in here for one full maximum segment lifetime.
they have all been in here for more than one.', 6400); + ctx.phile('msl', 'a full lap', 'you waited out the whole clock on purpose. nobody asked you to.'); + } S.fin.g.position.y = 1.0 + Math.sin(S.t * 1.1) * 0.03; S.fin.halo.rotation.y -= dt * 0.25; // who are we close to let near = null, bestD = 2.4; for (const p of S.people) { + if (p.gone) continue; const d = Math.hypot(b.pos.x - p.x, b.pos.z - p.z); if (d < bestD) { bestD = d; near = p; } } @@ -223,9 +266,17 @@ export default { S.cool = Math.max(0, S.cool - dt); if (In.useHit() && near && S.cool <= 0) { + // ── FIN will not talk to somebody who walked straight past everybody else. + if (near === S.fin && S.listened < 3) { + S.cool = 0.6; + ctx.say(FIN_REFUSE[Math.min(S.listened, FIN_REFUSE.length - 1)], 3800, 'FIN'); + S.listened += 0.001; // does not count. talking to him is not listening. + return; + } const d = near.data; const line = d.lines[Math.min(near.spoken, d.lines.length - 1)]; ctx.say(line, 3600, d.name); + if (near !== S.fin && near.spoken === 0) S.listened++; near.spoken++; S.cool = 0.5; if (near !== S.fin) ctx.phile('timewait'); diff --git a/src/philes.js b/src/philes.js index 6680b32..c20b30c 100644 --- a/src/philes.js +++ b/src/philes.js @@ -103,6 +103,31 @@ will finally have to care. they were right and it did not help anybody.`, }, + 'ageout': { + title: '2MSL expired', + from: 'the chair by the wall', + body: `they were halfway through a sentence. + +nobody in the room looked up. not one of them. it is not callousness — it +is that every single person in there has watched it happen more times than +they can count, and there is no ceremony for it, and inventing one would +be worse. + +the chair is still there. it goes on being a chair.`, + }, + + 'msl': { + title: 'a full lap', + from: 'two minutes', + body: `you stayed for the whole clock. + +nothing was down here. no item, no door, no achievement. the clock is for +the segments, not for you, and you waited it out anyway on the off chance +that waiting was the point. + +it was, a bit.`, + }, + 'irc': { title: 'how a channel is owned', from: 'a text file with no author on it', @@ -560,6 +585,18 @@ helpful. that was the bug.`, // ── hop 64 ───────────────────────────────────────────────────────────── + 'tuesday': { + title: 'the reply', + from: 'the 04:00 run, four days later', + body: `you wrote something at four in the morning and gave it to a modem, and the +modem dialled another modem, and that one dialled another, all night, +across the country, while everyone involved was asleep. + +it said it would get there by tuesday. + +nothing since has been slower and nothing since has been more certain.`, + }, + 'root': { title: 'root', from: 'uid 0',