/*
Copyright (C) 2024 memdmp
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
*/
export const biosSteps = 3;
export const biosStepInterval = 1000;
const wpmToTypingSpeed = (wpm: number, avgWordLen: number) =>
(1 / ((wpm * avgWordLen) / 60)) * 1000;
const averageWordLength = 4.793;
const typingInfo = (wpm = 80) => ({
typingSpeedAvg: wpmToTypingSpeed(wpm, averageWordLength),
typingDeviation: 20, // typing speed deviation is often correlated to the typing speed - TODO: at way lower speeds, the correlation is inverse; the more deviation there is (is this also the case for fast typing? what is the inflection point if not? i forgor i went down a rabbit hole yrs ago)
});
export type TypingSpeed = ReturnType;
export const getDelay = (typingInfo: TypingSpeed) =>
(Math.random() * 2 - 1) * typingInfo.typingDeviation +
typingInfo.typingSpeedAvg;
export const login = {
username: 'lain',
passwordLength: 12,
...typingInfo(100), // one usually has muscle memory for well-known frequently-typed credentials, hence higher wpm
};
export const ttyTypingInfo = typingInfo();
export type RenderBlock = {
value: string;
colour?: string;
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
italic?: boolean;
underlined?: boolean;
url?:
| `newtab:${string}`
| `currenttab:${string}`
| ((textObj: TTYText & { kind: 'text' }) => void);
bg?: string;
raw?: boolean;
dl?: string;
};
export type TTYText =
| {
kind: 'text';
renderrestriction?: 'everywhere' | 'js-only' | 'noscript';
value: RenderBlock[];
id: string;
classes: string[];
}
| {
kind: 'removeNode';
removedId: string;
removedItemClassList: string[];
}
| {
kind: 'time';
delay: number;
}
| {
kind: 'cursorVisibility';
visible: boolean;
}
| {
kind: 'clear';
};
export type Only = {
[k in Keys]: Obj[k];
};
export type Diff<
T extends {
[k: string]: unknown;
},
U extends keyof T,
> = ({ [P in keyof T]: P } & { [P in U]: never } & {
[x: string]: never;
})[keyof T];
export type Omit<
T extends {
[k: string]: unknown;
},
K extends keyof T,
> = Pick>;
export const ttyLines: TTYText[] = (() => {
const lines: TTYText[] = [];
let ids: string[] = [];
const byId = new Map();
let i = 69420;
let defaultRenderRestriction: Only<
TTYText & { kind: 'text' },
'renderrestriction'
>['renderrestriction'] = 'everywhere';
type LimitedRenderBlock = Omit;
type RenderBlockArg = [text: string, options?: LimitedRenderBlock];
/** due to hellish css constraints, each text() call creates its own line - however, each block in a text() is joined on the same line. */
const text = (
globalOptions:
| Only
| RenderBlockArg,
...blocks: RenderBlockArg[]
) => {
const id = (++i).toString();
ids.push(id);
if (Array.isArray(globalOptions)) {
blocks.unshift(globalOptions);
globalOptions = {};
}
const txt = {
kind: 'text',
renderrestriction:
globalOptions.renderrestriction ?? defaultRenderRestriction,
value: blocks.map(([t, o]) => ({
value: t,
colour: '#b9b9b9',
...o,
})),
id,
classes: [`ttytext-${id}`],
} as TTYText;
lines.push(txt);
byId.set(id, txt);
return id;
};
const wait = (time: number) =>
lines.push({
kind: 'time',
delay: time,
});
const del = (id: string | number) => {
const removedId =
typeof id === 'string' ? id : id >= 0 ? ids[id] : ids[ids.length + id];
const r = byId.get(removedId);
if (r?.kind === 'text') {
lines.push({
kind: 'removeNode',
removedId,
removedItemClassList: r.classes,
});
r.classes.push('ttytext-removed-after-done');
}
ids = ids.filter((v) => v !== removedId);
};
const delSince = (id: string | number) => {
const removedId =
typeof id === 'string' ? id : id >= 0 ? ids[id] : ids[ids.length + id];
const idIndex = ids.indexOf(removedId);
for (let i = idIndex; i < ids.length; i++) {
const removedId = ids[i];
const r = byId.get(removedId);
if (r?.kind === 'text') {
r.classes.push('ttytext-removed-after-done');
lines.push({
kind: 'removeNode',
removedId,
removedItemClassList: r.classes,
});
}
}
ids = ids.filter((_, i) => i < idIndex);
};
const getLast = () => {
const v = byId.get(ids[ids.length - 1]);
if (v?.kind == 'text') {
return v.value;
} else return null;
};
const overwriteLast = (...newValue: RenderBlockArg[]) => {
const v = byId.get(ids[ids.length - 1]);
const l = v?.kind == 'text' ? v.renderrestriction : 'everywhere';
del(-1);
return text(
{
renderrestriction: l,
},
...newValue,
);
};
const replaceLast = (
handler: (newValue: RenderBlockArg[]) => RenderBlockArg[],
) =>
overwriteLast(
...handler(
getLast()!.map(
(v) =>
[
v.value,
Object.fromEntries(
Object.entries(v).filter(([k]) => k !== 'value'),
),
] as const,
),
),
);
const everyTTYClear: (() => void)[] = [];
const clear = () => {
if (lines.find((v) => v.kind === 'clear')) {
delSince(lines.findLastIndex((v) => v.kind === 'clear'));
}
lines.push({ kind: 'clear' });
everyTTYClear.forEach((v) => v());
};
everyTTYClear.push(() => {
text(
{
renderrestriction: 'noscript',
},
[
'This browser does not support JS. Your experience may be degraded.',
{
bg: '#ff0000',
colour: '#dedede',
},
],
['\n', {}],
);
});
everyTTYClear.forEach((v) => v());
//
wait(300);
text({ renderrestriction: 'js-only' }, [
((v) => v[Math.floor(Math.random() * v.length)])([
'cool beats are stored in the balls',
'found xml documents in the firmware',
'overhead: "I don\'t consent" "hey thats my line"',
'uwu',
'i regret making this hellhole of a codebase',
]),
{
colour: '#777777',
},
]);
wait(1000);
clear();
text(
[
'lain',
{
colour: '#FFFF53',
weight: 700,
},
],
[' in '],
[
'mem.estrogen.zone',
{
colour: '#17B117',
weight: 700,
},
],
[' in '],
[
'~',
{
colour: '#53FFFF',
weight: 700,
},
],
);
text(
['❯ ', { colour: '#53FF53', weight: 600 }],
[''],
['', { colour: '#777777' }],
);
{
const targetstr = '/usr/bin/env wel';
const completions = [
'/bin/sh -c "$(/usr/bin/env curl -fsSL https://blahaj.estrogen.zone/)"',
'/usr/local/bin/become-estradiol',
'/usr/bin/shellutil ansi cheatsheet 24bit',
'/usr/bin/env sh',
'/usr/bin/env wc -l src/routes/anim.css',
'/usr/bin/env welcome -c ~/.local/share/welcome.toml',
];
for (const c of [...targetstr.split(''), 'COMPLETE']) {
replaceLast((l) => {
const prompt = l[l.length - 2];
const suggestions = l[l.length - 1];
if (c === 'COMPLETE') {
prompt[0] += suggestions[0];
suggestions[0] = '';
} else {
prompt[0] += c;
const completion = completions.find((v) => v.startsWith(prompt[0]));
if (completion) {
suggestions[0] = completion.substring(prompt[0].length);
} else suggestions[0] = '';
}
return l;
});
if (c === 'COMPLETE') {
wait(100);
} else wait(50 + Math.random() * 100);
}
text(['Preparing...']);
wait(200);
}
wait(1000);
clear();
text(['Welcome to'], [''], [' the Estrogen Zone!\n\n']);
wait(300);
for (const c of ' my corner of'.split('')) {
replaceLast((l) => {
l[1][0] += c;
return l;
});
wait(70);
}
text([
'Places you can find me:',
{
colour: '#7a7a7a',
},
]);
wait(300);
text(
[
' - ',
{
colour: '#7a7a7a',
},
],
['estrogen.zone git: ', { colour: '#cdcdcd' }],
);
wait(100);
replaceLast((v) => [
...v,
[
'git.estrogen.zone',
{
colour: '#99aaff',
underlined: true,
weight: 700,
url: 'newtab:https://git.estrogen.zone',
},
],
]);
wait(100);
text(
[
' - ',
{
colour: '#7a7a7a',
},
],
['On jsr: ', { colour: '#cdcdcd' }],
);
wait(600);
replaceLast((v) => [
...v,
[
'jsr.io/@memdmp',
{
colour: '#f7df23',
underlined: true,
weight: 700,
url: 'newtab:https://jsr.io/@memdmp',
},
],
]);
wait(200);
text(
[
' - ',
{
colour: '#7a7a7a',
},
],
['In random hackerspaces', { colour: '#aaaaaa' }],
);
wait(600);
text([
'\nYou may find these useful:',
{
colour: '#7a7a7a',
},
]);
wait(400);
text(
[
' - ',
{
colour: '#7a7a7a',
},
],
['GPG Root Key: ', { colour: '#cdcdcd' }],
);
wait(1000);
replaceLast((v) => [
...v,
[
'B546778F06BBCC8EC167DB3CD919706487B8B6DE',
{
colour: '#58C7F3',
underlined: true,
weight: 700,
url: `currenttab:/keys/memdmp/primary.pgp`,
dl: 'memdmp-primary.pgp',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
' (root key)',
{
colour: '#999999',
},
],
]);
wait(400);
text([
' - ',
{
colour: '#7a7a7a',
},
]);
wait(70);
replaceLast((v) => [
...v,
['GPG Git Key: ', { colour: '#cdcdcd' }],
]);
wait(100);
replaceLast((v) => [
...v,
[
'5134F05BD8D9DB8C6C0E1515A9439D346AB6DF4E',
{
colour: '#F0A3B3',
underlined: true,
weight: 700,
url: `currenttab:/keys/memdmp/git.pgp`,
dl: 'memdmp-git.pgp',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
' (',
{
colour: '#999999',
},
],
[
'signed',
{
colour: '#F0A3B3',
underlined: true,
weight: 500,
url: `currenttab:/keys/memdmp/git.pgp.sig`,
dl: 'memdmp-git.pgp.sig',
},
],
[')', { colour: '#999999' }],
]);
wait(45);
text([
' - ',
{
colour: '#7a7a7a',
},
]);
wait(70);
replaceLast((v) => [
...v,
[
'GPG Release Signing Key: ',
{
colour: '#cdcdcd',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
'0D93102265071798C7B65A4C9F0739B9E0C8FD60',
{
colour: '#ffffff',
underlined: true,
weight: 700,
url: `currenttab:/keys/memdmp/release.pgp`,
dl: 'memdmp-release.pgp',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
' (',
{
colour: '#999999',
},
],
[
'signed',
{
colour: '#ffffff',
underlined: true,
weight: 500,
url: `currenttab:/keys/memdmp/release.pgp.sig`,
dl: 'memdmp-release.pgp.sig',
},
],
[')', { colour: '#999999' }],
]);
wait(100);
text([
' - ',
{
colour: '#7a7a7a',
},
]);
wait(35);
replaceLast((v) => [
...v,
[
'Source Code: ',
{
colour: '#cdcdcd',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
'git.estrogen.zone/mem-estrogen-zone',
{
colour: '#F0A3B3',
underlined: true,
weight: 700,
url: 'newtab:/upstream/',
},
],
]);
wait(100);
text([
' - ',
{
colour: '#7a7a7a',
},
]);
wait(35);
replaceLast((v) => [
...v,
[
'Canaries: ',
{
colour: '#cdcdcd',
},
],
]);
wait(100);
replaceLast((v) => [
...v,
[
'./canaries/',
{
colour: '#58C7F3',
underlined: true,
weight: 700,
url: `currenttab:/canaries/`,
},
],
]);
wait(5000);
text([
``,
{
raw: true,
url: () => alert('i abused too much css for this i wanna cry now'),
},
]);
wait(200);
text(
{ renderrestriction: 'js-only' },
[
'next time, you may want to ',
{
colour: '#fff2',
},
],
[
`skip the animation
`,
{
url: `currenttab:/skip-animation`,
colour: '#fff2',
underlined: true,
raw: true,
},
],
);
//
return lines;
})();