keyvoid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/bin/keyvoid.js +121 -0
- package/package.json +77 -0
- package/src/app.js +294 -0
- package/src/engine/permissions.js +107 -0
- package/src/engine/suppressor/helpers/linux-helper.py +192 -0
- package/src/engine/suppressor/helpers/macos-helper.swift +108 -0
- package/src/engine/suppressor/helpers/macos-pynput-helper.py +179 -0
- package/src/engine/suppressor/helpers/windows-helper.ps1 +144 -0
- package/src/engine/suppressor/index.js +306 -0
- package/src/scripts/postinstall.js +33 -0
- package/src/ui/components/counter.js +64 -0
- package/src/ui/components/header.js +102 -0
- package/src/ui/components/status-bar.js +71 -0
- package/src/ui/components/unvoid-button.js +78 -0
- package/src/ui/mouse.js +97 -0
- package/src/ui/renderer.js +113 -0
- package/src/ui/skins/arcade.js +530 -0
- package/src/ui/skins/cat.js +223 -0
- package/src/ui/skins/clean.js +155 -0
- package/src/ui/skins/hacker.js +194 -0
- package/src/ui/skins/prank.js +195 -0
- package/src/ui/skins/toddler.js +131 -0
- package/src/ui/skins/zen.js +169 -0
- package/src/ui/unlock-sequence.js +105 -0
- package/src/utils/big-digits.js +130 -0
- package/src/utils/colors.js +114 -0
- package/src/utils/terminal.js +119 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// ─── Cat Skin ────────────────────────────────────────────────────────
|
|
2
|
+
// Cat defense mode: ASCII cat art that reacts to keystrokes,
|
|
3
|
+
// warm amber/orange palette, paw swipe animations.
|
|
4
|
+
|
|
5
|
+
import { renderCounter, renderSmallCounter } from '../components/counter.js';
|
|
6
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
7
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
8
|
+
import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
|
|
9
|
+
import { centerText } from '../../utils/terminal.js';
|
|
10
|
+
import { formatCount } from '../../utils/big-digits.js';
|
|
11
|
+
|
|
12
|
+
const P = palettes.cat;
|
|
13
|
+
|
|
14
|
+
// ASCII cat art — different expressions
|
|
15
|
+
const CAT_IDLE = [
|
|
16
|
+
' /\\_____/\\ ',
|
|
17
|
+
' / o o \\ ',
|
|
18
|
+
' ( == ^ == ) ',
|
|
19
|
+
' ) ( ',
|
|
20
|
+
' ( ) ',
|
|
21
|
+
' ( ( ) ( ) ) ',
|
|
22
|
+
'(__(__)___(__)__)',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const CAT_ALERT = [
|
|
26
|
+
' /\\_____/\\ ',
|
|
27
|
+
' / ◉ ◉ \\ ',
|
|
28
|
+
' ( == ^ == ) ',
|
|
29
|
+
' ) \\_____/ ( ',
|
|
30
|
+
' ( \\ / ) ',
|
|
31
|
+
' ( ( ) ( ) ) ',
|
|
32
|
+
'(__(__)___(__)__)',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const CAT_SWIPE = [
|
|
36
|
+
' /\\_____/\\ ',
|
|
37
|
+
' / ◉ ◉ \\ ',
|
|
38
|
+
' ( == ^ == ) ',
|
|
39
|
+
' ) \\_____/ ( ',
|
|
40
|
+
' ( )=======| ',
|
|
41
|
+
' ( ( ) ( ) ) ',
|
|
42
|
+
'(__(__)___(__)__)',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const CAT_ANGRY = [
|
|
46
|
+
' /\\_____/\\ ',
|
|
47
|
+
' / \\◉ ◉/ \\ ',
|
|
48
|
+
' ( == ^ == ) ',
|
|
49
|
+
' ) HISSSS ( ',
|
|
50
|
+
' ( \\|||||||/ ) ',
|
|
51
|
+
' ( ( ) ( ) ) ',
|
|
52
|
+
'(__(__)___(__)__)',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const PAW_FRAMES = [
|
|
56
|
+
' ╲ ',
|
|
57
|
+
' ╲╲ ',
|
|
58
|
+
' ╲╲╲ ',
|
|
59
|
+
'╲╲╲╲ ',
|
|
60
|
+
' 🐾 ',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const REACTIONS = [
|
|
64
|
+
'BLOCKED!', 'NOPE!', 'PAW DENIED!', 'NOT TODAY!',
|
|
65
|
+
'*hiss*', '*swipe*', 'MY KEYBOARD!', 'NICE TRY!',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
let catState = 'idle';
|
|
69
|
+
let catTimer = 0;
|
|
70
|
+
let lastCatKeyCount = 0;
|
|
71
|
+
let currentReaction = 'BLOCKED!';
|
|
72
|
+
let pawFrame = -1;
|
|
73
|
+
let streak = 0;
|
|
74
|
+
let maxStreak = 0;
|
|
75
|
+
|
|
76
|
+
export function renderCat(cols, rows, state, tick) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
const { keyCount = 0 } = state;
|
|
79
|
+
|
|
80
|
+
// Background
|
|
81
|
+
const bgLine = `${P.bg}${' '.repeat(cols)}${RESET}`;
|
|
82
|
+
for (let i = 0; i < rows; i++) lines.push(bgLine);
|
|
83
|
+
|
|
84
|
+
// Update cat state on new key
|
|
85
|
+
if (keyCount > lastCatKeyCount) {
|
|
86
|
+
const diff = keyCount - lastCatKeyCount;
|
|
87
|
+
streak += diff;
|
|
88
|
+
if (streak > maxStreak) maxStreak = streak;
|
|
89
|
+
|
|
90
|
+
if (streak > 20) {
|
|
91
|
+
catState = 'angry';
|
|
92
|
+
} else if (streak > 5) {
|
|
93
|
+
catState = 'swipe';
|
|
94
|
+
pawFrame = 0;
|
|
95
|
+
} else {
|
|
96
|
+
catState = 'alert';
|
|
97
|
+
}
|
|
98
|
+
catTimer = tick;
|
|
99
|
+
currentReaction = REACTIONS[Math.floor(Math.random() * REACTIONS.length)];
|
|
100
|
+
lastCatKeyCount = keyCount;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Reset cat state after a delay
|
|
104
|
+
if (tick - catTimer > 15 && catState !== 'idle') {
|
|
105
|
+
catState = 'idle';
|
|
106
|
+
if (tick - catTimer > 30) streak = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Advance paw animation
|
|
110
|
+
if (pawFrame >= 0) {
|
|
111
|
+
pawFrame = Math.min(pawFrame + 1, PAW_FRAMES.length - 1);
|
|
112
|
+
if (tick - catTimer > 10) pawFrame = -1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Banner ──
|
|
116
|
+
const bannerText = '🐱 CAT DEFENSE ACTIVE 🐱';
|
|
117
|
+
const bannerFlash = tick % 10 < 5 ? P.primary : P.secondary;
|
|
118
|
+
lines[1] = `${P.bg}${centerText(`${bannerFlash}${BOLD}${bannerText}`, cols)}${RESET}`;
|
|
119
|
+
|
|
120
|
+
// Animated banner border
|
|
121
|
+
const borderChar = tick % 6 < 3 ? '═' : '━';
|
|
122
|
+
const bannerWidth = Math.min(40, cols - 10);
|
|
123
|
+
lines[2] = `${P.bg}${centerText(`${P.dim}${borderChar.repeat(bannerWidth)}`, cols)}${RESET}`;
|
|
124
|
+
|
|
125
|
+
// ── Cat ASCII Art ──
|
|
126
|
+
let catArt;
|
|
127
|
+
switch (catState) {
|
|
128
|
+
case 'alert': catArt = CAT_ALERT; break;
|
|
129
|
+
case 'swipe': catArt = CAT_SWIPE; break;
|
|
130
|
+
case 'angry': catArt = CAT_ANGRY; break;
|
|
131
|
+
default: catArt = CAT_IDLE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const catColor = catState === 'angry' ? P.accent : P.fur;
|
|
135
|
+
const catStartRow = 4;
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < catArt.length; i++) {
|
|
138
|
+
if (catStartRow + i < rows - 10) {
|
|
139
|
+
// Color the eyes green
|
|
140
|
+
let artLine = catArt[i];
|
|
141
|
+
let colored = `${catColor}${artLine}`;
|
|
142
|
+
lines[catStartRow + i] = `${P.bg}${centerText(colored, cols)}${RESET}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Paw Animation ──
|
|
147
|
+
if (pawFrame >= 0 && pawFrame < PAW_FRAMES.length) {
|
|
148
|
+
const pawRow = catStartRow + 3;
|
|
149
|
+
const pawOffset = Math.floor(cols / 2) + 12;
|
|
150
|
+
if (pawRow < rows) {
|
|
151
|
+
const paw = `${P.primary}${BOLD}${PAW_FRAMES[pawFrame]}`;
|
|
152
|
+
lines[pawRow] = `${P.bg}${centerText(catArt[3] + ' ' + paw, cols)}${RESET}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Reaction Text ──
|
|
157
|
+
const reactionRow = catStartRow + catArt.length + 1;
|
|
158
|
+
if (reactionRow < rows - 8) {
|
|
159
|
+
if (catState !== 'idle') {
|
|
160
|
+
const reactionColor = catState === 'angry' ? P.accent : P.primary;
|
|
161
|
+
lines[reactionRow] = `${P.bg}${centerText(
|
|
162
|
+
`${reactionColor}${BOLD}✦ ${currentReaction} ✦`, cols
|
|
163
|
+
)}${RESET}`;
|
|
164
|
+
} else {
|
|
165
|
+
lines[reactionRow] = `${P.bg}${centerText(
|
|
166
|
+
`${P.dim}Watching for intruders...`, cols
|
|
167
|
+
)}${RESET}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Bottom Elements ──
|
|
172
|
+
const statusHeight = getStatusBarHeight(state);
|
|
173
|
+
const buttonRow = Math.max(25, rows - statusHeight - 7);
|
|
174
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
175
|
+
|
|
176
|
+
const streakRow = buttonRow - 3;
|
|
177
|
+
|
|
178
|
+
const counterLines = renderCounter(keyCount, cols, tick, {
|
|
179
|
+
r1: 255, g1: 140, b1: 0, // Orange
|
|
180
|
+
r2: 255, g2: 60, b2: 60, // Red
|
|
181
|
+
});
|
|
182
|
+
const counterRow = streakRow - counterLines.length - 1;
|
|
183
|
+
|
|
184
|
+
// ── Counter ──
|
|
185
|
+
for (let i = 0; i < counterLines.length; i++) {
|
|
186
|
+
if (counterRow + i < rows - 8 && counterRow + i >= reactionRow + 1) {
|
|
187
|
+
lines[counterRow + i] = `${P.bg}${counterLines[i]}${RESET}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Streak indicator ──
|
|
192
|
+
if (streakRow < rows - 6 && streakRow >= reactionRow + 2) {
|
|
193
|
+
const streakBar = '🐾'.repeat(Math.min(streak, 15));
|
|
194
|
+
const streakText = `${P.secondary}Streak: ${streakBar} ${streak > 0 ? `(${streak})` : ''} Best: ${maxStreak}`;
|
|
195
|
+
lines[streakRow] = `${P.bg}${centerText(streakText, cols)}${RESET}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── UNVOID Button ──
|
|
199
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
200
|
+
if (buttonRow + i >= 0 && buttonRow + i < rows - statusHeight) {
|
|
201
|
+
lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Status Bar ──
|
|
206
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'cat' }, tick);
|
|
207
|
+
const statusStartRow = rows - statusLines.length;
|
|
208
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
209
|
+
if (statusStartRow + i < rows) {
|
|
210
|
+
lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
lines,
|
|
216
|
+
buttonRegion: {
|
|
217
|
+
x: button.x,
|
|
218
|
+
y: buttonRow + 1,
|
|
219
|
+
width: button.width,
|
|
220
|
+
height: button.height,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// ─── Clean Skin ──────────────────────────────────────────────────────
|
|
2
|
+
// Default minimalist mode: high-contrast, big keystroke counter,
|
|
3
|
+
// subtle animations, and a premium dark aesthetic.
|
|
4
|
+
|
|
5
|
+
import { renderHeader } from '../components/header.js';
|
|
6
|
+
import { renderCounter, renderCounterLabel } from '../components/counter.js';
|
|
7
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
8
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
9
|
+
import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
|
|
10
|
+
import { centerText, horizontalLine } from '../../utils/terminal.js';
|
|
11
|
+
|
|
12
|
+
const P = palettes.clean;
|
|
13
|
+
|
|
14
|
+
const particles = [];
|
|
15
|
+
let lastKeyCount = 0;
|
|
16
|
+
const ASCII_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()';
|
|
17
|
+
|
|
18
|
+
export function renderClean(cols, rows, state, tick) {
|
|
19
|
+
const lines = [];
|
|
20
|
+
const { keyCount = 0 } = state;
|
|
21
|
+
|
|
22
|
+
// ── Physics & Particles ──
|
|
23
|
+
if (keyCount > lastKeyCount) {
|
|
24
|
+
const diff = Math.min(keyCount - lastKeyCount, 15); // Cap simultaneous spawns
|
|
25
|
+
lastKeyCount = keyCount;
|
|
26
|
+
for (let i = 0; i < diff; i++) {
|
|
27
|
+
particles.push({
|
|
28
|
+
x: Math.floor(Math.random() * (cols - 10)) + 5,
|
|
29
|
+
y: rows - 12, // Start rising from above the counter
|
|
30
|
+
char: ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)],
|
|
31
|
+
speed: 0.15 + Math.random() * 0.4,
|
|
32
|
+
opacity: 1.0,
|
|
33
|
+
r: Math.floor(Math.random() * 80) + 120, // Cyan/Purpleish colors
|
|
34
|
+
g: Math.floor(Math.random() * 150) + 50,
|
|
35
|
+
b: Math.floor(Math.random() * 100) + 155
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const particleMap = {};
|
|
41
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
42
|
+
const p = particles[i];
|
|
43
|
+
p.y -= p.speed;
|
|
44
|
+
p.opacity -= 0.015;
|
|
45
|
+
|
|
46
|
+
if (p.y <= 9 || p.opacity <= 0) {
|
|
47
|
+
particles.splice(i, 1);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const yi = Math.floor(p.y);
|
|
51
|
+
const xi = Math.floor(p.x);
|
|
52
|
+
if (yi >= 0 && yi < rows && xi >= 0 && xi < cols) {
|
|
53
|
+
if (!particleMap[yi]) particleMap[yi] = [];
|
|
54
|
+
particleMap[yi].push(p);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Background ──
|
|
59
|
+
for (let i = 0; i < rows; i++) {
|
|
60
|
+
if (particleMap[i]) {
|
|
61
|
+
particleMap[i].sort((a, b) => a.x - b.x);
|
|
62
|
+
let lineStr = P.bg;
|
|
63
|
+
let currX = 0;
|
|
64
|
+
for (const p of particleMap[i]) {
|
|
65
|
+
if (p.x < currX) continue;
|
|
66
|
+
lineStr += ' '.repeat(p.x - currX);
|
|
67
|
+
const r = Math.floor(p.r * p.opacity);
|
|
68
|
+
const g = Math.floor(p.g * p.opacity);
|
|
69
|
+
const b = Math.floor(p.b * p.opacity);
|
|
70
|
+
lineStr += `\x1b[38;2;${r};${g};${b}m${p.char}${P.bg}`;
|
|
71
|
+
currX = p.x + 1;
|
|
72
|
+
}
|
|
73
|
+
lineStr += ' '.repeat(Math.max(0, cols - currX)) + RESET;
|
|
74
|
+
lines.push(lineStr);
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(`${P.bg}${' '.repeat(cols)}${RESET}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Header (ASCII art title) ──
|
|
81
|
+
const headerLines = renderHeader(cols, tick);
|
|
82
|
+
let currentRow = 2; // Start with 2 rows padding
|
|
83
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
84
|
+
if (currentRow + i < rows) {
|
|
85
|
+
lines[currentRow + i] = `${P.bg}${headerLines[i]}${RESET}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
currentRow += headerLines.length + 1;
|
|
89
|
+
|
|
90
|
+
// ── Separator ──
|
|
91
|
+
if (currentRow < rows) {
|
|
92
|
+
const sepLine = centerText(
|
|
93
|
+
`${P.dim}${'┈'.repeat(Math.min(50, cols - 10))}`,
|
|
94
|
+
cols
|
|
95
|
+
) + RESET;
|
|
96
|
+
lines[currentRow] = `${P.bg}${sepLine}`;
|
|
97
|
+
currentRow += 2;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Bottom UI Elements ──
|
|
101
|
+
const statusHeight = getStatusBarHeight(state);
|
|
102
|
+
|
|
103
|
+
// UNVOID Button
|
|
104
|
+
const buttonRow = Math.max(currentRow + 8, rows - statusHeight - 7);
|
|
105
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
106
|
+
|
|
107
|
+
// Counter Label
|
|
108
|
+
const labelRow = buttonRow - 3;
|
|
109
|
+
|
|
110
|
+
// Big Counter
|
|
111
|
+
const counterLines = renderCounter(keyCount, cols, tick, {
|
|
112
|
+
r1: 0, g1: 255, b1: 224, // Cyan
|
|
113
|
+
r2: 191, g2: 64, b2: 255, // Purple
|
|
114
|
+
});
|
|
115
|
+
const counterRow = labelRow - counterLines.length - 1;
|
|
116
|
+
|
|
117
|
+
// Render Counter
|
|
118
|
+
for (let i = 0; i < counterLines.length; i++) {
|
|
119
|
+
if (counterRow + i < rows && counterRow + i >= currentRow) {
|
|
120
|
+
lines[counterRow + i] = `${P.bg}${counterLines[i]}${RESET}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Render Label
|
|
125
|
+
if (labelRow < rows && labelRow >= currentRow) {
|
|
126
|
+
lines[labelRow] = `${P.bg}${renderCounterLabel(cols, tick, P.dim)}${RESET}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Render Button
|
|
130
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
131
|
+
if (buttonRow + i < rows - statusHeight && buttonRow + i >= currentRow) {
|
|
132
|
+
lines[buttonRow + i] = `${P.bg}${button.lines[i]}${RESET}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Status Bar ──
|
|
137
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'clean' }, tick);
|
|
138
|
+
const statusStartRow = rows - statusLines.length;
|
|
139
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
140
|
+
if (statusStartRow + i < rows) {
|
|
141
|
+
lines[statusStartRow + i] = `${P.bg}${statusLines[i]}${RESET}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Return button position for mouse region
|
|
146
|
+
return {
|
|
147
|
+
lines,
|
|
148
|
+
buttonRegion: {
|
|
149
|
+
x: button.x,
|
|
150
|
+
y: buttonRow + 1, // 1-indexed for terminal
|
|
151
|
+
width: button.width,
|
|
152
|
+
height: button.height,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// ─── Hacker Skin ──────────────────────────────────────────────────────
|
|
2
|
+
// Intense cyberpunk matrix digital rain.
|
|
3
|
+
// Uses a highly optimized vertical rain engine and fake system errors.
|
|
4
|
+
|
|
5
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
6
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
7
|
+
import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
|
|
8
|
+
import { centerText, horizontalLine } from '../../utils/terminal.js';
|
|
9
|
+
|
|
10
|
+
const P = palettes.hacker;
|
|
11
|
+
const ASCII_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()<>{}[]|=+_~';
|
|
12
|
+
|
|
13
|
+
// Persistent rain engine state
|
|
14
|
+
const rainColumns = [];
|
|
15
|
+
let initialized = false;
|
|
16
|
+
let lastKeyCount = 0;
|
|
17
|
+
let errors = [];
|
|
18
|
+
|
|
19
|
+
function initRain(cols, rows) {
|
|
20
|
+
rainColumns.length = 0;
|
|
21
|
+
for (let c = 0; c < cols; c++) {
|
|
22
|
+
rainColumns.push({
|
|
23
|
+
head: Math.random() * rows - Math.random() * rows,
|
|
24
|
+
length: Math.floor(Math.random() * 20) + 10,
|
|
25
|
+
speed: 0.15 + Math.random() * 0.4,
|
|
26
|
+
chars: Array.from({ length: rows }, () => ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)])
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
initialized = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderHacker(cols, rows, state, tick) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
const { keyCount = 0 } = state;
|
|
35
|
+
|
|
36
|
+
// Handle terminal resizes naturally
|
|
37
|
+
if (!initialized || rainColumns.length !== cols) {
|
|
38
|
+
initRain(cols, rows);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Engine Updates ──
|
|
42
|
+
if (keyCount > lastKeyCount) {
|
|
43
|
+
const diff = keyCount - lastKeyCount;
|
|
44
|
+
// Rapidly boost rain speeds on keystrokes
|
|
45
|
+
for (let i = 0; i < Math.min(diff * 5, cols); i++) {
|
|
46
|
+
const colIdx = Math.floor(Math.random() * cols);
|
|
47
|
+
rainColumns[colIdx].speed += 0.5 + Math.random() * 1.5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add fake error
|
|
51
|
+
const ERROR_MSGS = [
|
|
52
|
+
'FATAL: KEYBOARD INPUT BLOCKED',
|
|
53
|
+
'WARNING: BRUTE FORCE DETECTED',
|
|
54
|
+
'SYSTEM: UNAUTHORIZED OVERRIDE',
|
|
55
|
+
'ACCESS DENIED: PROTOCOL 94',
|
|
56
|
+
'ERROR: IO_EXCEPTION_0x8F',
|
|
57
|
+
'SECURITY: INTRUDER LOCKDOWN'
|
|
58
|
+
];
|
|
59
|
+
errors.push(ERROR_MSGS[Math.floor(Math.random() * ERROR_MSGS.length)]);
|
|
60
|
+
if (errors.length > 6) errors.shift();
|
|
61
|
+
|
|
62
|
+
lastKeyCount = keyCount;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Update rain positions
|
|
66
|
+
for (let c = 0; c < cols; c++) {
|
|
67
|
+
const col = rainColumns[c];
|
|
68
|
+
col.head += col.speed;
|
|
69
|
+
|
|
70
|
+
// Slow down burst speeds gradually
|
|
71
|
+
if (col.speed > 0.6) {
|
|
72
|
+
col.speed *= 0.95;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (col.head - col.length > rows) {
|
|
76
|
+
col.head = -Math.floor(Math.random() * 15);
|
|
77
|
+
col.speed = 0.15 + Math.random() * 0.4;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Math.random() < 0.05) {
|
|
81
|
+
col.chars[Math.floor(Math.random() * rows)] = ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Render Rain Buffer ──
|
|
86
|
+
for (let r = 0; r < rows; r++) {
|
|
87
|
+
let rowStr = '';
|
|
88
|
+
let currColor = P.bg;
|
|
89
|
+
let currRun = '';
|
|
90
|
+
|
|
91
|
+
for (let c = 0; c < cols; c++) {
|
|
92
|
+
const col = rainColumns[c];
|
|
93
|
+
const headDist = Math.floor(col.head) - r;
|
|
94
|
+
|
|
95
|
+
let char = ' ';
|
|
96
|
+
let color = P.bg;
|
|
97
|
+
|
|
98
|
+
if (headDist === 0) {
|
|
99
|
+
char = col.chars[r];
|
|
100
|
+
color = `\x1b[38;2;200;255;200m`; // White/green flash at the head
|
|
101
|
+
} else if (headDist > 0 && headDist < col.length) {
|
|
102
|
+
char = col.chars[r];
|
|
103
|
+
const g = Math.max(20, 255 - (headDist * 16));
|
|
104
|
+
color = `\x1b[38;2;0;${g};0m`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Paint bg using the P.bg block string so gaps inherit the global terminal black
|
|
108
|
+
if (char === ' ') {
|
|
109
|
+
char = ' ';
|
|
110
|
+
color = P.bg;
|
|
111
|
+
} else {
|
|
112
|
+
color = P.bg + color;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (color !== currColor) {
|
|
116
|
+
rowStr += currColor + currRun;
|
|
117
|
+
currRun = char;
|
|
118
|
+
currColor = color;
|
|
119
|
+
} else {
|
|
120
|
+
currRun += char;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
rowStr += currColor + currRun + RESET;
|
|
125
|
+
lines.push(rowStr);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Central Error Console ──
|
|
129
|
+
const consoleW = Math.min(46, cols - 4);
|
|
130
|
+
const consoleH = 10;
|
|
131
|
+
const consoleY = Math.floor(rows / 2) - Math.floor(consoleH / 2) - 3;
|
|
132
|
+
|
|
133
|
+
if (consoleY > 2 && consoleW >= 30) {
|
|
134
|
+
const topBorder = `${P.terminalBg}${P.dim}┌${'─'.repeat(consoleW - 2)}┐`;
|
|
135
|
+
const bottomBorder = `${P.terminalBg}${P.dim}└${'─'.repeat(consoleW - 2)}┘`;
|
|
136
|
+
const emptyLine = `${P.terminalBg}${' '.repeat(consoleW)}`;
|
|
137
|
+
|
|
138
|
+
// Draw rigid background shadow for console box overwriting the rain
|
|
139
|
+
lines[consoleY] = `${P.bg}${centerText(topBorder, cols)}${RESET}`;
|
|
140
|
+
|
|
141
|
+
for (let i = 1; i < consoleH - 1; i++) {
|
|
142
|
+
let content = '';
|
|
143
|
+
if (i === 1) content = `${P.terminalBg}${P.error}${BOLD} SYSTEM LOCK PROTOCOL ACTIVE `;
|
|
144
|
+
else if (i === 2) content = `${P.terminalBg}${P.dim} ${'━'.repeat(consoleW - 4)} `;
|
|
145
|
+
else if (i - 3 < errors.length && i >= 3) {
|
|
146
|
+
content = `${P.terminalBg}\x1b[38;2;180;0;0m > ${errors[i - 3]} `;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let cText = content;
|
|
150
|
+
if (!cText) cText = emptyLine;
|
|
151
|
+
else {
|
|
152
|
+
// Pad the content with the background color to match width perfectly
|
|
153
|
+
// (Since centerText relies on ANSI stripping)
|
|
154
|
+
cText = cText.padEnd(consoleW + (cText.length - cText.replace(/\x1b\[[0-9;]*m/g, '').length), ' ');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Wrap with walls
|
|
158
|
+
const rowContent = `${P.terminalBg}${P.dim}│${RESET}${cText}${P.terminalBg}${P.dim}│`;
|
|
159
|
+
lines[consoleY + i] = `${P.bg}${centerText(rowContent, cols)}${RESET}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines[consoleY + consoleH - 1] = `${P.bg}${centerText(bottomBorder, cols)}${RESET}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── UNVOID Button ──
|
|
166
|
+
const statusHeight = getStatusBarHeight(state);
|
|
167
|
+
const buttonRow = Math.max(consoleY + consoleH + 2, rows - statusHeight - 7);
|
|
168
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
171
|
+
if (buttonRow + i < rows - statusHeight) {
|
|
172
|
+
lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Status Bar ──
|
|
177
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'hacker' }, tick);
|
|
178
|
+
const statusStartRow = rows - statusLines.length;
|
|
179
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
180
|
+
if (statusStartRow + i < rows) {
|
|
181
|
+
lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
lines,
|
|
187
|
+
buttonRegion: {
|
|
188
|
+
x: button.x,
|
|
189
|
+
y: buttonRow + 1,
|
|
190
|
+
width: button.width,
|
|
191
|
+
height: button.height,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|