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',