@hypersocial/cli-games 0.2.1 → 0.2.2
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/dist/cli.js +2646 -150
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2651 -164
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -537,6 +537,15 @@ function navigateMenu(currentSelection, itemCount, key, domEvent) {
|
|
|
537
537
|
}
|
|
538
538
|
return { newSelection, confirmed };
|
|
539
539
|
}
|
|
540
|
+
function checkShortcut(items, key) {
|
|
541
|
+
for (let i = 0; i < items.length; i++) {
|
|
542
|
+
const shortcut = items[i].shortcut;
|
|
543
|
+
if (shortcut && key === shortcut.toLowerCase()) {
|
|
544
|
+
return i;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
540
549
|
function renderSimpleMenu(items, selection, options) {
|
|
541
550
|
const themeColor = getCurrentThemeColor();
|
|
542
551
|
const { centerX, startY, showShortcuts = true } = options;
|
|
@@ -683,8 +692,8 @@ var TILE_BG_COLORS = {
|
|
|
683
692
|
};
|
|
684
693
|
function run2048Game(terminal) {
|
|
685
694
|
const themeColor = getCurrentThemeColor();
|
|
686
|
-
const
|
|
687
|
-
const
|
|
695
|
+
const MIN_COLS2 = 36;
|
|
696
|
+
const MIN_ROWS2 = 16;
|
|
688
697
|
const GRID_SIZE = 4;
|
|
689
698
|
const TILE_WIDTH = 8;
|
|
690
699
|
const TILE_HEIGHT = 3;
|
|
@@ -748,7 +757,7 @@ function run2048Game(terminal) {
|
|
|
748
757
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
749
758
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
750
759
|
}
|
|
751
|
-
function
|
|
760
|
+
function triggerShake3(frames, intensity) {
|
|
752
761
|
shakeFrames = frames;
|
|
753
762
|
shakeIntensity = intensity;
|
|
754
763
|
}
|
|
@@ -872,13 +881,13 @@ function run2048Game(terminal) {
|
|
|
872
881
|
if (anyMoved) {
|
|
873
882
|
score += totalMergeScore;
|
|
874
883
|
if (totalMergeScore > 0) {
|
|
875
|
-
|
|
884
|
+
triggerShake3(4, 1);
|
|
876
885
|
}
|
|
877
886
|
spawnTile();
|
|
878
887
|
if (!won && !continuedAfterWin && has2048()) {
|
|
879
888
|
won = true;
|
|
880
889
|
if (score > highScore) highScore = score;
|
|
881
|
-
|
|
890
|
+
triggerShake3(8, 2);
|
|
882
891
|
for (let y = 0; y < GRID_SIZE; y++) {
|
|
883
892
|
for (let x = 0; x < GRID_SIZE; x++) {
|
|
884
893
|
if (grid[y][x] === 2048) {
|
|
@@ -964,12 +973,12 @@ function run2048Game(terminal) {
|
|
|
964
973
|
if (shakeFrames > 0) shakeFrames--;
|
|
965
974
|
const cols = terminal.cols;
|
|
966
975
|
const rows = terminal.rows;
|
|
967
|
-
if (cols <
|
|
976
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
968
977
|
const msg1 = "Terminal too small!";
|
|
969
|
-
const needWidth = cols <
|
|
970
|
-
const needHeight = rows <
|
|
978
|
+
const needWidth = cols < MIN_COLS2;
|
|
979
|
+
const needHeight = rows < MIN_ROWS2;
|
|
971
980
|
let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider ->" : "Make pane taller v";
|
|
972
|
-
const msg2 = `Need: ${
|
|
981
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
973
982
|
const centerX = Math.floor(cols / 2);
|
|
974
983
|
const centerY = Math.floor(rows / 2);
|
|
975
984
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -1281,8 +1290,8 @@ function run2048Game(terminal) {
|
|
|
1281
1290
|
// src/games/asteroids/index.ts
|
|
1282
1291
|
function runAsteroidsGame(terminal) {
|
|
1283
1292
|
const themeColor = getCurrentThemeColor();
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1293
|
+
const MIN_COLS2 = 35;
|
|
1294
|
+
const MIN_ROWS2 = 16;
|
|
1286
1295
|
let GAME_WIDTH = 30;
|
|
1287
1296
|
let GAME_HEIGHT = 12;
|
|
1288
1297
|
const SHIP_TURN_SPEED = 0.2;
|
|
@@ -1380,7 +1389,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1380
1389
|
if (value >= max) return value - max;
|
|
1381
1390
|
return value;
|
|
1382
1391
|
}
|
|
1383
|
-
function
|
|
1392
|
+
function triggerShake3(frames, intensity) {
|
|
1384
1393
|
shakeFrames = frames;
|
|
1385
1394
|
shakeIntensity = intensity;
|
|
1386
1395
|
}
|
|
@@ -1417,7 +1426,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1417
1426
|
if (invincibilityFrames > 0) return;
|
|
1418
1427
|
lives--;
|
|
1419
1428
|
spawnParticles3(ship.x, ship.y, 15, "\x1B[91m");
|
|
1420
|
-
|
|
1429
|
+
triggerShake3(10, 3);
|
|
1421
1430
|
if (lives <= 0) {
|
|
1422
1431
|
gameOver = true;
|
|
1423
1432
|
if (score > highScore) highScore = score;
|
|
@@ -1498,7 +1507,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1498
1507
|
bullets.splice(j, 1);
|
|
1499
1508
|
splitAsteroid(ast);
|
|
1500
1509
|
asteroids.splice(i, 1);
|
|
1501
|
-
|
|
1510
|
+
triggerShake3(4, 1);
|
|
1502
1511
|
break;
|
|
1503
1512
|
}
|
|
1504
1513
|
}
|
|
@@ -1528,9 +1537,9 @@ function runAsteroidsGame(terminal) {
|
|
|
1528
1537
|
if (shakeFrames > 0) shakeFrames--;
|
|
1529
1538
|
const cols = terminal.cols;
|
|
1530
1539
|
const rows = terminal.rows;
|
|
1531
|
-
if (cols <
|
|
1540
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
1532
1541
|
const msg = "Terminal too small!";
|
|
1533
|
-
const need = `Need: ${
|
|
1542
|
+
const need = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
1534
1543
|
output += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor((cols - msg.length) / 2))}H${themeColor}${msg}\x1B[0m`;
|
|
1535
1544
|
output += `\x1B[${Math.floor(rows / 2) + 2};${Math.max(1, Math.floor((cols - need.length) / 2))}H\x1B[2m${need}\x1B[0m`;
|
|
1536
1545
|
terminal.write(output);
|
|
@@ -1721,8 +1730,8 @@ function runAsteroidsGame(terminal) {
|
|
|
1721
1730
|
// src/games/breakout/index.ts
|
|
1722
1731
|
function runBreakoutGame(terminal) {
|
|
1723
1732
|
const themeColor = getCurrentThemeColor();
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1733
|
+
const MIN_COLS2 = 40;
|
|
1734
|
+
const MIN_ROWS2 = 18;
|
|
1726
1735
|
const GAME_WIDTH = 46;
|
|
1727
1736
|
const GAME_HEIGHT = 20;
|
|
1728
1737
|
const PADDLE_WIDTH_NORMAL = 7;
|
|
@@ -1791,7 +1800,7 @@ function runBreakoutGame(terminal) {
|
|
|
1791
1800
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
1792
1801
|
scorePopups.push({ x, y, text, frames: 18, color });
|
|
1793
1802
|
}
|
|
1794
|
-
function
|
|
1803
|
+
function triggerShake3(frames, intensity) {
|
|
1795
1804
|
shakeFrames = frames;
|
|
1796
1805
|
shakeIntensity = intensity;
|
|
1797
1806
|
}
|
|
@@ -1848,7 +1857,7 @@ function runBreakoutGame(terminal) {
|
|
|
1848
1857
|
powerUps.push({ x, y, type: types[Math.floor(Math.random() * types.length)], vy: 0.15 });
|
|
1849
1858
|
}
|
|
1850
1859
|
function applyPowerUp(type) {
|
|
1851
|
-
|
|
1860
|
+
triggerShake3(8, 2);
|
|
1852
1861
|
borderFlash = 15;
|
|
1853
1862
|
switch (type) {
|
|
1854
1863
|
case "multiball": {
|
|
@@ -2024,7 +2033,7 @@ function runBreakoutGame(terminal) {
|
|
|
2024
2033
|
const totalPts = basePts + comboBonus;
|
|
2025
2034
|
score += totalPts;
|
|
2026
2035
|
const intensity = Math.min(comboCount, 8);
|
|
2027
|
-
|
|
2036
|
+
triggerShake3(3 + intensity, 1 + Math.floor(intensity / 3));
|
|
2028
2037
|
spawnParticles3(brick.x + brick.width / 2, brick.y, 6 + intensity, brickColors[brick.type]);
|
|
2029
2038
|
addScorePopup2(brick.x + 1, brick.y - 1, comboCount > 1 ? `+${totalPts}!` : `+${totalPts}`, brickColors[brick.type]);
|
|
2030
2039
|
spawnPowerUp(brick.x + brick.width / 2, brick.y);
|
|
@@ -2042,12 +2051,12 @@ function runBreakoutGame(terminal) {
|
|
|
2042
2051
|
if (lives <= 0) {
|
|
2043
2052
|
gameOver = true;
|
|
2044
2053
|
if (score > highScore) highScore = score;
|
|
2045
|
-
|
|
2054
|
+
triggerShake3(20, 4);
|
|
2046
2055
|
spawnParticles3(paddleX, GAME_HEIGHT - 2, 15, "\x1B[1;91m", ["\u2717", "\u2620", "\xD7", "\u2593"]);
|
|
2047
2056
|
} else {
|
|
2048
2057
|
ballAttached = true;
|
|
2049
2058
|
balls = [{ x: paddleX, y: GAME_HEIGHT - 3, vx: 0, vy: 0, active: true }];
|
|
2050
|
-
|
|
2059
|
+
triggerShake3(10, 2);
|
|
2051
2060
|
}
|
|
2052
2061
|
}
|
|
2053
2062
|
const aliveBricks = bricks.filter((b) => b.alive);
|
|
@@ -2056,7 +2065,7 @@ function runBreakoutGame(terminal) {
|
|
|
2056
2065
|
gameOver = true;
|
|
2057
2066
|
level++;
|
|
2058
2067
|
if (score > highScore) highScore = score;
|
|
2059
|
-
|
|
2068
|
+
triggerShake3(12, 2);
|
|
2060
2069
|
for (let i = 0; i < 5; i++) setTimeout(() => spawnParticles3(Math.random() * GAME_WIDTH, Math.random() * GAME_HEIGHT / 2, 15, brickColors[Math.floor(Math.random() * brickColors.length)], ["\u2605", "\u2726", "\u25C6", "\u25CF", "\u2727"]), i * 100);
|
|
2061
2070
|
}
|
|
2062
2071
|
}
|
|
@@ -2065,11 +2074,11 @@ function runBreakoutGame(terminal) {
|
|
|
2065
2074
|
if (shakeFrames > 0) shakeFrames--;
|
|
2066
2075
|
if (borderFlash > 0) borderFlash--;
|
|
2067
2076
|
const cols = terminal.cols, rows = terminal.rows;
|
|
2068
|
-
if (cols <
|
|
2077
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
2069
2078
|
const msg1 = "Terminal too small!";
|
|
2070
|
-
const needW = cols <
|
|
2079
|
+
const needW = cols < MIN_COLS2, needH = rows < MIN_ROWS2;
|
|
2071
2080
|
const hint2 = needW && needH ? "Make pane larger" : needW ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
2072
|
-
const msg2 = `Need: ${
|
|
2081
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
2073
2082
|
const cX = Math.floor(cols / 2), cY = Math.floor(rows / 2);
|
|
2074
2083
|
output += `\x1B[${cY - 1};${Math.max(1, cX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
2075
2084
|
output += `\x1B[${cY + 1};${Math.max(1, cX - Math.floor(msg2.length / 2))}H\x1B[2m${msg2}\x1B[0m`;
|
|
@@ -2631,8 +2640,8 @@ var ROPE_RETRACT_SPEED = 0.15;
|
|
|
2631
2640
|
var ROPE_FAST_RETRACT_SPEED = 0.3;
|
|
2632
2641
|
function runCourierGame(terminal) {
|
|
2633
2642
|
const themeColor = getCurrentThemeColor();
|
|
2634
|
-
const
|
|
2635
|
-
const
|
|
2643
|
+
const MIN_COLS2 = 40;
|
|
2644
|
+
const MIN_ROWS2 = 16;
|
|
2636
2645
|
let running = true;
|
|
2637
2646
|
let gameStarted = false;
|
|
2638
2647
|
let gameOver = false;
|
|
@@ -3261,8 +3270,8 @@ function runCourierGame(terminal) {
|
|
|
3261
3270
|
output += "\x1B[2J\x1B[H";
|
|
3262
3271
|
const cols = terminal.cols;
|
|
3263
3272
|
const rows = terminal.rows;
|
|
3264
|
-
if (cols <
|
|
3265
|
-
const msg = `Need ${
|
|
3273
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
3274
|
+
const msg = `Need ${MIN_COLS2}\xD7${MIN_ROWS2}, have ${cols}\xD7${rows}`;
|
|
3266
3275
|
const centerX = Math.floor(cols / 2);
|
|
3267
3276
|
const centerY = Math.floor(rows / 2);
|
|
3268
3277
|
output += `\x1B[${centerY};${Math.max(1, centerX - msg.length / 2)}H${themeColor}${msg}\x1B[0m`;
|
|
@@ -4021,8 +4030,8 @@ var LOG_MESSAGES = {
|
|
|
4021
4030
|
};
|
|
4022
4031
|
function runCrackGame(terminal) {
|
|
4023
4032
|
const themeColor = getCurrentThemeColor();
|
|
4024
|
-
const
|
|
4025
|
-
const
|
|
4033
|
+
const MIN_COLS2 = 40;
|
|
4034
|
+
const MIN_ROWS2 = 16;
|
|
4026
4035
|
let cols = terminal.cols;
|
|
4027
4036
|
let rows = terminal.rows;
|
|
4028
4037
|
const updateDimensions = () => {
|
|
@@ -4234,9 +4243,9 @@ function runCrackGame(terminal) {
|
|
|
4234
4243
|
function render() {
|
|
4235
4244
|
let output = "";
|
|
4236
4245
|
output += "\x1B[2J\x1B[H";
|
|
4237
|
-
if (cols <
|
|
4246
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
4238
4247
|
const msg1 = "Terminal too small!";
|
|
4239
|
-
const msg2 = `Need: ${
|
|
4248
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
4240
4249
|
const centerX = Math.floor(cols / 2);
|
|
4241
4250
|
const centerY = Math.floor(rows / 2);
|
|
4242
4251
|
output += `\x1B[${centerY};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -4689,8 +4698,8 @@ function runCrackGame(terminal) {
|
|
|
4689
4698
|
// src/games/frogger/index.ts
|
|
4690
4699
|
function runFroggerGame(terminal) {
|
|
4691
4700
|
const themeColor = getCurrentThemeColor();
|
|
4692
|
-
const
|
|
4693
|
-
const
|
|
4701
|
+
const MIN_COLS2 = 40;
|
|
4702
|
+
const MIN_ROWS2 = 16;
|
|
4694
4703
|
const LILY_PAD_COUNT = 5;
|
|
4695
4704
|
const TIME_LIMIT = 45;
|
|
4696
4705
|
const MOVE_COOLDOWN = 100;
|
|
@@ -5075,10 +5084,10 @@ function runFroggerGame(terminal) {
|
|
|
5075
5084
|
output += "\x1B[2J\x1B[H";
|
|
5076
5085
|
const cols = terminal.cols;
|
|
5077
5086
|
const rows = terminal.rows;
|
|
5078
|
-
if (cols <
|
|
5087
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
5079
5088
|
const msg1 = "Terminal too small!";
|
|
5080
|
-
const msg2 = `Need: ${
|
|
5081
|
-
const hint2 = cols <
|
|
5089
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
5090
|
+
const hint2 = cols < MIN_COLS2 && rows < MIN_ROWS2 ? "Make pane larger" : cols < MIN_COLS2 ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
5082
5091
|
const cx = Math.floor(cols / 2);
|
|
5083
5092
|
const cy = Math.floor(rows / 2);
|
|
5084
5093
|
output += `\x1B[${cy - 1};${Math.max(1, cx - msg1.length / 2)}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -5516,8 +5525,8 @@ var HANGMAN_STAGES = [
|
|
|
5516
5525
|
function runHangmanGame(terminal) {
|
|
5517
5526
|
const themeColor = getCurrentThemeColor();
|
|
5518
5527
|
const GAME_WIDTH = 40;
|
|
5519
|
-
const
|
|
5520
|
-
const
|
|
5528
|
+
const MIN_COLS2 = 32;
|
|
5529
|
+
const MIN_ROWS2 = 16;
|
|
5521
5530
|
let running = true;
|
|
5522
5531
|
let gameStarted = false;
|
|
5523
5532
|
let gameOver = false;
|
|
@@ -5681,10 +5690,10 @@ function runHangmanGame(terminal) {
|
|
|
5681
5690
|
}
|
|
5682
5691
|
const cols = terminal.cols;
|
|
5683
5692
|
const rows = terminal.rows;
|
|
5684
|
-
if (cols <
|
|
5693
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
5685
5694
|
const msg1 = "Terminal too small!";
|
|
5686
|
-
const needWidth = cols <
|
|
5687
|
-
const needHeight = rows <
|
|
5695
|
+
const needWidth = cols < MIN_COLS2;
|
|
5696
|
+
const needHeight = rows < MIN_ROWS2;
|
|
5688
5697
|
let hint2 = "";
|
|
5689
5698
|
if (needWidth && needHeight) {
|
|
5690
5699
|
hint2 = "Make pane larger";
|
|
@@ -5693,7 +5702,7 @@ function runHangmanGame(terminal) {
|
|
|
5693
5702
|
} else {
|
|
5694
5703
|
hint2 = "Make pane taller \u2193";
|
|
5695
5704
|
}
|
|
5696
|
-
const msg2 = `Need: ${
|
|
5705
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
5697
5706
|
const centerX = Math.floor(cols / 2);
|
|
5698
5707
|
const centerY = Math.floor(rows / 2);
|
|
5699
5708
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -6048,7 +6057,7 @@ function runMinesweeperGame(terminal) {
|
|
|
6048
6057
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
6049
6058
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
6050
6059
|
}
|
|
6051
|
-
function
|
|
6060
|
+
function triggerShake3(frames, intensity) {
|
|
6052
6061
|
shakeFrames = frames;
|
|
6053
6062
|
shakeIntensity = intensity;
|
|
6054
6063
|
}
|
|
@@ -6135,7 +6144,7 @@ function runMinesweeperGame(terminal) {
|
|
|
6135
6144
|
if (cell.isMine) {
|
|
6136
6145
|
gameOver = true;
|
|
6137
6146
|
won = false;
|
|
6138
|
-
|
|
6147
|
+
triggerShake3(25, 4);
|
|
6139
6148
|
spawnExplosion(x * 2 + 1, y);
|
|
6140
6149
|
addScorePopup2(x * 2, y - 1, "MALWARE!", "\x1B[1;91m");
|
|
6141
6150
|
revealAllMines();
|
|
@@ -6184,7 +6193,7 @@ function runMinesweeperGame(terminal) {
|
|
|
6184
6193
|
if (cellsRevealed >= totalNonMines) {
|
|
6185
6194
|
gameOver = true;
|
|
6186
6195
|
won = true;
|
|
6187
|
-
|
|
6196
|
+
triggerShake3(10, 2);
|
|
6188
6197
|
for (let i = 0; i < 5; i++) {
|
|
6189
6198
|
const x = Math.floor(Math.random() * difficulty.width);
|
|
6190
6199
|
const y = Math.floor(Math.random() * difficulty.height);
|
|
@@ -6582,8 +6591,8 @@ function runMinesweeperGame(terminal) {
|
|
|
6582
6591
|
// src/games/pong/index.ts
|
|
6583
6592
|
function runPongGame(terminal) {
|
|
6584
6593
|
const themeColor = getCurrentThemeColor();
|
|
6585
|
-
const
|
|
6586
|
-
const
|
|
6594
|
+
const MIN_COLS2 = 40;
|
|
6595
|
+
const MIN_ROWS2 = 16;
|
|
6587
6596
|
const getGameDimensions = () => {
|
|
6588
6597
|
const cols = terminal.cols;
|
|
6589
6598
|
const rows = terminal.rows;
|
|
@@ -6723,10 +6732,10 @@ function runPongGame(terminal) {
|
|
|
6723
6732
|
}
|
|
6724
6733
|
const cols = terminal.cols;
|
|
6725
6734
|
const rows = terminal.rows;
|
|
6726
|
-
if (cols <
|
|
6735
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
6727
6736
|
const msg1 = "Terminal too small!";
|
|
6728
|
-
const needWidth = cols <
|
|
6729
|
-
const needHeight = rows <
|
|
6737
|
+
const needWidth = cols < MIN_COLS2;
|
|
6738
|
+
const needHeight = rows < MIN_ROWS2;
|
|
6730
6739
|
let hint2 = "";
|
|
6731
6740
|
if (needWidth && needHeight) {
|
|
6732
6741
|
hint2 = "Make pane larger";
|
|
@@ -6735,7 +6744,7 @@ function runPongGame(terminal) {
|
|
|
6735
6744
|
} else {
|
|
6736
6745
|
hint2 = "Make pane taller \u2193";
|
|
6737
6746
|
}
|
|
6738
|
-
const msg2 = `Need: ${
|
|
6747
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
6739
6748
|
const centerX2 = Math.floor(cols / 2);
|
|
6740
6749
|
const centerY = Math.floor(rows / 2);
|
|
6741
6750
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -7171,8 +7180,8 @@ function runRunnerGame(terminal) {
|
|
|
7171
7180
|
const GAME_WIDTH = ROAD_WIDTH_BOTTOM + SCENERY_WIDTH * 2;
|
|
7172
7181
|
let gameTop = 5;
|
|
7173
7182
|
let gameLeft = 4;
|
|
7174
|
-
const
|
|
7175
|
-
const
|
|
7183
|
+
const MIN_COLS2 = GAME_WIDTH + 2;
|
|
7184
|
+
const MIN_ROWS2 = TRACK_HEIGHT + 8;
|
|
7176
7185
|
let running = true;
|
|
7177
7186
|
let gameStarted = false;
|
|
7178
7187
|
let gameOver = false;
|
|
@@ -7403,12 +7412,12 @@ function runRunnerGame(terminal) {
|
|
|
7403
7412
|
}
|
|
7404
7413
|
const cols = terminal.cols;
|
|
7405
7414
|
const rows = terminal.rows;
|
|
7406
|
-
if (cols <
|
|
7415
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
7407
7416
|
const msg1 = "Terminal too small!";
|
|
7408
|
-
const needWidth = cols <
|
|
7409
|
-
const needHeight = rows <
|
|
7417
|
+
const needWidth = cols < MIN_COLS2;
|
|
7418
|
+
const needHeight = rows < MIN_ROWS2;
|
|
7410
7419
|
let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
7411
|
-
const msg2 = `Need: ${
|
|
7420
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
7412
7421
|
const centerX2 = Math.floor(cols / 2);
|
|
7413
7422
|
const centerY = Math.floor(rows / 2);
|
|
7414
7423
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -7935,8 +7944,8 @@ function runRunnerGame(terminal) {
|
|
|
7935
7944
|
// src/games/simon/index.ts
|
|
7936
7945
|
function runSimonGame(terminal) {
|
|
7937
7946
|
const themeColor = getCurrentThemeColor();
|
|
7938
|
-
const
|
|
7939
|
-
const
|
|
7947
|
+
const MIN_COLS2 = 40;
|
|
7948
|
+
const MIN_ROWS2 = 18;
|
|
7940
7949
|
const GAME_WIDTH = 40;
|
|
7941
7950
|
const GAME_HEIGHT = 18;
|
|
7942
7951
|
const QUADRANT_COLORS = [
|
|
@@ -8050,7 +8059,7 @@ function runSimonGame(terminal) {
|
|
|
8050
8059
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
8051
8060
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
8052
8061
|
}
|
|
8053
|
-
function
|
|
8062
|
+
function triggerShake3(frames, intensity) {
|
|
8054
8063
|
shakeFrames = frames;
|
|
8055
8064
|
shakeIntensity = intensity;
|
|
8056
8065
|
}
|
|
@@ -8112,7 +8121,7 @@ function runSimonGame(terminal) {
|
|
|
8112
8121
|
statusMessage = "PATTERN ACCEPTED";
|
|
8113
8122
|
statusColor = "\x1B[1;92m";
|
|
8114
8123
|
statusBlink = true;
|
|
8115
|
-
|
|
8124
|
+
triggerShake3(6, 1);
|
|
8116
8125
|
}
|
|
8117
8126
|
} else {
|
|
8118
8127
|
phase = "failure";
|
|
@@ -8124,7 +8133,7 @@ function runSimonGame(terminal) {
|
|
|
8124
8133
|
statusMessage = "ACCESS DENIED";
|
|
8125
8134
|
statusColor = "\x1B[1;91m";
|
|
8126
8135
|
statusBlink = true;
|
|
8127
|
-
|
|
8136
|
+
triggerShake3(15, 3);
|
|
8128
8137
|
const centerX = GAME_WIDTH / 2;
|
|
8129
8138
|
const centerY = GAME_HEIGHT / 2;
|
|
8130
8139
|
spawnParticles3(centerX, centerY, 15, "\x1B[1;91m", ["\u2717", "\u2716", "\xD7", "\u2573"]);
|
|
@@ -8250,12 +8259,12 @@ function runSimonGame(terminal) {
|
|
|
8250
8259
|
if (shakeFrames > 0) shakeFrames--;
|
|
8251
8260
|
const cols = terminal.cols;
|
|
8252
8261
|
const rows = terminal.rows;
|
|
8253
|
-
if (cols <
|
|
8262
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
8254
8263
|
const msg1 = "Terminal too small!";
|
|
8255
|
-
const needWidth = cols <
|
|
8256
|
-
const needHeight = rows <
|
|
8264
|
+
const needWidth = cols < MIN_COLS2;
|
|
8265
|
+
const needHeight = rows < MIN_ROWS2;
|
|
8257
8266
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
8258
|
-
const msg2 = `Need: ${
|
|
8267
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
8259
8268
|
const centerX = Math.floor(cols / 2);
|
|
8260
8269
|
const centerY = Math.floor(rows / 2);
|
|
8261
8270
|
outputParts.push(`\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`);
|
|
@@ -8517,8 +8526,8 @@ function runSimonGame(terminal) {
|
|
|
8517
8526
|
// src/games/snake/index.ts
|
|
8518
8527
|
function runSnakeGame(terminal) {
|
|
8519
8528
|
const themeColor = getCurrentThemeColor();
|
|
8520
|
-
const
|
|
8521
|
-
const
|
|
8529
|
+
const MIN_COLS2 = 36;
|
|
8530
|
+
const MIN_ROWS2 = 16;
|
|
8522
8531
|
let cols = terminal.cols;
|
|
8523
8532
|
let rows = terminal.rows;
|
|
8524
8533
|
let gameTop = 6;
|
|
@@ -8621,7 +8630,7 @@ function runSnakeGame(terminal) {
|
|
|
8621
8630
|
});
|
|
8622
8631
|
}
|
|
8623
8632
|
}
|
|
8624
|
-
function
|
|
8633
|
+
function updateParticles4() {
|
|
8625
8634
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
8626
8635
|
const p = particles[i];
|
|
8627
8636
|
p.x += p.vx;
|
|
@@ -8646,7 +8655,7 @@ function runSnakeGame(terminal) {
|
|
|
8646
8655
|
scorePopup.y -= 0.3;
|
|
8647
8656
|
if (scorePopup.frames <= 0) scorePopup = null;
|
|
8648
8657
|
}
|
|
8649
|
-
|
|
8658
|
+
updateParticles4();
|
|
8650
8659
|
let renderGameLeft = gameLeft;
|
|
8651
8660
|
let renderGameTop = gameTop;
|
|
8652
8661
|
if (shakeFrames > 0) {
|
|
@@ -8655,10 +8664,10 @@ function runSnakeGame(terminal) {
|
|
|
8655
8664
|
renderGameLeft = Math.max(1, gameLeft + shakeX);
|
|
8656
8665
|
renderGameTop = Math.max(3, gameTop + shakeY);
|
|
8657
8666
|
}
|
|
8658
|
-
if (cols <
|
|
8667
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
8659
8668
|
const msg1 = "Terminal too small!";
|
|
8660
|
-
const needWidth = cols <
|
|
8661
|
-
const needHeight = rows <
|
|
8669
|
+
const needWidth = cols < MIN_COLS2;
|
|
8670
|
+
const needHeight = rows < MIN_ROWS2;
|
|
8662
8671
|
let hint2 = "";
|
|
8663
8672
|
if (needWidth && needHeight) {
|
|
8664
8673
|
hint2 = "Make pane larger";
|
|
@@ -8667,7 +8676,7 @@ function runSnakeGame(terminal) {
|
|
|
8667
8676
|
} else {
|
|
8668
8677
|
hint2 = "Make pane taller \u2193";
|
|
8669
8678
|
}
|
|
8670
|
-
const msg2 = `Need: ${
|
|
8679
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
8671
8680
|
const centerX = Math.floor(cols / 2);
|
|
8672
8681
|
const centerY = Math.floor(rows / 2);
|
|
8673
8682
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -8989,8 +8998,8 @@ function runSnakeGame(terminal) {
|
|
|
8989
8998
|
// src/games/spaceinvaders/index.ts
|
|
8990
8999
|
function runSpaceInvadersGame(terminal) {
|
|
8991
9000
|
const themeColor = getCurrentThemeColor();
|
|
8992
|
-
const
|
|
8993
|
-
const
|
|
9001
|
+
const MIN_COLS2 = 40;
|
|
9002
|
+
const MIN_ROWS2 = 16;
|
|
8994
9003
|
const getGameDimensions = () => {
|
|
8995
9004
|
const cols = terminal.cols;
|
|
8996
9005
|
const rows = terminal.rows;
|
|
@@ -9168,10 +9177,10 @@ function runSpaceInvadersGame(terminal) {
|
|
|
9168
9177
|
}
|
|
9169
9178
|
const cols = terminal.cols;
|
|
9170
9179
|
const rows = terminal.rows;
|
|
9171
|
-
if (cols <
|
|
9180
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
9172
9181
|
const msg1 = "Terminal too small!";
|
|
9173
|
-
const needWidth = cols <
|
|
9174
|
-
const needHeight = rows <
|
|
9182
|
+
const needWidth = cols < MIN_COLS2;
|
|
9183
|
+
const needHeight = rows < MIN_ROWS2;
|
|
9175
9184
|
let hint2 = "";
|
|
9176
9185
|
if (needWidth && needHeight) {
|
|
9177
9186
|
hint2 = "Make pane larger";
|
|
@@ -9180,7 +9189,7 @@ function runSpaceInvadersGame(terminal) {
|
|
|
9180
9189
|
} else {
|
|
9181
9190
|
hint2 = "Make pane taller \u2193";
|
|
9182
9191
|
}
|
|
9183
|
-
const msg2 = `Need: ${
|
|
9192
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
9184
9193
|
const centerX = Math.floor(cols / 2);
|
|
9185
9194
|
const centerY = Math.floor(rows / 2);
|
|
9186
9195
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -9653,13 +9662,13 @@ function runTetrisGame(terminal) {
|
|
|
9653
9662
|
const themeColor = getCurrentThemeColor();
|
|
9654
9663
|
const BOARD_WIDTH = 12;
|
|
9655
9664
|
const BOARD_HEIGHT = 20;
|
|
9656
|
-
const
|
|
9665
|
+
const SIDE_PANEL_WIDTH2 = 14;
|
|
9657
9666
|
const BOARD_ONLY_WIDTH = BOARD_WIDTH * 2 + 2;
|
|
9658
|
-
const TOTAL_WIDTH = BOARD_WIDTH * 2 +
|
|
9659
|
-
const
|
|
9667
|
+
const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH2 + 4;
|
|
9668
|
+
const TICK_MS2 = 25;
|
|
9660
9669
|
const SPRINT_TARGET_LINES = 40;
|
|
9661
|
-
const
|
|
9662
|
-
const
|
|
9670
|
+
const MIN_COLS2 = BOARD_ONLY_WIDTH + 2;
|
|
9671
|
+
const MIN_ROWS2 = BOARD_HEIGHT + 3;
|
|
9663
9672
|
const MIN_ROWS_WITH_TITLE = BOARD_HEIGHT + 4;
|
|
9664
9673
|
let running = true;
|
|
9665
9674
|
let gameStarted = false;
|
|
@@ -9742,7 +9751,7 @@ function runTetrisGame(terminal) {
|
|
|
9742
9751
|
const shape = TETROMINOES[currentPiece][0];
|
|
9743
9752
|
pieceX = Math.floor((BOARD_WIDTH - shape[0].length) / 2);
|
|
9744
9753
|
pieceY = 0;
|
|
9745
|
-
if (!
|
|
9754
|
+
if (!isValidPosition2(pieceX, pieceY, currentRotation)) {
|
|
9746
9755
|
gameOver = true;
|
|
9747
9756
|
if (score > highScore) highScore = score;
|
|
9748
9757
|
}
|
|
@@ -9751,7 +9760,7 @@ function runTetrisGame(terminal) {
|
|
|
9751
9760
|
const rotations = TETROMINOES[currentPiece];
|
|
9752
9761
|
return rotations[rotation % rotations.length];
|
|
9753
9762
|
}
|
|
9754
|
-
function
|
|
9763
|
+
function isValidPosition2(x, y, rotation) {
|
|
9755
9764
|
const shape = TETROMINOES[currentPiece][rotation % TETROMINOES[currentPiece].length];
|
|
9756
9765
|
for (let row = 0; row < shape.length; row++) {
|
|
9757
9766
|
for (let col = 0; col < shape[row].length; col++) {
|
|
@@ -9868,23 +9877,23 @@ function runTetrisGame(terminal) {
|
|
|
9868
9877
|
}
|
|
9869
9878
|
}
|
|
9870
9879
|
function moveLeft() {
|
|
9871
|
-
if (
|
|
9880
|
+
if (isValidPosition2(pieceX - 1, pieceY, currentRotation)) {
|
|
9872
9881
|
pieceX--;
|
|
9873
9882
|
}
|
|
9874
9883
|
}
|
|
9875
9884
|
function moveRight() {
|
|
9876
|
-
if (
|
|
9885
|
+
if (isValidPosition2(pieceX + 1, pieceY, currentRotation)) {
|
|
9877
9886
|
pieceX++;
|
|
9878
9887
|
}
|
|
9879
9888
|
}
|
|
9880
9889
|
function moveDown() {
|
|
9881
|
-
if (
|
|
9890
|
+
if (isValidPosition2(pieceX, pieceY + 1, currentRotation)) {
|
|
9882
9891
|
pieceY++;
|
|
9883
9892
|
return true;
|
|
9884
9893
|
}
|
|
9885
9894
|
return false;
|
|
9886
9895
|
}
|
|
9887
|
-
function
|
|
9896
|
+
function hardDrop2() {
|
|
9888
9897
|
if (hardDropping) return;
|
|
9889
9898
|
if (hardDropConsumed) return;
|
|
9890
9899
|
hardDropping = true;
|
|
@@ -9892,13 +9901,13 @@ function runTetrisGame(terminal) {
|
|
|
9892
9901
|
}
|
|
9893
9902
|
function rotate() {
|
|
9894
9903
|
const newRotation = (currentRotation + 1) % TETROMINOES[currentPiece].length;
|
|
9895
|
-
if (
|
|
9904
|
+
if (isValidPosition2(pieceX, pieceY, newRotation)) {
|
|
9896
9905
|
currentRotation = newRotation;
|
|
9897
9906
|
return;
|
|
9898
9907
|
}
|
|
9899
9908
|
const kicks = [-1, 1, -2, 2];
|
|
9900
9909
|
for (const kick of kicks) {
|
|
9901
|
-
if (
|
|
9910
|
+
if (isValidPosition2(pieceX + kick, pieceY, newRotation)) {
|
|
9902
9911
|
pieceX += kick;
|
|
9903
9912
|
currentRotation = newRotation;
|
|
9904
9913
|
return;
|
|
@@ -9910,10 +9919,10 @@ function runTetrisGame(terminal) {
|
|
|
9910
9919
|
output += "\x1B[2J\x1B[H";
|
|
9911
9920
|
const cols = terminal.cols;
|
|
9912
9921
|
const rows = terminal.rows;
|
|
9913
|
-
if (cols <
|
|
9922
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
9914
9923
|
const msg1 = "Terminal too small!";
|
|
9915
|
-
const needWidth = cols <
|
|
9916
|
-
const needHeight = rows <
|
|
9924
|
+
const needWidth = cols < MIN_COLS2;
|
|
9925
|
+
const needHeight = rows < MIN_ROWS2;
|
|
9917
9926
|
let hint = "";
|
|
9918
9927
|
if (needWidth && needHeight) {
|
|
9919
9928
|
hint = "Make pane larger";
|
|
@@ -9922,7 +9931,7 @@ function runTetrisGame(terminal) {
|
|
|
9922
9931
|
} else {
|
|
9923
9932
|
hint = "Make pane taller \u2193";
|
|
9924
9933
|
}
|
|
9925
|
-
const msg2 = `Need: ${
|
|
9934
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
9926
9935
|
const centerX = Math.floor(cols / 2);
|
|
9927
9936
|
const centerY = Math.floor(rows / 2);
|
|
9928
9937
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -10056,7 +10065,7 @@ function runTetrisGame(terminal) {
|
|
|
10056
10065
|
}
|
|
10057
10066
|
}
|
|
10058
10067
|
let ghostY = pieceY;
|
|
10059
|
-
while (
|
|
10068
|
+
while (isValidPosition2(pieceX, ghostY + 1, currentRotation)) {
|
|
10060
10069
|
ghostY++;
|
|
10061
10070
|
}
|
|
10062
10071
|
if (ghostY !== pieceY) {
|
|
@@ -10156,7 +10165,7 @@ function runTetrisGame(terminal) {
|
|
|
10156
10165
|
return;
|
|
10157
10166
|
}
|
|
10158
10167
|
dropCounter++;
|
|
10159
|
-
if (dropCounter *
|
|
10168
|
+
if (dropCounter * TICK_MS2 >= dropInterval) {
|
|
10160
10169
|
dropCounter = 0;
|
|
10161
10170
|
if (!moveDown()) {
|
|
10162
10171
|
lockPiece();
|
|
@@ -10187,14 +10196,14 @@ function runTetrisGame(terminal) {
|
|
|
10187
10196
|
return;
|
|
10188
10197
|
}
|
|
10189
10198
|
render();
|
|
10190
|
-
},
|
|
10199
|
+
}, TICK_MS2);
|
|
10191
10200
|
const gameInterval = setInterval(() => {
|
|
10192
10201
|
if (!running) {
|
|
10193
10202
|
clearInterval(gameInterval);
|
|
10194
10203
|
return;
|
|
10195
10204
|
}
|
|
10196
10205
|
update();
|
|
10197
|
-
},
|
|
10206
|
+
}, TICK_MS2);
|
|
10198
10207
|
const handleKeyUp = (e) => {
|
|
10199
10208
|
if (e.key === " ") {
|
|
10200
10209
|
hardDropConsumed = false;
|
|
@@ -10331,7 +10340,7 @@ function runTetrisGame(terminal) {
|
|
|
10331
10340
|
rotate();
|
|
10332
10341
|
break;
|
|
10333
10342
|
case " ":
|
|
10334
|
-
|
|
10343
|
+
hardDrop2();
|
|
10335
10344
|
break;
|
|
10336
10345
|
}
|
|
10337
10346
|
});
|
|
@@ -10366,8 +10375,8 @@ var BLOCK_COLORS = [
|
|
|
10366
10375
|
];
|
|
10367
10376
|
function runTowerGame(terminal) {
|
|
10368
10377
|
const themeColor = getCurrentThemeColor();
|
|
10369
|
-
const
|
|
10370
|
-
const
|
|
10378
|
+
const MIN_COLS2 = 30;
|
|
10379
|
+
const MIN_ROWS2 = 16;
|
|
10371
10380
|
const INITIAL_BLOCK_WIDTH = 8;
|
|
10372
10381
|
const MIN_BLOCK_WIDTH = 2;
|
|
10373
10382
|
const PERFECT_THRESHOLD = 0.5;
|
|
@@ -10446,7 +10455,7 @@ function runTowerGame(terminal) {
|
|
|
10446
10455
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
10447
10456
|
scorePopups.push({ x, y, text, frames: 24, color });
|
|
10448
10457
|
}
|
|
10449
|
-
function
|
|
10458
|
+
function triggerShake3(frames, intensity) {
|
|
10450
10459
|
shakeFrames = frames;
|
|
10451
10460
|
shakeIntensity = intensity;
|
|
10452
10461
|
}
|
|
@@ -10507,7 +10516,7 @@ function runTowerGame(terminal) {
|
|
|
10507
10516
|
if (score > highScore) highScore = score;
|
|
10508
10517
|
if (perfectCombo > maxCombo) maxCombo = perfectCombo;
|
|
10509
10518
|
spawnParticles3(dropX + dropWidth / 2, landY, 15, "\x1B[91m");
|
|
10510
|
-
|
|
10519
|
+
triggerShake3(10, 3);
|
|
10511
10520
|
return;
|
|
10512
10521
|
}
|
|
10513
10522
|
const isPerfect = Math.abs(dropLeft - topLeft) < PERFECT_THRESHOLD && Math.abs(dropRight - topRight) < PERFECT_THRESHOLD;
|
|
@@ -10525,7 +10534,7 @@ function runTowerGame(terminal) {
|
|
|
10525
10534
|
popupText,
|
|
10526
10535
|
perfectCombo > 3 ? "\x1B[1;91m" : "\x1B[1;93m"
|
|
10527
10536
|
);
|
|
10528
|
-
|
|
10537
|
+
triggerShake3(4, 1);
|
|
10529
10538
|
} else {
|
|
10530
10539
|
if (perfectCombo > maxCombo) maxCombo = perfectCombo;
|
|
10531
10540
|
perfectCombo = 0;
|
|
@@ -10539,7 +10548,7 @@ function runTowerGame(terminal) {
|
|
|
10539
10548
|
spawnFallingPiece(topRight, landY, overhangWidth, dropColor);
|
|
10540
10549
|
spawnParticles3(topRight + overhangWidth / 2, landY, 5, dropColor);
|
|
10541
10550
|
}
|
|
10542
|
-
|
|
10551
|
+
triggerShake3(3, 1);
|
|
10543
10552
|
}
|
|
10544
10553
|
score += points;
|
|
10545
10554
|
height++;
|
|
@@ -10607,12 +10616,12 @@ function runTowerGame(terminal) {
|
|
|
10607
10616
|
if (shakeFrames > 0) shakeFrames--;
|
|
10608
10617
|
const cols = terminal.cols;
|
|
10609
10618
|
const rows = terminal.rows;
|
|
10610
|
-
if (cols <
|
|
10619
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
10611
10620
|
const msg1 = "Terminal too small!";
|
|
10612
|
-
const needWidth = cols <
|
|
10613
|
-
const needHeight = rows <
|
|
10621
|
+
const needWidth = cols < MIN_COLS2;
|
|
10622
|
+
const needHeight = rows < MIN_ROWS2;
|
|
10614
10623
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
10615
|
-
const msg2 = `Need: ${
|
|
10624
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
10616
10625
|
const centerX = Math.floor(cols / 2);
|
|
10617
10626
|
const centerY = Math.floor(rows / 2);
|
|
10618
10627
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -10857,8 +10866,8 @@ function runTowerGame(terminal) {
|
|
|
10857
10866
|
// src/games/tron/index.ts
|
|
10858
10867
|
function runTronGame(terminal) {
|
|
10859
10868
|
const themeColor = getCurrentThemeColor();
|
|
10860
|
-
const
|
|
10861
|
-
const
|
|
10869
|
+
const MIN_COLS2 = 40;
|
|
10870
|
+
const MIN_ROWS2 = 18;
|
|
10862
10871
|
const GAME_WIDTH = 46;
|
|
10863
10872
|
const GAME_HEIGHT = 18;
|
|
10864
10873
|
const ROUNDS_TO_WIN = 3;
|
|
@@ -10927,9 +10936,9 @@ function runTronGame(terminal) {
|
|
|
10927
10936
|
}
|
|
10928
10937
|
function spawnExplosion(x, y, color) {
|
|
10929
10938
|
spawnParticles3(x, y, 20, color, ["\u2717", "\xD7", "\u2591", "\u2592", "\u2593", "\u2588"]);
|
|
10930
|
-
|
|
10939
|
+
triggerShake3(12, 3);
|
|
10931
10940
|
}
|
|
10932
|
-
function
|
|
10941
|
+
function triggerShake3(frames, intensity) {
|
|
10933
10942
|
shakeFrames = frames;
|
|
10934
10943
|
shakeIntensity = intensity;
|
|
10935
10944
|
}
|
|
@@ -11144,7 +11153,7 @@ function runTronGame(terminal) {
|
|
|
11144
11153
|
arenaMinY += SHRINK_AMOUNT;
|
|
11145
11154
|
arenaMaxY -= SHRINK_AMOUNT;
|
|
11146
11155
|
}
|
|
11147
|
-
|
|
11156
|
+
triggerShake3(4, 1);
|
|
11148
11157
|
}
|
|
11149
11158
|
}
|
|
11150
11159
|
}
|
|
@@ -11211,12 +11220,12 @@ function runTronGame(terminal) {
|
|
|
11211
11220
|
if (shakeFrames > 0) shakeFrames--;
|
|
11212
11221
|
const cols = terminal.cols;
|
|
11213
11222
|
const rows = terminal.rows;
|
|
11214
|
-
if (cols <
|
|
11223
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
11215
11224
|
const msg1 = "Terminal too small!";
|
|
11216
|
-
const needWidth = cols <
|
|
11217
|
-
const needHeight = rows <
|
|
11225
|
+
const needWidth = cols < MIN_COLS2;
|
|
11226
|
+
const needHeight = rows < MIN_ROWS2;
|
|
11218
11227
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
11219
|
-
const msg2 = `Need: ${
|
|
11228
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
11220
11229
|
const centerX = Math.floor(cols / 2);
|
|
11221
11230
|
const centerY = Math.floor(rows / 2);
|
|
11222
11231
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -11305,7 +11314,7 @@ function runTronGame(terminal) {
|
|
|
11305
11314
|
const nextMsg = "Press SPACE for next round";
|
|
11306
11315
|
const nextX = gameLeft + Math.floor((GAME_WIDTH - nextMsg.length) / 2) + 1;
|
|
11307
11316
|
output += `\x1B[${msgY + 2};${nextX}H\x1B[2m${themeColor}${nextMsg}\x1B[0m`;
|
|
11308
|
-
output =
|
|
11317
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11309
11318
|
} else if (gameOver) {
|
|
11310
11319
|
output = renderTrailsAndCycles(output, renderLeft, renderTop);
|
|
11311
11320
|
const overColor = playerWins >= ROUNDS_TO_WIN ? "\x1B[1;92m" : "\x1B[1;91m";
|
|
@@ -11319,7 +11328,7 @@ function runTronGame(terminal) {
|
|
|
11319
11328
|
const restart = "\u255A [R] RESTART [Q] QUIT \u255D";
|
|
11320
11329
|
const restartX = gameLeft + Math.floor((GAME_WIDTH - restart.length) / 2) + 1;
|
|
11321
11330
|
output += `\x1B[${overY + 2};${restartX}H\x1B[2m${themeColor}${restart}\x1B[0m`;
|
|
11322
|
-
output =
|
|
11331
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11323
11332
|
} else {
|
|
11324
11333
|
for (const pos of player.trail) {
|
|
11325
11334
|
if (pos.x > arenaMinX && pos.x < arenaMaxX && pos.y > arenaMinY && pos.y < arenaMaxY) {
|
|
@@ -11337,7 +11346,7 @@ function runTronGame(terminal) {
|
|
|
11337
11346
|
if (ai.alive) {
|
|
11338
11347
|
output += `\x1B[${renderTop + 1 + ai.y};${renderLeft + 1 + ai.x}H\x1B[1m${ai.color}${ai.char}\x1B[0m`;
|
|
11339
11348
|
}
|
|
11340
|
-
output =
|
|
11349
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11341
11350
|
}
|
|
11342
11351
|
const hint = gameStarted && !gameOver && !paused ? `SPEED: ${Math.round((1 - gameSpeed / BASE_SPEED) * 100) + 100}% [ ESC ] MENU` : "";
|
|
11343
11352
|
const hintX = Math.floor((cols - hint.length) / 2);
|
|
@@ -11359,7 +11368,7 @@ function runTronGame(terminal) {
|
|
|
11359
11368
|
}
|
|
11360
11369
|
return output;
|
|
11361
11370
|
}
|
|
11362
|
-
function
|
|
11371
|
+
function renderParticles2(output, renderLeft, renderTop) {
|
|
11363
11372
|
for (const p of particles) {
|
|
11364
11373
|
const screenX = Math.round(renderLeft + 1 + p.x);
|
|
11365
11374
|
const screenY = Math.round(renderTop + 1 + p.y);
|
|
@@ -11694,8 +11703,8 @@ var SENTENCES = [
|
|
|
11694
11703
|
function runTypingTest(terminal) {
|
|
11695
11704
|
const themeColor = getCurrentThemeColor();
|
|
11696
11705
|
const GAME_WIDTH = 60;
|
|
11697
|
-
const
|
|
11698
|
-
const
|
|
11706
|
+
const MIN_COLS2 = 40;
|
|
11707
|
+
const MIN_ROWS2 = 18;
|
|
11699
11708
|
let running = true;
|
|
11700
11709
|
let gameStarted = false;
|
|
11701
11710
|
let gameOver = false;
|
|
@@ -11894,10 +11903,10 @@ function runTypingTest(terminal) {
|
|
|
11894
11903
|
}
|
|
11895
11904
|
const cols = terminal.cols;
|
|
11896
11905
|
const rows = terminal.rows;
|
|
11897
|
-
if (cols <
|
|
11906
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
11898
11907
|
const msg1 = "Terminal too small!";
|
|
11899
|
-
const needWidth = cols <
|
|
11900
|
-
const needHeight = rows <
|
|
11908
|
+
const needWidth = cols < MIN_COLS2;
|
|
11909
|
+
const needHeight = rows < MIN_ROWS2;
|
|
11901
11910
|
let hint2 = "";
|
|
11902
11911
|
if (needWidth && needHeight) {
|
|
11903
11912
|
hint2 = "Make pane larger";
|
|
@@ -11906,7 +11915,7 @@ function runTypingTest(terminal) {
|
|
|
11906
11915
|
} else {
|
|
11907
11916
|
hint2 = "Make pane taller \u2193";
|
|
11908
11917
|
}
|
|
11909
|
-
const msg2 = `Need: ${
|
|
11918
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
11910
11919
|
const centerX = Math.floor(cols / 2);
|
|
11911
11920
|
const centerY = Math.floor(rows / 2);
|
|
11912
11921
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -12551,8 +12560,8 @@ var WORDS2 = [
|
|
|
12551
12560
|
];
|
|
12552
12561
|
function runWordleGame(terminal) {
|
|
12553
12562
|
const themeColor = getCurrentThemeColor();
|
|
12554
|
-
const
|
|
12555
|
-
const
|
|
12563
|
+
const MIN_COLS2 = 36;
|
|
12564
|
+
const MIN_ROWS2 = 20;
|
|
12556
12565
|
const MAX_GUESSES = 6;
|
|
12557
12566
|
const WORD_LENGTH = 5;
|
|
12558
12567
|
const KEYBOARD_ROWS = [
|
|
@@ -12617,7 +12626,7 @@ function runWordleGame(terminal) {
|
|
|
12617
12626
|
});
|
|
12618
12627
|
}
|
|
12619
12628
|
}
|
|
12620
|
-
function
|
|
12629
|
+
function spawnFirework4(x, y) {
|
|
12621
12630
|
const colors = ["\x1B[1;92m", "\x1B[1;93m", "\x1B[1;96m", "\x1B[1;95m"];
|
|
12622
12631
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
12623
12632
|
const chars = ["*", "+", "o", ".", "`", "'"];
|
|
@@ -12638,7 +12647,7 @@ function runWordleGame(terminal) {
|
|
|
12638
12647
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
12639
12648
|
scorePopups.push({ x, y, text, frames: 25, color });
|
|
12640
12649
|
}
|
|
12641
|
-
function
|
|
12650
|
+
function triggerShake3(frames, intensity) {
|
|
12642
12651
|
shakeFrames = frames;
|
|
12643
12652
|
shakeIntensity = intensity;
|
|
12644
12653
|
}
|
|
@@ -12696,7 +12705,7 @@ function runWordleGame(terminal) {
|
|
|
12696
12705
|
}
|
|
12697
12706
|
function submitGuess() {
|
|
12698
12707
|
if (currentGuess.length !== WORD_LENGTH) {
|
|
12699
|
-
|
|
12708
|
+
triggerShake3(4, 2);
|
|
12700
12709
|
addScorePopup2(Math.floor(cols / 2), 8, "NOT ENOUGH LETTERS", "\x1B[1;91m");
|
|
12701
12710
|
return;
|
|
12702
12711
|
}
|
|
@@ -12722,7 +12731,7 @@ function runWordleGame(terminal) {
|
|
|
12722
12731
|
}
|
|
12723
12732
|
stats.guessDistribution[guesses.length - 1]++;
|
|
12724
12733
|
borderFlash = 20;
|
|
12725
|
-
|
|
12734
|
+
triggerShake3(10, 3);
|
|
12726
12735
|
const messages = ["CIPHER BREACHED!", "CODE CRACKED!", "DECRYPTED!", "BRILLIANT!"];
|
|
12727
12736
|
addScorePopup2(centerX, guessY - 2, messages[Math.floor(Math.random() * messages.length)], "\x1B[1;92m");
|
|
12728
12737
|
setTimeout(() => {
|
|
@@ -12730,7 +12739,7 @@ function runWordleGame(terminal) {
|
|
|
12730
12739
|
for (let i = 0; i < 5; i++) {
|
|
12731
12740
|
setTimeout(() => {
|
|
12732
12741
|
if (running && won) {
|
|
12733
|
-
|
|
12742
|
+
spawnFirework4(
|
|
12734
12743
|
10 + Math.random() * (cols - 20),
|
|
12735
12744
|
5 + Math.random() * 10
|
|
12736
12745
|
);
|
|
@@ -12743,7 +12752,7 @@ function runWordleGame(terminal) {
|
|
|
12743
12752
|
gameOver = true;
|
|
12744
12753
|
stats.gamesPlayed++;
|
|
12745
12754
|
stats.currentStreak = 0;
|
|
12746
|
-
|
|
12755
|
+
triggerShake3(8, 4);
|
|
12747
12756
|
borderFlash = 15;
|
|
12748
12757
|
addScorePopup2(centerX, guessY - 2, "DECRYPTION FAILED", "\x1B[1;91m");
|
|
12749
12758
|
spawnParticles3(centerX, guessY, 10, "\x1B[1;91m", ["X", "x", ".", "*"]);
|
|
@@ -12814,12 +12823,12 @@ function runWordleGame(terminal) {
|
|
|
12814
12823
|
rows = terminal.rows;
|
|
12815
12824
|
if (shakeFrames > 0) shakeFrames--;
|
|
12816
12825
|
if (borderFlash > 0) borderFlash--;
|
|
12817
|
-
if (cols <
|
|
12826
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
12818
12827
|
const msg1 = "Terminal too small!";
|
|
12819
|
-
const needWidth = cols <
|
|
12820
|
-
const needHeight = rows <
|
|
12828
|
+
const needWidth = cols < MIN_COLS2;
|
|
12829
|
+
const needHeight = rows < MIN_ROWS2;
|
|
12821
12830
|
let hint = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider" : "Make pane taller";
|
|
12822
|
-
const msg2 = `Need: ${
|
|
12831
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
12823
12832
|
const centerX2 = Math.floor(cols / 2);
|
|
12824
12833
|
const centerY = Math.floor(rows / 2);
|
|
12825
12834
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -13138,6 +13147,2492 @@ function runWordleGame(terminal) {
|
|
|
13138
13147
|
return controller;
|
|
13139
13148
|
}
|
|
13140
13149
|
|
|
13150
|
+
// src/games/hyper-fighter/engine.ts
|
|
13151
|
+
var BOARD_ROWS = 12;
|
|
13152
|
+
var BOARD_COLS = 6;
|
|
13153
|
+
var COLORS = ["red", "green", "blue", "yellow"];
|
|
13154
|
+
var CRASH_GEM_CHANCE = 0.25;
|
|
13155
|
+
var DROP_ALLEY_COL = 3;
|
|
13156
|
+
var DIAMOND_INTERVAL = 25;
|
|
13157
|
+
var nextPowerGemId = 1;
|
|
13158
|
+
function createBoard() {
|
|
13159
|
+
const board = [];
|
|
13160
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13161
|
+
board.push(new Array(BOARD_COLS).fill(null));
|
|
13162
|
+
}
|
|
13163
|
+
return board;
|
|
13164
|
+
}
|
|
13165
|
+
function randomColor() {
|
|
13166
|
+
return COLORS[Math.floor(Math.random() * COLORS.length)];
|
|
13167
|
+
}
|
|
13168
|
+
function randomGem() {
|
|
13169
|
+
const isCrash = Math.random() < CRASH_GEM_CHANCE;
|
|
13170
|
+
return {
|
|
13171
|
+
color: randomColor(),
|
|
13172
|
+
type: isCrash ? "crash" : "normal"
|
|
13173
|
+
};
|
|
13174
|
+
}
|
|
13175
|
+
function generatePair() {
|
|
13176
|
+
return {
|
|
13177
|
+
primary: randomGem(),
|
|
13178
|
+
secondary: randomGem(),
|
|
13179
|
+
col: DROP_ALLEY_COL,
|
|
13180
|
+
row: 0,
|
|
13181
|
+
orientation: 0
|
|
13182
|
+
};
|
|
13183
|
+
}
|
|
13184
|
+
function getSecondaryPos(pair) {
|
|
13185
|
+
switch (pair.orientation) {
|
|
13186
|
+
case 0:
|
|
13187
|
+
return { row: pair.row - 1, col: pair.col };
|
|
13188
|
+
// up
|
|
13189
|
+
case 1:
|
|
13190
|
+
return { row: pair.row, col: pair.col + 1 };
|
|
13191
|
+
// right
|
|
13192
|
+
case 2:
|
|
13193
|
+
return { row: pair.row + 1, col: pair.col };
|
|
13194
|
+
// down
|
|
13195
|
+
case 3:
|
|
13196
|
+
return { row: pair.row, col: pair.col - 1 };
|
|
13197
|
+
// left
|
|
13198
|
+
default:
|
|
13199
|
+
return { row: pair.row - 1, col: pair.col };
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
function isValidPosition(pair, board) {
|
|
13203
|
+
const sec = getSecondaryPos(pair);
|
|
13204
|
+
if (pair.col < 0 || pair.col >= BOARD_COLS) return false;
|
|
13205
|
+
if (pair.row >= BOARD_ROWS) return false;
|
|
13206
|
+
if (sec.col < 0 || sec.col >= BOARD_COLS) return false;
|
|
13207
|
+
if (sec.row >= BOARD_ROWS) return false;
|
|
13208
|
+
if (pair.row >= 0 && board[pair.row][pair.col] !== null) return false;
|
|
13209
|
+
if (sec.row >= 0 && board[sec.row][sec.col] !== null) return false;
|
|
13210
|
+
return true;
|
|
13211
|
+
}
|
|
13212
|
+
function movePair(pair, board, dx) {
|
|
13213
|
+
const test = { ...pair, col: pair.col + dx };
|
|
13214
|
+
if (isValidPosition(test, board)) {
|
|
13215
|
+
pair.col += dx;
|
|
13216
|
+
return true;
|
|
13217
|
+
}
|
|
13218
|
+
return false;
|
|
13219
|
+
}
|
|
13220
|
+
function rotatePair(pair, board, clockwise) {
|
|
13221
|
+
const newOrientation = clockwise ? (pair.orientation + 1) % 4 : (pair.orientation + 3) % 4;
|
|
13222
|
+
const test = { ...pair, orientation: newOrientation };
|
|
13223
|
+
if (isValidPosition(test, board)) {
|
|
13224
|
+
pair.orientation = newOrientation;
|
|
13225
|
+
return true;
|
|
13226
|
+
}
|
|
13227
|
+
for (const kick of [1, -1, 2, -2]) {
|
|
13228
|
+
const kicked = { ...test, col: test.col + kick };
|
|
13229
|
+
if (isValidPosition(kicked, board)) {
|
|
13230
|
+
pair.orientation = newOrientation;
|
|
13231
|
+
pair.col += kick;
|
|
13232
|
+
return true;
|
|
13233
|
+
}
|
|
13234
|
+
}
|
|
13235
|
+
const upKick = { ...test, row: test.row - 1 };
|
|
13236
|
+
if (isValidPosition(upKick, board)) {
|
|
13237
|
+
pair.orientation = newOrientation;
|
|
13238
|
+
pair.row -= 1;
|
|
13239
|
+
return true;
|
|
13240
|
+
}
|
|
13241
|
+
return false;
|
|
13242
|
+
}
|
|
13243
|
+
function dropPair(pair, board) {
|
|
13244
|
+
const test = { ...pair, row: pair.row + 1 };
|
|
13245
|
+
if (isValidPosition(test, board)) {
|
|
13246
|
+
pair.row += 1;
|
|
13247
|
+
return true;
|
|
13248
|
+
}
|
|
13249
|
+
return false;
|
|
13250
|
+
}
|
|
13251
|
+
function hardDrop(pair, board) {
|
|
13252
|
+
let dropped = 0;
|
|
13253
|
+
while (dropPair(pair, board)) {
|
|
13254
|
+
dropped++;
|
|
13255
|
+
}
|
|
13256
|
+
return dropped;
|
|
13257
|
+
}
|
|
13258
|
+
function lockPair(pair, board) {
|
|
13259
|
+
if (pair.row >= 0 && pair.row < BOARD_ROWS) {
|
|
13260
|
+
board[pair.row][pair.col] = { ...pair.primary };
|
|
13261
|
+
}
|
|
13262
|
+
const sec = getSecondaryPos(pair);
|
|
13263
|
+
if (sec.row >= 0 && sec.row < BOARD_ROWS) {
|
|
13264
|
+
board[sec.row][sec.col] = { ...pair.secondary };
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13267
|
+
function applyGravityFull(board, powerGems) {
|
|
13268
|
+
void powerGems;
|
|
13269
|
+
stripPowerGemIds(board);
|
|
13270
|
+
let moved = false;
|
|
13271
|
+
for (let col = 0; col < BOARD_COLS; col++) {
|
|
13272
|
+
let writeRow = BOARD_ROWS - 1;
|
|
13273
|
+
for (let row = BOARD_ROWS - 1; row >= 0; row--) {
|
|
13274
|
+
if (board[row][col] !== null) {
|
|
13275
|
+
if (row !== writeRow) {
|
|
13276
|
+
board[writeRow][col] = board[row][col];
|
|
13277
|
+
board[row][col] = null;
|
|
13278
|
+
moved = true;
|
|
13279
|
+
}
|
|
13280
|
+
writeRow--;
|
|
13281
|
+
}
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
return moved;
|
|
13285
|
+
}
|
|
13286
|
+
function stripPowerGemIds(board) {
|
|
13287
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13288
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13289
|
+
const gem = board[r][c];
|
|
13290
|
+
if (gem && gem.powerGemId !== void 0) {
|
|
13291
|
+
delete gem.powerGemId;
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
13294
|
+
}
|
|
13295
|
+
}
|
|
13296
|
+
function detectPowerGems(board) {
|
|
13297
|
+
const found = [];
|
|
13298
|
+
const used = /* @__PURE__ */ new Set();
|
|
13299
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13300
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13301
|
+
const gem = board[r][c];
|
|
13302
|
+
if (!gem || gem.type !== "normal") continue;
|
|
13303
|
+
for (let h = 2; h <= BOARD_ROWS - r; h++) {
|
|
13304
|
+
for (let w = 2; w <= BOARD_COLS - c; w++) {
|
|
13305
|
+
if (isUniformRect(board, r, c, w, h, gem.color)) {
|
|
13306
|
+
const key = `${r},${c},${w},${h}`;
|
|
13307
|
+
if (!used.has(key)) {
|
|
13308
|
+
found.push({
|
|
13309
|
+
id: nextPowerGemId++,
|
|
13310
|
+
color: gem.color,
|
|
13311
|
+
x: c,
|
|
13312
|
+
y: r,
|
|
13313
|
+
width: w,
|
|
13314
|
+
height: h
|
|
13315
|
+
});
|
|
13316
|
+
}
|
|
13317
|
+
}
|
|
13318
|
+
}
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13321
|
+
}
|
|
13322
|
+
found.sort((a, b) => b.width * b.height - a.width * a.height);
|
|
13323
|
+
const result = [];
|
|
13324
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
13325
|
+
for (const pg of found) {
|
|
13326
|
+
let overlap = false;
|
|
13327
|
+
for (let r = pg.y; r < pg.y + pg.height; r++) {
|
|
13328
|
+
for (let c = pg.x; c < pg.x + pg.width; c++) {
|
|
13329
|
+
if (claimed.has(`${r},${c}`)) {
|
|
13330
|
+
overlap = true;
|
|
13331
|
+
break;
|
|
13332
|
+
}
|
|
13333
|
+
}
|
|
13334
|
+
if (overlap) break;
|
|
13335
|
+
}
|
|
13336
|
+
if (!overlap) {
|
|
13337
|
+
result.push(pg);
|
|
13338
|
+
for (let r = pg.y; r < pg.y + pg.height; r++) {
|
|
13339
|
+
for (let c = pg.x; c < pg.x + pg.width; c++) {
|
|
13340
|
+
claimed.add(`${r},${c}`);
|
|
13341
|
+
const gem = board[r][c];
|
|
13342
|
+
if (gem) gem.powerGemId = pg.id;
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
}
|
|
13346
|
+
}
|
|
13347
|
+
return result;
|
|
13348
|
+
}
|
|
13349
|
+
function isUniformRect(board, startRow, startCol, width, height, color) {
|
|
13350
|
+
for (let r = startRow; r < startRow + height; r++) {
|
|
13351
|
+
for (let c = startCol; c < startCol + width; c++) {
|
|
13352
|
+
const gem = board[r][c];
|
|
13353
|
+
if (!gem || gem.type !== "normal" || gem.color !== color) return false;
|
|
13354
|
+
}
|
|
13355
|
+
}
|
|
13356
|
+
return true;
|
|
13357
|
+
}
|
|
13358
|
+
function findCrashTargets(board, _powerGems) {
|
|
13359
|
+
void _powerGems;
|
|
13360
|
+
const targets = findCrashTargetsCore(board);
|
|
13361
|
+
return targets;
|
|
13362
|
+
}
|
|
13363
|
+
function findCrashTargetsWithPowerInfo(board, powerGems) {
|
|
13364
|
+
const targets = findCrashTargetsCore(board);
|
|
13365
|
+
const destroyedPgIds = /* @__PURE__ */ new Set();
|
|
13366
|
+
for (const t of targets) {
|
|
13367
|
+
const gem = board[t.row][t.col];
|
|
13368
|
+
if (gem && gem.powerGemId !== void 0) {
|
|
13369
|
+
destroyedPgIds.add(gem.powerGemId);
|
|
13370
|
+
}
|
|
13371
|
+
}
|
|
13372
|
+
const destroyedPowerGemSizes = [];
|
|
13373
|
+
for (const pg of powerGems) {
|
|
13374
|
+
if (destroyedPgIds.has(pg.id)) {
|
|
13375
|
+
destroyedPowerGemSizes.push(pg.width * pg.height);
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
return { targets, destroyedPowerGemSizes };
|
|
13379
|
+
}
|
|
13380
|
+
function findCrashTargetsCore(board) {
|
|
13381
|
+
const targetsByCell = /* @__PURE__ */ new Map();
|
|
13382
|
+
const visited = /* @__PURE__ */ new Set();
|
|
13383
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13384
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13385
|
+
const gem = board[r][c];
|
|
13386
|
+
if (!gem || gem.type !== "crash") continue;
|
|
13387
|
+
const neighbors = getNeighbors(r, c);
|
|
13388
|
+
let hasTarget = false;
|
|
13389
|
+
for (const [nr, nc] of neighbors) {
|
|
13390
|
+
const ng = board[nr][nc];
|
|
13391
|
+
if (ng && ng.color === gem.color && ng.type === "normal") {
|
|
13392
|
+
hasTarget = true;
|
|
13393
|
+
break;
|
|
13394
|
+
}
|
|
13395
|
+
}
|
|
13396
|
+
if (hasTarget) {
|
|
13397
|
+
const connected = floodFillCrashGroup(board, r, c, gem.color, visited);
|
|
13398
|
+
for (const cell of connected) {
|
|
13399
|
+
targetsByCell.set(`${cell.row},${cell.col}`, { ...cell, color: gem.color });
|
|
13400
|
+
}
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
const targetSet = new Set(Array.from(targetsByCell.keys()));
|
|
13405
|
+
for (const key of Array.from(targetSet)) {
|
|
13406
|
+
const [rStr, cStr] = key.split(",");
|
|
13407
|
+
const r = parseInt(rStr, 10);
|
|
13408
|
+
const c = parseInt(cStr, 10);
|
|
13409
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13410
|
+
const nKey = `${nr},${nc}`;
|
|
13411
|
+
if (targetSet.has(nKey)) continue;
|
|
13412
|
+
const ng = board[nr][nc];
|
|
13413
|
+
if (ng && ng.type === "counter") {
|
|
13414
|
+
targetsByCell.set(nKey, { row: nr, col: nc, color: ng.color });
|
|
13415
|
+
targetSet.add(nKey);
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
}
|
|
13419
|
+
return Array.from(targetsByCell.values());
|
|
13420
|
+
}
|
|
13421
|
+
function getNeighbors(row, col) {
|
|
13422
|
+
const n = [];
|
|
13423
|
+
if (row > 0) n.push([row - 1, col]);
|
|
13424
|
+
if (row < BOARD_ROWS - 1) n.push([row + 1, col]);
|
|
13425
|
+
if (col > 0) n.push([row, col - 1]);
|
|
13426
|
+
if (col < BOARD_COLS - 1) n.push([row, col + 1]);
|
|
13427
|
+
return n;
|
|
13428
|
+
}
|
|
13429
|
+
function floodFillCrashGroup(board, startRow, startCol, color, globalVisited) {
|
|
13430
|
+
const result = [];
|
|
13431
|
+
const stack = [[startRow, startCol]];
|
|
13432
|
+
const localVisited = /* @__PURE__ */ new Set();
|
|
13433
|
+
while (stack.length > 0) {
|
|
13434
|
+
const [r, c] = stack.pop();
|
|
13435
|
+
const key = `${r},${c}`;
|
|
13436
|
+
if (localVisited.has(key) || globalVisited.has(key)) continue;
|
|
13437
|
+
const gem = board[r][c];
|
|
13438
|
+
if (!gem) continue;
|
|
13439
|
+
if (gem.color !== color) continue;
|
|
13440
|
+
if (gem.type !== "normal" && gem.type !== "crash") continue;
|
|
13441
|
+
localVisited.add(key);
|
|
13442
|
+
globalVisited.add(key);
|
|
13443
|
+
result.push({ row: r, col: c });
|
|
13444
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13445
|
+
stack.push([nr, nc]);
|
|
13446
|
+
}
|
|
13447
|
+
}
|
|
13448
|
+
return result;
|
|
13449
|
+
}
|
|
13450
|
+
function clearGems(board, targets) {
|
|
13451
|
+
for (const t of targets) {
|
|
13452
|
+
board[t.row][t.col] = null;
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
function resolveDiamond(board) {
|
|
13456
|
+
const cleared = [];
|
|
13457
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13458
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13459
|
+
const gem = board[r][c];
|
|
13460
|
+
if (!gem || gem.type !== "diamond") continue;
|
|
13461
|
+
let targetColor = null;
|
|
13462
|
+
if (r + 1 < BOARD_ROWS && board[r + 1][c]) {
|
|
13463
|
+
targetColor = board[r + 1][c].color;
|
|
13464
|
+
}
|
|
13465
|
+
if (!targetColor) {
|
|
13466
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13467
|
+
if (board[nr][nc] && board[nr][nc].type !== "diamond") {
|
|
13468
|
+
targetColor = board[nr][nc].color;
|
|
13469
|
+
break;
|
|
13470
|
+
}
|
|
13471
|
+
}
|
|
13472
|
+
}
|
|
13473
|
+
board[r][c] = null;
|
|
13474
|
+
cleared.push({ row: r, col: c, color: targetColor || "red" });
|
|
13475
|
+
if (!targetColor) continue;
|
|
13476
|
+
for (let dr = 0; dr < BOARD_ROWS; dr++) {
|
|
13477
|
+
for (let dc = 0; dc < BOARD_COLS; dc++) {
|
|
13478
|
+
const g = board[dr][dc];
|
|
13479
|
+
if (g && g.color === targetColor) {
|
|
13480
|
+
cleared.push({ row: dr, col: dc, color: targetColor });
|
|
13481
|
+
board[dr][dc] = null;
|
|
13482
|
+
}
|
|
13483
|
+
}
|
|
13484
|
+
}
|
|
13485
|
+
}
|
|
13486
|
+
}
|
|
13487
|
+
return cleared;
|
|
13488
|
+
}
|
|
13489
|
+
function shouldSpawnDiamond(totalDrops) {
|
|
13490
|
+
return totalDrops > 0 && totalDrops % DIAMOND_INTERVAL === 0;
|
|
13491
|
+
}
|
|
13492
|
+
function generateDiamondPair() {
|
|
13493
|
+
return {
|
|
13494
|
+
primary: { color: "red", type: "diamond" },
|
|
13495
|
+
secondary: randomGem(),
|
|
13496
|
+
col: DROP_ALLEY_COL,
|
|
13497
|
+
row: 0,
|
|
13498
|
+
orientation: 0
|
|
13499
|
+
};
|
|
13500
|
+
}
|
|
13501
|
+
function calculateStepAttack(info) {
|
|
13502
|
+
let pgBonus = 0;
|
|
13503
|
+
for (const area of info.powerGemSizes) {
|
|
13504
|
+
pgBonus += Math.floor(area / 8);
|
|
13505
|
+
}
|
|
13506
|
+
return Math.floor((info.gemsCleared + pgBonus) * info.chainStep);
|
|
13507
|
+
}
|
|
13508
|
+
function applyAttackModifiers(total, damageModifier = 1, isDiamondClear = false) {
|
|
13509
|
+
let attack = Math.floor(total * damageModifier);
|
|
13510
|
+
if (isDiamondClear) attack = Math.floor(attack * 0.5);
|
|
13511
|
+
return attack;
|
|
13512
|
+
}
|
|
13513
|
+
function resolveCounterAttack(attack, pendingGarbage, ratio = 2) {
|
|
13514
|
+
if (attack <= 0 || pendingGarbage <= 0) {
|
|
13515
|
+
return {
|
|
13516
|
+
remainingAttack: attack,
|
|
13517
|
+
remainingPending: pendingGarbage,
|
|
13518
|
+
canceledGems: 0,
|
|
13519
|
+
pendingStartsAtThree: false
|
|
13520
|
+
};
|
|
13521
|
+
}
|
|
13522
|
+
const cancelable = Math.floor(attack / ratio);
|
|
13523
|
+
const canceledGems = Math.min(cancelable, pendingGarbage);
|
|
13524
|
+
const remainingPending = pendingGarbage - canceledGems;
|
|
13525
|
+
const remainingAttack = attack - canceledGems * ratio;
|
|
13526
|
+
return {
|
|
13527
|
+
remainingAttack,
|
|
13528
|
+
remainingPending,
|
|
13529
|
+
canceledGems,
|
|
13530
|
+
pendingStartsAtThree: canceledGems > 0 && remainingPending > 0
|
|
13531
|
+
};
|
|
13532
|
+
}
|
|
13533
|
+
function decrementCounters(player) {
|
|
13534
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13535
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13536
|
+
const gem = player.board[r][c];
|
|
13537
|
+
if (gem && gem.type === "counter" && gem.counterTimer !== void 0) {
|
|
13538
|
+
gem.counterTimer--;
|
|
13539
|
+
if (gem.counterTimer <= 0) {
|
|
13540
|
+
gem.type = "normal";
|
|
13541
|
+
delete gem.counterTimer;
|
|
13542
|
+
}
|
|
13543
|
+
}
|
|
13544
|
+
}
|
|
13545
|
+
}
|
|
13546
|
+
}
|
|
13547
|
+
function checkGameOver(board) {
|
|
13548
|
+
return board[0][DROP_ALLEY_COL] !== null;
|
|
13549
|
+
}
|
|
13550
|
+
function getGhostPosition(pair, board) {
|
|
13551
|
+
const ghost = { ...pair };
|
|
13552
|
+
while (true) {
|
|
13553
|
+
const test = { ...ghost, row: ghost.row + 1 };
|
|
13554
|
+
if (!isValidPosition(test, board)) break;
|
|
13555
|
+
ghost.row++;
|
|
13556
|
+
}
|
|
13557
|
+
const sec = getSecondaryPos(ghost);
|
|
13558
|
+
return {
|
|
13559
|
+
primaryRow: ghost.row,
|
|
13560
|
+
primaryCol: ghost.col,
|
|
13561
|
+
secondaryRow: sec.row,
|
|
13562
|
+
secondaryCol: sec.col
|
|
13563
|
+
};
|
|
13564
|
+
}
|
|
13565
|
+
function createPlayerState() {
|
|
13566
|
+
return {
|
|
13567
|
+
board: createBoard(),
|
|
13568
|
+
currentPair: null,
|
|
13569
|
+
nextPair: generatePair(),
|
|
13570
|
+
powerGems: [],
|
|
13571
|
+
score: 0,
|
|
13572
|
+
pendingGarbage: 0,
|
|
13573
|
+
pendingCounteredGarbage: 0,
|
|
13574
|
+
alive: true,
|
|
13575
|
+
totalDrops: 0
|
|
13576
|
+
};
|
|
13577
|
+
}
|
|
13578
|
+
function spawnPair(player) {
|
|
13579
|
+
player.currentPair = player.nextPair;
|
|
13580
|
+
player.currentPair.col = DROP_ALLEY_COL;
|
|
13581
|
+
player.currentPair.row = 0;
|
|
13582
|
+
player.currentPair.orientation = 0;
|
|
13583
|
+
player.totalDrops++;
|
|
13584
|
+
if (shouldSpawnDiamond(player.totalDrops + 1)) {
|
|
13585
|
+
player.nextPair = generateDiamondPair();
|
|
13586
|
+
} else {
|
|
13587
|
+
player.nextPair = generatePair();
|
|
13588
|
+
}
|
|
13589
|
+
if (!isValidPosition(player.currentPair, player.board)) {
|
|
13590
|
+
player.alive = false;
|
|
13591
|
+
return false;
|
|
13592
|
+
}
|
|
13593
|
+
return true;
|
|
13594
|
+
}
|
|
13595
|
+
|
|
13596
|
+
// src/games/hyper-fighter/ai.ts
|
|
13597
|
+
var DIFFICULTIES2 = {
|
|
13598
|
+
easy: { name: "EASY", thinkFrames: 14, mistakeRate: 0.3, dropSpeedBoost: 1, simulateDepth: 0 },
|
|
13599
|
+
normal: { name: "NORMAL", thinkFrames: 8, mistakeRate: 0.1, dropSpeedBoost: 1.5, simulateDepth: 0 },
|
|
13600
|
+
hard: { name: "HARD", thinkFrames: 4, mistakeRate: 0.02, dropSpeedBoost: 2, simulateDepth: 1 }
|
|
13601
|
+
};
|
|
13602
|
+
function createAIState(difficulty) {
|
|
13603
|
+
return {
|
|
13604
|
+
targetCol: 2,
|
|
13605
|
+
targetOrientation: 0,
|
|
13606
|
+
thinkTimer: 0,
|
|
13607
|
+
moveTimer: 0,
|
|
13608
|
+
decided: false,
|
|
13609
|
+
difficulty
|
|
13610
|
+
};
|
|
13611
|
+
}
|
|
13612
|
+
function getColumnHeight(board, col) {
|
|
13613
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13614
|
+
if (board[r][col] !== null) return BOARD_ROWS - r;
|
|
13615
|
+
}
|
|
13616
|
+
return 0;
|
|
13617
|
+
}
|
|
13618
|
+
function evaluateBoard(board) {
|
|
13619
|
+
let score = 0;
|
|
13620
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13621
|
+
const h = getColumnHeight(board, c);
|
|
13622
|
+
score -= h * h * 2;
|
|
13623
|
+
}
|
|
13624
|
+
const heights = [];
|
|
13625
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13626
|
+
heights.push(getColumnHeight(board, c));
|
|
13627
|
+
}
|
|
13628
|
+
for (let c = 0; c < BOARD_COLS - 1; c++) {
|
|
13629
|
+
const diff = Math.abs(heights[c] - heights[c + 1]);
|
|
13630
|
+
score -= diff * 3;
|
|
13631
|
+
}
|
|
13632
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13633
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13634
|
+
const gem = board[r][c];
|
|
13635
|
+
if (!gem || gem.type === "counter") continue;
|
|
13636
|
+
if (c + 1 < BOARD_COLS) {
|
|
13637
|
+
const right = board[r][c + 1];
|
|
13638
|
+
if (right && right.color === gem.color && right.type !== "counter") {
|
|
13639
|
+
score += 4;
|
|
13640
|
+
}
|
|
13641
|
+
}
|
|
13642
|
+
if (r + 1 < BOARD_ROWS) {
|
|
13643
|
+
const below = board[r + 1][c];
|
|
13644
|
+
if (below && below.color === gem.color && below.type !== "counter") {
|
|
13645
|
+
score += 4;
|
|
13646
|
+
}
|
|
13647
|
+
}
|
|
13648
|
+
}
|
|
13649
|
+
}
|
|
13650
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13651
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13652
|
+
const gem = board[r][c];
|
|
13653
|
+
if (!gem || gem.type !== "crash") continue;
|
|
13654
|
+
const neighbors = [
|
|
13655
|
+
[r - 1, c],
|
|
13656
|
+
[r + 1, c],
|
|
13657
|
+
[r, c - 1],
|
|
13658
|
+
[r, c + 1]
|
|
13659
|
+
];
|
|
13660
|
+
for (const [nr, nc] of neighbors) {
|
|
13661
|
+
if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) {
|
|
13662
|
+
const ng = board[nr][nc];
|
|
13663
|
+
if (ng && ng.color === gem.color && ng.type === "normal") {
|
|
13664
|
+
score += 15;
|
|
13665
|
+
}
|
|
13666
|
+
}
|
|
13667
|
+
}
|
|
13668
|
+
}
|
|
13669
|
+
}
|
|
13670
|
+
const powerGems = detectPowerGems(board);
|
|
13671
|
+
for (const pg of powerGems) {
|
|
13672
|
+
score += pg.width * pg.height * 8;
|
|
13673
|
+
}
|
|
13674
|
+
const targets = findCrashTargets(board, powerGems);
|
|
13675
|
+
score += targets.length * 10;
|
|
13676
|
+
const maxHeight = Math.max(...heights);
|
|
13677
|
+
if (maxHeight >= BOARD_ROWS - 2) score -= 200;
|
|
13678
|
+
if (maxHeight >= BOARD_ROWS - 1) score -= 500;
|
|
13679
|
+
const deathHeight = heights[DROP_ALLEY_COL];
|
|
13680
|
+
if (deathHeight >= BOARD_ROWS - 3) score -= 100;
|
|
13681
|
+
return score;
|
|
13682
|
+
}
|
|
13683
|
+
function cloneBoard(board) {
|
|
13684
|
+
return board.map((row) => row.map((gem) => gem ? { ...gem } : null));
|
|
13685
|
+
}
|
|
13686
|
+
function evaluatePlacement(pair, board, simulateDepth = 0) {
|
|
13687
|
+
const testBoard = cloneBoard(board);
|
|
13688
|
+
const testPair = {
|
|
13689
|
+
primary: { ...pair.primary },
|
|
13690
|
+
secondary: { ...pair.secondary },
|
|
13691
|
+
col: pair.col,
|
|
13692
|
+
row: pair.row,
|
|
13693
|
+
orientation: pair.orientation
|
|
13694
|
+
};
|
|
13695
|
+
while (true) {
|
|
13696
|
+
const next = { ...testPair, row: testPair.row + 1 };
|
|
13697
|
+
if (!isValidPosition(next, testBoard)) break;
|
|
13698
|
+
testPair.row++;
|
|
13699
|
+
}
|
|
13700
|
+
lockPair(testPair, testBoard);
|
|
13701
|
+
applyGravityFull(testBoard, []);
|
|
13702
|
+
if (simulateDepth > 0) {
|
|
13703
|
+
const pg = detectPowerGems(testBoard);
|
|
13704
|
+
const targets = findCrashTargets(testBoard, pg);
|
|
13705
|
+
if (targets.length > 0) {
|
|
13706
|
+
clearGems(testBoard, targets);
|
|
13707
|
+
applyGravityFull(testBoard, []);
|
|
13708
|
+
}
|
|
13709
|
+
}
|
|
13710
|
+
return evaluateBoard(testBoard);
|
|
13711
|
+
}
|
|
13712
|
+
function selectMove(player, ai) {
|
|
13713
|
+
const pair = player.currentPair;
|
|
13714
|
+
if (!pair) return;
|
|
13715
|
+
if (Math.random() < ai.difficulty.mistakeRate) {
|
|
13716
|
+
ai.targetCol = Math.floor(Math.random() * BOARD_COLS);
|
|
13717
|
+
ai.targetOrientation = Math.floor(Math.random() * 4);
|
|
13718
|
+
ai.decided = true;
|
|
13719
|
+
return;
|
|
13720
|
+
}
|
|
13721
|
+
const options = [];
|
|
13722
|
+
for (let orient = 0; orient < 4; orient++) {
|
|
13723
|
+
for (let col = 0; col < BOARD_COLS; col++) {
|
|
13724
|
+
const testPair = {
|
|
13725
|
+
primary: { ...pair.primary },
|
|
13726
|
+
secondary: { ...pair.secondary },
|
|
13727
|
+
col,
|
|
13728
|
+
row: 0,
|
|
13729
|
+
orientation: orient
|
|
13730
|
+
};
|
|
13731
|
+
if (!isValidPosition(testPair, player.board)) continue;
|
|
13732
|
+
const score = evaluatePlacement(testPair, player.board, ai.difficulty.simulateDepth);
|
|
13733
|
+
options.push({ col, orientation: orient, score });
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13736
|
+
if (options.length === 0) {
|
|
13737
|
+
ai.targetCol = pair.col;
|
|
13738
|
+
ai.targetOrientation = pair.orientation;
|
|
13739
|
+
ai.decided = true;
|
|
13740
|
+
return;
|
|
13741
|
+
}
|
|
13742
|
+
options.sort((a, b) => b.score - a.score);
|
|
13743
|
+
const best = options[0];
|
|
13744
|
+
ai.targetCol = best.col;
|
|
13745
|
+
ai.targetOrientation = best.orientation;
|
|
13746
|
+
ai.decided = true;
|
|
13747
|
+
}
|
|
13748
|
+
function aiTick(player, ai) {
|
|
13749
|
+
if (!player.currentPair) return "none";
|
|
13750
|
+
ai.thinkTimer++;
|
|
13751
|
+
if (!ai.decided) {
|
|
13752
|
+
if (ai.thinkTimer >= ai.difficulty.thinkFrames) {
|
|
13753
|
+
selectMove(player, ai);
|
|
13754
|
+
ai.thinkTimer = 0;
|
|
13755
|
+
}
|
|
13756
|
+
return "none";
|
|
13757
|
+
}
|
|
13758
|
+
ai.moveTimer++;
|
|
13759
|
+
const moveInterval = Math.max(2, Math.floor(ai.difficulty.thinkFrames / 3));
|
|
13760
|
+
if (ai.moveTimer < moveInterval) return "none";
|
|
13761
|
+
ai.moveTimer = 0;
|
|
13762
|
+
const pair = player.currentPair;
|
|
13763
|
+
if (pair.orientation !== ai.targetOrientation) {
|
|
13764
|
+
const cwDist = (ai.targetOrientation - pair.orientation + 4) % 4;
|
|
13765
|
+
const ccwDist = (pair.orientation - ai.targetOrientation + 4) % 4;
|
|
13766
|
+
return cwDist <= ccwDist ? "rotate_cw" : "rotate_ccw";
|
|
13767
|
+
}
|
|
13768
|
+
if (pair.col < ai.targetCol) {
|
|
13769
|
+
return "move";
|
|
13770
|
+
} else if (pair.col > ai.targetCol) {
|
|
13771
|
+
return "move";
|
|
13772
|
+
}
|
|
13773
|
+
return "drop";
|
|
13774
|
+
}
|
|
13775
|
+
function getAIMoveDirection(player, ai) {
|
|
13776
|
+
if (!player.currentPair) return 0;
|
|
13777
|
+
if (player.currentPair.col < ai.targetCol) return 1;
|
|
13778
|
+
if (player.currentPair.col > ai.targetCol) return -1;
|
|
13779
|
+
return 0;
|
|
13780
|
+
}
|
|
13781
|
+
|
|
13782
|
+
// src/games/hyper-fighter/effects.ts
|
|
13783
|
+
var GEM_ANSI = {
|
|
13784
|
+
red: "\x1B[1;38;5;196m",
|
|
13785
|
+
green: "\x1B[1;38;5;46m",
|
|
13786
|
+
blue: "\x1B[1;38;5;27m",
|
|
13787
|
+
yellow: "\x1B[1;38;5;226m"
|
|
13788
|
+
};
|
|
13789
|
+
var PARTICLE_CHARS = ["\u2726", "\u2605", "\u25C6", "\u25CF", "\xB7", "*", "\u25AA"];
|
|
13790
|
+
function spawnClearParticles(x, y, color, count, particles) {
|
|
13791
|
+
const ansi = GEM_ANSI[color];
|
|
13792
|
+
for (let i = 0; i < count; i++) {
|
|
13793
|
+
particles.push({
|
|
13794
|
+
x: x + (Math.random() - 0.5) * 2,
|
|
13795
|
+
y: y + (Math.random() - 0.5),
|
|
13796
|
+
char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
|
|
13797
|
+
color: ansi,
|
|
13798
|
+
vx: (Math.random() - 0.5) * 3,
|
|
13799
|
+
vy: (Math.random() - 0.8) * 2,
|
|
13800
|
+
life: 8 + Math.floor(Math.random() * 8),
|
|
13801
|
+
maxLife: 16
|
|
13802
|
+
});
|
|
13803
|
+
}
|
|
13804
|
+
}
|
|
13805
|
+
function spawnFirework2(x, y, particles) {
|
|
13806
|
+
const colors = ["\x1B[91m", "\x1B[93m", "\x1B[92m", "\x1B[96m", "\x1B[95m"];
|
|
13807
|
+
for (let i = 0; i < 20; i++) {
|
|
13808
|
+
const angle = Math.PI * 2 * i / 20;
|
|
13809
|
+
const speed = 1.5 + Math.random();
|
|
13810
|
+
particles.push({
|
|
13811
|
+
x,
|
|
13812
|
+
y,
|
|
13813
|
+
char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
|
|
13814
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
13815
|
+
vx: Math.cos(angle) * speed,
|
|
13816
|
+
vy: Math.sin(angle) * speed * 0.5,
|
|
13817
|
+
life: 12 + Math.floor(Math.random() * 8),
|
|
13818
|
+
maxLife: 20
|
|
13819
|
+
});
|
|
13820
|
+
}
|
|
13821
|
+
}
|
|
13822
|
+
function spawnCollapse(startX, startY, width, height, particles) {
|
|
13823
|
+
for (let r = 0; r < height; r++) {
|
|
13824
|
+
for (let c = 0; c < width; c++) {
|
|
13825
|
+
if (Math.random() < 0.4) {
|
|
13826
|
+
particles.push({
|
|
13827
|
+
x: startX + c * 2,
|
|
13828
|
+
y: startY + r,
|
|
13829
|
+
char: "\u2593",
|
|
13830
|
+
color: "\x1B[90m",
|
|
13831
|
+
vx: (Math.random() - 0.5) * 0.5,
|
|
13832
|
+
vy: 0.3 + Math.random() * 0.5,
|
|
13833
|
+
life: 10 + Math.floor(Math.random() * 15),
|
|
13834
|
+
maxLife: 25
|
|
13835
|
+
});
|
|
13836
|
+
}
|
|
13837
|
+
}
|
|
13838
|
+
}
|
|
13839
|
+
}
|
|
13840
|
+
function updateParticles2(particles) {
|
|
13841
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
13842
|
+
const p = particles[i];
|
|
13843
|
+
p.x += p.vx;
|
|
13844
|
+
p.y += p.vy;
|
|
13845
|
+
p.vy += 0.08;
|
|
13846
|
+
p.life--;
|
|
13847
|
+
if (p.life <= 0) {
|
|
13848
|
+
particles.splice(i, 1);
|
|
13849
|
+
}
|
|
13850
|
+
}
|
|
13851
|
+
}
|
|
13852
|
+
function renderParticles(particles, minX, minY, maxX, maxY) {
|
|
13853
|
+
let output = "";
|
|
13854
|
+
for (const p of particles) {
|
|
13855
|
+
const px = Math.round(p.x);
|
|
13856
|
+
const py = Math.round(p.y);
|
|
13857
|
+
if (px < minX || px > maxX || py < minY || py > maxY) continue;
|
|
13858
|
+
const fade = p.life > p.maxLife * 0.5 ? "\x1B[1m" : "\x1B[2m";
|
|
13859
|
+
output += `\x1B[${py};${px}H${fade}${p.color}${p.char}\x1B[0m`;
|
|
13860
|
+
}
|
|
13861
|
+
return output;
|
|
13862
|
+
}
|
|
13863
|
+
var COMBO_MESSAGES = [
|
|
13864
|
+
{ text: "NICE!", color: "\x1B[92m" },
|
|
13865
|
+
{ text: "GREAT!", color: "\x1B[93m" },
|
|
13866
|
+
{ text: "AMAZING!", color: "\x1B[96m" },
|
|
13867
|
+
{ text: "UNSTOPPABLE!", color: "\x1B[95m" },
|
|
13868
|
+
{ text: "GODLIKE!!", color: "\x1B[91m" },
|
|
13869
|
+
{ text: "GODLIKE!!", color: "\x1B[1;91m" }
|
|
13870
|
+
];
|
|
13871
|
+
function spawnComboText(chains, x, y, texts) {
|
|
13872
|
+
const idx = Math.min(chains - 1, COMBO_MESSAGES.length - 1);
|
|
13873
|
+
const msg = COMBO_MESSAGES[idx];
|
|
13874
|
+
texts.push({
|
|
13875
|
+
text: `${msg.text} \xD7${chains}`,
|
|
13876
|
+
x: x - Math.floor(msg.text.length / 2),
|
|
13877
|
+
y,
|
|
13878
|
+
color: msg.color,
|
|
13879
|
+
frames: 20,
|
|
13880
|
+
maxFrames: 20
|
|
13881
|
+
});
|
|
13882
|
+
}
|
|
13883
|
+
function spawnChainCounter(chains, x, y, texts) {
|
|
13884
|
+
texts.push({
|
|
13885
|
+
text: `${chains} CHAIN`,
|
|
13886
|
+
x: x - 3,
|
|
13887
|
+
y: y + 1,
|
|
13888
|
+
color: "\x1B[1;97m",
|
|
13889
|
+
frames: 16,
|
|
13890
|
+
maxFrames: 16
|
|
13891
|
+
});
|
|
13892
|
+
}
|
|
13893
|
+
function updateFloatingTexts(texts) {
|
|
13894
|
+
for (let i = texts.length - 1; i >= 0; i--) {
|
|
13895
|
+
const t = texts[i];
|
|
13896
|
+
t.frames--;
|
|
13897
|
+
if (t.frames % 3 === 0) t.y -= 1;
|
|
13898
|
+
if (t.frames <= 0) {
|
|
13899
|
+
texts.splice(i, 1);
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13902
|
+
}
|
|
13903
|
+
function renderFloatingTexts(texts) {
|
|
13904
|
+
let output = "";
|
|
13905
|
+
for (const t of texts) {
|
|
13906
|
+
if (t.y < 1) continue;
|
|
13907
|
+
const fade = t.frames > t.maxFrames * 0.4 ? "\x1B[1m" : "\x1B[2m";
|
|
13908
|
+
output += `\x1B[${t.y};${Math.max(1, t.x)}H${fade}${t.color}${t.text}\x1B[0m`;
|
|
13909
|
+
}
|
|
13910
|
+
return output;
|
|
13911
|
+
}
|
|
13912
|
+
function spawnProjectile(fromX, toX, y, count, projectiles) {
|
|
13913
|
+
let char;
|
|
13914
|
+
let color;
|
|
13915
|
+
if (count >= 8) {
|
|
13916
|
+
char = "\u25C8";
|
|
13917
|
+
color = "\x1B[1;91m";
|
|
13918
|
+
} else if (count >= 4) {
|
|
13919
|
+
char = "\u25C6";
|
|
13920
|
+
color = "\x1B[1;93m";
|
|
13921
|
+
} else {
|
|
13922
|
+
char = "\u25CF";
|
|
13923
|
+
color = "\x1B[1;97m";
|
|
13924
|
+
}
|
|
13925
|
+
projectiles.push({ fromX, toX, y, progress: 0, char, color, count });
|
|
13926
|
+
}
|
|
13927
|
+
function updateProjectiles(projectiles) {
|
|
13928
|
+
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
13929
|
+
const p = projectiles[i];
|
|
13930
|
+
p.progress += 1 / 7;
|
|
13931
|
+
if (p.progress >= 1) {
|
|
13932
|
+
projectiles.splice(i, 1);
|
|
13933
|
+
}
|
|
13934
|
+
}
|
|
13935
|
+
}
|
|
13936
|
+
function renderProjectiles(projectiles) {
|
|
13937
|
+
let output = "";
|
|
13938
|
+
for (const p of projectiles) {
|
|
13939
|
+
const x = Math.round(p.fromX + (p.toX - p.fromX) * p.progress);
|
|
13940
|
+
if (x < 1) continue;
|
|
13941
|
+
output += `\x1B[${p.y};${x}H${p.color}${p.char}\x1B[0m`;
|
|
13942
|
+
}
|
|
13943
|
+
return output;
|
|
13944
|
+
}
|
|
13945
|
+
function renderPortrait(lines, x, y, color) {
|
|
13946
|
+
let output = "";
|
|
13947
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13948
|
+
output += `\x1B[${y + i};${x}H${color}${lines[i]}\x1B[0m`;
|
|
13949
|
+
}
|
|
13950
|
+
return output;
|
|
13951
|
+
}
|
|
13952
|
+
function triggerShake(shake, intensity, frames) {
|
|
13953
|
+
shake.intensity = Math.max(shake.intensity, intensity);
|
|
13954
|
+
shake.frames = Math.max(shake.frames, frames);
|
|
13955
|
+
}
|
|
13956
|
+
function updateShake(shake) {
|
|
13957
|
+
if (shake.frames <= 0) return { dx: 0, dy: 0 };
|
|
13958
|
+
shake.frames--;
|
|
13959
|
+
if (shake.frames <= 0) {
|
|
13960
|
+
shake.intensity = 0;
|
|
13961
|
+
return { dx: 0, dy: 0 };
|
|
13962
|
+
}
|
|
13963
|
+
return {
|
|
13964
|
+
dx: Math.round((Math.random() - 0.5) * shake.intensity * 2),
|
|
13965
|
+
dy: Math.round((Math.random() - 0.5) * shake.intensity)
|
|
13966
|
+
};
|
|
13967
|
+
}
|
|
13968
|
+
function triggerFlash(flash, color, frames) {
|
|
13969
|
+
flash.color = color;
|
|
13970
|
+
flash.frames = frames;
|
|
13971
|
+
}
|
|
13972
|
+
function updateFlash(flash) {
|
|
13973
|
+
if (flash.frames <= 0) return null;
|
|
13974
|
+
flash.frames--;
|
|
13975
|
+
return flash.color;
|
|
13976
|
+
}
|
|
13977
|
+
function renderEnergyBar(x, y, level, maxLevel) {
|
|
13978
|
+
const barHeight = 6;
|
|
13979
|
+
const filled = Math.min(barHeight, Math.round(level / Math.max(1, maxLevel) * barHeight));
|
|
13980
|
+
let output = "";
|
|
13981
|
+
for (let i = 0; i < barHeight; i++) {
|
|
13982
|
+
const isFilled = i >= barHeight - filled;
|
|
13983
|
+
const char = isFilled ? "\u2588" : "\u2591";
|
|
13984
|
+
const color = isFilled ? filled >= barHeight - 1 ? "\x1B[91m" : filled >= barHeight / 2 ? "\x1B[93m" : "\x1B[92m" : "\x1B[90m";
|
|
13985
|
+
output += `\x1B[${y + i};${x}H${color}${char}\x1B[0m`;
|
|
13986
|
+
}
|
|
13987
|
+
return output;
|
|
13988
|
+
}
|
|
13989
|
+
|
|
13990
|
+
// src/games/hyper-fighter/characters.ts
|
|
13991
|
+
var R = "red";
|
|
13992
|
+
var G = "green";
|
|
13993
|
+
var B = "blue";
|
|
13994
|
+
var Y = "yellow";
|
|
13995
|
+
var ryu = {
|
|
13996
|
+
id: "ryu",
|
|
13997
|
+
name: "Ryu",
|
|
13998
|
+
description: "Vertical columns",
|
|
13999
|
+
damageModifier: 1,
|
|
14000
|
+
dropPattern: [
|
|
14001
|
+
[R, G, B, Y, R, G],
|
|
14002
|
+
[R, G, B, Y, R, G],
|
|
14003
|
+
[R, G, B, Y, R, G],
|
|
14004
|
+
[R, G, B, Y, R, G]
|
|
14005
|
+
],
|
|
14006
|
+
portraits: {
|
|
14007
|
+
idle: [" __ ", " (-_-)", " /| "],
|
|
14008
|
+
attack: [" _\\__ ", " (>o<)", " =|/ "],
|
|
14009
|
+
hit: [" __ ", " (x_x)", " /| "],
|
|
14010
|
+
win: [" \\__/ ", " (^o^)", " /\\ "],
|
|
14011
|
+
lose: [" __ ", " (;_;)", " | "]
|
|
14012
|
+
}
|
|
14013
|
+
};
|
|
14014
|
+
var ken = {
|
|
14015
|
+
id: "ken",
|
|
14016
|
+
name: "Ken",
|
|
14017
|
+
description: "Horizontal rows",
|
|
14018
|
+
damageModifier: 1,
|
|
14019
|
+
dropPattern: [
|
|
14020
|
+
[Y, Y, Y, Y, Y, Y],
|
|
14021
|
+
[G, G, G, G, G, G],
|
|
14022
|
+
[B, B, B, B, B, B],
|
|
14023
|
+
[R, R, R, R, R, R]
|
|
14024
|
+
],
|
|
14025
|
+
portraits: {
|
|
14026
|
+
idle: [" ^^^ ", " [>_>]", " /| "],
|
|
14027
|
+
attack: [" ^^^/ ", " [>o<]", " /=| "],
|
|
14028
|
+
hit: [" ^^^ ", " [x_x]", " /| "],
|
|
14029
|
+
win: [" \\^^/ ", " [^o^]", " /\\ "],
|
|
14030
|
+
lose: [" ^^^ ", " [;_;]", " | "]
|
|
14031
|
+
}
|
|
14032
|
+
};
|
|
14033
|
+
var chunLi = {
|
|
14034
|
+
id: "chunli",
|
|
14035
|
+
name: "Chun-Li",
|
|
14036
|
+
description: "2x2 color blocks",
|
|
14037
|
+
damageModifier: 1.2,
|
|
14038
|
+
dropPattern: [
|
|
14039
|
+
[R, R, G, G, B, B],
|
|
14040
|
+
[R, R, G, G, B, B],
|
|
14041
|
+
[Y, Y, R, R, G, G],
|
|
14042
|
+
[Y, Y, R, R, G, G]
|
|
14043
|
+
],
|
|
14044
|
+
portraits: {
|
|
14045
|
+
idle: [" @ @ ", " {^.^}", " /| "],
|
|
14046
|
+
attack: [" @ @\\", " {>.<}", " /|= "],
|
|
14047
|
+
hit: [" @ @ ", " {x.x}", " /| "],
|
|
14048
|
+
win: ["\\@ @/", " {^o^}", " /\\ "],
|
|
14049
|
+
lose: [" @ @ ", " {;.;}", " | "]
|
|
14050
|
+
}
|
|
14051
|
+
};
|
|
14052
|
+
var sakura = {
|
|
14053
|
+
id: "sakura",
|
|
14054
|
+
name: "Sakura",
|
|
14055
|
+
description: "Fixed edges, alt middle",
|
|
14056
|
+
damageModifier: 1,
|
|
14057
|
+
dropPattern: [
|
|
14058
|
+
[G, R, B, R, B, Y],
|
|
14059
|
+
[G, B, R, B, R, Y],
|
|
14060
|
+
[G, R, B, R, B, Y],
|
|
14061
|
+
[G, B, R, B, R, Y]
|
|
14062
|
+
],
|
|
14063
|
+
portraits: {
|
|
14064
|
+
idle: [" >> ", " <*_*>", " /| "],
|
|
14065
|
+
attack: [" >>/ ", " <*o*>", " /|= "],
|
|
14066
|
+
hit: [" >> ", " <x_x>", " /| "],
|
|
14067
|
+
win: [" \\>>/ ", " <^o^>", " /\\ "],
|
|
14068
|
+
lose: [" >> ", " <;_;>", " | "]
|
|
14069
|
+
}
|
|
14070
|
+
};
|
|
14071
|
+
var morrigan = {
|
|
14072
|
+
id: "morrigan",
|
|
14073
|
+
name: "Morrigan",
|
|
14074
|
+
description: "Symmetric mirrored",
|
|
14075
|
+
damageModifier: 1,
|
|
14076
|
+
dropPattern: [
|
|
14077
|
+
[B, G, R, R, G, B],
|
|
14078
|
+
[G, B, R, R, B, G],
|
|
14079
|
+
[R, G, B, B, G, R],
|
|
14080
|
+
[G, R, B, B, R, G]
|
|
14081
|
+
],
|
|
14082
|
+
portraits: {
|
|
14083
|
+
idle: [" ~ ~ ", " ~^_^~", " /| "],
|
|
14084
|
+
attack: [" ~/~\\ ", " ~>_<~", " /| "],
|
|
14085
|
+
hit: [" ~ ~ ", " ~x_x~", " /| "],
|
|
14086
|
+
win: ["\\~ ~/", " ~^o~ ", " /\\ "],
|
|
14087
|
+
lose: [" ~ ~ ", " ~;_;~", " | "]
|
|
14088
|
+
}
|
|
14089
|
+
};
|
|
14090
|
+
var hsienKo = {
|
|
14091
|
+
id: "hsienKo",
|
|
14092
|
+
name: "Hsien-Ko",
|
|
14093
|
+
description: "Diagonal staircase",
|
|
14094
|
+
damageModifier: 1,
|
|
14095
|
+
dropPattern: [
|
|
14096
|
+
[R, G, B, Y, R, G],
|
|
14097
|
+
[G, B, Y, R, G, B],
|
|
14098
|
+
[B, Y, R, G, B, Y],
|
|
14099
|
+
[Y, R, G, B, Y, R]
|
|
14100
|
+
],
|
|
14101
|
+
portraits: {
|
|
14102
|
+
idle: [" == ", " |o_o|", " /| "],
|
|
14103
|
+
attack: [" ==\\ ", " |o_o|", " /|= "],
|
|
14104
|
+
hit: [" == ", " |x_x|", " /| "],
|
|
14105
|
+
win: [" \\==/ ", " |^o^|", " /\\ "],
|
|
14106
|
+
lose: [" == ", " |;_;|", " | "]
|
|
14107
|
+
}
|
|
14108
|
+
};
|
|
14109
|
+
var felicia = {
|
|
14110
|
+
id: "felicia",
|
|
14111
|
+
name: "Felicia",
|
|
14112
|
+
description: "Fixed edges, swap mid",
|
|
14113
|
+
damageModifier: 1,
|
|
14114
|
+
dropPattern: [
|
|
14115
|
+
[G, R, B, R, B, Y],
|
|
14116
|
+
[G, B, R, B, R, Y],
|
|
14117
|
+
[Y, R, B, R, B, G],
|
|
14118
|
+
[Y, B, R, B, R, G]
|
|
14119
|
+
],
|
|
14120
|
+
portraits: {
|
|
14121
|
+
idle: [" /\\/\\ ", " =^w^=", " /| "],
|
|
14122
|
+
attack: [" /\\/\\\\", " =^o^=", " /|= "],
|
|
14123
|
+
hit: [" /\\/\\ ", " =x_x=", " /| "],
|
|
14124
|
+
win: ["\\/\\/\\/", " =^w^=", " /\\ "],
|
|
14125
|
+
lose: [" /\\/\\ ", " =;w;=", " | "]
|
|
14126
|
+
}
|
|
14127
|
+
};
|
|
14128
|
+
var donovan = {
|
|
14129
|
+
id: "donovan",
|
|
14130
|
+
name: "Donovan",
|
|
14131
|
+
description: "3-col halves + alt base",
|
|
14132
|
+
damageModifier: 1,
|
|
14133
|
+
dropPattern: [
|
|
14134
|
+
[R, R, R, G, G, G],
|
|
14135
|
+
[R, R, R, G, G, G],
|
|
14136
|
+
[B, B, B, Y, Y, Y],
|
|
14137
|
+
[R, G, B, R, G, B]
|
|
14138
|
+
],
|
|
14139
|
+
portraits: {
|
|
14140
|
+
idle: [" || ", " #-_-#", " /| "],
|
|
14141
|
+
attack: [" ||/ ", " #>_<#", " /|= "],
|
|
14142
|
+
hit: [" || ", " #x_x#", " /| "],
|
|
14143
|
+
win: [" \\||/ ", " #^o^#", " /\\ "],
|
|
14144
|
+
lose: [" || ", " #;_;#", " | "]
|
|
14145
|
+
}
|
|
14146
|
+
};
|
|
14147
|
+
var dan = {
|
|
14148
|
+
id: "dan",
|
|
14149
|
+
name: "Dan",
|
|
14150
|
+
description: "ALL RED (joke char)",
|
|
14151
|
+
damageModifier: 1,
|
|
14152
|
+
dropPattern: [
|
|
14153
|
+
[R, R, R, R, R, R],
|
|
14154
|
+
[R, R, R, R, R, R],
|
|
14155
|
+
[R, R, R, R, R, R],
|
|
14156
|
+
[R, R, R, R, R, R]
|
|
14157
|
+
],
|
|
14158
|
+
portraits: {
|
|
14159
|
+
idle: [" ^^ ", " (?_?)", " /| "],
|
|
14160
|
+
attack: [" ^^! ", " (!o!)", " /|~ "],
|
|
14161
|
+
hit: [" ^^ ", " (x_x)", " /| "],
|
|
14162
|
+
win: [" \\^^/ ", " (^o^)", " /\\ "],
|
|
14163
|
+
lose: [" ^^ ", " (T_T)", " | "]
|
|
14164
|
+
}
|
|
14165
|
+
};
|
|
14166
|
+
var akuma = {
|
|
14167
|
+
id: "akuma",
|
|
14168
|
+
name: "Akuma",
|
|
14169
|
+
description: "Diagonal rainbow cycle",
|
|
14170
|
+
damageModifier: 0.7,
|
|
14171
|
+
dropPattern: [
|
|
14172
|
+
[R, Y, B, G, R, Y],
|
|
14173
|
+
[Y, B, G, R, Y, B],
|
|
14174
|
+
[B, G, R, Y, B, G],
|
|
14175
|
+
[G, R, Y, B, G, R]
|
|
14176
|
+
],
|
|
14177
|
+
portraits: {
|
|
14178
|
+
idle: [" /MM\\ ", " !>_<!", " /| "],
|
|
14179
|
+
attack: [" /MM\\|", " !>o<!", " =/| "],
|
|
14180
|
+
hit: [" /MM\\ ", " !x_x!", " /| "],
|
|
14181
|
+
win: ["\\/MM\\/", " !^_^!", " /\\ "],
|
|
14182
|
+
lose: [" /MM\\ ", " !;_;!", " | "]
|
|
14183
|
+
}
|
|
14184
|
+
};
|
|
14185
|
+
var devilotte = {
|
|
14186
|
+
id: "devilotte",
|
|
14187
|
+
name: "Devilotte",
|
|
14188
|
+
description: "Reverse diagonal rainbow",
|
|
14189
|
+
damageModifier: 0.7,
|
|
14190
|
+
dropPattern: [
|
|
14191
|
+
[G, B, Y, R, G, B],
|
|
14192
|
+
[B, Y, R, G, B, Y],
|
|
14193
|
+
[Y, R, G, B, Y, R],
|
|
14194
|
+
[R, G, B, Y, R, G]
|
|
14195
|
+
],
|
|
14196
|
+
portraits: {
|
|
14197
|
+
idle: [" vVv ", " $v_v$", " /| "],
|
|
14198
|
+
attack: [" vVv\\", " $>_<$", " /|= "],
|
|
14199
|
+
hit: [" vVv ", " $x_x$", " /| "],
|
|
14200
|
+
win: [" \\vVv/", " $^_^$", " /\\ "],
|
|
14201
|
+
lose: [" vVv ", " $;_;$", " | "]
|
|
14202
|
+
}
|
|
14203
|
+
};
|
|
14204
|
+
var CHARACTERS = [
|
|
14205
|
+
ryu,
|
|
14206
|
+
ken,
|
|
14207
|
+
chunLi,
|
|
14208
|
+
sakura,
|
|
14209
|
+
morrigan,
|
|
14210
|
+
hsienKo,
|
|
14211
|
+
felicia,
|
|
14212
|
+
donovan,
|
|
14213
|
+
dan,
|
|
14214
|
+
akuma,
|
|
14215
|
+
devilotte
|
|
14216
|
+
];
|
|
14217
|
+
var CHAR_GRID = [
|
|
14218
|
+
[0, 1, 2, 3],
|
|
14219
|
+
[4, 5, 6, 7],
|
|
14220
|
+
[8, 9, 10]
|
|
14221
|
+
];
|
|
14222
|
+
function getRandomCharacter() {
|
|
14223
|
+
return CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
|
|
14224
|
+
}
|
|
14225
|
+
|
|
14226
|
+
// src/games/hyper-fighter/index.ts
|
|
14227
|
+
var TICK_MS = 50;
|
|
14228
|
+
var MIN_COLS = 60;
|
|
14229
|
+
var MIN_ROWS = 36;
|
|
14230
|
+
var VS_COL_WIDTH = 12;
|
|
14231
|
+
var SIDE_PANEL_WIDTH = 14;
|
|
14232
|
+
var HEADER_HEIGHT = 2;
|
|
14233
|
+
var FOOTER_HEIGHT = 2;
|
|
14234
|
+
var MIN_CELL_WIDTH = 3;
|
|
14235
|
+
var MAX_CELL_WIDTH = 6;
|
|
14236
|
+
var MIN_CELL_HEIGHT = 2;
|
|
14237
|
+
var MAX_CELL_HEIGHT = 4;
|
|
14238
|
+
var GEM_COLORS = {
|
|
14239
|
+
red: "\x1B[1;38;5;196m",
|
|
14240
|
+
green: "\x1B[1;38;5;46m",
|
|
14241
|
+
blue: "\x1B[1;38;5;27m",
|
|
14242
|
+
yellow: "\x1B[1;38;5;226m"
|
|
14243
|
+
};
|
|
14244
|
+
var COUNTER_BG_COLORS = {
|
|
14245
|
+
red: "\x1B[48;5;196m",
|
|
14246
|
+
green: "\x1B[48;5;34m",
|
|
14247
|
+
blue: "\x1B[48;5;27m",
|
|
14248
|
+
yellow: "\x1B[48;5;178m"
|
|
14249
|
+
};
|
|
14250
|
+
var BASE_DROP_SPEED = 16;
|
|
14251
|
+
var MIN_DROP_SPEED = 3;
|
|
14252
|
+
var SPEED_RAMP_DROPS = 8;
|
|
14253
|
+
var PHASE_NONE = 0;
|
|
14254
|
+
var PHASE_FLASH = 1;
|
|
14255
|
+
var PHASE_DISSOLVE = 2;
|
|
14256
|
+
var PHASE_GRAVITY = 3;
|
|
14257
|
+
var PHASE_CHECK = 4;
|
|
14258
|
+
var PHASE_GARBAGE = 5;
|
|
14259
|
+
var FLASH_FRAMES = 6;
|
|
14260
|
+
var DISSOLVE_FRAMES = 4;
|
|
14261
|
+
var GRAVITY_FRAMES = 1;
|
|
14262
|
+
var COUNTER_TIMER_NORMAL = 5;
|
|
14263
|
+
var COUNTER_TIMER_DEFENDED = 3;
|
|
14264
|
+
var GARBAGE_DROP_STEP_FRAMES = 2;
|
|
14265
|
+
function runHyperFighterGame(terminal) {
|
|
14266
|
+
const themeColor = getCurrentThemeColor();
|
|
14267
|
+
let running = true;
|
|
14268
|
+
let gameState = "difficulty";
|
|
14269
|
+
let difficultySelection = 1;
|
|
14270
|
+
let pauseMenuSelection = 0;
|
|
14271
|
+
let p1;
|
|
14272
|
+
let p2;
|
|
14273
|
+
let aiState;
|
|
14274
|
+
let selectedDifficulty = DIFFICULTIES2.normal;
|
|
14275
|
+
let p1Character = CHARACTERS[0];
|
|
14276
|
+
let p2Character = getRandomCharacter();
|
|
14277
|
+
let charGridRow = 0;
|
|
14278
|
+
let charGridCol = 0;
|
|
14279
|
+
let p1DropTimer = 0;
|
|
14280
|
+
let p2DropTimer = 0;
|
|
14281
|
+
let p1DropSpeed = BASE_DROP_SPEED;
|
|
14282
|
+
let p2DropSpeed = BASE_DROP_SPEED;
|
|
14283
|
+
let p1Phase = PHASE_NONE;
|
|
14284
|
+
let p2Phase = PHASE_NONE;
|
|
14285
|
+
let p1PhaseTimer = 0;
|
|
14286
|
+
let p2PhaseTimer = 0;
|
|
14287
|
+
let p1ClearedCells = [];
|
|
14288
|
+
let p2ClearedCells = [];
|
|
14289
|
+
let p1ChainCount = 0;
|
|
14290
|
+
let p2ChainCount = 0;
|
|
14291
|
+
let p1TotalCleared = 0;
|
|
14292
|
+
let p2TotalCleared = 0;
|
|
14293
|
+
let p1GarbageDrop = null;
|
|
14294
|
+
let p2GarbageDrop = null;
|
|
14295
|
+
let p1GarbagePatternCursor = 0;
|
|
14296
|
+
let p2GarbagePatternCursor = 0;
|
|
14297
|
+
let p1IsDiamondClear = false;
|
|
14298
|
+
let p2IsDiamondClear = false;
|
|
14299
|
+
let p1AttackAccum = 0;
|
|
14300
|
+
let p2AttackAccum = 0;
|
|
14301
|
+
let particles = [];
|
|
14302
|
+
let floatingTexts = [];
|
|
14303
|
+
let projectiles = [];
|
|
14304
|
+
let p1Shake = { intensity: 0, frames: 0 };
|
|
14305
|
+
let p2Shake = { intensity: 0, frames: 0 };
|
|
14306
|
+
let p1Flash = { color: "", frames: 0 };
|
|
14307
|
+
let p2Flash = { color: "", frames: 0 };
|
|
14308
|
+
let p1Pose = "idle";
|
|
14309
|
+
let p2Pose = "idle";
|
|
14310
|
+
let p1PoseTimer = 0;
|
|
14311
|
+
let p2PoseTimer = 0;
|
|
14312
|
+
let winner = 0;
|
|
14313
|
+
let gameOverTimer = 0;
|
|
14314
|
+
let glitchFrame = 0;
|
|
14315
|
+
let cellWidth = 3;
|
|
14316
|
+
let cellHeight = 2;
|
|
14317
|
+
let boardDisplayWidth = BOARD_COLS * cellWidth + 2;
|
|
14318
|
+
let boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
|
|
14319
|
+
let cellEmpty = " ".repeat(cellWidth);
|
|
14320
|
+
let cellSolid = "\u2588".repeat(cellWidth);
|
|
14321
|
+
let cellPower = "\u2593".repeat(cellWidth);
|
|
14322
|
+
let cellGhost = "\u2591".repeat(cellWidth);
|
|
14323
|
+
let cellCrash = "\u25C6".repeat(cellWidth);
|
|
14324
|
+
let cellDiamond = "\u25C7".repeat(cellWidth);
|
|
14325
|
+
let cellEmptyDot = " ".repeat(Math.floor((cellWidth - 1) / 2)) + "\x1B[38;5;238m\xB7\x1B[0m" + " ".repeat(cellWidth - Math.floor((cellWidth - 1) / 2) - 1);
|
|
14326
|
+
let showSidePanels = false;
|
|
14327
|
+
let sidePanel1X = 0;
|
|
14328
|
+
let sidePanel2X = 0;
|
|
14329
|
+
let boardLeft1 = 2;
|
|
14330
|
+
let boardLeft2 = 28;
|
|
14331
|
+
let vsColX = 16;
|
|
14332
|
+
let boardTop = 3;
|
|
14333
|
+
function recalcCellStrings() {
|
|
14334
|
+
boardDisplayWidth = BOARD_COLS * cellWidth + 2;
|
|
14335
|
+
boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
|
|
14336
|
+
cellEmpty = " ".repeat(cellWidth);
|
|
14337
|
+
cellSolid = "\u2588".repeat(cellWidth);
|
|
14338
|
+
cellPower = "\u2593".repeat(cellWidth);
|
|
14339
|
+
cellGhost = "\u2591".repeat(cellWidth);
|
|
14340
|
+
cellCrash = "\u25C6".repeat(cellWidth);
|
|
14341
|
+
cellDiamond = "\u25C7".repeat(cellWidth);
|
|
14342
|
+
const padLeft = Math.floor((cellWidth - 1) / 2);
|
|
14343
|
+
cellEmptyDot = " ".repeat(padLeft) + "\xB7" + " ".repeat(cellWidth - padLeft - 1);
|
|
14344
|
+
}
|
|
14345
|
+
const controller = {
|
|
14346
|
+
stop: () => {
|
|
14347
|
+
if (!running) return;
|
|
14348
|
+
running = false;
|
|
14349
|
+
},
|
|
14350
|
+
get isRunning() {
|
|
14351
|
+
return running;
|
|
14352
|
+
}
|
|
14353
|
+
};
|
|
14354
|
+
const title = [
|
|
14355
|
+
"\u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588",
|
|
14356
|
+
"\u2588\u2580\u2588 \u2580\u2584\u2580 \u2588\u2580\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584",
|
|
14357
|
+
"\u2588\u2580\u2580 \u2588 \u2588\u2580\u2580 \u2588 \u2588 \u2580\u2588\u2580 \u2588\u2580\u2580 \u2588\u2580\u2588",
|
|
14358
|
+
"\u2588\u2580 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588 \u2588\u2588\u2584 \u2588\u2580\u2584"
|
|
14359
|
+
];
|
|
14360
|
+
const GLITCH_CHARS2 = "!@#$%^&*\u2591\u2592\u2593\u2588\u2580\u2584";
|
|
14361
|
+
function initGame() {
|
|
14362
|
+
p1 = createPlayerState();
|
|
14363
|
+
p2 = createPlayerState();
|
|
14364
|
+
aiState = createAIState(selectedDifficulty);
|
|
14365
|
+
p1DropTimer = 0;
|
|
14366
|
+
p2DropTimer = 0;
|
|
14367
|
+
p1DropSpeed = BASE_DROP_SPEED;
|
|
14368
|
+
p2DropSpeed = BASE_DROP_SPEED;
|
|
14369
|
+
p1Phase = PHASE_NONE;
|
|
14370
|
+
p2Phase = PHASE_NONE;
|
|
14371
|
+
p1PhaseTimer = 0;
|
|
14372
|
+
p2PhaseTimer = 0;
|
|
14373
|
+
p1ClearedCells = [];
|
|
14374
|
+
p2ClearedCells = [];
|
|
14375
|
+
p1ChainCount = 0;
|
|
14376
|
+
p2ChainCount = 0;
|
|
14377
|
+
p1TotalCleared = 0;
|
|
14378
|
+
p2TotalCleared = 0;
|
|
14379
|
+
p1GarbageDrop = null;
|
|
14380
|
+
p2GarbageDrop = null;
|
|
14381
|
+
p1GarbagePatternCursor = 0;
|
|
14382
|
+
p2GarbagePatternCursor = 0;
|
|
14383
|
+
p1IsDiamondClear = false;
|
|
14384
|
+
p2IsDiamondClear = false;
|
|
14385
|
+
p1AttackAccum = 0;
|
|
14386
|
+
p2AttackAccum = 0;
|
|
14387
|
+
particles = [];
|
|
14388
|
+
floatingTexts = [];
|
|
14389
|
+
projectiles = [];
|
|
14390
|
+
p1Shake = { intensity: 0, frames: 0 };
|
|
14391
|
+
p2Shake = { intensity: 0, frames: 0 };
|
|
14392
|
+
p1Flash = { color: "", frames: 0 };
|
|
14393
|
+
p2Flash = { color: "", frames: 0 };
|
|
14394
|
+
p1Pose = "idle";
|
|
14395
|
+
p2Pose = "idle";
|
|
14396
|
+
p1PoseTimer = 0;
|
|
14397
|
+
p2PoseTimer = 0;
|
|
14398
|
+
winner = 0;
|
|
14399
|
+
gameOverTimer = 0;
|
|
14400
|
+
pauseMenuSelection = 0;
|
|
14401
|
+
spawnPair(p1);
|
|
14402
|
+
spawnPair(p2);
|
|
14403
|
+
}
|
|
14404
|
+
function calculateLayout() {
|
|
14405
|
+
const cols = terminal.cols;
|
|
14406
|
+
const rows = terminal.rows;
|
|
14407
|
+
cellHeight = Math.max(MIN_CELL_HEIGHT, Math.min(
|
|
14408
|
+
MAX_CELL_HEIGHT,
|
|
14409
|
+
Math.floor((rows - HEADER_HEIGHT - FOOTER_HEIGHT - 2 - 2) / BOARD_ROWS)
|
|
14410
|
+
));
|
|
14411
|
+
const availWidthWithPanels = cols - 2 * SIDE_PANEL_WIDTH - VS_COL_WIDTH - 4;
|
|
14412
|
+
const availWidthWithout = cols - VS_COL_WIDTH - 4;
|
|
14413
|
+
const cwWithPanels = Math.floor(availWidthWithPanels / (2 * BOARD_COLS));
|
|
14414
|
+
const cwWithout = Math.floor(availWidthWithout / (2 * BOARD_COLS));
|
|
14415
|
+
if (cwWithPanels >= MIN_CELL_WIDTH) {
|
|
14416
|
+
showSidePanels = true;
|
|
14417
|
+
cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithPanels));
|
|
14418
|
+
} else {
|
|
14419
|
+
showSidePanels = false;
|
|
14420
|
+
cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithout));
|
|
14421
|
+
}
|
|
14422
|
+
const maxAspectWidth = Math.floor(cellHeight * 1.5);
|
|
14423
|
+
if (cellWidth > maxAspectWidth && maxAspectWidth >= MIN_CELL_WIDTH) {
|
|
14424
|
+
cellWidth = maxAspectWidth;
|
|
14425
|
+
}
|
|
14426
|
+
recalcCellStrings();
|
|
14427
|
+
if (showSidePanels) {
|
|
14428
|
+
const totalWidth = SIDE_PANEL_WIDTH + boardDisplayWidth + VS_COL_WIDTH + boardDisplayWidth + SIDE_PANEL_WIDTH;
|
|
14429
|
+
const startX = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
|
|
14430
|
+
sidePanel1X = startX;
|
|
14431
|
+
boardLeft1 = startX + SIDE_PANEL_WIDTH;
|
|
14432
|
+
vsColX = boardLeft1 + boardDisplayWidth + 1;
|
|
14433
|
+
boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
|
|
14434
|
+
sidePanel2X = boardLeft2 + boardDisplayWidth;
|
|
14435
|
+
} else {
|
|
14436
|
+
const totalWidth = boardDisplayWidth * 2 + VS_COL_WIDTH;
|
|
14437
|
+
boardLeft1 = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
|
|
14438
|
+
boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
|
|
14439
|
+
vsColX = boardLeft1 + boardDisplayWidth + 1;
|
|
14440
|
+
sidePanel1X = 0;
|
|
14441
|
+
sidePanel2X = 0;
|
|
14442
|
+
}
|
|
14443
|
+
const availHeight = rows - HEADER_HEIGHT - FOOTER_HEIGHT;
|
|
14444
|
+
boardTop = HEADER_HEIGHT + Math.max(1, Math.floor((availHeight - boardDisplayHeight) / 2));
|
|
14445
|
+
}
|
|
14446
|
+
function getDropSpeed(player) {
|
|
14447
|
+
const ramp = Math.floor(player.totalDrops / SPEED_RAMP_DROPS);
|
|
14448
|
+
return Math.max(MIN_DROP_SPEED, BASE_DROP_SPEED - ramp);
|
|
14449
|
+
}
|
|
14450
|
+
function startResolution(player, isP1) {
|
|
14451
|
+
if (isP1) p1AttackAccum = 0;
|
|
14452
|
+
else p2AttackAccum = 0;
|
|
14453
|
+
const diamondCleared = resolveDiamond(player.board);
|
|
14454
|
+
if (diamondCleared.length > 0) {
|
|
14455
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14456
|
+
if (isP1) p1IsDiamondClear = true;
|
|
14457
|
+
else p2IsDiamondClear = true;
|
|
14458
|
+
} else {
|
|
14459
|
+
if (isP1) p1IsDiamondClear = false;
|
|
14460
|
+
else p2IsDiamondClear = false;
|
|
14461
|
+
}
|
|
14462
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14463
|
+
const { cleared, clearedCells: crashCells, powerGemSizes } = resolveOneStep(player);
|
|
14464
|
+
const clearedCells = [...diamondCleared, ...crashCells];
|
|
14465
|
+
const totalStepCleared = cleared + diamondCleared.length;
|
|
14466
|
+
if (clearedCells.length === 0) {
|
|
14467
|
+
finishResolution(player, isP1);
|
|
14468
|
+
return;
|
|
14469
|
+
}
|
|
14470
|
+
if (isP1) {
|
|
14471
|
+
p1ClearedCells = clearedCells;
|
|
14472
|
+
p1ChainCount++;
|
|
14473
|
+
p1TotalCleared += totalStepCleared;
|
|
14474
|
+
p1Phase = PHASE_FLASH;
|
|
14475
|
+
p1PhaseTimer = FLASH_FRAMES;
|
|
14476
|
+
const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p1ChainCount };
|
|
14477
|
+
p1AttackAccum += calculateStepAttack(stepInfo);
|
|
14478
|
+
} else {
|
|
14479
|
+
p2ClearedCells = clearedCells;
|
|
14480
|
+
p2ChainCount++;
|
|
14481
|
+
p2TotalCleared += totalStepCleared;
|
|
14482
|
+
p2Phase = PHASE_FLASH;
|
|
14483
|
+
p2PhaseTimer = FLASH_FRAMES;
|
|
14484
|
+
const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p2ChainCount };
|
|
14485
|
+
p2AttackAccum += calculateStepAttack(stepInfo);
|
|
14486
|
+
}
|
|
14487
|
+
const bLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14488
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14489
|
+
if (chainCount >= 1) {
|
|
14490
|
+
spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
|
|
14491
|
+
if (chainCount >= 2) {
|
|
14492
|
+
spawnChainCounter(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 3, floatingTexts);
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14495
|
+
for (const cell of clearedCells) {
|
|
14496
|
+
const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
|
|
14497
|
+
const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
|
|
14498
|
+
spawnClearParticles(px, py, cell.color, 3, particles);
|
|
14499
|
+
}
|
|
14500
|
+
const shakeAmount = Math.min(3, Math.ceil(clearedCells.length / 4));
|
|
14501
|
+
if (isP1) {
|
|
14502
|
+
triggerShake(p1Shake, shakeAmount, 6);
|
|
14503
|
+
triggerFlash(p1Flash, "\x1B[97m", 4);
|
|
14504
|
+
p1Pose = "attack";
|
|
14505
|
+
p1PoseTimer = 12;
|
|
14506
|
+
} else {
|
|
14507
|
+
triggerShake(p2Shake, shakeAmount, 6);
|
|
14508
|
+
triggerFlash(p2Flash, "\x1B[97m", 4);
|
|
14509
|
+
p2Pose = "attack";
|
|
14510
|
+
p2PoseTimer = 12;
|
|
14511
|
+
}
|
|
14512
|
+
}
|
|
14513
|
+
function resolveOneStep(player) {
|
|
14514
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14515
|
+
const { targets, destroyedPowerGemSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14516
|
+
if (targets.length === 0) return { cleared: 0, chains: 0, clearedCells: [], powerGemSizes: [] };
|
|
14517
|
+
for (const t of targets) {
|
|
14518
|
+
player.board[t.row][t.col] = null;
|
|
14519
|
+
}
|
|
14520
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14521
|
+
player.powerGems = [];
|
|
14522
|
+
return { cleared: targets.length, chains: 1, clearedCells: targets, powerGemSizes: destroyedPowerGemSizes };
|
|
14523
|
+
}
|
|
14524
|
+
function buildGarbageDropState(player, defendedCount, normalCount, startCursor, dropPattern) {
|
|
14525
|
+
const occupied = player.board.map((row) => row.map((cell) => cell !== null));
|
|
14526
|
+
const timers = [];
|
|
14527
|
+
for (let i = 0; i < defendedCount; i++) timers.push(COUNTER_TIMER_DEFENDED);
|
|
14528
|
+
for (let i = 0; i < normalCount; i++) timers.push(COUNTER_TIMER_NORMAL);
|
|
14529
|
+
const gems = [];
|
|
14530
|
+
const patternRows = dropPattern.length;
|
|
14531
|
+
const patternCols = dropPattern[0].length;
|
|
14532
|
+
const patternLen = patternRows * patternCols;
|
|
14533
|
+
let placedCount = 0;
|
|
14534
|
+
function findLandingRow(col) {
|
|
14535
|
+
for (let r = BOARD_ROWS - 1; r >= 0; r--) {
|
|
14536
|
+
if (!occupied[r][col]) return r;
|
|
14537
|
+
}
|
|
14538
|
+
return -1;
|
|
14539
|
+
}
|
|
14540
|
+
for (let i = 0; i < timers.length; i++) {
|
|
14541
|
+
const patternIdx = (startCursor + i) % patternLen;
|
|
14542
|
+
const pRow = Math.floor(patternIdx / patternCols);
|
|
14543
|
+
const pCol = patternIdx % patternCols;
|
|
14544
|
+
const color = dropPattern[pRow][pCol];
|
|
14545
|
+
let placeCol = -1;
|
|
14546
|
+
let placeRow = -1;
|
|
14547
|
+
const landing = findLandingRow(pCol);
|
|
14548
|
+
if (landing >= 0) {
|
|
14549
|
+
placeCol = pCol;
|
|
14550
|
+
placeRow = landing;
|
|
14551
|
+
} else {
|
|
14552
|
+
for (let offset = 1; offset < BOARD_COLS; offset++) {
|
|
14553
|
+
for (const dir of [1, -1]) {
|
|
14554
|
+
const adjCol = pCol + offset * dir;
|
|
14555
|
+
if (adjCol >= 0 && adjCol < BOARD_COLS) {
|
|
14556
|
+
const adjLanding = findLandingRow(adjCol);
|
|
14557
|
+
if (adjLanding >= 0) {
|
|
14558
|
+
placeCol = adjCol;
|
|
14559
|
+
placeRow = adjLanding;
|
|
14560
|
+
break;
|
|
14561
|
+
}
|
|
14562
|
+
}
|
|
14563
|
+
}
|
|
14564
|
+
if (placeRow >= 0) break;
|
|
14565
|
+
}
|
|
14566
|
+
}
|
|
14567
|
+
if (placeRow < 0 || placeCol < 0) continue;
|
|
14568
|
+
occupied[placeRow][placeCol] = true;
|
|
14569
|
+
gems.push({
|
|
14570
|
+
col: placeCol,
|
|
14571
|
+
targetRow: placeRow,
|
|
14572
|
+
currentRow: 0,
|
|
14573
|
+
timer: timers[i],
|
|
14574
|
+
color,
|
|
14575
|
+
delayFrames: i
|
|
14576
|
+
});
|
|
14577
|
+
placedCount++;
|
|
14578
|
+
}
|
|
14579
|
+
if (gems.length === 0) {
|
|
14580
|
+
return { dropState: null, nextCursor: startCursor };
|
|
14581
|
+
}
|
|
14582
|
+
const nextCursor = (startCursor + placedCount) % patternLen;
|
|
14583
|
+
return { dropState: { gems, frameTick: 0 }, nextCursor };
|
|
14584
|
+
}
|
|
14585
|
+
function triggerLoss(isP1) {
|
|
14586
|
+
const loser = isP1 ? p1 : p2;
|
|
14587
|
+
loser.alive = false;
|
|
14588
|
+
gameState = "gameOver";
|
|
14589
|
+
winner = isP1 ? 2 : 1;
|
|
14590
|
+
gameOverTimer = 0;
|
|
14591
|
+
pauseMenuSelection = 0;
|
|
14592
|
+
if (isP1) {
|
|
14593
|
+
p1Pose = "lose";
|
|
14594
|
+
p2Pose = "win";
|
|
14595
|
+
} else {
|
|
14596
|
+
p2Pose = "lose";
|
|
14597
|
+
p1Pose = "win";
|
|
14598
|
+
}
|
|
14599
|
+
const loserLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14600
|
+
const winnerLeft = isP1 ? boardLeft2 : boardLeft1;
|
|
14601
|
+
spawnCollapse(loserLeft, boardTop, boardDisplayWidth, BOARD_ROWS * cellHeight, particles);
|
|
14602
|
+
spawnFirework2(winnerLeft + boardDisplayWidth / 2, boardTop + 3, particles);
|
|
14603
|
+
}
|
|
14604
|
+
function finalizePostResolution(player, isP1) {
|
|
14605
|
+
if (checkGameOver(player.board)) {
|
|
14606
|
+
triggerLoss(isP1);
|
|
14607
|
+
return;
|
|
14608
|
+
}
|
|
14609
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14610
|
+
if (!spawnPair(player)) {
|
|
14611
|
+
triggerLoss(isP1);
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14614
|
+
function tickGarbageDrop(player, isP1) {
|
|
14615
|
+
const dropState = isP1 ? p1GarbageDrop : p2GarbageDrop;
|
|
14616
|
+
if (!dropState) {
|
|
14617
|
+
if (isP1) p1Phase = PHASE_NONE;
|
|
14618
|
+
else p2Phase = PHASE_NONE;
|
|
14619
|
+
finalizePostResolution(player, isP1);
|
|
14620
|
+
return;
|
|
14621
|
+
}
|
|
14622
|
+
dropState.frameTick++;
|
|
14623
|
+
if (dropState.frameTick < GARBAGE_DROP_STEP_FRAMES) return;
|
|
14624
|
+
dropState.frameTick = 0;
|
|
14625
|
+
let stillDropping = false;
|
|
14626
|
+
for (const gem of dropState.gems) {
|
|
14627
|
+
if (gem.delayFrames > 0) {
|
|
14628
|
+
gem.delayFrames--;
|
|
14629
|
+
stillDropping = true;
|
|
14630
|
+
continue;
|
|
14631
|
+
}
|
|
14632
|
+
if (gem.currentRow < gem.targetRow) {
|
|
14633
|
+
gem.currentRow++;
|
|
14634
|
+
stillDropping = true;
|
|
14635
|
+
}
|
|
14636
|
+
}
|
|
14637
|
+
if (stillDropping) return;
|
|
14638
|
+
for (const gem of dropState.gems) {
|
|
14639
|
+
if (player.board[gem.targetRow][gem.col] !== null) continue;
|
|
14640
|
+
player.board[gem.targetRow][gem.col] = {
|
|
14641
|
+
color: gem.color,
|
|
14642
|
+
type: "counter",
|
|
14643
|
+
counterTimer: gem.timer
|
|
14644
|
+
};
|
|
14645
|
+
}
|
|
14646
|
+
if (isP1) {
|
|
14647
|
+
p1GarbageDrop = null;
|
|
14648
|
+
p1Phase = PHASE_NONE;
|
|
14649
|
+
} else {
|
|
14650
|
+
p2GarbageDrop = null;
|
|
14651
|
+
p2Phase = PHASE_NONE;
|
|
14652
|
+
}
|
|
14653
|
+
finalizePostResolution(player, isP1);
|
|
14654
|
+
}
|
|
14655
|
+
function tickResolution(isP1) {
|
|
14656
|
+
const phase = isP1 ? p1Phase : p2Phase;
|
|
14657
|
+
const timer = isP1 ? p1PhaseTimer : p2PhaseTimer;
|
|
14658
|
+
const player = isP1 ? p1 : p2;
|
|
14659
|
+
if (phase === PHASE_NONE) return;
|
|
14660
|
+
if (timer > 0) {
|
|
14661
|
+
if (isP1) p1PhaseTimer--;
|
|
14662
|
+
else p2PhaseTimer--;
|
|
14663
|
+
return;
|
|
14664
|
+
}
|
|
14665
|
+
switch (phase) {
|
|
14666
|
+
case PHASE_FLASH:
|
|
14667
|
+
if (isP1) {
|
|
14668
|
+
p1Phase = PHASE_DISSOLVE;
|
|
14669
|
+
p1PhaseTimer = DISSOLVE_FRAMES;
|
|
14670
|
+
} else {
|
|
14671
|
+
p2Phase = PHASE_DISSOLVE;
|
|
14672
|
+
p2PhaseTimer = DISSOLVE_FRAMES;
|
|
14673
|
+
}
|
|
14674
|
+
break;
|
|
14675
|
+
case PHASE_DISSOLVE:
|
|
14676
|
+
if (isP1) {
|
|
14677
|
+
p1Phase = PHASE_GRAVITY;
|
|
14678
|
+
p1PhaseTimer = GRAVITY_FRAMES;
|
|
14679
|
+
} else {
|
|
14680
|
+
p2Phase = PHASE_GRAVITY;
|
|
14681
|
+
p2PhaseTimer = GRAVITY_FRAMES;
|
|
14682
|
+
}
|
|
14683
|
+
break;
|
|
14684
|
+
case PHASE_GRAVITY:
|
|
14685
|
+
if (isP1) {
|
|
14686
|
+
p1Phase = PHASE_CHECK;
|
|
14687
|
+
p1PhaseTimer = 0;
|
|
14688
|
+
} else {
|
|
14689
|
+
p2Phase = PHASE_CHECK;
|
|
14690
|
+
p2PhaseTimer = 0;
|
|
14691
|
+
}
|
|
14692
|
+
break;
|
|
14693
|
+
case PHASE_GARBAGE:
|
|
14694
|
+
tickGarbageDrop(player, isP1);
|
|
14695
|
+
break;
|
|
14696
|
+
case PHASE_CHECK: {
|
|
14697
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14698
|
+
const { targets: more, destroyedPowerGemSizes: pgSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14699
|
+
if (more.length > 0) {
|
|
14700
|
+
for (const t of more) {
|
|
14701
|
+
player.board[t.row][t.col] = null;
|
|
14702
|
+
}
|
|
14703
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14704
|
+
player.powerGems = [];
|
|
14705
|
+
if (isP1) {
|
|
14706
|
+
p1ClearedCells = more;
|
|
14707
|
+
p1ChainCount++;
|
|
14708
|
+
p1TotalCleared += more.length;
|
|
14709
|
+
p1Phase = PHASE_FLASH;
|
|
14710
|
+
p1PhaseTimer = FLASH_FRAMES;
|
|
14711
|
+
const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p1ChainCount };
|
|
14712
|
+
p1AttackAccum += calculateStepAttack(stepInfo);
|
|
14713
|
+
} else {
|
|
14714
|
+
p2ClearedCells = more;
|
|
14715
|
+
p2ChainCount++;
|
|
14716
|
+
p2TotalCleared += more.length;
|
|
14717
|
+
p2Phase = PHASE_FLASH;
|
|
14718
|
+
p2PhaseTimer = FLASH_FRAMES;
|
|
14719
|
+
const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p2ChainCount };
|
|
14720
|
+
p2AttackAccum += calculateStepAttack(stepInfo);
|
|
14721
|
+
}
|
|
14722
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14723
|
+
const bLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14724
|
+
spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
|
|
14725
|
+
for (const cell of more) {
|
|
14726
|
+
const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
|
|
14727
|
+
const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
|
|
14728
|
+
spawnClearParticles(px, py, cell.color, 4 + chainCount, particles);
|
|
14729
|
+
}
|
|
14730
|
+
triggerShake(isP1 ? p1Shake : p2Shake, Math.min(4, chainCount), 8);
|
|
14731
|
+
} else {
|
|
14732
|
+
const totalCleared = isP1 ? p1TotalCleared : p2TotalCleared;
|
|
14733
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14734
|
+
const dmgMod = isP1 ? p1Character.damageModifier : p2Character.damageModifier;
|
|
14735
|
+
const isDiamond = isP1 ? p1IsDiamondClear : p2IsDiamondClear;
|
|
14736
|
+
const rawAttack = isP1 ? p1AttackAccum : p2AttackAccum;
|
|
14737
|
+
let attack = applyAttackModifiers(rawAttack, dmgMod, isDiamond);
|
|
14738
|
+
if (attack > 0 && player.pendingGarbage > 0) {
|
|
14739
|
+
const defense = resolveCounterAttack(attack, player.pendingGarbage);
|
|
14740
|
+
attack = defense.remainingAttack;
|
|
14741
|
+
player.pendingGarbage = defense.remainingPending;
|
|
14742
|
+
player.pendingCounteredGarbage = defense.pendingStartsAtThree ? defense.remainingPending : 0;
|
|
14743
|
+
if (defense.canceledGems > 0) {
|
|
14744
|
+
const left = isP1 ? boardLeft1 : boardLeft2;
|
|
14745
|
+
floatingTexts.push({
|
|
14746
|
+
text: `DEFENSE -${defense.canceledGems}`,
|
|
14747
|
+
x: left + 2,
|
|
14748
|
+
y: Math.max(1, boardTop - 1),
|
|
14749
|
+
color: "\x1B[96m",
|
|
14750
|
+
frames: 18,
|
|
14751
|
+
maxFrames: 18
|
|
14752
|
+
});
|
|
14753
|
+
}
|
|
14754
|
+
}
|
|
14755
|
+
if (attack > 0) {
|
|
14756
|
+
const opponent = isP1 ? p2 : p1;
|
|
14757
|
+
opponent.pendingGarbage += attack;
|
|
14758
|
+
const fromBLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14759
|
+
const toBLeft = isP1 ? boardLeft2 : boardLeft1;
|
|
14760
|
+
spawnProjectile(
|
|
14761
|
+
fromBLeft + boardDisplayWidth / 2,
|
|
14762
|
+
toBLeft + boardDisplayWidth / 2,
|
|
14763
|
+
boardTop + Math.floor(boardDisplayHeight / 2),
|
|
14764
|
+
attack,
|
|
14765
|
+
projectiles
|
|
14766
|
+
);
|
|
14767
|
+
if (isP1) {
|
|
14768
|
+
p2Pose = "hit";
|
|
14769
|
+
p2PoseTimer = 10;
|
|
14770
|
+
} else {
|
|
14771
|
+
p1Pose = "hit";
|
|
14772
|
+
p1PoseTimer = 10;
|
|
14773
|
+
}
|
|
14774
|
+
triggerFlash(isP1 ? p2Flash : p1Flash, "\x1B[91m", 6);
|
|
14775
|
+
triggerShake(isP1 ? p2Shake : p1Shake, Math.min(3, Math.ceil(attack / 2)), 6);
|
|
14776
|
+
}
|
|
14777
|
+
player.score += totalCleared * 10 + (chainCount > 1 ? chainCount * 50 : 0);
|
|
14778
|
+
finishResolution(player, isP1);
|
|
14779
|
+
}
|
|
14780
|
+
break;
|
|
14781
|
+
}
|
|
14782
|
+
}
|
|
14783
|
+
}
|
|
14784
|
+
function finishResolution(player, isP1) {
|
|
14785
|
+
if (isP1) {
|
|
14786
|
+
p1Phase = PHASE_NONE;
|
|
14787
|
+
p1PhaseTimer = 0;
|
|
14788
|
+
p1ChainCount = 0;
|
|
14789
|
+
p1TotalCleared = 0;
|
|
14790
|
+
p1ClearedCells = [];
|
|
14791
|
+
p1AttackAccum = 0;
|
|
14792
|
+
} else {
|
|
14793
|
+
p2Phase = PHASE_NONE;
|
|
14794
|
+
p2PhaseTimer = 0;
|
|
14795
|
+
p2ChainCount = 0;
|
|
14796
|
+
p2TotalCleared = 0;
|
|
14797
|
+
p2ClearedCells = [];
|
|
14798
|
+
p2AttackAccum = 0;
|
|
14799
|
+
}
|
|
14800
|
+
decrementCounters(player);
|
|
14801
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14802
|
+
const { targets: postCounterTargets } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14803
|
+
if (postCounterTargets.length > 0) {
|
|
14804
|
+
startResolution(player, isP1);
|
|
14805
|
+
return;
|
|
14806
|
+
}
|
|
14807
|
+
if (player.pendingGarbage > 0) {
|
|
14808
|
+
const incomingCount = player.pendingGarbage;
|
|
14809
|
+
const defendedCount = Math.min(player.pendingCounteredGarbage, player.pendingGarbage);
|
|
14810
|
+
const normalCount = player.pendingGarbage - defendedCount;
|
|
14811
|
+
const cursor = isP1 ? p1GarbagePatternCursor : p2GarbagePatternCursor;
|
|
14812
|
+
const attackerPattern = isP1 ? p2Character.dropPattern : p1Character.dropPattern;
|
|
14813
|
+
const { dropState, nextCursor } = buildGarbageDropState(player, defendedCount, normalCount, cursor, attackerPattern);
|
|
14814
|
+
if (isP1) p1GarbagePatternCursor = nextCursor;
|
|
14815
|
+
else p2GarbagePatternCursor = nextCursor;
|
|
14816
|
+
player.pendingGarbage = 0;
|
|
14817
|
+
player.pendingCounteredGarbage = 0;
|
|
14818
|
+
const left = isP1 ? boardLeft1 : boardLeft2;
|
|
14819
|
+
floatingTexts.push({
|
|
14820
|
+
text: `DROP +${incomingCount}`,
|
|
14821
|
+
x: left + 2,
|
|
14822
|
+
y: Math.max(1, boardTop - 1),
|
|
14823
|
+
color: "\x1B[1;91m",
|
|
14824
|
+
frames: 20,
|
|
14825
|
+
maxFrames: 20
|
|
14826
|
+
});
|
|
14827
|
+
if (dropState) {
|
|
14828
|
+
if (isP1) {
|
|
14829
|
+
p1GarbageDrop = dropState;
|
|
14830
|
+
p1Phase = PHASE_GARBAGE;
|
|
14831
|
+
p1PhaseTimer = 0;
|
|
14832
|
+
} else {
|
|
14833
|
+
p2GarbageDrop = dropState;
|
|
14834
|
+
p2Phase = PHASE_GARBAGE;
|
|
14835
|
+
p2PhaseTimer = 0;
|
|
14836
|
+
}
|
|
14837
|
+
return;
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
finalizePostResolution(player, isP1);
|
|
14841
|
+
}
|
|
14842
|
+
function update() {
|
|
14843
|
+
if (gameState !== "running") return;
|
|
14844
|
+
glitchFrame++;
|
|
14845
|
+
updateParticles2(particles);
|
|
14846
|
+
updateFloatingTexts(floatingTexts);
|
|
14847
|
+
updateProjectiles(projectiles);
|
|
14848
|
+
if (p1PoseTimer > 0) {
|
|
14849
|
+
p1PoseTimer--;
|
|
14850
|
+
if (p1PoseTimer <= 0) p1Pose = "idle";
|
|
14851
|
+
}
|
|
14852
|
+
if (p2PoseTimer > 0) {
|
|
14853
|
+
p2PoseTimer--;
|
|
14854
|
+
if (p2PoseTimer <= 0) p2Pose = "idle";
|
|
14855
|
+
}
|
|
14856
|
+
if (p1Phase !== PHASE_NONE) {
|
|
14857
|
+
tickResolution(true);
|
|
14858
|
+
}
|
|
14859
|
+
if (p2Phase !== PHASE_NONE) {
|
|
14860
|
+
tickResolution(false);
|
|
14861
|
+
}
|
|
14862
|
+
if (p1Phase === PHASE_NONE && p1.currentPair && p1.alive) {
|
|
14863
|
+
p1DropSpeed = getDropSpeed(p1);
|
|
14864
|
+
p1DropTimer++;
|
|
14865
|
+
if (p1DropTimer >= p1DropSpeed) {
|
|
14866
|
+
p1DropTimer = 0;
|
|
14867
|
+
if (!dropPair(p1.currentPair, p1.board)) {
|
|
14868
|
+
lockAndResolve(p1, true);
|
|
14869
|
+
}
|
|
14870
|
+
}
|
|
14871
|
+
}
|
|
14872
|
+
if (p2Phase === PHASE_NONE && p2.currentPair && p2.alive) {
|
|
14873
|
+
p2DropSpeed = Math.max(MIN_DROP_SPEED, Math.floor(getDropSpeed(p2) / aiState.difficulty.dropSpeedBoost));
|
|
14874
|
+
const action = aiTick(p2, aiState);
|
|
14875
|
+
switch (action) {
|
|
14876
|
+
case "rotate_cw":
|
|
14877
|
+
rotatePair(p2.currentPair, p2.board, true);
|
|
14878
|
+
break;
|
|
14879
|
+
case "rotate_ccw":
|
|
14880
|
+
rotatePair(p2.currentPair, p2.board, false);
|
|
14881
|
+
break;
|
|
14882
|
+
case "move": {
|
|
14883
|
+
const dir = getAIMoveDirection(p2, aiState);
|
|
14884
|
+
if (dir !== 0) movePair(p2.currentPair, p2.board, dir);
|
|
14885
|
+
break;
|
|
14886
|
+
}
|
|
14887
|
+
case "drop":
|
|
14888
|
+
if (!dropPair(p2.currentPair, p2.board)) {
|
|
14889
|
+
lockAndResolve(p2, false);
|
|
14890
|
+
}
|
|
14891
|
+
break;
|
|
14892
|
+
}
|
|
14893
|
+
p2DropTimer++;
|
|
14894
|
+
if (p2DropTimer >= p2DropSpeed) {
|
|
14895
|
+
p2DropTimer = 0;
|
|
14896
|
+
if (p2.currentPair && !dropPair(p2.currentPair, p2.board)) {
|
|
14897
|
+
lockAndResolve(p2, false);
|
|
14898
|
+
}
|
|
14899
|
+
}
|
|
14900
|
+
}
|
|
14901
|
+
}
|
|
14902
|
+
function lockAndResolve(player, isP1) {
|
|
14903
|
+
if (!player.currentPair) return;
|
|
14904
|
+
lockPair(player.currentPair, player.board);
|
|
14905
|
+
player.currentPair = null;
|
|
14906
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14907
|
+
if (!isP1) {
|
|
14908
|
+
aiState.decided = false;
|
|
14909
|
+
aiState.thinkTimer = 0;
|
|
14910
|
+
aiState.moveTimer = 0;
|
|
14911
|
+
}
|
|
14912
|
+
startResolution(player, isP1);
|
|
14913
|
+
}
|
|
14914
|
+
function render() {
|
|
14915
|
+
if (!running) return;
|
|
14916
|
+
const cols = terminal.cols;
|
|
14917
|
+
const rows = terminal.rows;
|
|
14918
|
+
if (cols < MIN_COLS || rows < MIN_ROWS) {
|
|
14919
|
+
let output2 = "\x1B[2J\x1B[H";
|
|
14920
|
+
output2 += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor(cols / 2) - 10)}H`;
|
|
14921
|
+
output2 += `${themeColor}Need ${MIN_COLS}\xD7${MIN_ROWS} (have ${cols}\xD7${rows})\x1B[0m`;
|
|
14922
|
+
terminal.write(output2);
|
|
14923
|
+
return;
|
|
14924
|
+
}
|
|
14925
|
+
calculateLayout();
|
|
14926
|
+
let output = "\x1B[2J\x1B[H";
|
|
14927
|
+
switch (gameState) {
|
|
14928
|
+
case "difficulty":
|
|
14929
|
+
output += renderDifficultyScreen();
|
|
14930
|
+
break;
|
|
14931
|
+
case "characterSelect":
|
|
14932
|
+
output += renderCharacterSelectScreen();
|
|
14933
|
+
break;
|
|
14934
|
+
case "running":
|
|
14935
|
+
output += renderGame();
|
|
14936
|
+
break;
|
|
14937
|
+
case "paused":
|
|
14938
|
+
output += renderGame();
|
|
14939
|
+
output += renderPauseOverlay();
|
|
14940
|
+
break;
|
|
14941
|
+
case "gameOver":
|
|
14942
|
+
output += renderGame({
|
|
14943
|
+
showEffects: false,
|
|
14944
|
+
showHud: false,
|
|
14945
|
+
showControls: false,
|
|
14946
|
+
showVs: false
|
|
14947
|
+
});
|
|
14948
|
+
output += renderGameOverOverlay();
|
|
14949
|
+
break;
|
|
14950
|
+
}
|
|
14951
|
+
terminal.write(output);
|
|
14952
|
+
}
|
|
14953
|
+
function renderDifficultyScreen() {
|
|
14954
|
+
const cols = terminal.cols;
|
|
14955
|
+
const rows = terminal.rows;
|
|
14956
|
+
const centerX = Math.floor(cols / 2);
|
|
14957
|
+
let output = "";
|
|
14958
|
+
const titleY = Math.max(2, Math.floor(rows / 2) - 8);
|
|
14959
|
+
for (let i = 0; i < title.length; i++) {
|
|
14960
|
+
let line = title[i];
|
|
14961
|
+
if (Math.random() < 0.15) {
|
|
14962
|
+
const pos = Math.floor(Math.random() * line.length);
|
|
14963
|
+
const glitchChar = GLITCH_CHARS2[Math.floor(Math.random() * GLITCH_CHARS2.length)];
|
|
14964
|
+
line = line.substring(0, pos) + glitchChar + line.substring(pos + 1);
|
|
14965
|
+
}
|
|
14966
|
+
const x = Math.max(1, centerX - Math.floor(line.length / 2));
|
|
14967
|
+
const color = i < 2 ? themeColor : "\x1B[93m";
|
|
14968
|
+
output += `\x1B[${titleY + i};${x}H${color}\x1B[1m${line}\x1B[0m`;
|
|
14969
|
+
}
|
|
14970
|
+
const subtitle = "\u2554\u2550\u2550 GEM BATTLE VS AI \u2550\u2550\u2557";
|
|
14971
|
+
const subX = Math.max(1, centerX - Math.floor(subtitle.length / 2));
|
|
14972
|
+
output += `\x1B[${titleY + title.length + 1};${subX}H${themeColor}${subtitle}\x1B[0m`;
|
|
14973
|
+
const diffY = titleY + title.length + 4;
|
|
14974
|
+
const diffs = ["easy", "normal", "hard"];
|
|
14975
|
+
const diffLabels = ["EASY", "NORMAL", "HARD"];
|
|
14976
|
+
const diffDescs = ["Relaxed pace, AI makes mistakes", "Balanced challenge", "Fast & ruthless AI"];
|
|
14977
|
+
for (let i = 0; i < diffs.length; i++) {
|
|
14978
|
+
const isSelected = i === difficultySelection;
|
|
14979
|
+
const label = `[${i + 1}] ${diffLabels[i]}`;
|
|
14980
|
+
const text = isSelected ? `\u25BA ${label} \u25C4` : ` ${label} `;
|
|
14981
|
+
const style = isSelected ? "\x1B[1;93m" : `\x1B[2m${themeColor}`;
|
|
14982
|
+
const x = Math.max(1, centerX - Math.floor(text.length / 2));
|
|
14983
|
+
output += `\x1B[${diffY + i * 2};${x}H${style}${text}\x1B[0m`;
|
|
14984
|
+
if (isSelected) {
|
|
14985
|
+
const descX = Math.max(1, centerX - Math.floor(diffDescs[i].length / 2));
|
|
14986
|
+
output += `\x1B[${diffY + i * 2 + 1};${descX}H\x1B[2m${themeColor}${diffDescs[i]}\x1B[0m`;
|
|
14987
|
+
}
|
|
14988
|
+
}
|
|
14989
|
+
const controlsY = diffY + 8;
|
|
14990
|
+
const controls = "\u2191\u2193 Select Enter Confirm Q Quit";
|
|
14991
|
+
const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
|
|
14992
|
+
output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
|
|
14993
|
+
const refY = controlsY + 2;
|
|
14994
|
+
const refLines = [
|
|
14995
|
+
"\u2190\u2192/AD Move \u2191/W Rotate Z Counter-rotate",
|
|
14996
|
+
"\u2193/S Soft drop Space Hard drop ESC Pause"
|
|
14997
|
+
];
|
|
14998
|
+
for (let i = 0; i < refLines.length; i++) {
|
|
14999
|
+
const rx = Math.max(1, centerX - Math.floor(refLines[i].length / 2));
|
|
15000
|
+
output += `\x1B[${refY + i};${rx}H\x1B[2m\x1B[90m${refLines[i]}\x1B[0m`;
|
|
15001
|
+
}
|
|
15002
|
+
return output;
|
|
15003
|
+
}
|
|
15004
|
+
const PATTERN_PREVIEW_COLORS = {
|
|
15005
|
+
red: "\x1B[1;38;5;196m",
|
|
15006
|
+
green: "\x1B[1;38;5;46m",
|
|
15007
|
+
blue: "\x1B[1;38;5;27m",
|
|
15008
|
+
yellow: "\x1B[1;38;5;226m"
|
|
15009
|
+
};
|
|
15010
|
+
function renderCharacterSelectScreen() {
|
|
15011
|
+
const cols = terminal.cols;
|
|
15012
|
+
const rows = terminal.rows;
|
|
15013
|
+
const centerX = Math.floor(cols / 2);
|
|
15014
|
+
let output = "";
|
|
15015
|
+
const titleY = Math.max(2, Math.floor(rows / 2) - 14);
|
|
15016
|
+
const selectTitle = "\u2554\u2550\u2550 SELECT YOUR FIGHTER \u2550\u2550\u2557";
|
|
15017
|
+
const stX = Math.max(1, centerX - Math.floor(selectTitle.length / 2));
|
|
15018
|
+
output += `\x1B[${titleY};${stX}H${themeColor}\x1B[1m${selectTitle}\x1B[0m`;
|
|
15019
|
+
const badge = `[${selectedDifficulty.name}]`;
|
|
15020
|
+
const bX = Math.max(1, centerX - Math.floor(badge.length / 2));
|
|
15021
|
+
output += `\x1B[${titleY + 1};${bX}H\x1B[2m${themeColor}${badge}\x1B[0m`;
|
|
15022
|
+
const gridY = titleY + 3;
|
|
15023
|
+
const cellW = 11;
|
|
15024
|
+
const selectedIdx = CHAR_GRID[charGridRow][charGridCol];
|
|
15025
|
+
for (let row = 0; row < CHAR_GRID.length; row++) {
|
|
15026
|
+
const rowChars = CHAR_GRID[row];
|
|
15027
|
+
const rowWidth = rowChars.length * cellW;
|
|
15028
|
+
const rowStartX = Math.max(1, centerX - Math.floor(rowWidth / 2));
|
|
15029
|
+
for (let col = 0; col < rowChars.length; col++) {
|
|
15030
|
+
const charIdx = rowChars[col];
|
|
15031
|
+
const char2 = CHARACTERS[charIdx];
|
|
15032
|
+
const isSelected = charIdx === selectedIdx;
|
|
15033
|
+
const x = rowStartX + col * cellW;
|
|
15034
|
+
const y = gridY + row * 3;
|
|
15035
|
+
const name = char2.name.slice(0, cellW - 2).padEnd(cellW - 2);
|
|
15036
|
+
if (isSelected) {
|
|
15037
|
+
output += `\x1B[${y};${x}H\x1B[1;93m\u25BA${name}\u25C4\x1B[0m`;
|
|
15038
|
+
} else {
|
|
15039
|
+
output += `\x1B[${y};${x}H${themeColor} ${name} \x1B[0m`;
|
|
15040
|
+
}
|
|
15041
|
+
const dmgStr = char2.damageModifier !== 1 ? `${Math.round(char2.damageModifier * 100)}%` : " ";
|
|
15042
|
+
const dmgColor = char2.damageModifier > 1 ? "\x1B[92m" : char2.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
|
|
15043
|
+
output += `\x1B[${y + 1};${x + 1}H${dmgColor}${dmgStr}\x1B[0m`;
|
|
15044
|
+
}
|
|
15045
|
+
}
|
|
15046
|
+
const char = CHARACTERS[selectedIdx];
|
|
15047
|
+
const detailY = gridY + CHAR_GRID.length * 3 + 1;
|
|
15048
|
+
const charTitle = `${char.name} \u2014 ${char.description}`;
|
|
15049
|
+
const ctX = Math.max(1, centerX - Math.floor(charTitle.length / 2));
|
|
15050
|
+
output += `\x1B[${detailY};${ctX}H\x1B[1;97m${charTitle}\x1B[0m`;
|
|
15051
|
+
const dmgLabel = char.damageModifier === 1 ? "Damage: 100% (standard)" : char.damageModifier > 1 ? `Damage: ${Math.round(char.damageModifier * 100)}% (bonus)` : `Damage: ${Math.round(char.damageModifier * 100)}% (reduced)`;
|
|
15052
|
+
const dmgLabelColor = char.damageModifier > 1 ? "\x1B[92m" : char.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
|
|
15053
|
+
const dlX = Math.max(1, centerX - Math.floor(dmgLabel.length / 2));
|
|
15054
|
+
output += `\x1B[${detailY + 1};${dlX}H${dmgLabelColor}${dmgLabel}\x1B[0m`;
|
|
15055
|
+
const previewWidth = 6 * 2;
|
|
15056
|
+
const previewX = Math.max(1, centerX - Math.floor(previewWidth / 2));
|
|
15057
|
+
const previewY = detailY + 3;
|
|
15058
|
+
output += `\x1B[${previewY - 1};${previewX}H\x1B[2m${themeColor}Drop Pattern:\x1B[0m`;
|
|
15059
|
+
for (let pr = 0; pr < char.dropPattern.length; pr++) {
|
|
15060
|
+
let rowStr = "";
|
|
15061
|
+
for (let pc = 0; pc < char.dropPattern[pr].length; pc++) {
|
|
15062
|
+
const color = char.dropPattern[pr][pc];
|
|
15063
|
+
rowStr += `${PATTERN_PREVIEW_COLORS[color]}\u2588\u2588\x1B[0m`;
|
|
15064
|
+
}
|
|
15065
|
+
output += `\x1B[${previewY + pr};${previewX}H${rowStr}`;
|
|
15066
|
+
}
|
|
15067
|
+
const portraitX = previewX + previewWidth + 3;
|
|
15068
|
+
const portraitLines = char.portraits.idle;
|
|
15069
|
+
output += renderPortrait(portraitLines, portraitX, previewY, themeColor);
|
|
15070
|
+
const controlsY = previewY + char.dropPattern.length + 2;
|
|
15071
|
+
const controls = "\u2190\u2192\u2191\u2193 Select Enter Confirm Esc Back";
|
|
15072
|
+
const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
|
|
15073
|
+
output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
|
|
15074
|
+
return output;
|
|
15075
|
+
}
|
|
15076
|
+
function renderGame(options) {
|
|
15077
|
+
const config = {
|
|
15078
|
+
showEffects: options?.showEffects ?? true,
|
|
15079
|
+
showHud: options?.showHud ?? true,
|
|
15080
|
+
showControls: options?.showControls ?? true,
|
|
15081
|
+
showVs: options?.showVs ?? true
|
|
15082
|
+
};
|
|
15083
|
+
let output = "";
|
|
15084
|
+
output += renderHeaderBar();
|
|
15085
|
+
const s1 = updateShake(p1Shake);
|
|
15086
|
+
const s2 = updateShake(p2Shake);
|
|
15087
|
+
output += renderBoard(p1, boardLeft1 + s1.dx, boardTop + s1.dy, true, true, p1Phase, p1PhaseTimer, p1ClearedCells, p1Flash);
|
|
15088
|
+
output += renderBoard(p2, boardLeft2 + s2.dx, boardTop + s2.dy, false, false, p2Phase, p2PhaseTimer, p2ClearedCells, p2Flash);
|
|
15089
|
+
if (config.showVs) {
|
|
15090
|
+
output += renderVSColumn();
|
|
15091
|
+
}
|
|
15092
|
+
if (showSidePanels && config.showHud) {
|
|
15093
|
+
output += renderSidePanel(true);
|
|
15094
|
+
output += renderSidePanel(false);
|
|
15095
|
+
}
|
|
15096
|
+
if (!showSidePanels) {
|
|
15097
|
+
output += `\x1B[${boardTop - 1};${boardLeft1 + 2}H${themeColor}${p1Character.name}\x1B[0m`;
|
|
15098
|
+
output += `\x1B[${boardTop - 1};${boardLeft2 + 2}H\x1B[91m${p2Character.name}\x1B[0m`;
|
|
15099
|
+
output += renderNextStrip(p1, boardLeft1, Math.max(1, boardTop - 2), themeColor);
|
|
15100
|
+
output += renderNextStrip(p2, boardLeft2, Math.max(1, boardTop - 2), "\x1B[91m");
|
|
15101
|
+
if (config.showHud) {
|
|
15102
|
+
const panelY = boardTop + boardDisplayHeight + 1;
|
|
15103
|
+
output += renderScorePanel(
|
|
15104
|
+
boardLeft1,
|
|
15105
|
+
panelY,
|
|
15106
|
+
p1.score,
|
|
15107
|
+
p1.pendingGarbage,
|
|
15108
|
+
themeColor,
|
|
15109
|
+
`S${Math.round(BASE_DROP_SPEED / Math.max(1, p1DropSpeed) * 100)}%`
|
|
15110
|
+
);
|
|
15111
|
+
output += renderScorePanel(
|
|
15112
|
+
boardLeft2,
|
|
15113
|
+
panelY,
|
|
15114
|
+
p2.score,
|
|
15115
|
+
p2.pendingGarbage,
|
|
15116
|
+
"\x1B[1;38;5;203m",
|
|
15117
|
+
"AI"
|
|
15118
|
+
);
|
|
15119
|
+
}
|
|
15120
|
+
}
|
|
15121
|
+
if (config.showEffects) {
|
|
15122
|
+
output += renderParticles(particles, 1, 1, terminal.cols, terminal.rows);
|
|
15123
|
+
output += renderFloatingTexts(floatingTexts);
|
|
15124
|
+
output += renderProjectiles(projectiles);
|
|
15125
|
+
}
|
|
15126
|
+
if (config.showControls) {
|
|
15127
|
+
output += renderFooterBar();
|
|
15128
|
+
}
|
|
15129
|
+
return output;
|
|
15130
|
+
}
|
|
15131
|
+
function renderNextStrip(player, left, y, accent) {
|
|
15132
|
+
if (!player.nextPair) return "";
|
|
15133
|
+
let output = "";
|
|
15134
|
+
output += `\x1B[${y};${left}H${accent}\x1B[1mNEXT\x1B[0m `;
|
|
15135
|
+
output += renderGemCell(player.nextPair.primary, false, false);
|
|
15136
|
+
output += renderGemCell(player.nextPair.secondary, false, false);
|
|
15137
|
+
output += ` \x1B[2m${accent}\u25CF\u25CF\u25CF\x1B[0m`;
|
|
15138
|
+
return output;
|
|
15139
|
+
}
|
|
15140
|
+
function renderScorePanel(left, y, score, pending, accent, tag) {
|
|
15141
|
+
const { levelLabel, levelColor } = getIncomingThreatLevel(pending);
|
|
15142
|
+
const meterWidth = 5;
|
|
15143
|
+
const filled = Math.min(meterWidth, pending);
|
|
15144
|
+
const meter = `${"\u25A0".repeat(filled)}${"\xB7".repeat(meterWidth - filled)}`;
|
|
15145
|
+
const scoreLine = `SCORE ${score.toString().padStart(6, "0")} ${tag}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
|
|
15146
|
+
const statusLine = `IN${pending.toString().padStart(2, "0")} ${meter} ${levelLabel}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
|
|
15147
|
+
let output = "";
|
|
15148
|
+
output += `\x1B[${y};${left}H${accent}\x1B[48;5;237m${scoreLine}\x1B[0m`;
|
|
15149
|
+
output += `\x1B[${y + 1};${left}H${levelColor}\x1B[48;5;235m${statusLine}\x1B[0m`;
|
|
15150
|
+
return output;
|
|
15151
|
+
}
|
|
15152
|
+
function getIncomingThreatLevel(pending) {
|
|
15153
|
+
if (pending <= 0) return { levelLabel: "CLEAR", levelColor: "\x1B[92m" };
|
|
15154
|
+
if (pending <= 2) return { levelLabel: "LOW", levelColor: "\x1B[93m" };
|
|
15155
|
+
if (pending <= 5) return { levelLabel: "HIGH", levelColor: "\x1B[91m" };
|
|
15156
|
+
return { levelLabel: "DANGER", levelColor: "\x1B[1;91m" };
|
|
15157
|
+
}
|
|
15158
|
+
function renderBoard(player, left, top, isP1Board, showGhost, phase, phaseTimer, clearedCells, flash) {
|
|
15159
|
+
let output = "";
|
|
15160
|
+
const board = player.board;
|
|
15161
|
+
const pair = player.currentPair;
|
|
15162
|
+
const flashColor = updateFlash(flash);
|
|
15163
|
+
const borderColor = flashColor || themeColor;
|
|
15164
|
+
output += `\x1B[${top};${left}H${borderColor}\u2554${"\u2550".repeat(BOARD_COLS * cellWidth)}\u2557\x1B[0m`;
|
|
15165
|
+
const clearedSet = new Set(clearedCells.map((c) => `${c.row},${c.col}`));
|
|
15166
|
+
let ghost = null;
|
|
15167
|
+
if (showGhost && pair) {
|
|
15168
|
+
ghost = getGhostPosition(pair, board);
|
|
15169
|
+
}
|
|
15170
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
15171
|
+
let rowContent0 = "";
|
|
15172
|
+
let rowContentN = "";
|
|
15173
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
15174
|
+
const gem = board[r][c];
|
|
15175
|
+
const isCleared = clearedSet.has(`${r},${c}`);
|
|
15176
|
+
let pairGem = null;
|
|
15177
|
+
if (pair) {
|
|
15178
|
+
if (pair.row === r && pair.col === c) pairGem = pair.primary;
|
|
15179
|
+
const sec = getSecondaryPos(pair);
|
|
15180
|
+
if (sec.row === r && sec.col === c) pairGem = pair.secondary;
|
|
15181
|
+
}
|
|
15182
|
+
if (pairGem) {
|
|
15183
|
+
const cell = renderGemCell(pairGem, false, false);
|
|
15184
|
+
rowContent0 += cell;
|
|
15185
|
+
rowContentN += cell;
|
|
15186
|
+
} else if (isCleared && phase === PHASE_FLASH) {
|
|
15187
|
+
const cell = clearedCells.find((cc) => cc.row === r && cc.col === c);
|
|
15188
|
+
let s;
|
|
15189
|
+
if (cell && phaseTimer % 2 === 0) {
|
|
15190
|
+
s = `\x1B[1;97m${cellSolid}\x1B[0m`;
|
|
15191
|
+
} else {
|
|
15192
|
+
s = `${GEM_COLORS[cell?.color || "red"]}${cellSolid}\x1B[0m`;
|
|
15193
|
+
}
|
|
15194
|
+
rowContent0 += s;
|
|
15195
|
+
rowContentN += s;
|
|
15196
|
+
} else if (isCleared && phase === PHASE_DISSOLVE) {
|
|
15197
|
+
const stage = DISSOLVE_FRAMES - phaseTimer;
|
|
15198
|
+
let s;
|
|
15199
|
+
if (stage === 0) s = `\x1B[2m${cellPower}\x1B[0m`;
|
|
15200
|
+
else if (stage === 1) s = `\x1B[2m${cellGhost}\x1B[0m`;
|
|
15201
|
+
else s = cellEmpty;
|
|
15202
|
+
rowContent0 += s;
|
|
15203
|
+
rowContentN += s;
|
|
15204
|
+
} else if (gem) {
|
|
15205
|
+
const inPowerGem = gem.powerGemId !== void 0;
|
|
15206
|
+
const cell = renderGemCell(gem, inPowerGem, false);
|
|
15207
|
+
rowContent0 += cell;
|
|
15208
|
+
rowContentN += cell;
|
|
15209
|
+
} else {
|
|
15210
|
+
let isGhost = false;
|
|
15211
|
+
if (ghost) {
|
|
15212
|
+
if (ghost.primaryRow === r && ghost.primaryCol === c || ghost.secondaryRow === r && ghost.secondaryCol === c) {
|
|
15213
|
+
isGhost = true;
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
15216
|
+
if (isGhost) {
|
|
15217
|
+
const g = `\x1B[2;90m${cellGhost}\x1B[0m`;
|
|
15218
|
+
rowContent0 += g;
|
|
15219
|
+
rowContentN += g;
|
|
15220
|
+
} else {
|
|
15221
|
+
rowContent0 += `\x1B[38;5;238m${cellEmptyDot}\x1B[0m`;
|
|
15222
|
+
rowContentN += cellEmpty;
|
|
15223
|
+
}
|
|
15224
|
+
}
|
|
15225
|
+
}
|
|
15226
|
+
for (let h = 0; h < cellHeight; h++) {
|
|
15227
|
+
const rowY = top + 1 + r * cellHeight + h;
|
|
15228
|
+
output += `\x1B[${rowY};${left}H${borderColor}\u2551\x1B[0m`;
|
|
15229
|
+
output += h === 0 ? rowContent0 : rowContentN;
|
|
15230
|
+
output += `${borderColor}\u2551\x1B[0m`;
|
|
15231
|
+
}
|
|
15232
|
+
}
|
|
15233
|
+
output += `\x1B[${top + boardDisplayHeight - 1};${left}H${borderColor}\u255A${"\u2550".repeat(BOARD_COLS * cellWidth)}\u255D\x1B[0m`;
|
|
15234
|
+
const garbageDrop = isP1Board ? p1GarbageDrop : p2GarbageDrop;
|
|
15235
|
+
if (garbageDrop) {
|
|
15236
|
+
for (const g of garbageDrop.gems) {
|
|
15237
|
+
if (g.delayFrames > 0) continue;
|
|
15238
|
+
const row = Math.max(0, Math.min(BOARD_ROWS - 1, g.currentRow));
|
|
15239
|
+
const cellStr = renderGemCell({ color: g.color, type: "counter", counterTimer: g.timer }, false, false);
|
|
15240
|
+
for (let h = 0; h < cellHeight; h++) {
|
|
15241
|
+
const y = top + 1 + row * cellHeight + h;
|
|
15242
|
+
const x = left + 1 + g.col * cellWidth;
|
|
15243
|
+
output += `\x1B[${y};${x}H${cellStr}`;
|
|
15244
|
+
}
|
|
15245
|
+
}
|
|
15246
|
+
}
|
|
15247
|
+
return output;
|
|
15248
|
+
}
|
|
15249
|
+
function renderGemCell(gem, isPowerGem, _dimmed) {
|
|
15250
|
+
const color = GEM_COLORS[gem.color];
|
|
15251
|
+
switch (gem.type) {
|
|
15252
|
+
case "normal":
|
|
15253
|
+
if (isPowerGem) {
|
|
15254
|
+
return `${color}${cellPower}\x1B[0m`;
|
|
15255
|
+
}
|
|
15256
|
+
return `${color}${cellSolid}\x1B[0m`;
|
|
15257
|
+
case "crash":
|
|
15258
|
+
return `\x1B[1m${color}${cellCrash}\x1B[0m`;
|
|
15259
|
+
case "counter": {
|
|
15260
|
+
const bg = COUNTER_BG_COLORS[gem.color];
|
|
15261
|
+
if (gem.counterTimer !== void 0) {
|
|
15262
|
+
const timerStr = gem.counterTimer.toString().padStart(2, " ");
|
|
15263
|
+
return `\x1B[1;97m${bg}${timerStr.padEnd(cellWidth, " ")}\x1B[0m`;
|
|
15264
|
+
}
|
|
15265
|
+
return `\x1B[1;97m${bg}${" ?".padEnd(cellWidth, " ")}\x1B[0m`;
|
|
15266
|
+
}
|
|
15267
|
+
case "diamond":
|
|
15268
|
+
return `\x1B[1;97m${cellDiamond}\x1B[0m`;
|
|
15269
|
+
default:
|
|
15270
|
+
return `${color}${cellSolid}\x1B[0m`;
|
|
15271
|
+
}
|
|
15272
|
+
}
|
|
15273
|
+
function renderVSColumn() {
|
|
15274
|
+
let output = "";
|
|
15275
|
+
const x = vsColX;
|
|
15276
|
+
const y = boardTop + Math.max(2, Math.floor(boardDisplayHeight / 2) - 5);
|
|
15277
|
+
output += `\x1B[${Math.max(1, y - 2)};${x + 1}H\x1B[1;96mHYPER\x1B[0m`;
|
|
15278
|
+
output += `\x1B[${Math.max(1, y - 1)};${x + 1}H\x1B[1;95mFIGHT\x1B[0m`;
|
|
15279
|
+
output += `\x1B[${y};${x + 2}H\x1B[1;93mVS\x1B[0m`;
|
|
15280
|
+
const p1Lines = p1Character.portraits[p1Pose];
|
|
15281
|
+
output += renderPortrait(p1Lines, x, y + 2, themeColor);
|
|
15282
|
+
const p2Lines = p2Character.portraits[p2Pose];
|
|
15283
|
+
output += renderPortrait(p2Lines, x, y + 6, "\x1B[91m");
|
|
15284
|
+
const maxEnergy = 10;
|
|
15285
|
+
const p1Energy = p1ChainCount * 3 + p1TotalCleared;
|
|
15286
|
+
output += renderEnergyBar(x + 8, y + 2, p1Energy, maxEnergy);
|
|
15287
|
+
return output;
|
|
15288
|
+
}
|
|
15289
|
+
function renderHeaderBar() {
|
|
15290
|
+
const cols = terminal.cols;
|
|
15291
|
+
let output = "";
|
|
15292
|
+
const leftText = " HYPER FIGHTER";
|
|
15293
|
+
const rightText = `${p1Character.name} vs ${p2Character.name} [${selectedDifficulty.name}] `;
|
|
15294
|
+
const padLen = Math.max(0, cols - leftText.length - rightText.length);
|
|
15295
|
+
const row1 = leftText + " ".repeat(padLen) + rightText;
|
|
15296
|
+
output += `\x1B[1;1H\x1B[1;97m\x1B[48;5;236m${row1.slice(0, cols).padEnd(cols, " ")}\x1B[0m`;
|
|
15297
|
+
output += `\x1B[2;1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
|
|
15298
|
+
return output;
|
|
15299
|
+
}
|
|
15300
|
+
function renderFooterBar() {
|
|
15301
|
+
const cols = terminal.cols;
|
|
15302
|
+
const rows = terminal.rows;
|
|
15303
|
+
let output = "";
|
|
15304
|
+
output += `\x1B[${rows - 1};1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
|
|
15305
|
+
const controls = "\u2190\u2192 Move \u2191/W Rotate Z CCW \u2193/S Soft Space Drop ESC Pause";
|
|
15306
|
+
const cx = Math.max(1, Math.floor((cols - controls.length) / 2) + 1);
|
|
15307
|
+
output += `\x1B[${rows};${cx}H\x1B[2m\x1B[90m${controls}\x1B[0m`;
|
|
15308
|
+
return output;
|
|
15309
|
+
}
|
|
15310
|
+
function renderMiniBar(value, max, width, fillColor) {
|
|
15311
|
+
const filled = Math.min(width, Math.round(value / Math.max(1, max) * width));
|
|
15312
|
+
const empty = width - filled;
|
|
15313
|
+
return `${fillColor}${"\u25A0".repeat(filled)}\x1B[38;5;238m${"\xB7".repeat(empty)}\x1B[0m`;
|
|
15314
|
+
}
|
|
15315
|
+
function renderSidePanel(isLeft) {
|
|
15316
|
+
const player = isLeft ? p1 : p2;
|
|
15317
|
+
const x = isLeft ? sidePanel1X : sidePanel2X;
|
|
15318
|
+
const accent = isLeft ? themeColor : "\x1B[1;38;5;203m";
|
|
15319
|
+
const dropSpeed = isLeft ? p1DropSpeed : p2DropSpeed;
|
|
15320
|
+
const chainCount = isLeft ? p1ChainCount : p2ChainCount;
|
|
15321
|
+
let output = "";
|
|
15322
|
+
let y = boardTop + 1;
|
|
15323
|
+
output += `\x1B[${y};${x}H${accent}\x1B[1mNEXT\x1B[0m`;
|
|
15324
|
+
y++;
|
|
15325
|
+
if (player.nextPair) {
|
|
15326
|
+
output += `\x1B[${y};${x}H`;
|
|
15327
|
+
output += renderGemCell(player.nextPair.primary, false, false);
|
|
15328
|
+
output += renderGemCell(player.nextPair.secondary, false, false);
|
|
15329
|
+
}
|
|
15330
|
+
y += 2;
|
|
15331
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mSCORE\x1B[0m`;
|
|
15332
|
+
y++;
|
|
15333
|
+
output += `\x1B[${y};${x}H${accent}${player.score.toString().padStart(7, "0")}\x1B[0m`;
|
|
15334
|
+
y += 2;
|
|
15335
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mSPEED\x1B[0m`;
|
|
15336
|
+
y++;
|
|
15337
|
+
const speedPct = Math.round(BASE_DROP_SPEED / Math.max(1, dropSpeed) * 100);
|
|
15338
|
+
output += `\x1B[${y};${x}H${speedPct >= 200 ? "\x1B[91m" : speedPct >= 150 ? "\x1B[93m" : "\x1B[92m"}${speedPct}%\x1B[0m `;
|
|
15339
|
+
output += renderMiniBar(speedPct, 300, 6, speedPct >= 200 ? "\x1B[91m" : "\x1B[92m");
|
|
15340
|
+
y += 2;
|
|
15341
|
+
if (chainCount > 0) {
|
|
15342
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mCHAIN\x1B[0m`;
|
|
15343
|
+
y++;
|
|
15344
|
+
output += `\x1B[${y};${x}H\x1B[1;93m${chainCount}\x1B[0m`;
|
|
15345
|
+
y += 2;
|
|
15346
|
+
} else {
|
|
15347
|
+
y += 3;
|
|
15348
|
+
}
|
|
15349
|
+
const { levelLabel, levelColor } = getIncomingThreatLevel(player.pendingGarbage);
|
|
15350
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mINCOMING\x1B[0m`;
|
|
15351
|
+
y++;
|
|
15352
|
+
output += `\x1B[${y};${x}H${levelColor}${player.pendingGarbage.toString().padStart(2, "0")}\x1B[0m `;
|
|
15353
|
+
output += renderMiniBar(player.pendingGarbage, 10, 6, levelColor);
|
|
15354
|
+
y++;
|
|
15355
|
+
output += `\x1B[${y};${x}H${levelColor}${levelLabel}\x1B[0m`;
|
|
15356
|
+
return output;
|
|
15357
|
+
}
|
|
15358
|
+
function renderPauseOverlay() {
|
|
15359
|
+
const cols = terminal.cols;
|
|
15360
|
+
const rows = terminal.rows;
|
|
15361
|
+
const centerX = Math.floor(cols / 2);
|
|
15362
|
+
const centerY = Math.floor(rows / 2);
|
|
15363
|
+
let output = "";
|
|
15364
|
+
output += `\x1B[${centerY - 3};${centerX - 4}H\x1B[1;5m${themeColor}\u23F8 PAUSED\x1B[0m`;
|
|
15365
|
+
output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
|
|
15366
|
+
centerX,
|
|
15367
|
+
startY: centerY - 1,
|
|
15368
|
+
showShortcuts: true
|
|
15369
|
+
});
|
|
15370
|
+
return output;
|
|
15371
|
+
}
|
|
15372
|
+
function renderGameOverOverlay() {
|
|
15373
|
+
const cols = terminal.cols;
|
|
15374
|
+
const rows = terminal.rows;
|
|
15375
|
+
const centerX = Math.floor(cols / 2);
|
|
15376
|
+
const centerY = Math.floor(rows / 2);
|
|
15377
|
+
let output = "";
|
|
15378
|
+
gameOverTimer++;
|
|
15379
|
+
const panelWidth = Math.min(44, Math.max(34, cols - 8));
|
|
15380
|
+
const panelHeight = 12;
|
|
15381
|
+
const panelLeft = Math.max(2, centerX - Math.floor(panelWidth / 2));
|
|
15382
|
+
const panelTop = Math.max(2, centerY - Math.floor(panelHeight / 2));
|
|
15383
|
+
for (let y = 0; y < panelHeight; y++) {
|
|
15384
|
+
output += `\x1B[${panelTop + y};${panelLeft}H\x1B[40m${" ".repeat(panelWidth)}\x1B[0m`;
|
|
15385
|
+
}
|
|
15386
|
+
output += `\x1B[${panelTop};${panelLeft}H${themeColor}\u2554${"\u2550".repeat(panelWidth - 2)}\u2557\x1B[0m`;
|
|
15387
|
+
for (let y = 1; y < panelHeight - 1; y++) {
|
|
15388
|
+
output += `\x1B[${panelTop + y};${panelLeft}H${themeColor}\u2551\x1B[0m`;
|
|
15389
|
+
output += `\x1B[${panelTop + y};${panelLeft + panelWidth - 1}H${themeColor}\u2551\x1B[0m`;
|
|
15390
|
+
}
|
|
15391
|
+
output += `\x1B[${panelTop + panelHeight - 1};${panelLeft}H${themeColor}\u255A${"\u2550".repeat(panelWidth - 2)}\u255D\x1B[0m`;
|
|
15392
|
+
const winnerChar = winner === 1 ? p1Character : p2Character;
|
|
15393
|
+
const winText = winner === 1 ? `${winnerChar.name} WINS!` : `${winnerChar.name} WINS!`;
|
|
15394
|
+
const winColor = winner === 1 ? "\x1B[1;92m" : "\x1B[1;91m";
|
|
15395
|
+
const winX = Math.max(1, centerX - Math.floor(winText.length / 2));
|
|
15396
|
+
output += `\x1B[${panelTop + 2};${winX}H${winColor}${winText}\x1B[0m`;
|
|
15397
|
+
const scoreLine = `${p1Character.name}: ${p1.score} | ${p2Character.name}: ${p2.score}`;
|
|
15398
|
+
const sX = Math.max(1, centerX - Math.floor(scoreLine.length / 2));
|
|
15399
|
+
output += `\x1B[${panelTop + 4};${sX}H${themeColor}${scoreLine}\x1B[0m`;
|
|
15400
|
+
const menuItems = [
|
|
15401
|
+
{ label: "RESTART", shortcut: "R" },
|
|
15402
|
+
{ label: "QUIT", shortcut: "Q" },
|
|
15403
|
+
{ label: "LIST GAMES", shortcut: "L" },
|
|
15404
|
+
{ label: "NEXT GAME", shortcut: "N" }
|
|
15405
|
+
];
|
|
15406
|
+
output += renderSimpleMenu(menuItems, pauseMenuSelection, {
|
|
15407
|
+
centerX,
|
|
15408
|
+
startY: panelTop + 6,
|
|
15409
|
+
showShortcuts: true
|
|
15410
|
+
});
|
|
15411
|
+
return output;
|
|
15412
|
+
}
|
|
15413
|
+
const keyListener = terminal.onKey(({ domEvent }) => {
|
|
15414
|
+
if (!running) return;
|
|
15415
|
+
domEvent.preventDefault();
|
|
15416
|
+
domEvent.stopPropagation();
|
|
15417
|
+
const key = domEvent.key.toLowerCase();
|
|
15418
|
+
switch (gameState) {
|
|
15419
|
+
case "difficulty":
|
|
15420
|
+
handleDifficultyInput(key, domEvent);
|
|
15421
|
+
break;
|
|
15422
|
+
case "characterSelect":
|
|
15423
|
+
handleCharacterSelectInput(key, domEvent);
|
|
15424
|
+
break;
|
|
15425
|
+
case "running":
|
|
15426
|
+
handleGameInput(key, domEvent);
|
|
15427
|
+
break;
|
|
15428
|
+
case "paused":
|
|
15429
|
+
handlePauseInput(key, domEvent);
|
|
15430
|
+
break;
|
|
15431
|
+
case "gameOver":
|
|
15432
|
+
handleGameOverInput(key, domEvent);
|
|
15433
|
+
break;
|
|
15434
|
+
}
|
|
15435
|
+
});
|
|
15436
|
+
function handleDifficultyInput(key, domEvent) {
|
|
15437
|
+
if (domEvent.key === "ArrowUp" || key === "w") {
|
|
15438
|
+
difficultySelection = (difficultySelection - 1 + 3) % 3;
|
|
15439
|
+
} else if (domEvent.key === "ArrowDown" || key === "s") {
|
|
15440
|
+
difficultySelection = (difficultySelection + 1) % 3;
|
|
15441
|
+
} else if (domEvent.key === "Enter" || domEvent.key === " ") {
|
|
15442
|
+
const diffs = ["easy", "normal", "hard"];
|
|
15443
|
+
selectedDifficulty = DIFFICULTIES2[diffs[difficultySelection]];
|
|
15444
|
+
enterCharacterSelect();
|
|
15445
|
+
} else if (key === "1") {
|
|
15446
|
+
selectedDifficulty = DIFFICULTIES2.easy;
|
|
15447
|
+
difficultySelection = 0;
|
|
15448
|
+
enterCharacterSelect();
|
|
15449
|
+
} else if (key === "2") {
|
|
15450
|
+
selectedDifficulty = DIFFICULTIES2.normal;
|
|
15451
|
+
difficultySelection = 1;
|
|
15452
|
+
enterCharacterSelect();
|
|
15453
|
+
} else if (key === "3") {
|
|
15454
|
+
selectedDifficulty = DIFFICULTIES2.hard;
|
|
15455
|
+
difficultySelection = 2;
|
|
15456
|
+
enterCharacterSelect();
|
|
15457
|
+
} else if (key === "q") {
|
|
15458
|
+
cleanup();
|
|
15459
|
+
dispatchGameQuit(terminal);
|
|
15460
|
+
}
|
|
15461
|
+
}
|
|
15462
|
+
function enterCharacterSelect() {
|
|
15463
|
+
gameState = "characterSelect";
|
|
15464
|
+
charGridRow = 0;
|
|
15465
|
+
charGridCol = 0;
|
|
15466
|
+
}
|
|
15467
|
+
function handleCharacterSelectInput(key, domEvent) {
|
|
15468
|
+
if (key === "q" || key === "escape") {
|
|
15469
|
+
gameState = "difficulty";
|
|
15470
|
+
return;
|
|
15471
|
+
}
|
|
15472
|
+
if (domEvent.key === "ArrowLeft" || key === "a") {
|
|
15473
|
+
const row = CHAR_GRID[charGridRow];
|
|
15474
|
+
charGridCol = (charGridCol - 1 + row.length) % row.length;
|
|
15475
|
+
} else if (domEvent.key === "ArrowRight" || key === "d") {
|
|
15476
|
+
const row = CHAR_GRID[charGridRow];
|
|
15477
|
+
charGridCol = (charGridCol + 1) % row.length;
|
|
15478
|
+
} else if (domEvent.key === "ArrowUp" || key === "w") {
|
|
15479
|
+
charGridRow = (charGridRow - 1 + CHAR_GRID.length) % CHAR_GRID.length;
|
|
15480
|
+
charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
|
|
15481
|
+
} else if (domEvent.key === "ArrowDown" || key === "s") {
|
|
15482
|
+
charGridRow = (charGridRow + 1) % CHAR_GRID.length;
|
|
15483
|
+
charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
|
|
15484
|
+
} else if (domEvent.key === "Enter" || domEvent.key === " ") {
|
|
15485
|
+
const idx = CHAR_GRID[charGridRow][charGridCol];
|
|
15486
|
+
p1Character = CHARACTERS[idx];
|
|
15487
|
+
p2Character = getRandomCharacter();
|
|
15488
|
+
gameState = "running";
|
|
15489
|
+
initGame();
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
15492
|
+
function handleGameInput(key, domEvent) {
|
|
15493
|
+
if (key === "escape") {
|
|
15494
|
+
gameState = "paused";
|
|
15495
|
+
pauseMenuSelection = 0;
|
|
15496
|
+
return;
|
|
15497
|
+
}
|
|
15498
|
+
if (!p1.currentPair || p1Phase !== PHASE_NONE) return;
|
|
15499
|
+
switch (domEvent.key) {
|
|
15500
|
+
case "ArrowLeft":
|
|
15501
|
+
case "a":
|
|
15502
|
+
movePair(p1.currentPair, p1.board, -1);
|
|
15503
|
+
break;
|
|
15504
|
+
case "ArrowRight":
|
|
15505
|
+
case "d":
|
|
15506
|
+
movePair(p1.currentPair, p1.board, 1);
|
|
15507
|
+
break;
|
|
15508
|
+
case "ArrowUp":
|
|
15509
|
+
case "w":
|
|
15510
|
+
rotatePair(p1.currentPair, p1.board, true);
|
|
15511
|
+
break;
|
|
15512
|
+
case "z":
|
|
15513
|
+
rotatePair(p1.currentPair, p1.board, false);
|
|
15514
|
+
break;
|
|
15515
|
+
case "ArrowDown":
|
|
15516
|
+
case "s":
|
|
15517
|
+
if (dropPair(p1.currentPair, p1.board)) {
|
|
15518
|
+
p1DropTimer = 0;
|
|
15519
|
+
} else {
|
|
15520
|
+
lockAndResolve(p1, true);
|
|
15521
|
+
}
|
|
15522
|
+
break;
|
|
15523
|
+
case " ":
|
|
15524
|
+
hardDrop(p1.currentPair, p1.board);
|
|
15525
|
+
lockAndResolve(p1, true);
|
|
15526
|
+
break;
|
|
15527
|
+
}
|
|
15528
|
+
}
|
|
15529
|
+
function handlePauseInput(key, domEvent) {
|
|
15530
|
+
if (key === "escape") {
|
|
15531
|
+
gameState = "running";
|
|
15532
|
+
return;
|
|
15533
|
+
}
|
|
15534
|
+
const { newSelection, confirmed } = navigateMenu(
|
|
15535
|
+
pauseMenuSelection,
|
|
15536
|
+
PAUSE_MENU_ITEMS.length,
|
|
15537
|
+
key,
|
|
15538
|
+
domEvent
|
|
15539
|
+
);
|
|
15540
|
+
if (newSelection !== pauseMenuSelection) {
|
|
15541
|
+
pauseMenuSelection = newSelection;
|
|
15542
|
+
}
|
|
15543
|
+
const shortcutIdx = checkShortcut(PAUSE_MENU_ITEMS, key);
|
|
15544
|
+
if (confirmed || shortcutIdx >= 0) {
|
|
15545
|
+
const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
|
|
15546
|
+
const item = PAUSE_MENU_ITEMS[idx];
|
|
15547
|
+
switch (item.label) {
|
|
15548
|
+
case "RESUME":
|
|
15549
|
+
gameState = "running";
|
|
15550
|
+
break;
|
|
15551
|
+
case "RESTART":
|
|
15552
|
+
gameState = "running";
|
|
15553
|
+
initGame();
|
|
15554
|
+
break;
|
|
15555
|
+
case "QUIT":
|
|
15556
|
+
cleanup();
|
|
15557
|
+
dispatchGameQuit(terminal);
|
|
15558
|
+
break;
|
|
15559
|
+
case "LIST GAMES":
|
|
15560
|
+
cleanup();
|
|
15561
|
+
dispatchGamesMenu(terminal);
|
|
15562
|
+
break;
|
|
15563
|
+
case "NEXT GAME":
|
|
15564
|
+
cleanup();
|
|
15565
|
+
dispatchGameSwitch(terminal);
|
|
15566
|
+
break;
|
|
15567
|
+
}
|
|
15568
|
+
}
|
|
15569
|
+
}
|
|
15570
|
+
function handleGameOverInput(key, domEvent) {
|
|
15571
|
+
const menuItems = [
|
|
15572
|
+
{ label: "RESTART", shortcut: "R" },
|
|
15573
|
+
{ label: "QUIT", shortcut: "Q" },
|
|
15574
|
+
{ label: "LIST GAMES", shortcut: "L" },
|
|
15575
|
+
{ label: "NEXT GAME", shortcut: "N" }
|
|
15576
|
+
];
|
|
15577
|
+
const { newSelection, confirmed } = navigateMenu(
|
|
15578
|
+
pauseMenuSelection,
|
|
15579
|
+
menuItems.length,
|
|
15580
|
+
key,
|
|
15581
|
+
domEvent
|
|
15582
|
+
);
|
|
15583
|
+
if (newSelection !== pauseMenuSelection) {
|
|
15584
|
+
pauseMenuSelection = newSelection;
|
|
15585
|
+
}
|
|
15586
|
+
const shortcutIdx = checkShortcut(menuItems, key);
|
|
15587
|
+
if (confirmed || shortcutIdx >= 0) {
|
|
15588
|
+
const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
|
|
15589
|
+
switch (menuItems[idx].label) {
|
|
15590
|
+
case "RESTART":
|
|
15591
|
+
gameState = "difficulty";
|
|
15592
|
+
pauseMenuSelection = 0;
|
|
15593
|
+
break;
|
|
15594
|
+
case "QUIT":
|
|
15595
|
+
cleanup();
|
|
15596
|
+
dispatchGameQuit(terminal);
|
|
15597
|
+
break;
|
|
15598
|
+
case "LIST GAMES":
|
|
15599
|
+
cleanup();
|
|
15600
|
+
dispatchGamesMenu(terminal);
|
|
15601
|
+
break;
|
|
15602
|
+
case "NEXT GAME":
|
|
15603
|
+
cleanup();
|
|
15604
|
+
dispatchGameSwitch(terminal);
|
|
15605
|
+
break;
|
|
15606
|
+
}
|
|
15607
|
+
}
|
|
15608
|
+
}
|
|
15609
|
+
const resizeListener = terminal.onResize(() => {
|
|
15610
|
+
calculateLayout();
|
|
15611
|
+
});
|
|
15612
|
+
function cleanup() {
|
|
15613
|
+
running = false;
|
|
15614
|
+
clearInterval(gameLoop);
|
|
15615
|
+
keyListener.dispose();
|
|
15616
|
+
resizeListener.dispose();
|
|
15617
|
+
}
|
|
15618
|
+
enterAlternateBuffer(terminal, "hyper-fighter");
|
|
15619
|
+
const gameLoop = setInterval(() => {
|
|
15620
|
+
if (!running) {
|
|
15621
|
+
clearInterval(gameLoop);
|
|
15622
|
+
return;
|
|
15623
|
+
}
|
|
15624
|
+
update();
|
|
15625
|
+
render();
|
|
15626
|
+
}, TICK_MS);
|
|
15627
|
+
const originalStop = controller.stop;
|
|
15628
|
+
controller.stop = () => {
|
|
15629
|
+
cleanup();
|
|
15630
|
+
exitAlternateBuffer(terminal, "hyper-fighter");
|
|
15631
|
+
originalStop();
|
|
15632
|
+
};
|
|
15633
|
+
return controller;
|
|
15634
|
+
}
|
|
15635
|
+
|
|
13141
15636
|
// src/games/gamesMenu.ts
|
|
13142
15637
|
function showGamesMenu(terminal, optionsOrCallback) {
|
|
13143
15638
|
const options = typeof optionsOrCallback === "function" ? { onGameSelect: optionsOrCallback } : optionsOrCallback || {};
|
|
@@ -13157,8 +15652,8 @@ function showGamesMenu(terminal, optionsOrCallback) {
|
|
|
13157
15652
|
}
|
|
13158
15653
|
};
|
|
13159
15654
|
const title = [
|
|
13160
|
-
"\u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2580\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580",
|
|
13161
|
-
"\u2588\u2584\u2588 \u2588\u2580\u2588 \u2588 \u2580 \u2588 \u2588\u2588\u2584 \u2584\u2588"
|
|
15655
|
+
"\u2588 \u2588 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2584\u2580\u2588 \u2588\u2580\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580",
|
|
15656
|
+
"\u2588\u2580\u2588 \u2588 \u2588\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588 \u2580 \u2588 \u2588\u2588\u2584 \u2584\u2588"
|
|
13162
15657
|
];
|
|
13163
15658
|
function render() {
|
|
13164
15659
|
let output = "";
|
|
@@ -13425,7 +15920,8 @@ var games = [
|
|
|
13425
15920
|
{ id: "typingtest", name: "Typing Test", description: "Test your speed", run: runTypingTest },
|
|
13426
15921
|
{ id: "tron", name: "Tron", description: "Light cycle battle", run: runTronGame },
|
|
13427
15922
|
{ id: "crack", name: "Crack", description: "Hack the system", run: runCrackGame },
|
|
13428
|
-
{ id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame }
|
|
15923
|
+
{ id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame },
|
|
15924
|
+
{ id: "hyper-fighter", name: "Hyper Fighter", description: "Gem battle vs AI", run: runHyperFighterGame }
|
|
13429
15925
|
];
|
|
13430
15926
|
|
|
13431
15927
|
// src/cli.ts
|