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.
@@ -0,0 +1,195 @@
1
+ // ─── Prank Skin ──────────────────────────────────────────────────────
2
+ // Fake "Windows Update" screen. Progress bar stuck at 99%.
3
+ // Exit: triple-click in the top-right corner area.
4
+
5
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
6
+ import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
7
+ import { centerText, blocks } from '../../utils/terminal.js';
8
+
9
+ const P = palettes.prank;
10
+
11
+ // Fake file operations that scroll
12
+ const FAKE_FILES = [
13
+ 'Configuring system32/ntdll.dll...',
14
+ 'Updating kernel32.sys...',
15
+ 'Installing security patch KB5034441...',
16
+ 'Applying cumulative update...',
17
+ 'Downloading telemetry modules...',
18
+ 'Optimizing system registry...',
19
+ 'Reconfiguring boot loader...',
20
+ 'Updating display drivers...',
21
+ 'Applying .NET Framework 8.5 update...',
22
+ 'Installing Windows Defender definitions...',
23
+ 'Updating OneDrive integration...',
24
+ 'Configuring Hyper-V services...',
25
+ 'Applying power management settings...',
26
+ 'Updating Bluetooth stack...',
27
+ 'Reinstalling Cortana...',
28
+ 'Applying Group Policy updates...',
29
+ 'Updating Windows Search indexer...',
30
+ 'Installing optional features...',
31
+ 'Configuring Windows Update service...',
32
+ 'Finalizing pending operations...',
33
+ 'Running system file checker...',
34
+ 'Verifying system integrity...',
35
+ 'Applying network stack update...',
36
+ 'Updating USB controller firmware...',
37
+ 'Installing audio engine update...',
38
+ 'Please wait, this will take a while...',
39
+ 'Almost done, do not turn off your computer...',
40
+ 'Preparing to configure Windows...',
41
+ 'Running cleanup tasks...',
42
+ 'Registering system components...',
43
+ ];
44
+
45
+ // Track secret exit: triple-click in top-right 5×3 area
46
+ let clickTimestamps = [];
47
+ const TRIPLE_CLICK_WINDOW = 1500; // 1.5 seconds
48
+ const TRIPLE_CLICK_ZONE = { x: -10, y: 1, width: 10, height: 3 }; // relative to top-right
49
+
50
+ export function renderPrank(cols, rows, state, tick) {
51
+ const lines = [];
52
+
53
+ // Windows blue background
54
+ const bgLine = `${P.bg}${' '.repeat(cols)}${RESET}`;
55
+ for (let i = 0; i < rows; i++) lines.push(bgLine);
56
+
57
+ // ── Windows-style logo (simplified) ──
58
+ const logoY = Math.floor(rows * 0.15);
59
+ const logo = [
60
+ '⊞',
61
+ ];
62
+ lines[logoY] = `${P.bg}${centerText(`${P.primary}${BOLD}⊞`, cols)}${RESET}`;
63
+
64
+ // ── Main message ──
65
+ const msgY = logoY + 2;
66
+ lines[msgY] = `${P.bg}${centerText(
67
+ `${P.primary}${BOLD}Working on updates`,
68
+ cols
69
+ )}${RESET}`;
70
+
71
+ // Animated dots
72
+ const dots = '.'.repeat((Math.floor(tick / 10) % 4));
73
+ lines[msgY + 1] = `${P.bg}${centerText(
74
+ `${P.secondary}${dots.padEnd(3)}`,
75
+ cols
76
+ )}${RESET}`;
77
+
78
+ // ── Progress percentage ──
79
+ // Stuck at 99%, occasionally flickers to 98%
80
+ const pctFlicker = tick % 120 > 110 ? 98 : 99;
81
+ const pctY = msgY + 3;
82
+ lines[pctY] = `${P.bg}${centerText(
83
+ `${P.primary}${BOLD}${pctFlicker}% complete`,
84
+ cols
85
+ )}${RESET}`;
86
+
87
+ // ── Progress bar ──
88
+ const barWidth = Math.min(50, cols - 20);
89
+ const barY = pctY + 2;
90
+ const filledWidth = Math.floor(barWidth * 0.99);
91
+ const emptyWidth = barWidth - filledWidth;
92
+
93
+ // Animated shimmer on the progress bar
94
+ const shimmerPos = Math.floor((tick * 0.5) % (filledWidth + 5));
95
+
96
+ let barStr = '';
97
+ for (let i = 0; i < filledWidth; i++) {
98
+ if (i >= shimmerPos - 2 && i <= shimmerPos + 2) {
99
+ // Bright shimmer
100
+ barStr += `\x1b[48;2;120;180;255m `;
101
+ } else {
102
+ barStr += `\x1b[48;2;255;255;255m `;
103
+ }
104
+ }
105
+ barStr += `\x1b[48;2;0;60;130m${' '.repeat(emptyWidth)}`;
106
+
107
+ // Re-apply the Windows Blue background (P.bg) after barStr, so centerText's right padding uses it
108
+ lines[barY] = `${P.bg}${centerText(`${barStr}${P.bg}`, cols)}${RESET}`;
109
+
110
+ // ── "Don't turn off" message ──
111
+ const warnY = barY + 2;
112
+ lines[warnY] = `${P.bg}${centerText(
113
+ `${P.secondary}Don't turn off your computer`,
114
+ cols
115
+ )}${RESET}`;
116
+
117
+ // ── Scrolling fake file operations ──
118
+ const fileY = warnY + 3;
119
+ const visibleFiles = Math.min(6, rows - fileY - 5);
120
+ const fileOffset = Math.floor(tick / 8) % FAKE_FILES.length;
121
+
122
+ for (let i = 0; i < visibleFiles; i++) {
123
+ const fileIdx = (fileOffset + i) % FAKE_FILES.length;
124
+ const fade = i === 0 ? P.dim : (i === visibleFiles - 1 ? P.dim : P.secondary);
125
+ const fileRow = fileY + i;
126
+ if (fileRow < rows - 3) {
127
+ lines[fileRow] = `${P.bg}${centerText(
128
+ `${fade}${DIM}${FAKE_FILES[fileIdx]}`,
129
+ cols
130
+ )}${RESET}`;
131
+ }
132
+ }
133
+
134
+ // ── ETA (fake, keeps increasing) ──
135
+ const etaY = fileY + visibleFiles + 1;
136
+ const etaMinutes = 45 + Math.floor(tick / 60);
137
+ if (etaY < rows - 3) {
138
+ lines[etaY] = `${P.bg}${centerText(
139
+ `${P.dim}Estimated time remaining: ${etaMinutes} minutes`,
140
+ cols
141
+ )}${RESET}`;
142
+ }
143
+
144
+ // ── Visible hint (bottom of screen) ──
145
+ const hintRow = rows - 1;
146
+ const hintColor = '\x1b[38;2;150;200;255m'; // Light blue, visible but unobtrusive
147
+ const hintText = '(triple click on top right to exit)';
148
+
149
+ // Center the hint text at the bottom
150
+ lines[hintRow] = `${P.bg}${centerText(`${hintColor}${DIM}${hintText}`, cols)}${RESET}`;
151
+
152
+ // NOTE: In prank mode, the UNVOID button is HIDDEN.
153
+ // Exit is via triple-click in the top-right corner area.
154
+
155
+ // The secret clickable region is the top-right 10×3 area
156
+ const secretRegion = {
157
+ x: Math.max(1, cols - 10),
158
+ y: 1,
159
+ width: 10,
160
+ height: 3,
161
+ };
162
+
163
+ return {
164
+ lines,
165
+ buttonRegion: null, // No visible UNVOID button
166
+ secretRegion, // Secret exit zone
167
+ isPrank: true,
168
+ };
169
+ }
170
+
171
+ // Check if a triple-click has occurred in the secret zone
172
+ export function checkPrankExit(clickX, clickY, cols) {
173
+ const now = Date.now();
174
+
175
+ // Check if click is in the top-right zone
176
+ if (clickX >= cols - 10 && clickY <= 3) {
177
+ clickTimestamps.push(now);
178
+
179
+ // Remove clicks older than the time window
180
+ clickTimestamps = clickTimestamps.filter(t => now - t < TRIPLE_CLICK_WINDOW);
181
+
182
+ // Triple-click detected!
183
+ if (clickTimestamps.length >= 3) {
184
+ clickTimestamps = [];
185
+ return true;
186
+ }
187
+ }
188
+
189
+ return false;
190
+ }
191
+
192
+ // Reset prank state
193
+ export function resetPrankState() {
194
+ clickTimestamps = [];
195
+ }
@@ -0,0 +1,131 @@
1
+ // ─── Toddler Skin ────────────────────────────────────────────────────
2
+ // Fun mode: random emojis, cycling rainbow colors, playful messages.
3
+ // Designed to entertain a child while keeping the system safe.
4
+
5
+ import { renderUnvoidButton } from '../components/unvoid-button.js';
6
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
7
+ import { palettes, rainbow, RESET, BOLD } from '../../utils/colors.js';
8
+ import { centerText } from '../../utils/terminal.js';
9
+ import { formatCount } from '../../utils/big-digits.js';
10
+
11
+ const P = palettes.toddler;
12
+
13
+ // Emoji collections for different "reactions"
14
+ const ANIMALS = ['🦕', '🦖', '🐘', '🦋', '🐙', '🦄', '🐢', '🐬', '🦊', '🐼', '🐨', '🦜', '🐳', '🦁', '🐸'];
15
+ const SPACE = ['🚀', '🌍', '⭐', '🌙', '🛸', '☄️', '🪐', '🌈', '✨', '💫', '🌟', '⚡'];
16
+ const FOOD = ['🍕', '🍦', '🍩', '🧁', '🍪', '🎂', '🍭', '🍬', '🍫', '🍓', '🍉', '🥝'];
17
+ const FUN = ['🎨', '🎪', '🎠', '🎡', '🎵', '🎶', '🎸', '🥁', '🎺', '🎹', '🪩', '🎯'];
18
+
19
+ const ALL_EMOJIS = [...ANIMALS, ...SPACE, ...FOOD, ...FUN];
20
+
21
+ const MESSAGES = [
22
+ 'WOW!', 'COOL!', 'AMAZING!', 'YAY!', 'WHEEE!',
23
+ 'AWESOME!', 'SUPER!', 'WOOHOO!', 'TADA!', 'MAGIC!',
24
+ 'BOOM!', 'POW!', 'ZAP!', 'KABOOM!', 'SPARKLE!',
25
+ ];
26
+
27
+ // State for the toddler animations
28
+ let lastEmoji = '🦕';
29
+ let lastMessage = 'WOW!';
30
+ let currentBgIndex = 0;
31
+ let lastKeyCount = 0;
32
+
33
+ export function renderToddler(cols, rows, state, tick) {
34
+ const lines = [];
35
+ const { keyCount = 0 } = state;
36
+
37
+ // Update emoji and message when new key is pressed
38
+ if (keyCount > lastKeyCount) {
39
+ lastEmoji = ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)];
40
+ lastMessage = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
41
+ currentBgIndex = (currentBgIndex + 1) % rainbow.length;
42
+ lastKeyCount = keyCount;
43
+ }
44
+
45
+ // Cycling background color (shifts even without key presses, slowly)
46
+ const bgIdx = (currentBgIndex + Math.floor(tick / 30)) % rainbow.length;
47
+ const bg = rainbow[bgIdx];
48
+ const bgLine = `${bg}${' '.repeat(cols)}${RESET}`;
49
+
50
+ // Fill background
51
+ for (let i = 0; i < rows; i++) {
52
+ lines.push(bgLine);
53
+ }
54
+
55
+ // ── Title ──
56
+ const title = '✨ K E Y V O I D ✨';
57
+ const titleColored = `${P.primary}${BOLD}${title}`;
58
+ lines[2] = `${bg}${centerText(titleColored, cols)}${RESET}`;
59
+
60
+ const subtitle = '~ toddler mode ~';
61
+ lines[3] = `${bg}${centerText(`${P.secondary}${subtitle}`, cols)}${RESET}`;
62
+
63
+ // ── Bottom Elements ──
64
+ const statusHeight = getStatusBarHeight(state);
65
+ const buttonRow = Math.max(20, rows - statusHeight - 7);
66
+ const button = renderUnvoidButton(cols, buttonRow, tick);
67
+
68
+ const countRow = buttonRow - 3;
69
+ const msgRow = countRow - 2;
70
+
71
+ // ── Giant Emoji (Floating in available space) ──
72
+ const availableSpace = msgRow - 5;
73
+ const centerY = 5 + Math.floor(availableSpace / 2);
74
+ const bounce = Math.floor(Math.sin(tick * 0.2) * 2); // Bouncing animation
75
+ const emojiRow = Math.max(5, Math.min(centerY + bounce, rows - 15));
76
+
77
+ // Build a huge emoji display (using text around emoji)
78
+ const sparkles = '✨'.repeat(3);
79
+ const emojiDisplay = `${sparkles} ${lastEmoji} ${sparkles}`;
80
+ lines[emojiRow] = `${bg}${centerText(emojiDisplay, cols)}${RESET}`;
81
+
82
+ // Repeat emoji bigger in surrounding rows
83
+ lines[emojiRow - 1] = `${bg}${centerText(`${lastEmoji} ${lastEmoji} ${lastEmoji}`, cols)}${RESET}`;
84
+ lines[emojiRow + 1] = `${bg}${centerText(`${lastEmoji} ${lastEmoji} ${lastEmoji}`, cols)}${RESET}`;
85
+
86
+ // ── Fun Message ──
87
+ if (msgRow < rows - 5 && msgRow > emojiRow + 1) {
88
+ const msgColors = [P.primary, P.secondary, P.accent, P.fun1, P.fun2, P.fun3, P.fun4];
89
+ const msgColor = msgColors[Math.floor(tick / 5) % msgColors.length];
90
+ const msgStr = `${msgColor}${BOLD} ★ ${lastMessage} ★ `;
91
+ lines[msgRow] = `${bg}${centerText(msgStr, cols)}${RESET}`;
92
+ }
93
+
94
+ // ── Key Count (playful) ──
95
+ if (countRow < rows - 5 && countRow > emojiRow + 2) {
96
+ const countStr = `${P.secondary}${formatCount(keyCount)} little taps blocked! 🛡️`;
97
+ lines[countRow] = `${bg}${centerText(countStr, cols)}${RESET}`;
98
+ }
99
+
100
+ // ── Stars border animation ──
101
+ const star1 = tick % 4 === 0 ? '⭐' : '✨';
102
+ const star2 = tick % 4 === 2 ? '⭐' : '✨';
103
+ const starLine = (star1 + ' ').repeat(Math.floor(cols / 3));
104
+ lines[0] = `${bg}${starLine.substring(0, cols)}${RESET}`;
105
+
106
+ // ── UNVOID Button ──
107
+ for (let i = 0; i < button.lines.length; i++) {
108
+ if (buttonRow + i < rows - statusHeight) {
109
+ lines[buttonRow + i] = `${bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + bg)}${RESET}`;
110
+ }
111
+ }
112
+
113
+ // ── Status Bar ──
114
+ const statusLines = renderStatusBar(cols, { ...state, skinName: 'toddler' }, tick);
115
+ const statusStartRow = rows - statusLines.length;
116
+ for (let i = 0; i < statusLines.length; i++) {
117
+ if (statusStartRow + i < rows) {
118
+ lines[statusStartRow + i] = `${bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + bg)}${RESET}`;
119
+ }
120
+ }
121
+
122
+ return {
123
+ lines,
124
+ buttonRegion: {
125
+ x: button.x,
126
+ y: buttonRow + 1,
127
+ width: button.width,
128
+ height: button.height,
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,169 @@
1
+ // ─── Zen Skin ─────────────────────────────────────────────────────────
2
+ // A calm, meditative breathing visual. Blocked keys create gentle ripples.
3
+
4
+ import { renderUnvoidButton } from '../components/unvoid-button.js';
5
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
6
+ import { palettes, RESET } from '../../utils/colors.js';
7
+ import { centerText } from '../../utils/terminal.js';
8
+
9
+ const P = palettes.zen;
10
+ let lastKeyCount = 0;
11
+
12
+ // Track active ripples: { radius: 0, life: 1.0 }
13
+ const activeRipples = [];
14
+
15
+ export function renderZen(cols, rows, state, tick) {
16
+ const lines = Array(rows).fill(`${P.bg}${' '.repeat(cols)}${RESET}`);
17
+ const statusHeight = getStatusBarHeight(state);
18
+ const playAreaH = rows - statusHeight - 4;
19
+
20
+ const { keyCount = 0 } = state;
21
+
22
+ // Trigger ripples on key press
23
+ if (keyCount > lastKeyCount) {
24
+ const diff = Math.min(keyCount - lastKeyCount, 3);
25
+ for (let i = 0; i < diff; i++) {
26
+ activeRipples.push({
27
+ radius: 0 - (i * 2), // Stagger slightly if multiple
28
+ life: 1.0
29
+ });
30
+ }
31
+ lastKeyCount = keyCount;
32
+ }
33
+
34
+ // Update ripples
35
+ for (let i = activeRipples.length - 1; i >= 0; i--) {
36
+ const r = activeRipples[i];
37
+ r.radius += 0.8; // expansion speed
38
+ if (r.radius > 0) {
39
+ r.life -= 0.012; // fade out
40
+ }
41
+ if (r.life <= 0) {
42
+ activeRipples.splice(i, 1);
43
+ }
44
+ }
45
+
46
+ // Breathing animation (slow sine wave)
47
+ const breath = Math.sin(tick * 0.02); // -1 to 1
48
+ const baseRadius = 8 + breath * 2;
49
+
50
+ const cx = Math.floor(cols / 2);
51
+ const cy = Math.floor(playAreaH / 2);
52
+
53
+ // Precompute RGB interpolation helper
54
+ const interpolatePhaseColor = (dist, rippleIntensity = 0) => {
55
+ // Base phase colors based on distance and breath
56
+ const phase = dist * 0.5 - (tick * 0.05);
57
+ const val = (Math.sin(phase) + 1) / 2; // 0 to 1
58
+
59
+ // We will interpolate between dim and primary
60
+ // Then add ripple brightness
61
+
62
+ // parse values from RGB tuples string for speed, or just use hardcoded mapped values
63
+ // Primary: 180, 180, 255
64
+ // Dim: 60, 60, 90
65
+
66
+ let red = 60 + (120 * val);
67
+ let green = 60 + (120 * val);
68
+ let blue = 90 + (165 * val);
69
+
70
+ if (rippleIntensity > 0) {
71
+ // Add white peak to the ripple
72
+ red += (255 - red) * rippleIntensity;
73
+ green += (255 - green) * rippleIntensity;
74
+ blue += (255 - blue) * rippleIntensity;
75
+ }
76
+
77
+ return `\x1b[38;2;${Math.floor(red)};${Math.floor(green)};${Math.floor(blue)}m`;
78
+ };
79
+
80
+ for (let r = 0; r < playAreaH; r++) {
81
+ let rowStr = '';
82
+ let currColor = P.bg;
83
+ let currRun = '';
84
+
85
+ for (let c = 0; c < cols; c++) {
86
+ // Terminal fonts are roughly 2:1 aspect ratio
87
+ const dx = (c - cx) / 2;
88
+ const dy = (r - cy);
89
+ const dist = Math.sqrt(dx*dx + dy*dy);
90
+
91
+ let char = ' ';
92
+ let color = P.bg;
93
+
94
+ // Check ripples
95
+ let rippleIntensity = 0;
96
+ for (const rip of activeRipples) {
97
+ if (rip.radius > 0) {
98
+ const rDist = Math.abs(dist - rip.radius);
99
+ if (rDist < 1.5) {
100
+ // Sharp peak
101
+ rippleIntensity = Math.max(rippleIntensity, rip.life * (1 - (rDist/1.5)));
102
+ }
103
+ }
104
+ }
105
+
106
+ if (dist < baseRadius || rippleIntensity > 0.1 || (dist > baseRadius && dist < baseRadius + 12 && Math.sin(dist*2 - tick*0.1) > 0.5)) {
107
+ char = '·';
108
+
109
+ if (dist < baseRadius - 1) {
110
+ char = '█';
111
+ } else if (dist < baseRadius) {
112
+ char = '▓';
113
+ } else if (dist < baseRadius + 1) {
114
+ char = '▒';
115
+ }
116
+
117
+ if (rippleIntensity > 0.5) char = '█';
118
+ else if (rippleIntensity > 0.2) char = '▓';
119
+
120
+ color = P.bg + interpolatePhaseColor(dist, rippleIntensity);
121
+ }
122
+
123
+ if (color !== currColor) {
124
+ rowStr += currColor + currRun;
125
+ currColor = color;
126
+ currRun = char;
127
+ } else {
128
+ currRun += char;
129
+ }
130
+ }
131
+ rowStr += currColor + currRun + RESET;
132
+ lines[r] = rowStr;
133
+ }
134
+
135
+ // ── UI Overlays ──
136
+ const titleLine = `${P.bg}${P.secondary}Z E N M O D E${RESET}`;
137
+ lines[1] = `${P.bg}${centerText(titleLine, cols)}${RESET}`;
138
+
139
+ const subtitleLine = `${P.bg}${P.dim}Take a deep breath. Focus on your flow.${RESET}`;
140
+ lines[3] = `${P.bg}${centerText(subtitleLine, cols)}${RESET}`;
141
+
142
+ // UNVOID Button
143
+ const buttonRow = playAreaH + 1;
144
+ const button = renderUnvoidButton(cols, buttonRow, tick);
145
+ for (let i = 0; i < button.lines.length; i++) {
146
+ if (buttonRow + i < rows - statusHeight) {
147
+ lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
148
+ }
149
+ }
150
+
151
+ // Status Bar
152
+ const statusLines = renderStatusBar(cols, { ...state, skinName: 'zen' }, tick);
153
+ const statusStartRow = rows - statusLines.length;
154
+ for (let i = 0; i < statusLines.length; i++) {
155
+ if (statusStartRow + i < rows) {
156
+ lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
157
+ }
158
+ }
159
+
160
+ return {
161
+ lines,
162
+ buttonRegion: {
163
+ x: button.x,
164
+ y: buttonRow + 1,
165
+ width: button.width,
166
+ height: button.height,
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,105 @@
1
+ // ─── Unlock Sequence ──────────────────────────────────────────────────
2
+ // A cinematic glitch-out and power-down sequence transitioning the
3
+ // user back to the terminal prompt.
4
+
5
+ import chalk from 'chalk';
6
+ import { RESET, BOLD } from '../utils/colors.js';
7
+ import { centerText } from '../utils/terminal.js';
8
+
9
+ const GLITCH_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()_+-=[]{}|;:\'<>,.?/';
10
+
11
+ export function renderUnlockSequence(cols, rows, tick, unlockTick) {
12
+ const elapsed = tick - unlockTick;
13
+ const frames = 60; // 1 second exactly
14
+
15
+ const progress = Math.min(elapsed / frames, 1.0);
16
+
17
+ const lines = [];
18
+
19
+ // Phase 1 (0 -> 0.3): White noise static glitch
20
+ // Phase 2 (0.3 -> 0.6): Curtain drop (black screen wiping down)
21
+ // Phase 3 (0.6 -> 1.0): "SESSION UNLOCKED" text blooming in center
22
+
23
+ const cx = Math.floor(cols / 2);
24
+ const cy = Math.floor(rows / 2);
25
+
26
+ if (progress < 0.3) {
27
+ // White noise static glitch
28
+ const intensity = 1 - (progress / 0.3); // 1 to 0
29
+ for (let r = 0; r < rows; r++) {
30
+ let line = '';
31
+ for (let c = 0; c < cols; c++) {
32
+ if (Math.random() < intensity * 0.4) {
33
+ const char = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
34
+ // Random green, cyan, or white
35
+ const isGreen = Math.random() > 0.5;
36
+ line += isGreen ? `\x1b[38;2;0;255;150m${char}\x1b[0m` : `\x1b[38;2;150;255;255m${char}\x1b[0m`;
37
+ } else {
38
+ line += ' ';
39
+ }
40
+ }
41
+ lines.push(line);
42
+ }
43
+ } else if (progress < 0.6) {
44
+ // Curtain drop wipe (top to bottom fade to black)
45
+ const dropProgress = (progress - 0.3) / 0.3;
46
+ const curtainRow = Math.floor(rows * dropProgress);
47
+
48
+ for (let r = 0; r < rows; r++) {
49
+ if (r > curtainRow) {
50
+ // Still some sparse glitching below the curtain
51
+ let line = '';
52
+ for (let c = 0; c < cols; c++) {
53
+ if (Math.random() < 0.05) {
54
+ const char = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
55
+ line += `\x1b[38;5;${235 + Math.floor(Math.random() * 8)}m${char}\x1b[0m`;
56
+ } else {
57
+ line += ' ';
58
+ }
59
+ }
60
+ lines.push(line);
61
+ } else {
62
+ // Pure black void above the curtain
63
+ lines.push(' '.repeat(cols));
64
+ }
65
+ }
66
+ } else {
67
+ // Final bloom: Just the UNLOCKED text in the middle
68
+ const textProgress = (progress - 0.6) / 0.4;
69
+
70
+ for (let r = 0; r < rows; r++) {
71
+ lines.push(' '.repeat(cols));
72
+ }
73
+
74
+ // Box height expands
75
+ const boxHeight = Math.floor(7 * textProgress);
76
+ if (boxHeight > 0) {
77
+ const top = cy - Math.floor(boxHeight / 2);
78
+ const bot = cy + Math.floor(boxHeight / 2);
79
+
80
+ for (let r = top; r <= bot; r++) {
81
+ if (r >= 0 && r < rows) {
82
+ // Draw a sleek frame
83
+ const width = Math.floor(cols * 0.4 * textProgress);
84
+ const str = `\x1b[48;2;10;15;20m${' '.repeat(width)}\x1b[0m`;
85
+ lines[r] = centerText(str, cols);
86
+ }
87
+ }
88
+
89
+ if (boxHeight > 2) {
90
+ const width = Math.floor(cols * 0.4 * textProgress);
91
+ const pulse = Math.floor(100 + 155 * textProgress);
92
+ const headerColor = `\x1b[38;2;0;${pulse};${Math.floor(pulse * 0.6)}m`;
93
+
94
+ const text = ' [ SYSTEM UNLOCKED ] ';
95
+ const padTotal = Math.max(0, width - text.length);
96
+ const padStr = ' '.repeat(Math.floor(padTotal / 2));
97
+
98
+ const innerBg = `\x1b[48;2;10;15;20m${padStr}${headerColor}${BOLD}${text}\x1b[0m\x1b[48;2;10;15;20m${' '.repeat(padTotal - Math.floor(padTotal / 2))}\x1b[0m`;
99
+ lines[cy] = centerText(innerBg, cols);
100
+ }
101
+ }
102
+ }
103
+
104
+ return lines;
105
+ }