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,530 @@
|
|
|
1
|
+
// ─── Arcade Skin ──────────────────────────────────────────────────────
|
|
2
|
+
// An interactive retro 8-bit alien shooter.
|
|
3
|
+
// Blocked keys shoot lasers at scrolling alien sprites.
|
|
4
|
+
|
|
5
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
6
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
7
|
+
import { palettes, RESET, BOLD } from '../../utils/colors.js';
|
|
8
|
+
import { centerText } from '../../utils/terminal.js';
|
|
9
|
+
import { renderBigNumber } from '../../utils/big-digits.js';
|
|
10
|
+
|
|
11
|
+
const P = palettes.arcade;
|
|
12
|
+
|
|
13
|
+
let initialized = false;
|
|
14
|
+
let lastKeyCount = 0;
|
|
15
|
+
let score = 0;
|
|
16
|
+
let playerHearts = 3;
|
|
17
|
+
let dangerFlash = 0;
|
|
18
|
+
let isGameOver = false;
|
|
19
|
+
let spaceHeldTicks = 0;
|
|
20
|
+
|
|
21
|
+
// Game state
|
|
22
|
+
const aliens = [];
|
|
23
|
+
const lasers = [];
|
|
24
|
+
const particles = [];
|
|
25
|
+
const turretFlashes = [0, 0, 0, 0];
|
|
26
|
+
|
|
27
|
+
const ALIEN_SPRITES = [
|
|
28
|
+
[
|
|
29
|
+
' ▄▄████▄▄ ',
|
|
30
|
+
'██████████',
|
|
31
|
+
'██▄████▄██',
|
|
32
|
+
' ▀ ▀ '
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
' ▄████▄ ',
|
|
36
|
+
'▄████████▄',
|
|
37
|
+
'▀████████▀',
|
|
38
|
+
' ▄ ▄ '
|
|
39
|
+
]
|
|
40
|
+
];
|
|
41
|
+
const ALIEN_W = 10;
|
|
42
|
+
const ALIEN_H = 4;
|
|
43
|
+
|
|
44
|
+
function spawnAliens(cols, rows) {
|
|
45
|
+
aliens.length = 0;
|
|
46
|
+
const maxAliens = 2; // Always start easy
|
|
47
|
+
for (let i = 0; i < maxAliens; i++) {
|
|
48
|
+
aliens.push({
|
|
49
|
+
x: 5 + i * 18,
|
|
50
|
+
y: 1 + (i % 3) * 6, // Push Y down so they have room
|
|
51
|
+
dir: (i % 2 === 0) ? 1 : -1,
|
|
52
|
+
type: i % 2,
|
|
53
|
+
hp: 5, // Require 5 hits
|
|
54
|
+
flashTicks: 0, // Flash white when hit
|
|
55
|
+
active: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderArcade(cols, rows, state, tick) {
|
|
61
|
+
const lines = Array(rows).fill(`${P.bg}${' '.repeat(cols)}${RESET}`);
|
|
62
|
+
const statusHeight = getStatusBarHeight(state);
|
|
63
|
+
const playAreaH = rows - statusHeight - 4; // leave room for unvoid button
|
|
64
|
+
|
|
65
|
+
if (!initialized) {
|
|
66
|
+
spawnAliens(cols, rows);
|
|
67
|
+
initialized = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { keyCount = 0, lastKeyCode = -1 } = state;
|
|
71
|
+
|
|
72
|
+
// ── GAME OVER STATE ──
|
|
73
|
+
if (isGameOver) {
|
|
74
|
+
if (state.isSpaceDown) {
|
|
75
|
+
spaceHeldTicks++;
|
|
76
|
+
if (spaceHeldTicks >= 180) { // 3 seconds at 60fps
|
|
77
|
+
// Restart Game
|
|
78
|
+
isGameOver = false;
|
|
79
|
+
score = 0;
|
|
80
|
+
playerHearts = 3;
|
|
81
|
+
aliens.length = 0;
|
|
82
|
+
lasers.length = 0;
|
|
83
|
+
particles.length = 0;
|
|
84
|
+
spawnAliens(cols, rows);
|
|
85
|
+
spaceHeldTicks = 0;
|
|
86
|
+
lastKeyCount = keyCount;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
spaceHeldTicks = Math.max(0, spaceHeldTicks - 5); // Decay quickly when released
|
|
90
|
+
}
|
|
91
|
+
lastKeyCount = keyCount;
|
|
92
|
+
} else {
|
|
93
|
+
// ── GAMEPLAY MECHANICS ──
|
|
94
|
+
|
|
95
|
+
// Continuous Spawning Mechanics (Scales with Score)
|
|
96
|
+
// Base: start with 2. Every 400 pts (killing ~8 aliens), add 1 to the max concurrent limit.
|
|
97
|
+
const maxAliens = Math.min(Math.floor(cols / 12), 2 + Math.floor(score / 400));
|
|
98
|
+
const activeAliens = aliens.filter(a => a.active).length;
|
|
99
|
+
if (activeAliens < maxAliens && tick % 30 === 0) {
|
|
100
|
+
aliens.push({
|
|
101
|
+
x: Math.floor(Math.random() * (cols - ALIEN_W - 4)) + 2,
|
|
102
|
+
y: 2,
|
|
103
|
+
dir: Math.random() > 0.5 ? 1.5 : -1.5,
|
|
104
|
+
type: Math.floor(Math.random() * 2),
|
|
105
|
+
hp: 5,
|
|
106
|
+
flashTicks: 0,
|
|
107
|
+
active: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 1. Shoot lasers on keypress
|
|
112
|
+
if (keyCount > lastKeyCount) {
|
|
113
|
+
const diff = keyCount - lastKeyCount;
|
|
114
|
+
|
|
115
|
+
// Determine horizontal spawn position based on keycode
|
|
116
|
+
let targetPct = 0.5;
|
|
117
|
+
if (lastKeyCode !== -1) {
|
|
118
|
+
// macOS QWERTY layout estimation map
|
|
119
|
+
const leftKeys = new Set([0,1,2,6,7,8,12,13,14,18,19,20,53,48,50]);
|
|
120
|
+
const midLeftKeys = new Set([3,4,5,9,11,15,17,21,22,23]);
|
|
121
|
+
const midRightKeys = new Set([32,34,45,46,16,38,40,43,28,26,49]);
|
|
122
|
+
const rightKeys = new Set([31,35,37,33,30,42,41,39,44,47,25,29,27,24,36,51,123,124,125,126,56,60]);
|
|
123
|
+
|
|
124
|
+
if (leftKeys.has(lastKeyCode)) targetPct = 0.2;
|
|
125
|
+
else if (midLeftKeys.has(lastKeyCode)) targetPct = 0.4;
|
|
126
|
+
else if (midRightKeys.has(lastKeyCode)) targetPct = 0.6;
|
|
127
|
+
else if (rightKeys.has(lastKeyCode)) targetPct = 0.8;
|
|
128
|
+
else {
|
|
129
|
+
// Deterministic hash so unknown keys aim consistently
|
|
130
|
+
targetPct = 0.1 + ((lastKeyCode * 37) % 100) / 100.0 * 0.8;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Cap laser spam to prevent lag
|
|
135
|
+
let activeTurret = 0;
|
|
136
|
+
if (targetPct === 0.2) activeTurret = 0;
|
|
137
|
+
else if (targetPct === 0.4) activeTurret = 1;
|
|
138
|
+
else if (targetPct === 0.6) activeTurret = 2;
|
|
139
|
+
else activeTurret = 3; // Right or fallback
|
|
140
|
+
|
|
141
|
+
turretFlashes[activeTurret] = 4; // 4 ticks of bright flash
|
|
142
|
+
|
|
143
|
+
for(let i = 0; i < Math.min(diff, 5); i++) {
|
|
144
|
+
// slight jitter +/- 3 chars around the key's central column target
|
|
145
|
+
const jitter = (Math.random() - 0.5) * 6;
|
|
146
|
+
const spawnX = Math.floor(cols * targetPct + jitter);
|
|
147
|
+
|
|
148
|
+
lasers.push({
|
|
149
|
+
x: Math.max(2, Math.min(cols - 4, spawnX)),
|
|
150
|
+
y: playAreaH - 3, // Spawn directly above the tank UI
|
|
151
|
+
speed: 0.8 + Math.random() * 0.5
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
lastKeyCount = keyCount;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Update Aliens
|
|
158
|
+
// Difficulty Scaling: Fall speed increases as you score points!
|
|
159
|
+
const currentFallSpeed = 0.15 + Math.min(0.25, (score * 0.00005));
|
|
160
|
+
|
|
161
|
+
aliens.forEach(a => {
|
|
162
|
+
if (!a.active) return;
|
|
163
|
+
a.x += 0.3 * a.dir;
|
|
164
|
+
a.y += currentFallSpeed;
|
|
165
|
+
|
|
166
|
+
// Bounds check
|
|
167
|
+
if (a.x >= cols - ALIEN_W) {
|
|
168
|
+
a.x = cols - ALIEN_W;
|
|
169
|
+
a.dir = -1;
|
|
170
|
+
a.y += 1;
|
|
171
|
+
} else if (a.x <= 0) {
|
|
172
|
+
a.x = 0;
|
|
173
|
+
a.dir = 1;
|
|
174
|
+
a.y += 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Reached the bottom (Tank Base line) = Player takes damage!
|
|
178
|
+
if (a.y > playAreaH - 6) {
|
|
179
|
+
a.active = false;
|
|
180
|
+
playerHearts -= 1;
|
|
181
|
+
dangerFlash = 15; // flash screen red
|
|
182
|
+
|
|
183
|
+
if (playerHearts <= 0) {
|
|
184
|
+
playerHearts = 0; // clamp
|
|
185
|
+
isGameOver = true;
|
|
186
|
+
} else {
|
|
187
|
+
// Penalize score if not game over yet
|
|
188
|
+
score = Math.max(0, score - 500);
|
|
189
|
+
aliens.forEach(al => al.active = false); // Board wipe for breather
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 3. Update Lasers & Collisions
|
|
195
|
+
for (let i = lasers.length - 1; i >= 0; i--) {
|
|
196
|
+
let l = lasers[i];
|
|
197
|
+
l.y -= l.speed;
|
|
198
|
+
|
|
199
|
+
// Remove if off-screen
|
|
200
|
+
if (l.y < 0) {
|
|
201
|
+
lasers.splice(i, 1);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check collision
|
|
206
|
+
let hit = false;
|
|
207
|
+
for (let j = 0; j < aliens.length; j++) {
|
|
208
|
+
let a = aliens[j];
|
|
209
|
+
if (!a.active) continue;
|
|
210
|
+
|
|
211
|
+
if (l.x >= Math.floor(a.x) && l.x < Math.floor(a.x) + ALIEN_W) {
|
|
212
|
+
if (Math.floor(l.y) >= a.y && Math.floor(l.y) <= a.y + ALIEN_H) {
|
|
213
|
+
// IT'S A HIT!
|
|
214
|
+
hit = true;
|
|
215
|
+
a.hp -= 1;
|
|
216
|
+
|
|
217
|
+
if (a.hp <= 0) {
|
|
218
|
+
a.active = false;
|
|
219
|
+
score += 100;
|
|
220
|
+
|
|
221
|
+
// Big explosion
|
|
222
|
+
for (let p = 0; p < 12; p++) {
|
|
223
|
+
particles.push({
|
|
224
|
+
x: l.x,
|
|
225
|
+
y: l.y,
|
|
226
|
+
vx: (Math.random() - 0.5) * 2.0,
|
|
227
|
+
vy: (Math.random() - 0.5) * 2.0,
|
|
228
|
+
life: 15 + Math.random() * 15
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Small explosion + flash
|
|
233
|
+
a.flashTicks = 3;
|
|
234
|
+
for (let p = 0; p < 3; p++) {
|
|
235
|
+
particles.push({
|
|
236
|
+
x: l.x,
|
|
237
|
+
y: l.y + ALIEN_H,
|
|
238
|
+
vx: (Math.random() - 0.5) * 1.0,
|
|
239
|
+
vy: Math.random() * 1.0,
|
|
240
|
+
life: 5 + Math.random() * 5
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (hit) lasers.splice(i, 1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Flash ticks countdown
|
|
252
|
+
aliens.forEach(a => {
|
|
253
|
+
if (a.flashTicks > 0) a.flashTicks--;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 4. Update Particles
|
|
257
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
258
|
+
let p = particles[i];
|
|
259
|
+
p.x += p.vx;
|
|
260
|
+
p.y += p.vy;
|
|
261
|
+
p.vy += 0.05; // gravity
|
|
262
|
+
p.life--;
|
|
263
|
+
if (p.life <= 0) particles.splice(i, 1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
} // <--- End of GAMEPLAY MECHANICS `if (!isGameOver)` block
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
// ── Rendering Engine ──
|
|
270
|
+
if (dangerFlash > 0) dangerFlash--;
|
|
271
|
+
|
|
272
|
+
// Create a red-tinted background if we took damage
|
|
273
|
+
const currentBg = dangerFlash > 0
|
|
274
|
+
? `\x1b[48;2;${Math.min(150, dangerFlash * 10)};0;0m`
|
|
275
|
+
: P.bg;
|
|
276
|
+
|
|
277
|
+
const buffer = [];
|
|
278
|
+
for (let r = 0; r < playAreaH; r++) {
|
|
279
|
+
buffer.push(Array(cols).fill(' '));
|
|
280
|
+
}
|
|
281
|
+
const colorBuffer = [];
|
|
282
|
+
for (let r = 0; r < playAreaH; r++) {
|
|
283
|
+
colorBuffer.push(Array(cols).fill(currentBg));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Draw Background Score Big Numbers (Rendered first so it acts as background)
|
|
287
|
+
const scoreDigits = renderBigNumber(score);
|
|
288
|
+
if (scoreDigits && scoreDigits.length > 0) {
|
|
289
|
+
const scoreW = scoreDigits[0].length;
|
|
290
|
+
const scoreX = Math.floor(cols / 2) - Math.floor(scoreW / 2);
|
|
291
|
+
const scoreY = 1;
|
|
292
|
+
|
|
293
|
+
for (let r = 0; r < scoreDigits.length; r++) {
|
|
294
|
+
if (scoreY + r >= playAreaH) continue;
|
|
295
|
+
for (let c = 0; c < scoreW; c++) {
|
|
296
|
+
if (scoreX + c >= 0 && scoreX + c < cols) {
|
|
297
|
+
const char = scoreDigits[r][c];
|
|
298
|
+
// Dim the score string so it looks like background
|
|
299
|
+
buffer[scoreY + r][scoreX + c] = char;
|
|
300
|
+
colorBuffer[scoreY + r][scoreX + c] = P.bg + P.accent + BOLD;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Draw Aliens and HP bars
|
|
307
|
+
aliens.forEach(a => {
|
|
308
|
+
if (!a.active) return;
|
|
309
|
+
const sprite = ALIEN_SPRITES[a.type];
|
|
310
|
+
const cx = Math.floor(a.x);
|
|
311
|
+
const cy = Math.floor(a.y);
|
|
312
|
+
// If flashed, turn white, otherwise normal color
|
|
313
|
+
const color = a.flashTicks > 0 ? '\x1b[38;2;255;255;255m' : ((a.type === 0) ? P.alien : P.primary);
|
|
314
|
+
|
|
315
|
+
// Draw HP Bar
|
|
316
|
+
const hpColor = a.hp > 2 ? '\x1b[38;2;0;255;0m' : '\x1b[38;2;255;0;0m';
|
|
317
|
+
const hpBar = '[' + '■'.repeat(Math.max(0, a.hp)) + ' '.repeat(Math.max(0, 5 - a.hp)) + ']';
|
|
318
|
+
const hpX = cx + 1;
|
|
319
|
+
const hpY = cy - 1;
|
|
320
|
+
if (hpY >= 0 && hpY < playAreaH) {
|
|
321
|
+
for (let c = 0; c < hpBar.length; c++) {
|
|
322
|
+
if (hpX + c >= 0 && hpX + c < cols) {
|
|
323
|
+
buffer[hpY][hpX + c] = hpBar[c];
|
|
324
|
+
colorBuffer[hpY][hpX + c] = P.bg + hpColor;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (let r = 0; r < ALIEN_H; r++) {
|
|
330
|
+
if (cy + r >= playAreaH) continue;
|
|
331
|
+
if (cy + r < 0) continue;
|
|
332
|
+
for (let c = 0; c < ALIEN_W; c++) {
|
|
333
|
+
if (cx + c >= 0 && cx + c < cols) {
|
|
334
|
+
const char = sprite[r][c];
|
|
335
|
+
if (char !== ' ') {
|
|
336
|
+
buffer[cy + r][cx + c] = char;
|
|
337
|
+
colorBuffer[cy + r][cx + c] = currentBg + color;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Draw Explicit Top-Left Score
|
|
345
|
+
const scoreText = ` 🎯 SCORE: ${score} `;
|
|
346
|
+
for (let c = 0; c < scoreText.length; c++) {
|
|
347
|
+
if (c < cols) {
|
|
348
|
+
buffer[0][c] = scoreText[c];
|
|
349
|
+
colorBuffer[0][c] = currentBg + '\x1b[38;2;255;255;0m' + BOLD;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Draw Health Bar Top-Right
|
|
354
|
+
const hText = `HEALTH: ${'♥'.repeat(playerHearts)}${'♡'.repeat(3 - playerHearts)}`;
|
|
355
|
+
const hPad = cols - hText.length - 1;
|
|
356
|
+
for (let c = 0; c < hText.length; c++) {
|
|
357
|
+
if (hPad + c < cols && hPad + c >= 0) {
|
|
358
|
+
buffer[0][hPad + c] = hText[c];
|
|
359
|
+
colorBuffer[0][hPad + c] = currentBg + '\x1b[38;2;255;50;50m' + BOLD;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Draw Lasers
|
|
364
|
+
lasers.forEach(l => {
|
|
365
|
+
const cx = Math.floor(l.x);
|
|
366
|
+
const cy = Math.floor(l.y);
|
|
367
|
+
if (cy >= 0 && cy < playAreaH && cx >= 0 && cx < cols) {
|
|
368
|
+
buffer[cy][cx] = '┃';
|
|
369
|
+
colorBuffer[cy][cx] = currentBg + P.laser + BOLD;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Draw Particles
|
|
374
|
+
const pChars = ['*', '#', '+', '·'];
|
|
375
|
+
particles.forEach((p, idx) => {
|
|
376
|
+
const cx = Math.floor(p.x);
|
|
377
|
+
const cy = Math.floor(p.y);
|
|
378
|
+
if (cy >= 0 && cy < playAreaH && cx >= 0 && cx < cols) {
|
|
379
|
+
buffer[cy][cx] = pChars[idx % pChars.length];
|
|
380
|
+
colorBuffer[cy][cx] = currentBg + P.explosion;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Draw Tank UI Base at Bottom
|
|
385
|
+
// 4 Turrets dividing spacing equally
|
|
386
|
+
for (let i = 0; i < 4; i++) {
|
|
387
|
+
if (turretFlashes[i] > 0) turretFlashes[i]--;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const baseRow1 = playAreaH - 2;
|
|
391
|
+
const baseRow2 = playAreaH - 1;
|
|
392
|
+
const positions = [0.2, 0.4, 0.6, 0.8];
|
|
393
|
+
const keysLabel = ['LEFT KEYS', 'MID-LEFT', 'MID-RIGHT', 'RIGHT KEYS'];
|
|
394
|
+
|
|
395
|
+
// Draw solid base connection line
|
|
396
|
+
for (let c = 0; c < cols; c++) {
|
|
397
|
+
if (baseRow2 >= 0 && baseRow2 < playAreaH) {
|
|
398
|
+
buffer[baseRow2][c] = '▄';
|
|
399
|
+
colorBuffer[baseRow2][c] = currentBg + '\x1b[38;2;80;90;100m';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Draw each of the 4 turrets
|
|
404
|
+
positions.forEach((pct, idx) => {
|
|
405
|
+
const isFlashing = turretFlashes[idx] > 0;
|
|
406
|
+
const tColor = isFlashing ? '\x1b[38;2;255;255;0m' : '\x1b[38;2;120;150;180m'; // Yellow if shooting, blue-grey otherwise
|
|
407
|
+
const txtColor = isFlashing ? '\x1b[38;2;255;255;255m' : '\x1b[38;2;80;100;120m'; // High contrast text if flashing
|
|
408
|
+
|
|
409
|
+
const cx = Math.floor(cols * pct);
|
|
410
|
+
|
|
411
|
+
// Cannon tip
|
|
412
|
+
if (baseRow1 >= 0 && cx >= 0 && cx < cols) {
|
|
413
|
+
buffer[baseRow1][cx] = '▲';
|
|
414
|
+
colorBuffer[baseRow1][cx] = currentBg + tColor;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Treads & labels
|
|
418
|
+
const label = `[====${keysLabel[idx]}====]`;
|
|
419
|
+
const startX = cx - Math.floor(label.length / 2);
|
|
420
|
+
|
|
421
|
+
for (let i = 0; i < label.length; i++) {
|
|
422
|
+
const drawX = startX + i;
|
|
423
|
+
if (baseRow2 >= 0 && drawX >= 0 && drawX < cols) {
|
|
424
|
+
buffer[baseRow2][drawX] = label[i];
|
|
425
|
+
colorBuffer[baseRow2][drawX] = currentBg + (label[i] === '=' || label[i] === '[' || label[i] === ']' ? tColor : txtColor);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Compile buffer to ANSI strings
|
|
431
|
+
for (let r = 0; r < playAreaH; r++) {
|
|
432
|
+
let rowStr = '';
|
|
433
|
+
let currCol = currentBg;
|
|
434
|
+
let currRun = '';
|
|
435
|
+
|
|
436
|
+
for (let c = 0; c < cols; c++) {
|
|
437
|
+
const char = buffer[r][c];
|
|
438
|
+
const colorCode = colorBuffer[r][c];
|
|
439
|
+
|
|
440
|
+
if (colorCode !== currCol) {
|
|
441
|
+
rowStr += currCol + currRun;
|
|
442
|
+
currCol = colorCode;
|
|
443
|
+
currRun = char;
|
|
444
|
+
} else {
|
|
445
|
+
currRun += char;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
rowStr += currCol + currRun + RESET;
|
|
449
|
+
lines[r] = rowStr;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Draw Game Over Overlay
|
|
453
|
+
if (isGameOver) {
|
|
454
|
+
const goLines = [
|
|
455
|
+
' ▄██████▄ ▄▀▀▀▄ █▄ ▄█ ▄██████▄ ',
|
|
456
|
+
' ▐█▀ ▀█ █ ▄ █ ██▀██ ▐█▀ ▀█ ',
|
|
457
|
+
' ▐█ ▄▄▄▄ ▐█▄▄▄▄▄█▌ █ ▀ █ ▐█▄▄▄▄▄▄▌ ',
|
|
458
|
+
' ▐█▌ ██ █ █ █ █ ▐█ █▌ ',
|
|
459
|
+
' ▀██████▀ █ █ █ █ ▀██████▀ ',
|
|
460
|
+
' ',
|
|
461
|
+
' ▄██████▄ ▀█ █▀ ▄██████▄ ██▀▀▀▀',
|
|
462
|
+
' ██▀ ▀██ ▀▄ ▄▀ ██▀ ▀██ ██ ',
|
|
463
|
+
' ██ ██ ▀█ █▀ ██▄▄▄▄▄▄██ █████ ',
|
|
464
|
+
' ██▄ ▄██ ▀▄▄▀ ██▀▀▀▀▀▀▀▀ ██ ',
|
|
465
|
+
' ▀██████▀ ▀▀ ▀██████▀ ██████',
|
|
466
|
+
'',
|
|
467
|
+
` FINAL SCORE: ${score} `,
|
|
468
|
+
'',
|
|
469
|
+
` HOLD SPACE TO RESTART `
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
const goH = goLines.length;
|
|
473
|
+
const overlayY = Math.floor((playAreaH - goH) / 2) - 1;
|
|
474
|
+
|
|
475
|
+
for (let r = 0; r < goH; r++) {
|
|
476
|
+
const screenY = overlayY + r;
|
|
477
|
+
if (screenY < 0 || screenY >= playAreaH) continue;
|
|
478
|
+
|
|
479
|
+
let color = r >= 12 ? P.laser : P.alien; // Fixed undefined color reference
|
|
480
|
+
if (r === 12) color = '\x1b[38;2;255;255;255m';
|
|
481
|
+
|
|
482
|
+
let text = goLines[r];
|
|
483
|
+
|
|
484
|
+
if (r === 14) {
|
|
485
|
+
const heldPct = Math.min(1.0, spaceHeldTicks / 180);
|
|
486
|
+
const barLen = Math.floor(heldPct * 22);
|
|
487
|
+
const bar = '█'.repeat(barLen) + '·'.repeat(22 - barLen);
|
|
488
|
+
text = ` HOLD SPACE: [${bar}] `;
|
|
489
|
+
color = (tick % 40 < 20 || heldPct > 0) ? '\x1b[38;2;255;255;0m' : '\x1b[38;2;100;100;0m';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
lines[screenY] = centerText(
|
|
493
|
+
text === '' ? '' : `${color}${BOLD}${text}${RESET}`,
|
|
494
|
+
cols,
|
|
495
|
+
`${currentBg} `
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── UI Overlays ──
|
|
501
|
+
// (Score rendering moved into the buffer so it renders deeply behind aliens)
|
|
502
|
+
|
|
503
|
+
// UNVOID Button
|
|
504
|
+
const buttonRow = playAreaH + 1;
|
|
505
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
506
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
507
|
+
if (buttonRow + i < rows - statusHeight) {
|
|
508
|
+
lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Status Bar
|
|
513
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'arcade' }, tick);
|
|
514
|
+
const statusStartRow = rows - statusLines.length;
|
|
515
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
516
|
+
if (statusStartRow + i < rows) {
|
|
517
|
+
lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
lines,
|
|
523
|
+
buttonRegion: {
|
|
524
|
+
x: button.x,
|
|
525
|
+
y: buttonRow + 1,
|
|
526
|
+
width: button.width,
|
|
527
|
+
height: button.height,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|