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