@hypersocial/cli-games 0.2.0 → 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 +2799 -296
- package/dist/cli.js.map +1 -1
- package/dist/{create-NVLKKJX6.js → create-CIK3AD2D.js} +3 -1
- package/dist/{create-NVLKKJX6.js.map → create-CIK3AD2D.js.map} +1 -1
- package/dist/index.js +2798 -307
- package/dist/index.js.map +1 -1
- package/dist/update-check-PNB4U4P7.js +130 -0
- package/dist/update-check-PNB4U4P7.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -623,8 +623,8 @@ var TILE_BG_COLORS = {
|
|
|
623
623
|
};
|
|
624
624
|
function run2048Game(terminal) {
|
|
625
625
|
const themeColor = getCurrentThemeColor();
|
|
626
|
-
const
|
|
627
|
-
const
|
|
626
|
+
const MIN_COLS2 = 36;
|
|
627
|
+
const MIN_ROWS2 = 16;
|
|
628
628
|
const GRID_SIZE = 4;
|
|
629
629
|
const TILE_WIDTH = 8;
|
|
630
630
|
const TILE_HEIGHT = 3;
|
|
@@ -688,7 +688,7 @@ function run2048Game(terminal) {
|
|
|
688
688
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
689
689
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
690
690
|
}
|
|
691
|
-
function
|
|
691
|
+
function triggerShake3(frames, intensity) {
|
|
692
692
|
shakeFrames = frames;
|
|
693
693
|
shakeIntensity = intensity;
|
|
694
694
|
}
|
|
@@ -812,13 +812,13 @@ function run2048Game(terminal) {
|
|
|
812
812
|
if (anyMoved) {
|
|
813
813
|
score += totalMergeScore;
|
|
814
814
|
if (totalMergeScore > 0) {
|
|
815
|
-
|
|
815
|
+
triggerShake3(4, 1);
|
|
816
816
|
}
|
|
817
817
|
spawnTile();
|
|
818
818
|
if (!won && !continuedAfterWin && has2048()) {
|
|
819
819
|
won = true;
|
|
820
820
|
if (score > highScore) highScore = score;
|
|
821
|
-
|
|
821
|
+
triggerShake3(8, 2);
|
|
822
822
|
for (let y = 0; y < GRID_SIZE; y++) {
|
|
823
823
|
for (let x = 0; x < GRID_SIZE; x++) {
|
|
824
824
|
if (grid[y][x] === 2048) {
|
|
@@ -904,12 +904,12 @@ function run2048Game(terminal) {
|
|
|
904
904
|
if (shakeFrames > 0) shakeFrames--;
|
|
905
905
|
const cols = terminal.cols;
|
|
906
906
|
const rows = terminal.rows;
|
|
907
|
-
if (cols <
|
|
907
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
908
908
|
const msg1 = "Terminal too small!";
|
|
909
|
-
const needWidth = cols <
|
|
910
|
-
const needHeight = rows <
|
|
909
|
+
const needWidth = cols < MIN_COLS2;
|
|
910
|
+
const needHeight = rows < MIN_ROWS2;
|
|
911
911
|
let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider ->" : "Make pane taller v";
|
|
912
|
-
const msg2 = `Need: ${
|
|
912
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
913
913
|
const centerX = Math.floor(cols / 2);
|
|
914
914
|
const centerY = Math.floor(rows / 2);
|
|
915
915
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -1221,8 +1221,8 @@ function run2048Game(terminal) {
|
|
|
1221
1221
|
// src/games/asteroids/index.ts
|
|
1222
1222
|
function runAsteroidsGame(terminal) {
|
|
1223
1223
|
const themeColor = getCurrentThemeColor();
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1224
|
+
const MIN_COLS2 = 35;
|
|
1225
|
+
const MIN_ROWS2 = 16;
|
|
1226
1226
|
let GAME_WIDTH = 30;
|
|
1227
1227
|
let GAME_HEIGHT = 12;
|
|
1228
1228
|
const SHIP_TURN_SPEED = 0.2;
|
|
@@ -1320,7 +1320,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1320
1320
|
if (value >= max) return value - max;
|
|
1321
1321
|
return value;
|
|
1322
1322
|
}
|
|
1323
|
-
function
|
|
1323
|
+
function triggerShake3(frames, intensity) {
|
|
1324
1324
|
shakeFrames = frames;
|
|
1325
1325
|
shakeIntensity = intensity;
|
|
1326
1326
|
}
|
|
@@ -1357,7 +1357,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1357
1357
|
if (invincibilityFrames > 0) return;
|
|
1358
1358
|
lives--;
|
|
1359
1359
|
spawnParticles3(ship.x, ship.y, 15, "\x1B[91m");
|
|
1360
|
-
|
|
1360
|
+
triggerShake3(10, 3);
|
|
1361
1361
|
if (lives <= 0) {
|
|
1362
1362
|
gameOver = true;
|
|
1363
1363
|
if (score > highScore) highScore = score;
|
|
@@ -1438,7 +1438,7 @@ function runAsteroidsGame(terminal) {
|
|
|
1438
1438
|
bullets.splice(j, 1);
|
|
1439
1439
|
splitAsteroid(ast);
|
|
1440
1440
|
asteroids.splice(i, 1);
|
|
1441
|
-
|
|
1441
|
+
triggerShake3(4, 1);
|
|
1442
1442
|
break;
|
|
1443
1443
|
}
|
|
1444
1444
|
}
|
|
@@ -1468,9 +1468,9 @@ function runAsteroidsGame(terminal) {
|
|
|
1468
1468
|
if (shakeFrames > 0) shakeFrames--;
|
|
1469
1469
|
const cols = terminal.cols;
|
|
1470
1470
|
const rows = terminal.rows;
|
|
1471
|
-
if (cols <
|
|
1471
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
1472
1472
|
const msg = "Terminal too small!";
|
|
1473
|
-
const need = `Need: ${
|
|
1473
|
+
const need = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
1474
1474
|
output += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor((cols - msg.length) / 2))}H${themeColor}${msg}\x1B[0m`;
|
|
1475
1475
|
output += `\x1B[${Math.floor(rows / 2) + 2};${Math.max(1, Math.floor((cols - need.length) / 2))}H\x1B[2m${need}\x1B[0m`;
|
|
1476
1476
|
terminal.write(output);
|
|
@@ -1661,8 +1661,8 @@ function runAsteroidsGame(terminal) {
|
|
|
1661
1661
|
// src/games/breakout/index.ts
|
|
1662
1662
|
function runBreakoutGame(terminal) {
|
|
1663
1663
|
const themeColor = getCurrentThemeColor();
|
|
1664
|
-
const
|
|
1665
|
-
const
|
|
1664
|
+
const MIN_COLS2 = 40;
|
|
1665
|
+
const MIN_ROWS2 = 18;
|
|
1666
1666
|
const GAME_WIDTH = 46;
|
|
1667
1667
|
const GAME_HEIGHT = 20;
|
|
1668
1668
|
const PADDLE_WIDTH_NORMAL = 7;
|
|
@@ -1731,7 +1731,7 @@ function runBreakoutGame(terminal) {
|
|
|
1731
1731
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
1732
1732
|
scorePopups.push({ x, y, text, frames: 18, color });
|
|
1733
1733
|
}
|
|
1734
|
-
function
|
|
1734
|
+
function triggerShake3(frames, intensity) {
|
|
1735
1735
|
shakeFrames = frames;
|
|
1736
1736
|
shakeIntensity = intensity;
|
|
1737
1737
|
}
|
|
@@ -1788,7 +1788,7 @@ function runBreakoutGame(terminal) {
|
|
|
1788
1788
|
powerUps.push({ x, y, type: types[Math.floor(Math.random() * types.length)], vy: 0.15 });
|
|
1789
1789
|
}
|
|
1790
1790
|
function applyPowerUp(type) {
|
|
1791
|
-
|
|
1791
|
+
triggerShake3(8, 2);
|
|
1792
1792
|
borderFlash = 15;
|
|
1793
1793
|
switch (type) {
|
|
1794
1794
|
case "multiball": {
|
|
@@ -1964,7 +1964,7 @@ function runBreakoutGame(terminal) {
|
|
|
1964
1964
|
const totalPts = basePts + comboBonus;
|
|
1965
1965
|
score += totalPts;
|
|
1966
1966
|
const intensity = Math.min(comboCount, 8);
|
|
1967
|
-
|
|
1967
|
+
triggerShake3(3 + intensity, 1 + Math.floor(intensity / 3));
|
|
1968
1968
|
spawnParticles3(brick.x + brick.width / 2, brick.y, 6 + intensity, brickColors[brick.type]);
|
|
1969
1969
|
addScorePopup2(brick.x + 1, brick.y - 1, comboCount > 1 ? `+${totalPts}!` : `+${totalPts}`, brickColors[brick.type]);
|
|
1970
1970
|
spawnPowerUp(brick.x + brick.width / 2, brick.y);
|
|
@@ -1982,12 +1982,12 @@ function runBreakoutGame(terminal) {
|
|
|
1982
1982
|
if (lives <= 0) {
|
|
1983
1983
|
gameOver = true;
|
|
1984
1984
|
if (score > highScore) highScore = score;
|
|
1985
|
-
|
|
1985
|
+
triggerShake3(20, 4);
|
|
1986
1986
|
spawnParticles3(paddleX, GAME_HEIGHT - 2, 15, "\x1B[1;91m", ["\u2717", "\u2620", "\xD7", "\u2593"]);
|
|
1987
1987
|
} else {
|
|
1988
1988
|
ballAttached = true;
|
|
1989
1989
|
balls = [{ x: paddleX, y: GAME_HEIGHT - 3, vx: 0, vy: 0, active: true }];
|
|
1990
|
-
|
|
1990
|
+
triggerShake3(10, 2);
|
|
1991
1991
|
}
|
|
1992
1992
|
}
|
|
1993
1993
|
const aliveBricks = bricks.filter((b) => b.alive);
|
|
@@ -1996,7 +1996,7 @@ function runBreakoutGame(terminal) {
|
|
|
1996
1996
|
gameOver = true;
|
|
1997
1997
|
level++;
|
|
1998
1998
|
if (score > highScore) highScore = score;
|
|
1999
|
-
|
|
1999
|
+
triggerShake3(12, 2);
|
|
2000
2000
|
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);
|
|
2001
2001
|
}
|
|
2002
2002
|
}
|
|
@@ -2005,11 +2005,11 @@ function runBreakoutGame(terminal) {
|
|
|
2005
2005
|
if (shakeFrames > 0) shakeFrames--;
|
|
2006
2006
|
if (borderFlash > 0) borderFlash--;
|
|
2007
2007
|
const cols = terminal.cols, rows = terminal.rows;
|
|
2008
|
-
if (cols <
|
|
2008
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
2009
2009
|
const msg1 = "Terminal too small!";
|
|
2010
|
-
const needW = cols <
|
|
2010
|
+
const needW = cols < MIN_COLS2, needH = rows < MIN_ROWS2;
|
|
2011
2011
|
const hint2 = needW && needH ? "Make pane larger" : needW ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
2012
|
-
const msg2 = `Need: ${
|
|
2012
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
2013
2013
|
const cX = Math.floor(cols / 2), cY = Math.floor(rows / 2);
|
|
2014
2014
|
output += `\x1B[${cY - 1};${Math.max(1, cX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
2015
2015
|
output += `\x1B[${cY + 1};${Math.max(1, cX - Math.floor(msg2.length / 2))}H\x1B[2m${msg2}\x1B[0m`;
|
|
@@ -2571,8 +2571,8 @@ var ROPE_RETRACT_SPEED = 0.15;
|
|
|
2571
2571
|
var ROPE_FAST_RETRACT_SPEED = 0.3;
|
|
2572
2572
|
function runCourierGame(terminal) {
|
|
2573
2573
|
const themeColor = getCurrentThemeColor();
|
|
2574
|
-
const
|
|
2575
|
-
const
|
|
2574
|
+
const MIN_COLS2 = 40;
|
|
2575
|
+
const MIN_ROWS2 = 16;
|
|
2576
2576
|
let running = true;
|
|
2577
2577
|
let gameStarted = false;
|
|
2578
2578
|
let gameOver = false;
|
|
@@ -3201,8 +3201,8 @@ function runCourierGame(terminal) {
|
|
|
3201
3201
|
output += "\x1B[2J\x1B[H";
|
|
3202
3202
|
const cols = terminal.cols;
|
|
3203
3203
|
const rows = terminal.rows;
|
|
3204
|
-
if (cols <
|
|
3205
|
-
const msg = `Need ${
|
|
3204
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
3205
|
+
const msg = `Need ${MIN_COLS2}\xD7${MIN_ROWS2}, have ${cols}\xD7${rows}`;
|
|
3206
3206
|
const centerX = Math.floor(cols / 2);
|
|
3207
3207
|
const centerY = Math.floor(rows / 2);
|
|
3208
3208
|
output += `\x1B[${centerY};${Math.max(1, centerX - msg.length / 2)}H${themeColor}${msg}\x1B[0m`;
|
|
@@ -3961,8 +3961,8 @@ var LOG_MESSAGES = {
|
|
|
3961
3961
|
};
|
|
3962
3962
|
function runCrackGame(terminal) {
|
|
3963
3963
|
const themeColor = getCurrentThemeColor();
|
|
3964
|
-
const
|
|
3965
|
-
const
|
|
3964
|
+
const MIN_COLS2 = 40;
|
|
3965
|
+
const MIN_ROWS2 = 16;
|
|
3966
3966
|
let cols = terminal.cols;
|
|
3967
3967
|
let rows = terminal.rows;
|
|
3968
3968
|
const updateDimensions = () => {
|
|
@@ -4174,9 +4174,9 @@ function runCrackGame(terminal) {
|
|
|
4174
4174
|
function render() {
|
|
4175
4175
|
let output = "";
|
|
4176
4176
|
output += "\x1B[2J\x1B[H";
|
|
4177
|
-
if (cols <
|
|
4177
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
4178
4178
|
const msg1 = "Terminal too small!";
|
|
4179
|
-
const msg2 = `Need: ${
|
|
4179
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
4180
4180
|
const centerX = Math.floor(cols / 2);
|
|
4181
4181
|
const centerY = Math.floor(rows / 2);
|
|
4182
4182
|
output += `\x1B[${centerY};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -4629,8 +4629,8 @@ function runCrackGame(terminal) {
|
|
|
4629
4629
|
// src/games/frogger/index.ts
|
|
4630
4630
|
function runFroggerGame(terminal) {
|
|
4631
4631
|
const themeColor = getCurrentThemeColor();
|
|
4632
|
-
const
|
|
4633
|
-
const
|
|
4632
|
+
const MIN_COLS2 = 40;
|
|
4633
|
+
const MIN_ROWS2 = 16;
|
|
4634
4634
|
const LILY_PAD_COUNT = 5;
|
|
4635
4635
|
const TIME_LIMIT = 45;
|
|
4636
4636
|
const MOVE_COOLDOWN = 100;
|
|
@@ -5015,10 +5015,10 @@ function runFroggerGame(terminal) {
|
|
|
5015
5015
|
output += "\x1B[2J\x1B[H";
|
|
5016
5016
|
const cols = terminal.cols;
|
|
5017
5017
|
const rows = terminal.rows;
|
|
5018
|
-
if (cols <
|
|
5018
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
5019
5019
|
const msg1 = "Terminal too small!";
|
|
5020
|
-
const msg2 = `Need: ${
|
|
5021
|
-
const hint2 = cols <
|
|
5020
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
5021
|
+
const hint2 = cols < MIN_COLS2 && rows < MIN_ROWS2 ? "Make pane larger" : cols < MIN_COLS2 ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
5022
5022
|
const cx = Math.floor(cols / 2);
|
|
5023
5023
|
const cy = Math.floor(rows / 2);
|
|
5024
5024
|
output += `\x1B[${cy - 1};${Math.max(1, cx - msg1.length / 2)}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -5456,8 +5456,8 @@ var HANGMAN_STAGES = [
|
|
|
5456
5456
|
function runHangmanGame(terminal) {
|
|
5457
5457
|
const themeColor = getCurrentThemeColor();
|
|
5458
5458
|
const GAME_WIDTH = 40;
|
|
5459
|
-
const
|
|
5460
|
-
const
|
|
5459
|
+
const MIN_COLS2 = 32;
|
|
5460
|
+
const MIN_ROWS2 = 16;
|
|
5461
5461
|
let running = true;
|
|
5462
5462
|
let gameStarted = false;
|
|
5463
5463
|
let gameOver = false;
|
|
@@ -5621,10 +5621,10 @@ function runHangmanGame(terminal) {
|
|
|
5621
5621
|
}
|
|
5622
5622
|
const cols = terminal.cols;
|
|
5623
5623
|
const rows = terminal.rows;
|
|
5624
|
-
if (cols <
|
|
5624
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
5625
5625
|
const msg1 = "Terminal too small!";
|
|
5626
|
-
const needWidth = cols <
|
|
5627
|
-
const needHeight = rows <
|
|
5626
|
+
const needWidth = cols < MIN_COLS2;
|
|
5627
|
+
const needHeight = rows < MIN_ROWS2;
|
|
5628
5628
|
let hint2 = "";
|
|
5629
5629
|
if (needWidth && needHeight) {
|
|
5630
5630
|
hint2 = "Make pane larger";
|
|
@@ -5633,7 +5633,7 @@ function runHangmanGame(terminal) {
|
|
|
5633
5633
|
} else {
|
|
5634
5634
|
hint2 = "Make pane taller \u2193";
|
|
5635
5635
|
}
|
|
5636
|
-
const msg2 = `Need: ${
|
|
5636
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
5637
5637
|
const centerX = Math.floor(cols / 2);
|
|
5638
5638
|
const centerY = Math.floor(rows / 2);
|
|
5639
5639
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -5988,7 +5988,7 @@ function runMinesweeperGame(terminal) {
|
|
|
5988
5988
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
5989
5989
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
5990
5990
|
}
|
|
5991
|
-
function
|
|
5991
|
+
function triggerShake3(frames, intensity) {
|
|
5992
5992
|
shakeFrames = frames;
|
|
5993
5993
|
shakeIntensity = intensity;
|
|
5994
5994
|
}
|
|
@@ -6075,7 +6075,7 @@ function runMinesweeperGame(terminal) {
|
|
|
6075
6075
|
if (cell.isMine) {
|
|
6076
6076
|
gameOver = true;
|
|
6077
6077
|
won = false;
|
|
6078
|
-
|
|
6078
|
+
triggerShake3(25, 4);
|
|
6079
6079
|
spawnExplosion(x * 2 + 1, y);
|
|
6080
6080
|
addScorePopup2(x * 2, y - 1, "MALWARE!", "\x1B[1;91m");
|
|
6081
6081
|
revealAllMines();
|
|
@@ -6124,7 +6124,7 @@ function runMinesweeperGame(terminal) {
|
|
|
6124
6124
|
if (cellsRevealed >= totalNonMines) {
|
|
6125
6125
|
gameOver = true;
|
|
6126
6126
|
won = true;
|
|
6127
|
-
|
|
6127
|
+
triggerShake3(10, 2);
|
|
6128
6128
|
for (let i = 0; i < 5; i++) {
|
|
6129
6129
|
const x = Math.floor(Math.random() * difficulty.width);
|
|
6130
6130
|
const y = Math.floor(Math.random() * difficulty.height);
|
|
@@ -6522,8 +6522,8 @@ function runMinesweeperGame(terminal) {
|
|
|
6522
6522
|
// src/games/pong/index.ts
|
|
6523
6523
|
function runPongGame(terminal) {
|
|
6524
6524
|
const themeColor = getCurrentThemeColor();
|
|
6525
|
-
const
|
|
6526
|
-
const
|
|
6525
|
+
const MIN_COLS2 = 40;
|
|
6526
|
+
const MIN_ROWS2 = 16;
|
|
6527
6527
|
const getGameDimensions = () => {
|
|
6528
6528
|
const cols = terminal.cols;
|
|
6529
6529
|
const rows = terminal.rows;
|
|
@@ -6663,10 +6663,10 @@ function runPongGame(terminal) {
|
|
|
6663
6663
|
}
|
|
6664
6664
|
const cols = terminal.cols;
|
|
6665
6665
|
const rows = terminal.rows;
|
|
6666
|
-
if (cols <
|
|
6666
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
6667
6667
|
const msg1 = "Terminal too small!";
|
|
6668
|
-
const needWidth = cols <
|
|
6669
|
-
const needHeight = rows <
|
|
6668
|
+
const needWidth = cols < MIN_COLS2;
|
|
6669
|
+
const needHeight = rows < MIN_ROWS2;
|
|
6670
6670
|
let hint2 = "";
|
|
6671
6671
|
if (needWidth && needHeight) {
|
|
6672
6672
|
hint2 = "Make pane larger";
|
|
@@ -6675,7 +6675,7 @@ function runPongGame(terminal) {
|
|
|
6675
6675
|
} else {
|
|
6676
6676
|
hint2 = "Make pane taller \u2193";
|
|
6677
6677
|
}
|
|
6678
|
-
const msg2 = `Need: ${
|
|
6678
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
6679
6679
|
const centerX2 = Math.floor(cols / 2);
|
|
6680
6680
|
const centerY = Math.floor(rows / 2);
|
|
6681
6681
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -7111,8 +7111,8 @@ function runRunnerGame(terminal) {
|
|
|
7111
7111
|
const GAME_WIDTH = ROAD_WIDTH_BOTTOM + SCENERY_WIDTH * 2;
|
|
7112
7112
|
let gameTop = 5;
|
|
7113
7113
|
let gameLeft = 4;
|
|
7114
|
-
const
|
|
7115
|
-
const
|
|
7114
|
+
const MIN_COLS2 = GAME_WIDTH + 2;
|
|
7115
|
+
const MIN_ROWS2 = TRACK_HEIGHT + 8;
|
|
7116
7116
|
let running = true;
|
|
7117
7117
|
let gameStarted = false;
|
|
7118
7118
|
let gameOver = false;
|
|
@@ -7343,12 +7343,12 @@ function runRunnerGame(terminal) {
|
|
|
7343
7343
|
}
|
|
7344
7344
|
const cols = terminal.cols;
|
|
7345
7345
|
const rows = terminal.rows;
|
|
7346
|
-
if (cols <
|
|
7346
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
7347
7347
|
const msg1 = "Terminal too small!";
|
|
7348
|
-
const needWidth = cols <
|
|
7349
|
-
const needHeight = rows <
|
|
7348
|
+
const needWidth = cols < MIN_COLS2;
|
|
7349
|
+
const needHeight = rows < MIN_ROWS2;
|
|
7350
7350
|
let hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
7351
|
-
const msg2 = `Need: ${
|
|
7351
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
7352
7352
|
const centerX2 = Math.floor(cols / 2);
|
|
7353
7353
|
const centerY = Math.floor(rows / 2);
|
|
7354
7354
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -7875,8 +7875,8 @@ function runRunnerGame(terminal) {
|
|
|
7875
7875
|
// src/games/simon/index.ts
|
|
7876
7876
|
function runSimonGame(terminal) {
|
|
7877
7877
|
const themeColor = getCurrentThemeColor();
|
|
7878
|
-
const
|
|
7879
|
-
const
|
|
7878
|
+
const MIN_COLS2 = 40;
|
|
7879
|
+
const MIN_ROWS2 = 18;
|
|
7880
7880
|
const GAME_WIDTH = 40;
|
|
7881
7881
|
const GAME_HEIGHT = 18;
|
|
7882
7882
|
const QUADRANT_COLORS = [
|
|
@@ -7990,7 +7990,7 @@ function runSimonGame(terminal) {
|
|
|
7990
7990
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
7991
7991
|
scorePopups.push({ x, y, text, frames: 20, color });
|
|
7992
7992
|
}
|
|
7993
|
-
function
|
|
7993
|
+
function triggerShake3(frames, intensity) {
|
|
7994
7994
|
shakeFrames = frames;
|
|
7995
7995
|
shakeIntensity = intensity;
|
|
7996
7996
|
}
|
|
@@ -8052,7 +8052,7 @@ function runSimonGame(terminal) {
|
|
|
8052
8052
|
statusMessage = "PATTERN ACCEPTED";
|
|
8053
8053
|
statusColor = "\x1B[1;92m";
|
|
8054
8054
|
statusBlink = true;
|
|
8055
|
-
|
|
8055
|
+
triggerShake3(6, 1);
|
|
8056
8056
|
}
|
|
8057
8057
|
} else {
|
|
8058
8058
|
phase = "failure";
|
|
@@ -8064,7 +8064,7 @@ function runSimonGame(terminal) {
|
|
|
8064
8064
|
statusMessage = "ACCESS DENIED";
|
|
8065
8065
|
statusColor = "\x1B[1;91m";
|
|
8066
8066
|
statusBlink = true;
|
|
8067
|
-
|
|
8067
|
+
triggerShake3(15, 3);
|
|
8068
8068
|
const centerX = GAME_WIDTH / 2;
|
|
8069
8069
|
const centerY = GAME_HEIGHT / 2;
|
|
8070
8070
|
spawnParticles3(centerX, centerY, 15, "\x1B[1;91m", ["\u2717", "\u2716", "\xD7", "\u2573"]);
|
|
@@ -8190,12 +8190,12 @@ function runSimonGame(terminal) {
|
|
|
8190
8190
|
if (shakeFrames > 0) shakeFrames--;
|
|
8191
8191
|
const cols = terminal.cols;
|
|
8192
8192
|
const rows = terminal.rows;
|
|
8193
|
-
if (cols <
|
|
8193
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
8194
8194
|
const msg1 = "Terminal too small!";
|
|
8195
|
-
const needWidth = cols <
|
|
8196
|
-
const needHeight = rows <
|
|
8195
|
+
const needWidth = cols < MIN_COLS2;
|
|
8196
|
+
const needHeight = rows < MIN_ROWS2;
|
|
8197
8197
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
8198
|
-
const msg2 = `Need: ${
|
|
8198
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
8199
8199
|
const centerX = Math.floor(cols / 2);
|
|
8200
8200
|
const centerY = Math.floor(rows / 2);
|
|
8201
8201
|
outputParts.push(`\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`);
|
|
@@ -8457,8 +8457,8 @@ function runSimonGame(terminal) {
|
|
|
8457
8457
|
// src/games/snake/index.ts
|
|
8458
8458
|
function runSnakeGame(terminal) {
|
|
8459
8459
|
const themeColor = getCurrentThemeColor();
|
|
8460
|
-
const
|
|
8461
|
-
const
|
|
8460
|
+
const MIN_COLS2 = 36;
|
|
8461
|
+
const MIN_ROWS2 = 16;
|
|
8462
8462
|
let cols = terminal.cols;
|
|
8463
8463
|
let rows = terminal.rows;
|
|
8464
8464
|
let gameTop = 6;
|
|
@@ -8561,7 +8561,7 @@ function runSnakeGame(terminal) {
|
|
|
8561
8561
|
});
|
|
8562
8562
|
}
|
|
8563
8563
|
}
|
|
8564
|
-
function
|
|
8564
|
+
function updateParticles4() {
|
|
8565
8565
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
8566
8566
|
const p = particles[i];
|
|
8567
8567
|
p.x += p.vx;
|
|
@@ -8586,7 +8586,7 @@ function runSnakeGame(terminal) {
|
|
|
8586
8586
|
scorePopup.y -= 0.3;
|
|
8587
8587
|
if (scorePopup.frames <= 0) scorePopup = null;
|
|
8588
8588
|
}
|
|
8589
|
-
|
|
8589
|
+
updateParticles4();
|
|
8590
8590
|
let renderGameLeft = gameLeft;
|
|
8591
8591
|
let renderGameTop = gameTop;
|
|
8592
8592
|
if (shakeFrames > 0) {
|
|
@@ -8595,10 +8595,10 @@ function runSnakeGame(terminal) {
|
|
|
8595
8595
|
renderGameLeft = Math.max(1, gameLeft + shakeX);
|
|
8596
8596
|
renderGameTop = Math.max(3, gameTop + shakeY);
|
|
8597
8597
|
}
|
|
8598
|
-
if (cols <
|
|
8598
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
8599
8599
|
const msg1 = "Terminal too small!";
|
|
8600
|
-
const needWidth = cols <
|
|
8601
|
-
const needHeight = rows <
|
|
8600
|
+
const needWidth = cols < MIN_COLS2;
|
|
8601
|
+
const needHeight = rows < MIN_ROWS2;
|
|
8602
8602
|
let hint2 = "";
|
|
8603
8603
|
if (needWidth && needHeight) {
|
|
8604
8604
|
hint2 = "Make pane larger";
|
|
@@ -8607,7 +8607,7 @@ function runSnakeGame(terminal) {
|
|
|
8607
8607
|
} else {
|
|
8608
8608
|
hint2 = "Make pane taller \u2193";
|
|
8609
8609
|
}
|
|
8610
|
-
const msg2 = `Need: ${
|
|
8610
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
8611
8611
|
const centerX = Math.floor(cols / 2);
|
|
8612
8612
|
const centerY = Math.floor(rows / 2);
|
|
8613
8613
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -8929,8 +8929,8 @@ function runSnakeGame(terminal) {
|
|
|
8929
8929
|
// src/games/spaceinvaders/index.ts
|
|
8930
8930
|
function runSpaceInvadersGame(terminal) {
|
|
8931
8931
|
const themeColor = getCurrentThemeColor();
|
|
8932
|
-
const
|
|
8933
|
-
const
|
|
8932
|
+
const MIN_COLS2 = 40;
|
|
8933
|
+
const MIN_ROWS2 = 16;
|
|
8934
8934
|
const getGameDimensions = () => {
|
|
8935
8935
|
const cols = terminal.cols;
|
|
8936
8936
|
const rows = terminal.rows;
|
|
@@ -9108,10 +9108,10 @@ function runSpaceInvadersGame(terminal) {
|
|
|
9108
9108
|
}
|
|
9109
9109
|
const cols = terminal.cols;
|
|
9110
9110
|
const rows = terminal.rows;
|
|
9111
|
-
if (cols <
|
|
9111
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
9112
9112
|
const msg1 = "Terminal too small!";
|
|
9113
|
-
const needWidth = cols <
|
|
9114
|
-
const needHeight = rows <
|
|
9113
|
+
const needWidth = cols < MIN_COLS2;
|
|
9114
|
+
const needHeight = rows < MIN_ROWS2;
|
|
9115
9115
|
let hint2 = "";
|
|
9116
9116
|
if (needWidth && needHeight) {
|
|
9117
9117
|
hint2 = "Make pane larger";
|
|
@@ -9120,7 +9120,7 @@ function runSpaceInvadersGame(terminal) {
|
|
|
9120
9120
|
} else {
|
|
9121
9121
|
hint2 = "Make pane taller \u2193";
|
|
9122
9122
|
}
|
|
9123
|
-
const msg2 = `Need: ${
|
|
9123
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
9124
9124
|
const centerX = Math.floor(cols / 2);
|
|
9125
9125
|
const centerY = Math.floor(rows / 2);
|
|
9126
9126
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -9593,13 +9593,13 @@ function runTetrisGame(terminal) {
|
|
|
9593
9593
|
const themeColor = getCurrentThemeColor();
|
|
9594
9594
|
const BOARD_WIDTH = 12;
|
|
9595
9595
|
const BOARD_HEIGHT = 20;
|
|
9596
|
-
const
|
|
9596
|
+
const SIDE_PANEL_WIDTH2 = 14;
|
|
9597
9597
|
const BOARD_ONLY_WIDTH = BOARD_WIDTH * 2 + 2;
|
|
9598
|
-
const TOTAL_WIDTH = BOARD_WIDTH * 2 +
|
|
9599
|
-
const
|
|
9598
|
+
const TOTAL_WIDTH = BOARD_WIDTH * 2 + SIDE_PANEL_WIDTH2 + 4;
|
|
9599
|
+
const TICK_MS2 = 25;
|
|
9600
9600
|
const SPRINT_TARGET_LINES = 40;
|
|
9601
|
-
const
|
|
9602
|
-
const
|
|
9601
|
+
const MIN_COLS2 = BOARD_ONLY_WIDTH + 2;
|
|
9602
|
+
const MIN_ROWS2 = BOARD_HEIGHT + 3;
|
|
9603
9603
|
const MIN_ROWS_WITH_TITLE = BOARD_HEIGHT + 4;
|
|
9604
9604
|
let running = true;
|
|
9605
9605
|
let gameStarted = false;
|
|
@@ -9682,7 +9682,7 @@ function runTetrisGame(terminal) {
|
|
|
9682
9682
|
const shape = TETROMINOES[currentPiece][0];
|
|
9683
9683
|
pieceX = Math.floor((BOARD_WIDTH - shape[0].length) / 2);
|
|
9684
9684
|
pieceY = 0;
|
|
9685
|
-
if (!
|
|
9685
|
+
if (!isValidPosition2(pieceX, pieceY, currentRotation)) {
|
|
9686
9686
|
gameOver = true;
|
|
9687
9687
|
if (score > highScore) highScore = score;
|
|
9688
9688
|
}
|
|
@@ -9691,7 +9691,7 @@ function runTetrisGame(terminal) {
|
|
|
9691
9691
|
const rotations = TETROMINOES[currentPiece];
|
|
9692
9692
|
return rotations[rotation % rotations.length];
|
|
9693
9693
|
}
|
|
9694
|
-
function
|
|
9694
|
+
function isValidPosition2(x, y, rotation) {
|
|
9695
9695
|
const shape = TETROMINOES[currentPiece][rotation % TETROMINOES[currentPiece].length];
|
|
9696
9696
|
for (let row = 0; row < shape.length; row++) {
|
|
9697
9697
|
for (let col = 0; col < shape[row].length; col++) {
|
|
@@ -9808,23 +9808,23 @@ function runTetrisGame(terminal) {
|
|
|
9808
9808
|
}
|
|
9809
9809
|
}
|
|
9810
9810
|
function moveLeft() {
|
|
9811
|
-
if (
|
|
9811
|
+
if (isValidPosition2(pieceX - 1, pieceY, currentRotation)) {
|
|
9812
9812
|
pieceX--;
|
|
9813
9813
|
}
|
|
9814
9814
|
}
|
|
9815
9815
|
function moveRight() {
|
|
9816
|
-
if (
|
|
9816
|
+
if (isValidPosition2(pieceX + 1, pieceY, currentRotation)) {
|
|
9817
9817
|
pieceX++;
|
|
9818
9818
|
}
|
|
9819
9819
|
}
|
|
9820
9820
|
function moveDown() {
|
|
9821
|
-
if (
|
|
9821
|
+
if (isValidPosition2(pieceX, pieceY + 1, currentRotation)) {
|
|
9822
9822
|
pieceY++;
|
|
9823
9823
|
return true;
|
|
9824
9824
|
}
|
|
9825
9825
|
return false;
|
|
9826
9826
|
}
|
|
9827
|
-
function
|
|
9827
|
+
function hardDrop2() {
|
|
9828
9828
|
if (hardDropping) return;
|
|
9829
9829
|
if (hardDropConsumed) return;
|
|
9830
9830
|
hardDropping = true;
|
|
@@ -9832,13 +9832,13 @@ function runTetrisGame(terminal) {
|
|
|
9832
9832
|
}
|
|
9833
9833
|
function rotate() {
|
|
9834
9834
|
const newRotation = (currentRotation + 1) % TETROMINOES[currentPiece].length;
|
|
9835
|
-
if (
|
|
9835
|
+
if (isValidPosition2(pieceX, pieceY, newRotation)) {
|
|
9836
9836
|
currentRotation = newRotation;
|
|
9837
9837
|
return;
|
|
9838
9838
|
}
|
|
9839
9839
|
const kicks = [-1, 1, -2, 2];
|
|
9840
9840
|
for (const kick of kicks) {
|
|
9841
|
-
if (
|
|
9841
|
+
if (isValidPosition2(pieceX + kick, pieceY, newRotation)) {
|
|
9842
9842
|
pieceX += kick;
|
|
9843
9843
|
currentRotation = newRotation;
|
|
9844
9844
|
return;
|
|
@@ -9850,10 +9850,10 @@ function runTetrisGame(terminal) {
|
|
|
9850
9850
|
output += "\x1B[2J\x1B[H";
|
|
9851
9851
|
const cols = terminal.cols;
|
|
9852
9852
|
const rows = terminal.rows;
|
|
9853
|
-
if (cols <
|
|
9853
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
9854
9854
|
const msg1 = "Terminal too small!";
|
|
9855
|
-
const needWidth = cols <
|
|
9856
|
-
const needHeight = rows <
|
|
9855
|
+
const needWidth = cols < MIN_COLS2;
|
|
9856
|
+
const needHeight = rows < MIN_ROWS2;
|
|
9857
9857
|
let hint = "";
|
|
9858
9858
|
if (needWidth && needHeight) {
|
|
9859
9859
|
hint = "Make pane larger";
|
|
@@ -9862,7 +9862,7 @@ function runTetrisGame(terminal) {
|
|
|
9862
9862
|
} else {
|
|
9863
9863
|
hint = "Make pane taller \u2193";
|
|
9864
9864
|
}
|
|
9865
|
-
const msg2 = `Need: ${
|
|
9865
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
9866
9866
|
const centerX = Math.floor(cols / 2);
|
|
9867
9867
|
const centerY = Math.floor(rows / 2);
|
|
9868
9868
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -9996,7 +9996,7 @@ function runTetrisGame(terminal) {
|
|
|
9996
9996
|
}
|
|
9997
9997
|
}
|
|
9998
9998
|
let ghostY = pieceY;
|
|
9999
|
-
while (
|
|
9999
|
+
while (isValidPosition2(pieceX, ghostY + 1, currentRotation)) {
|
|
10000
10000
|
ghostY++;
|
|
10001
10001
|
}
|
|
10002
10002
|
if (ghostY !== pieceY) {
|
|
@@ -10096,7 +10096,7 @@ function runTetrisGame(terminal) {
|
|
|
10096
10096
|
return;
|
|
10097
10097
|
}
|
|
10098
10098
|
dropCounter++;
|
|
10099
|
-
if (dropCounter *
|
|
10099
|
+
if (dropCounter * TICK_MS2 >= dropInterval) {
|
|
10100
10100
|
dropCounter = 0;
|
|
10101
10101
|
if (!moveDown()) {
|
|
10102
10102
|
lockPiece();
|
|
@@ -10127,14 +10127,14 @@ function runTetrisGame(terminal) {
|
|
|
10127
10127
|
return;
|
|
10128
10128
|
}
|
|
10129
10129
|
render();
|
|
10130
|
-
},
|
|
10130
|
+
}, TICK_MS2);
|
|
10131
10131
|
const gameInterval = setInterval(() => {
|
|
10132
10132
|
if (!running) {
|
|
10133
10133
|
clearInterval(gameInterval);
|
|
10134
10134
|
return;
|
|
10135
10135
|
}
|
|
10136
10136
|
update();
|
|
10137
|
-
},
|
|
10137
|
+
}, TICK_MS2);
|
|
10138
10138
|
const handleKeyUp = (e) => {
|
|
10139
10139
|
if (e.key === " ") {
|
|
10140
10140
|
hardDropConsumed = false;
|
|
@@ -10271,7 +10271,7 @@ function runTetrisGame(terminal) {
|
|
|
10271
10271
|
rotate();
|
|
10272
10272
|
break;
|
|
10273
10273
|
case " ":
|
|
10274
|
-
|
|
10274
|
+
hardDrop2();
|
|
10275
10275
|
break;
|
|
10276
10276
|
}
|
|
10277
10277
|
});
|
|
@@ -10306,8 +10306,8 @@ var BLOCK_COLORS = [
|
|
|
10306
10306
|
];
|
|
10307
10307
|
function runTowerGame(terminal) {
|
|
10308
10308
|
const themeColor = getCurrentThemeColor();
|
|
10309
|
-
const
|
|
10310
|
-
const
|
|
10309
|
+
const MIN_COLS2 = 30;
|
|
10310
|
+
const MIN_ROWS2 = 16;
|
|
10311
10311
|
const INITIAL_BLOCK_WIDTH = 8;
|
|
10312
10312
|
const MIN_BLOCK_WIDTH = 2;
|
|
10313
10313
|
const PERFECT_THRESHOLD = 0.5;
|
|
@@ -10386,7 +10386,7 @@ function runTowerGame(terminal) {
|
|
|
10386
10386
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
10387
10387
|
scorePopups.push({ x, y, text, frames: 24, color });
|
|
10388
10388
|
}
|
|
10389
|
-
function
|
|
10389
|
+
function triggerShake3(frames, intensity) {
|
|
10390
10390
|
shakeFrames = frames;
|
|
10391
10391
|
shakeIntensity = intensity;
|
|
10392
10392
|
}
|
|
@@ -10447,7 +10447,7 @@ function runTowerGame(terminal) {
|
|
|
10447
10447
|
if (score > highScore) highScore = score;
|
|
10448
10448
|
if (perfectCombo > maxCombo) maxCombo = perfectCombo;
|
|
10449
10449
|
spawnParticles3(dropX + dropWidth / 2, landY, 15, "\x1B[91m");
|
|
10450
|
-
|
|
10450
|
+
triggerShake3(10, 3);
|
|
10451
10451
|
return;
|
|
10452
10452
|
}
|
|
10453
10453
|
const isPerfect = Math.abs(dropLeft - topLeft) < PERFECT_THRESHOLD && Math.abs(dropRight - topRight) < PERFECT_THRESHOLD;
|
|
@@ -10465,7 +10465,7 @@ function runTowerGame(terminal) {
|
|
|
10465
10465
|
popupText,
|
|
10466
10466
|
perfectCombo > 3 ? "\x1B[1;91m" : "\x1B[1;93m"
|
|
10467
10467
|
);
|
|
10468
|
-
|
|
10468
|
+
triggerShake3(4, 1);
|
|
10469
10469
|
} else {
|
|
10470
10470
|
if (perfectCombo > maxCombo) maxCombo = perfectCombo;
|
|
10471
10471
|
perfectCombo = 0;
|
|
@@ -10479,7 +10479,7 @@ function runTowerGame(terminal) {
|
|
|
10479
10479
|
spawnFallingPiece(topRight, landY, overhangWidth, dropColor);
|
|
10480
10480
|
spawnParticles3(topRight + overhangWidth / 2, landY, 5, dropColor);
|
|
10481
10481
|
}
|
|
10482
|
-
|
|
10482
|
+
triggerShake3(3, 1);
|
|
10483
10483
|
}
|
|
10484
10484
|
score += points;
|
|
10485
10485
|
height++;
|
|
@@ -10547,12 +10547,12 @@ function runTowerGame(terminal) {
|
|
|
10547
10547
|
if (shakeFrames > 0) shakeFrames--;
|
|
10548
10548
|
const cols = terminal.cols;
|
|
10549
10549
|
const rows = terminal.rows;
|
|
10550
|
-
if (cols <
|
|
10550
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
10551
10551
|
const msg1 = "Terminal too small!";
|
|
10552
|
-
const needWidth = cols <
|
|
10553
|
-
const needHeight = rows <
|
|
10552
|
+
const needWidth = cols < MIN_COLS2;
|
|
10553
|
+
const needHeight = rows < MIN_ROWS2;
|
|
10554
10554
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
10555
|
-
const msg2 = `Need: ${
|
|
10555
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
10556
10556
|
const centerX = Math.floor(cols / 2);
|
|
10557
10557
|
const centerY = Math.floor(rows / 2);
|
|
10558
10558
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -10797,8 +10797,8 @@ function runTowerGame(terminal) {
|
|
|
10797
10797
|
// src/games/tron/index.ts
|
|
10798
10798
|
function runTronGame(terminal) {
|
|
10799
10799
|
const themeColor = getCurrentThemeColor();
|
|
10800
|
-
const
|
|
10801
|
-
const
|
|
10800
|
+
const MIN_COLS2 = 40;
|
|
10801
|
+
const MIN_ROWS2 = 18;
|
|
10802
10802
|
const GAME_WIDTH = 46;
|
|
10803
10803
|
const GAME_HEIGHT = 18;
|
|
10804
10804
|
const ROUNDS_TO_WIN = 3;
|
|
@@ -10867,9 +10867,9 @@ function runTronGame(terminal) {
|
|
|
10867
10867
|
}
|
|
10868
10868
|
function spawnExplosion(x, y, color) {
|
|
10869
10869
|
spawnParticles3(x, y, 20, color, ["\u2717", "\xD7", "\u2591", "\u2592", "\u2593", "\u2588"]);
|
|
10870
|
-
|
|
10870
|
+
triggerShake3(12, 3);
|
|
10871
10871
|
}
|
|
10872
|
-
function
|
|
10872
|
+
function triggerShake3(frames, intensity) {
|
|
10873
10873
|
shakeFrames = frames;
|
|
10874
10874
|
shakeIntensity = intensity;
|
|
10875
10875
|
}
|
|
@@ -11084,7 +11084,7 @@ function runTronGame(terminal) {
|
|
|
11084
11084
|
arenaMinY += SHRINK_AMOUNT;
|
|
11085
11085
|
arenaMaxY -= SHRINK_AMOUNT;
|
|
11086
11086
|
}
|
|
11087
|
-
|
|
11087
|
+
triggerShake3(4, 1);
|
|
11088
11088
|
}
|
|
11089
11089
|
}
|
|
11090
11090
|
}
|
|
@@ -11151,12 +11151,12 @@ function runTronGame(terminal) {
|
|
|
11151
11151
|
if (shakeFrames > 0) shakeFrames--;
|
|
11152
11152
|
const cols = terminal.cols;
|
|
11153
11153
|
const rows = terminal.rows;
|
|
11154
|
-
if (cols <
|
|
11154
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
11155
11155
|
const msg1 = "Terminal too small!";
|
|
11156
|
-
const needWidth = cols <
|
|
11157
|
-
const needHeight = rows <
|
|
11156
|
+
const needWidth = cols < MIN_COLS2;
|
|
11157
|
+
const needHeight = rows < MIN_ROWS2;
|
|
11158
11158
|
const hint2 = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider \u2192" : "Make pane taller \u2193";
|
|
11159
|
-
const msg2 = `Need: ${
|
|
11159
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
11160
11160
|
const centerX = Math.floor(cols / 2);
|
|
11161
11161
|
const centerY = Math.floor(rows / 2);
|
|
11162
11162
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -11245,7 +11245,7 @@ function runTronGame(terminal) {
|
|
|
11245
11245
|
const nextMsg = "Press SPACE for next round";
|
|
11246
11246
|
const nextX = gameLeft + Math.floor((GAME_WIDTH - nextMsg.length) / 2) + 1;
|
|
11247
11247
|
output += `\x1B[${msgY + 2};${nextX}H\x1B[2m${themeColor}${nextMsg}\x1B[0m`;
|
|
11248
|
-
output =
|
|
11248
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11249
11249
|
} else if (gameOver) {
|
|
11250
11250
|
output = renderTrailsAndCycles(output, renderLeft, renderTop);
|
|
11251
11251
|
const overColor = playerWins >= ROUNDS_TO_WIN ? "\x1B[1;92m" : "\x1B[1;91m";
|
|
@@ -11259,7 +11259,7 @@ function runTronGame(terminal) {
|
|
|
11259
11259
|
const restart = "\u255A [R] RESTART [Q] QUIT \u255D";
|
|
11260
11260
|
const restartX = gameLeft + Math.floor((GAME_WIDTH - restart.length) / 2) + 1;
|
|
11261
11261
|
output += `\x1B[${overY + 2};${restartX}H\x1B[2m${themeColor}${restart}\x1B[0m`;
|
|
11262
|
-
output =
|
|
11262
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11263
11263
|
} else {
|
|
11264
11264
|
for (const pos of player.trail) {
|
|
11265
11265
|
if (pos.x > arenaMinX && pos.x < arenaMaxX && pos.y > arenaMinY && pos.y < arenaMaxY) {
|
|
@@ -11277,7 +11277,7 @@ function runTronGame(terminal) {
|
|
|
11277
11277
|
if (ai.alive) {
|
|
11278
11278
|
output += `\x1B[${renderTop + 1 + ai.y};${renderLeft + 1 + ai.x}H\x1B[1m${ai.color}${ai.char}\x1B[0m`;
|
|
11279
11279
|
}
|
|
11280
|
-
output =
|
|
11280
|
+
output = renderParticles2(output, renderLeft, renderTop);
|
|
11281
11281
|
}
|
|
11282
11282
|
const hint = gameStarted && !gameOver && !paused ? `SPEED: ${Math.round((1 - gameSpeed / BASE_SPEED) * 100) + 100}% [ ESC ] MENU` : "";
|
|
11283
11283
|
const hintX = Math.floor((cols - hint.length) / 2);
|
|
@@ -11299,7 +11299,7 @@ function runTronGame(terminal) {
|
|
|
11299
11299
|
}
|
|
11300
11300
|
return output;
|
|
11301
11301
|
}
|
|
11302
|
-
function
|
|
11302
|
+
function renderParticles2(output, renderLeft, renderTop) {
|
|
11303
11303
|
for (const p of particles) {
|
|
11304
11304
|
const screenX = Math.round(renderLeft + 1 + p.x);
|
|
11305
11305
|
const screenY = Math.round(renderTop + 1 + p.y);
|
|
@@ -11634,8 +11634,8 @@ var SENTENCES = [
|
|
|
11634
11634
|
function runTypingTest(terminal) {
|
|
11635
11635
|
const themeColor = getCurrentThemeColor();
|
|
11636
11636
|
const GAME_WIDTH = 60;
|
|
11637
|
-
const
|
|
11638
|
-
const
|
|
11637
|
+
const MIN_COLS2 = 40;
|
|
11638
|
+
const MIN_ROWS2 = 18;
|
|
11639
11639
|
let running = true;
|
|
11640
11640
|
let gameStarted = false;
|
|
11641
11641
|
let gameOver = false;
|
|
@@ -11834,10 +11834,10 @@ function runTypingTest(terminal) {
|
|
|
11834
11834
|
}
|
|
11835
11835
|
const cols = terminal.cols;
|
|
11836
11836
|
const rows = terminal.rows;
|
|
11837
|
-
if (cols <
|
|
11837
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
11838
11838
|
const msg1 = "Terminal too small!";
|
|
11839
|
-
const needWidth = cols <
|
|
11840
|
-
const needHeight = rows <
|
|
11839
|
+
const needWidth = cols < MIN_COLS2;
|
|
11840
|
+
const needHeight = rows < MIN_ROWS2;
|
|
11841
11841
|
let hint2 = "";
|
|
11842
11842
|
if (needWidth && needHeight) {
|
|
11843
11843
|
hint2 = "Make pane larger";
|
|
@@ -11846,7 +11846,7 @@ function runTypingTest(terminal) {
|
|
|
11846
11846
|
} else {
|
|
11847
11847
|
hint2 = "Make pane taller \u2193";
|
|
11848
11848
|
}
|
|
11849
|
-
const msg2 = `Need: ${
|
|
11849
|
+
const msg2 = `Need: ${MIN_COLS2}\xD7${MIN_ROWS2} Have: ${cols}\xD7${rows}`;
|
|
11850
11850
|
const centerX = Math.floor(cols / 2);
|
|
11851
11851
|
const centerY = Math.floor(rows / 2);
|
|
11852
11852
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -12491,8 +12491,8 @@ var WORDS2 = [
|
|
|
12491
12491
|
];
|
|
12492
12492
|
function runWordleGame(terminal) {
|
|
12493
12493
|
const themeColor = getCurrentThemeColor();
|
|
12494
|
-
const
|
|
12495
|
-
const
|
|
12494
|
+
const MIN_COLS2 = 36;
|
|
12495
|
+
const MIN_ROWS2 = 20;
|
|
12496
12496
|
const MAX_GUESSES = 6;
|
|
12497
12497
|
const WORD_LENGTH = 5;
|
|
12498
12498
|
const KEYBOARD_ROWS = [
|
|
@@ -12557,7 +12557,7 @@ function runWordleGame(terminal) {
|
|
|
12557
12557
|
});
|
|
12558
12558
|
}
|
|
12559
12559
|
}
|
|
12560
|
-
function
|
|
12560
|
+
function spawnFirework4(x, y) {
|
|
12561
12561
|
const colors = ["\x1B[1;92m", "\x1B[1;93m", "\x1B[1;96m", "\x1B[1;95m"];
|
|
12562
12562
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
12563
12563
|
const chars = ["*", "+", "o", ".", "`", "'"];
|
|
@@ -12578,7 +12578,7 @@ function runWordleGame(terminal) {
|
|
|
12578
12578
|
function addScorePopup2(x, y, text, color = "\x1B[1;33m") {
|
|
12579
12579
|
scorePopups.push({ x, y, text, frames: 25, color });
|
|
12580
12580
|
}
|
|
12581
|
-
function
|
|
12581
|
+
function triggerShake3(frames, intensity) {
|
|
12582
12582
|
shakeFrames = frames;
|
|
12583
12583
|
shakeIntensity = intensity;
|
|
12584
12584
|
}
|
|
@@ -12636,7 +12636,7 @@ function runWordleGame(terminal) {
|
|
|
12636
12636
|
}
|
|
12637
12637
|
function submitGuess() {
|
|
12638
12638
|
if (currentGuess.length !== WORD_LENGTH) {
|
|
12639
|
-
|
|
12639
|
+
triggerShake3(4, 2);
|
|
12640
12640
|
addScorePopup2(Math.floor(cols / 2), 8, "NOT ENOUGH LETTERS", "\x1B[1;91m");
|
|
12641
12641
|
return;
|
|
12642
12642
|
}
|
|
@@ -12662,7 +12662,7 @@ function runWordleGame(terminal) {
|
|
|
12662
12662
|
}
|
|
12663
12663
|
stats.guessDistribution[guesses.length - 1]++;
|
|
12664
12664
|
borderFlash = 20;
|
|
12665
|
-
|
|
12665
|
+
triggerShake3(10, 3);
|
|
12666
12666
|
const messages = ["CIPHER BREACHED!", "CODE CRACKED!", "DECRYPTED!", "BRILLIANT!"];
|
|
12667
12667
|
addScorePopup2(centerX, guessY - 2, messages[Math.floor(Math.random() * messages.length)], "\x1B[1;92m");
|
|
12668
12668
|
setTimeout(() => {
|
|
@@ -12670,7 +12670,7 @@ function runWordleGame(terminal) {
|
|
|
12670
12670
|
for (let i = 0; i < 5; i++) {
|
|
12671
12671
|
setTimeout(() => {
|
|
12672
12672
|
if (running && won) {
|
|
12673
|
-
|
|
12673
|
+
spawnFirework4(
|
|
12674
12674
|
10 + Math.random() * (cols - 20),
|
|
12675
12675
|
5 + Math.random() * 10
|
|
12676
12676
|
);
|
|
@@ -12683,7 +12683,7 @@ function runWordleGame(terminal) {
|
|
|
12683
12683
|
gameOver = true;
|
|
12684
12684
|
stats.gamesPlayed++;
|
|
12685
12685
|
stats.currentStreak = 0;
|
|
12686
|
-
|
|
12686
|
+
triggerShake3(8, 4);
|
|
12687
12687
|
borderFlash = 15;
|
|
12688
12688
|
addScorePopup2(centerX, guessY - 2, "DECRYPTION FAILED", "\x1B[1;91m");
|
|
12689
12689
|
spawnParticles3(centerX, guessY, 10, "\x1B[1;91m", ["X", "x", ".", "*"]);
|
|
@@ -12754,12 +12754,12 @@ function runWordleGame(terminal) {
|
|
|
12754
12754
|
rows = terminal.rows;
|
|
12755
12755
|
if (shakeFrames > 0) shakeFrames--;
|
|
12756
12756
|
if (borderFlash > 0) borderFlash--;
|
|
12757
|
-
if (cols <
|
|
12757
|
+
if (cols < MIN_COLS2 || rows < MIN_ROWS2) {
|
|
12758
12758
|
const msg1 = "Terminal too small!";
|
|
12759
|
-
const needWidth = cols <
|
|
12760
|
-
const needHeight = rows <
|
|
12759
|
+
const needWidth = cols < MIN_COLS2;
|
|
12760
|
+
const needHeight = rows < MIN_ROWS2;
|
|
12761
12761
|
let hint = needWidth && needHeight ? "Make pane larger" : needWidth ? "Make pane wider" : "Make pane taller";
|
|
12762
|
-
const msg2 = `Need: ${
|
|
12762
|
+
const msg2 = `Need: ${MIN_COLS2}x${MIN_ROWS2} Have: ${cols}x${rows}`;
|
|
12763
12763
|
const centerX2 = Math.floor(cols / 2);
|
|
12764
12764
|
const centerY = Math.floor(rows / 2);
|
|
12765
12765
|
output += `\x1B[${centerY - 1};${Math.max(1, centerX2 - Math.floor(msg1.length / 2))}H${themeColor}${msg1}\x1B[0m`;
|
|
@@ -13078,164 +13078,2653 @@ function runWordleGame(terminal) {
|
|
|
13078
13078
|
return controller;
|
|
13079
13079
|
}
|
|
13080
13080
|
|
|
13081
|
-
// src/games/
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
const
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13081
|
+
// src/games/hyper-fighter/engine.ts
|
|
13082
|
+
var BOARD_ROWS = 12;
|
|
13083
|
+
var BOARD_COLS = 6;
|
|
13084
|
+
var COLORS = ["red", "green", "blue", "yellow"];
|
|
13085
|
+
var CRASH_GEM_CHANCE = 0.25;
|
|
13086
|
+
var DROP_ALLEY_COL = 3;
|
|
13087
|
+
var DIAMOND_INTERVAL = 25;
|
|
13088
|
+
var nextPowerGemId = 1;
|
|
13089
|
+
function createBoard() {
|
|
13090
|
+
const board = [];
|
|
13091
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13092
|
+
board.push(new Array(BOARD_COLS).fill(null));
|
|
13093
|
+
}
|
|
13094
|
+
return board;
|
|
13095
|
+
}
|
|
13096
|
+
function randomColor() {
|
|
13097
|
+
return COLORS[Math.floor(Math.random() * COLORS.length)];
|
|
13098
|
+
}
|
|
13099
|
+
function randomGem() {
|
|
13100
|
+
const isCrash = Math.random() < CRASH_GEM_CHANCE;
|
|
13101
|
+
return {
|
|
13102
|
+
color: randomColor(),
|
|
13103
|
+
type: isCrash ? "crash" : "normal"
|
|
13098
13104
|
};
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
13105
|
+
}
|
|
13106
|
+
function generatePair() {
|
|
13107
|
+
return {
|
|
13108
|
+
primary: randomGem(),
|
|
13109
|
+
secondary: randomGem(),
|
|
13110
|
+
col: DROP_ALLEY_COL,
|
|
13111
|
+
row: 0,
|
|
13112
|
+
orientation: 0
|
|
13113
|
+
};
|
|
13114
|
+
}
|
|
13115
|
+
function getSecondaryPos(pair) {
|
|
13116
|
+
switch (pair.orientation) {
|
|
13117
|
+
case 0:
|
|
13118
|
+
return { row: pair.row - 1, col: pair.col };
|
|
13119
|
+
// up
|
|
13120
|
+
case 1:
|
|
13121
|
+
return { row: pair.row, col: pair.col + 1 };
|
|
13122
|
+
// right
|
|
13123
|
+
case 2:
|
|
13124
|
+
return { row: pair.row + 1, col: pair.col };
|
|
13125
|
+
// down
|
|
13126
|
+
case 3:
|
|
13127
|
+
return { row: pair.row, col: pair.col - 1 };
|
|
13128
|
+
// left
|
|
13129
|
+
default:
|
|
13130
|
+
return { row: pair.row - 1, col: pair.col };
|
|
13131
|
+
}
|
|
13132
|
+
}
|
|
13133
|
+
function isValidPosition(pair, board) {
|
|
13134
|
+
const sec = getSecondaryPos(pair);
|
|
13135
|
+
if (pair.col < 0 || pair.col >= BOARD_COLS) return false;
|
|
13136
|
+
if (pair.row >= BOARD_ROWS) return false;
|
|
13137
|
+
if (sec.col < 0 || sec.col >= BOARD_COLS) return false;
|
|
13138
|
+
if (sec.row >= BOARD_ROWS) return false;
|
|
13139
|
+
if (pair.row >= 0 && board[pair.row][pair.col] !== null) return false;
|
|
13140
|
+
if (sec.row >= 0 && board[sec.row][sec.col] !== null) return false;
|
|
13141
|
+
return true;
|
|
13142
|
+
}
|
|
13143
|
+
function movePair(pair, board, dx) {
|
|
13144
|
+
const test = { ...pair, col: pair.col + dx };
|
|
13145
|
+
if (isValidPosition(test, board)) {
|
|
13146
|
+
pair.col += dx;
|
|
13147
|
+
return true;
|
|
13148
|
+
}
|
|
13149
|
+
return false;
|
|
13150
|
+
}
|
|
13151
|
+
function rotatePair(pair, board, clockwise) {
|
|
13152
|
+
const newOrientation = clockwise ? (pair.orientation + 1) % 4 : (pair.orientation + 3) % 4;
|
|
13153
|
+
const test = { ...pair, orientation: newOrientation };
|
|
13154
|
+
if (isValidPosition(test, board)) {
|
|
13155
|
+
pair.orientation = newOrientation;
|
|
13156
|
+
return true;
|
|
13157
|
+
}
|
|
13158
|
+
for (const kick of [1, -1, 2, -2]) {
|
|
13159
|
+
const kicked = { ...test, col: test.col + kick };
|
|
13160
|
+
if (isValidPosition(kicked, board)) {
|
|
13161
|
+
pair.orientation = newOrientation;
|
|
13162
|
+
pair.col += kick;
|
|
13163
|
+
return true;
|
|
13124
13164
|
}
|
|
13125
13165
|
}
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
13166
|
+
const upKick = { ...test, row: test.row - 1 };
|
|
13167
|
+
if (isValidPosition(upKick, board)) {
|
|
13168
|
+
pair.orientation = newOrientation;
|
|
13169
|
+
pair.row -= 1;
|
|
13170
|
+
return true;
|
|
13171
|
+
}
|
|
13172
|
+
return false;
|
|
13173
|
+
}
|
|
13174
|
+
function dropPair(pair, board) {
|
|
13175
|
+
const test = { ...pair, row: pair.row + 1 };
|
|
13176
|
+
if (isValidPosition(test, board)) {
|
|
13177
|
+
pair.row += 1;
|
|
13178
|
+
return true;
|
|
13179
|
+
}
|
|
13180
|
+
return false;
|
|
13181
|
+
}
|
|
13182
|
+
function hardDrop(pair, board) {
|
|
13183
|
+
let dropped = 0;
|
|
13184
|
+
while (dropPair(pair, board)) {
|
|
13185
|
+
dropped++;
|
|
13186
|
+
}
|
|
13187
|
+
return dropped;
|
|
13188
|
+
}
|
|
13189
|
+
function lockPair(pair, board) {
|
|
13190
|
+
if (pair.row >= 0 && pair.row < BOARD_ROWS) {
|
|
13191
|
+
board[pair.row][pair.col] = { ...pair.primary };
|
|
13192
|
+
}
|
|
13193
|
+
const sec = getSecondaryPos(pair);
|
|
13194
|
+
if (sec.row >= 0 && sec.row < BOARD_ROWS) {
|
|
13195
|
+
board[sec.row][sec.col] = { ...pair.secondary };
|
|
13196
|
+
}
|
|
13197
|
+
}
|
|
13198
|
+
function applyGravityFull(board, powerGems) {
|
|
13199
|
+
void powerGems;
|
|
13200
|
+
stripPowerGemIds(board);
|
|
13201
|
+
let moved = false;
|
|
13202
|
+
for (let col = 0; col < BOARD_COLS; col++) {
|
|
13203
|
+
let writeRow = BOARD_ROWS - 1;
|
|
13204
|
+
for (let row = BOARD_ROWS - 1; row >= 0; row--) {
|
|
13205
|
+
if (board[row][col] !== null) {
|
|
13206
|
+
if (row !== writeRow) {
|
|
13207
|
+
board[writeRow][col] = board[row][col];
|
|
13208
|
+
board[row][col] = null;
|
|
13209
|
+
moved = true;
|
|
13210
|
+
}
|
|
13211
|
+
writeRow--;
|
|
13212
|
+
}
|
|
13134
13213
|
}
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
const game = games[i];
|
|
13146
|
-
const y = listStartY + 1 + vi * 2;
|
|
13147
|
-
const isSelected = i === selectedIndex;
|
|
13148
|
-
output += renderGameEntry(game, i, isSelected, y, boxX, boxWidth);
|
|
13214
|
+
}
|
|
13215
|
+
return moved;
|
|
13216
|
+
}
|
|
13217
|
+
function stripPowerGemIds(board) {
|
|
13218
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13219
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13220
|
+
const gem = board[r][c];
|
|
13221
|
+
if (gem && gem.powerGemId !== void 0) {
|
|
13222
|
+
delete gem.powerGemId;
|
|
13223
|
+
}
|
|
13149
13224
|
}
|
|
13150
|
-
const bottomY = listStartY + 1 + visibleGames * 2;
|
|
13151
|
-
const bottomScrollIndicator = hasScrollDown ? " \u25BC more " : "\u2550".repeat(8);
|
|
13152
|
-
const bottomBorderWidth = boxWidth - 2 - bottomScrollIndicator.length;
|
|
13153
|
-
const bottomLeftPad = Math.floor(bottomBorderWidth / 2);
|
|
13154
|
-
const bottomRightPad = bottomBorderWidth - bottomLeftPad;
|
|
13155
|
-
output += `\x1B[${bottomY};${boxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeftPad)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRightPad)}\u255D\x1B[0m`;
|
|
13156
|
-
const controls = `\u2191\u2193 Navigate | ENTER Select | 1-9 Quick | Q Quit`;
|
|
13157
|
-
output += `\x1B[${rows - 1};${Math.floor((cols - controls.length) / 2)}H\x1B[2m${controls}\x1B[0m`;
|
|
13158
|
-
terminal.write(output);
|
|
13159
13225
|
}
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13226
|
+
}
|
|
13227
|
+
function detectPowerGems(board) {
|
|
13228
|
+
const found = [];
|
|
13229
|
+
const used = /* @__PURE__ */ new Set();
|
|
13230
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13231
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13232
|
+
const gem = board[r][c];
|
|
13233
|
+
if (!gem || gem.type !== "normal") continue;
|
|
13234
|
+
for (let h = 2; h <= BOARD_ROWS - r; h++) {
|
|
13235
|
+
for (let w = 2; w <= BOARD_COLS - c; w++) {
|
|
13236
|
+
if (isUniformRect(board, r, c, w, h, gem.color)) {
|
|
13237
|
+
const key = `${r},${c},${w},${h}`;
|
|
13238
|
+
if (!used.has(key)) {
|
|
13239
|
+
found.push({
|
|
13240
|
+
id: nextPowerGemId++,
|
|
13241
|
+
color: gem.color,
|
|
13242
|
+
x: c,
|
|
13243
|
+
y: r,
|
|
13244
|
+
width: w,
|
|
13245
|
+
height: h
|
|
13246
|
+
});
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13250
|
+
}
|
|
13168
13251
|
}
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
output += `\x1B[${listStartY};${leftBoxX}H${themeColor}\u2554${"\u2550".repeat(topLeft)}${hasScrollUp ? "\x1B[33m" : ""}${topScrollIndicator}${hasScrollUp ? themeColor : ""}${"\u2550".repeat(topRight)}\u2557\x1B[0m`;
|
|
13182
|
-
output += `\x1B[${listStartY};${rightBoxX}H${themeColor}\u2554${"\u2550".repeat(topLeft)}${hasScrollUp ? "\x1B[33m" : ""}${topScrollIndicator}${hasScrollUp ? themeColor : ""}${"\u2550".repeat(topRight)}\u2557\x1B[0m`;
|
|
13183
|
-
for (let row = 0; row < gamesPerColumn; row++) {
|
|
13184
|
-
const leftIdx = (scrollOffset + row) * 2;
|
|
13185
|
-
const rightIdx = leftIdx + 1;
|
|
13186
|
-
const y = listStartY + 1 + row * 2;
|
|
13187
|
-
if (leftIdx < games.length) {
|
|
13188
|
-
output += renderGameEntry(games[leftIdx], leftIdx, leftIdx === selectedIndex, y, leftBoxX, boxWidth);
|
|
13189
|
-
} else {
|
|
13190
|
-
output += `\x1B[${y};${leftBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
13191
|
-
output += `\x1B[${y + 1};${leftBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
13252
|
+
}
|
|
13253
|
+
found.sort((a, b) => b.width * b.height - a.width * a.height);
|
|
13254
|
+
const result = [];
|
|
13255
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
13256
|
+
for (const pg of found) {
|
|
13257
|
+
let overlap = false;
|
|
13258
|
+
for (let r = pg.y; r < pg.y + pg.height; r++) {
|
|
13259
|
+
for (let c = pg.x; c < pg.x + pg.width; c++) {
|
|
13260
|
+
if (claimed.has(`${r},${c}`)) {
|
|
13261
|
+
overlap = true;
|
|
13262
|
+
break;
|
|
13263
|
+
}
|
|
13192
13264
|
}
|
|
13193
|
-
if (
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13265
|
+
if (overlap) break;
|
|
13266
|
+
}
|
|
13267
|
+
if (!overlap) {
|
|
13268
|
+
result.push(pg);
|
|
13269
|
+
for (let r = pg.y; r < pg.y + pg.height; r++) {
|
|
13270
|
+
for (let c = pg.x; c < pg.x + pg.width; c++) {
|
|
13271
|
+
claimed.add(`${r},${c}`);
|
|
13272
|
+
const gem = board[r][c];
|
|
13273
|
+
if (gem) gem.powerGemId = pg.id;
|
|
13274
|
+
}
|
|
13198
13275
|
}
|
|
13199
13276
|
}
|
|
13200
|
-
const bottomY = listStartY + 1 + gamesPerColumn * 2;
|
|
13201
|
-
const bottomScrollIndicator = hasScrollDown ? " \u25BC " : "\u2550\u2550\u2550";
|
|
13202
|
-
const bottomBorderContent = boxWidth - 2 - bottomScrollIndicator.length;
|
|
13203
|
-
const bottomLeft = Math.floor(bottomBorderContent / 2);
|
|
13204
|
-
const bottomRight = bottomBorderContent - bottomLeft;
|
|
13205
|
-
output += `\x1B[${bottomY};${leftBoxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeft)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRight)}\u255D\x1B[0m`;
|
|
13206
|
-
output += `\x1B[${bottomY};${rightBoxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeft)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRight)}\u255D\x1B[0m`;
|
|
13207
|
-
const controls = `\u2191\u2193\u2190\u2192 Navigate | ENTER Select | 1-9 Quick | Q Quit`;
|
|
13208
|
-
output += `\x1B[${rows - 1};${Math.floor((cols - controls.length) / 2)}H\x1B[2m${controls}\x1B[0m`;
|
|
13209
|
-
terminal.write(output);
|
|
13210
13277
|
}
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
const lineContent = `${prefix} [${keyDisplay}] ${game.name}`;
|
|
13220
|
-
const padding = Math.max(0, contentWidth - lineContent.length);
|
|
13221
|
-
output += `\x1B[${y};${boxX}H${themeColor}\u2551\x1B[0m${highlight}${keyColor}${lineContent}${" ".repeat(padding)}\x1B[0m${themeColor}\u2551\x1B[0m`;
|
|
13222
|
-
const descContent = ` ${game.description}`;
|
|
13223
|
-
const descPadding = Math.max(0, contentWidth - descContent.length);
|
|
13224
|
-
const descColor = isSelected ? lightTheme ? "\x1B[2;7;97m" : "\x1B[2;7m" : "\x1B[2m";
|
|
13225
|
-
output += `\x1B[${y + 1};${boxX}H${themeColor}\u2551\x1B[0m${descColor}${descContent}${" ".repeat(descPadding)}\x1B[0m${themeColor}\u2551\x1B[0m`;
|
|
13226
|
-
return output;
|
|
13278
|
+
return result;
|
|
13279
|
+
}
|
|
13280
|
+
function isUniformRect(board, startRow, startCol, width, height, color) {
|
|
13281
|
+
for (let r = startRow; r < startRow + height; r++) {
|
|
13282
|
+
for (let c = startCol; c < startCol + width; c++) {
|
|
13283
|
+
const gem = board[r][c];
|
|
13284
|
+
if (!gem || gem.type !== "normal" || gem.color !== color) return false;
|
|
13285
|
+
}
|
|
13227
13286
|
}
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13287
|
+
return true;
|
|
13288
|
+
}
|
|
13289
|
+
function findCrashTargets(board, _powerGems) {
|
|
13290
|
+
void _powerGems;
|
|
13291
|
+
const targets = findCrashTargetsCore(board);
|
|
13292
|
+
return targets;
|
|
13293
|
+
}
|
|
13294
|
+
function findCrashTargetsWithPowerInfo(board, powerGems) {
|
|
13295
|
+
const targets = findCrashTargetsCore(board);
|
|
13296
|
+
const destroyedPgIds = /* @__PURE__ */ new Set();
|
|
13297
|
+
for (const t of targets) {
|
|
13298
|
+
const gem = board[t.row][t.col];
|
|
13299
|
+
if (gem && gem.powerGemId !== void 0) {
|
|
13300
|
+
destroyedPgIds.add(gem.powerGemId);
|
|
13234
13301
|
}
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13302
|
+
}
|
|
13303
|
+
const destroyedPowerGemSizes = [];
|
|
13304
|
+
for (const pg of powerGems) {
|
|
13305
|
+
if (destroyedPgIds.has(pg.id)) {
|
|
13306
|
+
destroyedPowerGemSizes.push(pg.width * pg.height);
|
|
13307
|
+
}
|
|
13308
|
+
}
|
|
13309
|
+
return { targets, destroyedPowerGemSizes };
|
|
13310
|
+
}
|
|
13311
|
+
function findCrashTargetsCore(board) {
|
|
13312
|
+
const targetsByCell = /* @__PURE__ */ new Map();
|
|
13313
|
+
const visited = /* @__PURE__ */ new Set();
|
|
13314
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13315
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13316
|
+
const gem = board[r][c];
|
|
13317
|
+
if (!gem || gem.type !== "crash") continue;
|
|
13318
|
+
const neighbors = getNeighbors(r, c);
|
|
13319
|
+
let hasTarget = false;
|
|
13320
|
+
for (const [nr, nc] of neighbors) {
|
|
13321
|
+
const ng = board[nr][nc];
|
|
13322
|
+
if (ng && ng.color === gem.color && ng.type === "normal") {
|
|
13323
|
+
hasTarget = true;
|
|
13324
|
+
break;
|
|
13325
|
+
}
|
|
13326
|
+
}
|
|
13327
|
+
if (hasTarget) {
|
|
13328
|
+
const connected = floodFillCrashGroup(board, r, c, gem.color, visited);
|
|
13329
|
+
for (const cell of connected) {
|
|
13330
|
+
targetsByCell.set(`${cell.row},${cell.col}`, { ...cell, color: gem.color });
|
|
13331
|
+
}
|
|
13332
|
+
}
|
|
13333
|
+
}
|
|
13334
|
+
}
|
|
13335
|
+
const targetSet = new Set(Array.from(targetsByCell.keys()));
|
|
13336
|
+
for (const key of Array.from(targetSet)) {
|
|
13337
|
+
const [rStr, cStr] = key.split(",");
|
|
13338
|
+
const r = parseInt(rStr, 10);
|
|
13339
|
+
const c = parseInt(cStr, 10);
|
|
13340
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13341
|
+
const nKey = `${nr},${nc}`;
|
|
13342
|
+
if (targetSet.has(nKey)) continue;
|
|
13343
|
+
const ng = board[nr][nc];
|
|
13344
|
+
if (ng && ng.type === "counter") {
|
|
13345
|
+
targetsByCell.set(nKey, { row: nr, col: nc, color: ng.color });
|
|
13346
|
+
targetSet.add(nKey);
|
|
13347
|
+
}
|
|
13348
|
+
}
|
|
13349
|
+
}
|
|
13350
|
+
return Array.from(targetsByCell.values());
|
|
13351
|
+
}
|
|
13352
|
+
function getNeighbors(row, col) {
|
|
13353
|
+
const n = [];
|
|
13354
|
+
if (row > 0) n.push([row - 1, col]);
|
|
13355
|
+
if (row < BOARD_ROWS - 1) n.push([row + 1, col]);
|
|
13356
|
+
if (col > 0) n.push([row, col - 1]);
|
|
13357
|
+
if (col < BOARD_COLS - 1) n.push([row, col + 1]);
|
|
13358
|
+
return n;
|
|
13359
|
+
}
|
|
13360
|
+
function floodFillCrashGroup(board, startRow, startCol, color, globalVisited) {
|
|
13361
|
+
const result = [];
|
|
13362
|
+
const stack = [[startRow, startCol]];
|
|
13363
|
+
const localVisited = /* @__PURE__ */ new Set();
|
|
13364
|
+
while (stack.length > 0) {
|
|
13365
|
+
const [r, c] = stack.pop();
|
|
13366
|
+
const key = `${r},${c}`;
|
|
13367
|
+
if (localVisited.has(key) || globalVisited.has(key)) continue;
|
|
13368
|
+
const gem = board[r][c];
|
|
13369
|
+
if (!gem) continue;
|
|
13370
|
+
if (gem.color !== color) continue;
|
|
13371
|
+
if (gem.type !== "normal" && gem.type !== "crash") continue;
|
|
13372
|
+
localVisited.add(key);
|
|
13373
|
+
globalVisited.add(key);
|
|
13374
|
+
result.push({ row: r, col: c });
|
|
13375
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13376
|
+
stack.push([nr, nc]);
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13379
|
+
return result;
|
|
13380
|
+
}
|
|
13381
|
+
function clearGems(board, targets) {
|
|
13382
|
+
for (const t of targets) {
|
|
13383
|
+
board[t.row][t.col] = null;
|
|
13384
|
+
}
|
|
13385
|
+
}
|
|
13386
|
+
function resolveDiamond(board) {
|
|
13387
|
+
const cleared = [];
|
|
13388
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13389
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13390
|
+
const gem = board[r][c];
|
|
13391
|
+
if (!gem || gem.type !== "diamond") continue;
|
|
13392
|
+
let targetColor = null;
|
|
13393
|
+
if (r + 1 < BOARD_ROWS && board[r + 1][c]) {
|
|
13394
|
+
targetColor = board[r + 1][c].color;
|
|
13395
|
+
}
|
|
13396
|
+
if (!targetColor) {
|
|
13397
|
+
for (const [nr, nc] of getNeighbors(r, c)) {
|
|
13398
|
+
if (board[nr][nc] && board[nr][nc].type !== "diamond") {
|
|
13399
|
+
targetColor = board[nr][nc].color;
|
|
13400
|
+
break;
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
board[r][c] = null;
|
|
13405
|
+
cleared.push({ row: r, col: c, color: targetColor || "red" });
|
|
13406
|
+
if (!targetColor) continue;
|
|
13407
|
+
for (let dr = 0; dr < BOARD_ROWS; dr++) {
|
|
13408
|
+
for (let dc = 0; dc < BOARD_COLS; dc++) {
|
|
13409
|
+
const g = board[dr][dc];
|
|
13410
|
+
if (g && g.color === targetColor) {
|
|
13411
|
+
cleared.push({ row: dr, col: dc, color: targetColor });
|
|
13412
|
+
board[dr][dc] = null;
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
return cleared;
|
|
13419
|
+
}
|
|
13420
|
+
function shouldSpawnDiamond(totalDrops) {
|
|
13421
|
+
return totalDrops > 0 && totalDrops % DIAMOND_INTERVAL === 0;
|
|
13422
|
+
}
|
|
13423
|
+
function generateDiamondPair() {
|
|
13424
|
+
return {
|
|
13425
|
+
primary: { color: "red", type: "diamond" },
|
|
13426
|
+
secondary: randomGem(),
|
|
13427
|
+
col: DROP_ALLEY_COL,
|
|
13428
|
+
row: 0,
|
|
13429
|
+
orientation: 0
|
|
13430
|
+
};
|
|
13431
|
+
}
|
|
13432
|
+
function calculateStepAttack(info) {
|
|
13433
|
+
let pgBonus = 0;
|
|
13434
|
+
for (const area of info.powerGemSizes) {
|
|
13435
|
+
pgBonus += Math.floor(area / 8);
|
|
13436
|
+
}
|
|
13437
|
+
return Math.floor((info.gemsCleared + pgBonus) * info.chainStep);
|
|
13438
|
+
}
|
|
13439
|
+
function applyAttackModifiers(total, damageModifier = 1, isDiamondClear = false) {
|
|
13440
|
+
let attack = Math.floor(total * damageModifier);
|
|
13441
|
+
if (isDiamondClear) attack = Math.floor(attack * 0.5);
|
|
13442
|
+
return attack;
|
|
13443
|
+
}
|
|
13444
|
+
function resolveCounterAttack(attack, pendingGarbage, ratio = 2) {
|
|
13445
|
+
if (attack <= 0 || pendingGarbage <= 0) {
|
|
13446
|
+
return {
|
|
13447
|
+
remainingAttack: attack,
|
|
13448
|
+
remainingPending: pendingGarbage,
|
|
13449
|
+
canceledGems: 0,
|
|
13450
|
+
pendingStartsAtThree: false
|
|
13451
|
+
};
|
|
13452
|
+
}
|
|
13453
|
+
const cancelable = Math.floor(attack / ratio);
|
|
13454
|
+
const canceledGems = Math.min(cancelable, pendingGarbage);
|
|
13455
|
+
const remainingPending = pendingGarbage - canceledGems;
|
|
13456
|
+
const remainingAttack = attack - canceledGems * ratio;
|
|
13457
|
+
return {
|
|
13458
|
+
remainingAttack,
|
|
13459
|
+
remainingPending,
|
|
13460
|
+
canceledGems,
|
|
13461
|
+
pendingStartsAtThree: canceledGems > 0 && remainingPending > 0
|
|
13462
|
+
};
|
|
13463
|
+
}
|
|
13464
|
+
function decrementCounters(player) {
|
|
13465
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13466
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13467
|
+
const gem = player.board[r][c];
|
|
13468
|
+
if (gem && gem.type === "counter" && gem.counterTimer !== void 0) {
|
|
13469
|
+
gem.counterTimer--;
|
|
13470
|
+
if (gem.counterTimer <= 0) {
|
|
13471
|
+
gem.type = "normal";
|
|
13472
|
+
delete gem.counterTimer;
|
|
13473
|
+
}
|
|
13474
|
+
}
|
|
13475
|
+
}
|
|
13476
|
+
}
|
|
13477
|
+
}
|
|
13478
|
+
function checkGameOver(board) {
|
|
13479
|
+
return board[0][DROP_ALLEY_COL] !== null;
|
|
13480
|
+
}
|
|
13481
|
+
function getGhostPosition(pair, board) {
|
|
13482
|
+
const ghost = { ...pair };
|
|
13483
|
+
while (true) {
|
|
13484
|
+
const test = { ...ghost, row: ghost.row + 1 };
|
|
13485
|
+
if (!isValidPosition(test, board)) break;
|
|
13486
|
+
ghost.row++;
|
|
13487
|
+
}
|
|
13488
|
+
const sec = getSecondaryPos(ghost);
|
|
13489
|
+
return {
|
|
13490
|
+
primaryRow: ghost.row,
|
|
13491
|
+
primaryCol: ghost.col,
|
|
13492
|
+
secondaryRow: sec.row,
|
|
13493
|
+
secondaryCol: sec.col
|
|
13494
|
+
};
|
|
13495
|
+
}
|
|
13496
|
+
function createPlayerState() {
|
|
13497
|
+
return {
|
|
13498
|
+
board: createBoard(),
|
|
13499
|
+
currentPair: null,
|
|
13500
|
+
nextPair: generatePair(),
|
|
13501
|
+
powerGems: [],
|
|
13502
|
+
score: 0,
|
|
13503
|
+
pendingGarbage: 0,
|
|
13504
|
+
pendingCounteredGarbage: 0,
|
|
13505
|
+
alive: true,
|
|
13506
|
+
totalDrops: 0
|
|
13507
|
+
};
|
|
13508
|
+
}
|
|
13509
|
+
function spawnPair(player) {
|
|
13510
|
+
player.currentPair = player.nextPair;
|
|
13511
|
+
player.currentPair.col = DROP_ALLEY_COL;
|
|
13512
|
+
player.currentPair.row = 0;
|
|
13513
|
+
player.currentPair.orientation = 0;
|
|
13514
|
+
player.totalDrops++;
|
|
13515
|
+
if (shouldSpawnDiamond(player.totalDrops + 1)) {
|
|
13516
|
+
player.nextPair = generateDiamondPair();
|
|
13517
|
+
} else {
|
|
13518
|
+
player.nextPair = generatePair();
|
|
13519
|
+
}
|
|
13520
|
+
if (!isValidPosition(player.currentPair, player.board)) {
|
|
13521
|
+
player.alive = false;
|
|
13522
|
+
return false;
|
|
13523
|
+
}
|
|
13524
|
+
return true;
|
|
13525
|
+
}
|
|
13526
|
+
|
|
13527
|
+
// src/games/hyper-fighter/ai.ts
|
|
13528
|
+
var DIFFICULTIES2 = {
|
|
13529
|
+
easy: { name: "EASY", thinkFrames: 14, mistakeRate: 0.3, dropSpeedBoost: 1, simulateDepth: 0 },
|
|
13530
|
+
normal: { name: "NORMAL", thinkFrames: 8, mistakeRate: 0.1, dropSpeedBoost: 1.5, simulateDepth: 0 },
|
|
13531
|
+
hard: { name: "HARD", thinkFrames: 4, mistakeRate: 0.02, dropSpeedBoost: 2, simulateDepth: 1 }
|
|
13532
|
+
};
|
|
13533
|
+
function createAIState(difficulty) {
|
|
13534
|
+
return {
|
|
13535
|
+
targetCol: 2,
|
|
13536
|
+
targetOrientation: 0,
|
|
13537
|
+
thinkTimer: 0,
|
|
13538
|
+
moveTimer: 0,
|
|
13539
|
+
decided: false,
|
|
13540
|
+
difficulty
|
|
13541
|
+
};
|
|
13542
|
+
}
|
|
13543
|
+
function getColumnHeight(board, col) {
|
|
13544
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13545
|
+
if (board[r][col] !== null) return BOARD_ROWS - r;
|
|
13546
|
+
}
|
|
13547
|
+
return 0;
|
|
13548
|
+
}
|
|
13549
|
+
function evaluateBoard(board) {
|
|
13550
|
+
let score = 0;
|
|
13551
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13552
|
+
const h = getColumnHeight(board, c);
|
|
13553
|
+
score -= h * h * 2;
|
|
13554
|
+
}
|
|
13555
|
+
const heights = [];
|
|
13556
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13557
|
+
heights.push(getColumnHeight(board, c));
|
|
13558
|
+
}
|
|
13559
|
+
for (let c = 0; c < BOARD_COLS - 1; c++) {
|
|
13560
|
+
const diff = Math.abs(heights[c] - heights[c + 1]);
|
|
13561
|
+
score -= diff * 3;
|
|
13562
|
+
}
|
|
13563
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13564
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13565
|
+
const gem = board[r][c];
|
|
13566
|
+
if (!gem || gem.type === "counter") continue;
|
|
13567
|
+
if (c + 1 < BOARD_COLS) {
|
|
13568
|
+
const right = board[r][c + 1];
|
|
13569
|
+
if (right && right.color === gem.color && right.type !== "counter") {
|
|
13570
|
+
score += 4;
|
|
13571
|
+
}
|
|
13572
|
+
}
|
|
13573
|
+
if (r + 1 < BOARD_ROWS) {
|
|
13574
|
+
const below = board[r + 1][c];
|
|
13575
|
+
if (below && below.color === gem.color && below.type !== "counter") {
|
|
13576
|
+
score += 4;
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
13579
|
+
}
|
|
13580
|
+
}
|
|
13581
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
13582
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
13583
|
+
const gem = board[r][c];
|
|
13584
|
+
if (!gem || gem.type !== "crash") continue;
|
|
13585
|
+
const neighbors = [
|
|
13586
|
+
[r - 1, c],
|
|
13587
|
+
[r + 1, c],
|
|
13588
|
+
[r, c - 1],
|
|
13589
|
+
[r, c + 1]
|
|
13590
|
+
];
|
|
13591
|
+
for (const [nr, nc] of neighbors) {
|
|
13592
|
+
if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) {
|
|
13593
|
+
const ng = board[nr][nc];
|
|
13594
|
+
if (ng && ng.color === gem.color && ng.type === "normal") {
|
|
13595
|
+
score += 15;
|
|
13596
|
+
}
|
|
13597
|
+
}
|
|
13598
|
+
}
|
|
13599
|
+
}
|
|
13600
|
+
}
|
|
13601
|
+
const powerGems = detectPowerGems(board);
|
|
13602
|
+
for (const pg of powerGems) {
|
|
13603
|
+
score += pg.width * pg.height * 8;
|
|
13604
|
+
}
|
|
13605
|
+
const targets = findCrashTargets(board, powerGems);
|
|
13606
|
+
score += targets.length * 10;
|
|
13607
|
+
const maxHeight = Math.max(...heights);
|
|
13608
|
+
if (maxHeight >= BOARD_ROWS - 2) score -= 200;
|
|
13609
|
+
if (maxHeight >= BOARD_ROWS - 1) score -= 500;
|
|
13610
|
+
const deathHeight = heights[DROP_ALLEY_COL];
|
|
13611
|
+
if (deathHeight >= BOARD_ROWS - 3) score -= 100;
|
|
13612
|
+
return score;
|
|
13613
|
+
}
|
|
13614
|
+
function cloneBoard(board) {
|
|
13615
|
+
return board.map((row) => row.map((gem) => gem ? { ...gem } : null));
|
|
13616
|
+
}
|
|
13617
|
+
function evaluatePlacement(pair, board, simulateDepth = 0) {
|
|
13618
|
+
const testBoard = cloneBoard(board);
|
|
13619
|
+
const testPair = {
|
|
13620
|
+
primary: { ...pair.primary },
|
|
13621
|
+
secondary: { ...pair.secondary },
|
|
13622
|
+
col: pair.col,
|
|
13623
|
+
row: pair.row,
|
|
13624
|
+
orientation: pair.orientation
|
|
13625
|
+
};
|
|
13626
|
+
while (true) {
|
|
13627
|
+
const next = { ...testPair, row: testPair.row + 1 };
|
|
13628
|
+
if (!isValidPosition(next, testBoard)) break;
|
|
13629
|
+
testPair.row++;
|
|
13630
|
+
}
|
|
13631
|
+
lockPair(testPair, testBoard);
|
|
13632
|
+
applyGravityFull(testBoard, []);
|
|
13633
|
+
if (simulateDepth > 0) {
|
|
13634
|
+
const pg = detectPowerGems(testBoard);
|
|
13635
|
+
const targets = findCrashTargets(testBoard, pg);
|
|
13636
|
+
if (targets.length > 0) {
|
|
13637
|
+
clearGems(testBoard, targets);
|
|
13638
|
+
applyGravityFull(testBoard, []);
|
|
13639
|
+
}
|
|
13640
|
+
}
|
|
13641
|
+
return evaluateBoard(testBoard);
|
|
13642
|
+
}
|
|
13643
|
+
function selectMove(player, ai) {
|
|
13644
|
+
const pair = player.currentPair;
|
|
13645
|
+
if (!pair) return;
|
|
13646
|
+
if (Math.random() < ai.difficulty.mistakeRate) {
|
|
13647
|
+
ai.targetCol = Math.floor(Math.random() * BOARD_COLS);
|
|
13648
|
+
ai.targetOrientation = Math.floor(Math.random() * 4);
|
|
13649
|
+
ai.decided = true;
|
|
13650
|
+
return;
|
|
13651
|
+
}
|
|
13652
|
+
const options = [];
|
|
13653
|
+
for (let orient = 0; orient < 4; orient++) {
|
|
13654
|
+
for (let col = 0; col < BOARD_COLS; col++) {
|
|
13655
|
+
const testPair = {
|
|
13656
|
+
primary: { ...pair.primary },
|
|
13657
|
+
secondary: { ...pair.secondary },
|
|
13658
|
+
col,
|
|
13659
|
+
row: 0,
|
|
13660
|
+
orientation: orient
|
|
13661
|
+
};
|
|
13662
|
+
if (!isValidPosition(testPair, player.board)) continue;
|
|
13663
|
+
const score = evaluatePlacement(testPair, player.board, ai.difficulty.simulateDepth);
|
|
13664
|
+
options.push({ col, orientation: orient, score });
|
|
13665
|
+
}
|
|
13666
|
+
}
|
|
13667
|
+
if (options.length === 0) {
|
|
13668
|
+
ai.targetCol = pair.col;
|
|
13669
|
+
ai.targetOrientation = pair.orientation;
|
|
13670
|
+
ai.decided = true;
|
|
13671
|
+
return;
|
|
13672
|
+
}
|
|
13673
|
+
options.sort((a, b) => b.score - a.score);
|
|
13674
|
+
const best = options[0];
|
|
13675
|
+
ai.targetCol = best.col;
|
|
13676
|
+
ai.targetOrientation = best.orientation;
|
|
13677
|
+
ai.decided = true;
|
|
13678
|
+
}
|
|
13679
|
+
function aiTick(player, ai) {
|
|
13680
|
+
if (!player.currentPair) return "none";
|
|
13681
|
+
ai.thinkTimer++;
|
|
13682
|
+
if (!ai.decided) {
|
|
13683
|
+
if (ai.thinkTimer >= ai.difficulty.thinkFrames) {
|
|
13684
|
+
selectMove(player, ai);
|
|
13685
|
+
ai.thinkTimer = 0;
|
|
13686
|
+
}
|
|
13687
|
+
return "none";
|
|
13688
|
+
}
|
|
13689
|
+
ai.moveTimer++;
|
|
13690
|
+
const moveInterval = Math.max(2, Math.floor(ai.difficulty.thinkFrames / 3));
|
|
13691
|
+
if (ai.moveTimer < moveInterval) return "none";
|
|
13692
|
+
ai.moveTimer = 0;
|
|
13693
|
+
const pair = player.currentPair;
|
|
13694
|
+
if (pair.orientation !== ai.targetOrientation) {
|
|
13695
|
+
const cwDist = (ai.targetOrientation - pair.orientation + 4) % 4;
|
|
13696
|
+
const ccwDist = (pair.orientation - ai.targetOrientation + 4) % 4;
|
|
13697
|
+
return cwDist <= ccwDist ? "rotate_cw" : "rotate_ccw";
|
|
13698
|
+
}
|
|
13699
|
+
if (pair.col < ai.targetCol) {
|
|
13700
|
+
return "move";
|
|
13701
|
+
} else if (pair.col > ai.targetCol) {
|
|
13702
|
+
return "move";
|
|
13703
|
+
}
|
|
13704
|
+
return "drop";
|
|
13705
|
+
}
|
|
13706
|
+
function getAIMoveDirection(player, ai) {
|
|
13707
|
+
if (!player.currentPair) return 0;
|
|
13708
|
+
if (player.currentPair.col < ai.targetCol) return 1;
|
|
13709
|
+
if (player.currentPair.col > ai.targetCol) return -1;
|
|
13710
|
+
return 0;
|
|
13711
|
+
}
|
|
13712
|
+
|
|
13713
|
+
// src/games/hyper-fighter/effects.ts
|
|
13714
|
+
var GEM_ANSI = {
|
|
13715
|
+
red: "\x1B[1;38;5;196m",
|
|
13716
|
+
green: "\x1B[1;38;5;46m",
|
|
13717
|
+
blue: "\x1B[1;38;5;27m",
|
|
13718
|
+
yellow: "\x1B[1;38;5;226m"
|
|
13719
|
+
};
|
|
13720
|
+
var PARTICLE_CHARS = ["\u2726", "\u2605", "\u25C6", "\u25CF", "\xB7", "*", "\u25AA"];
|
|
13721
|
+
function spawnClearParticles(x, y, color, count, particles) {
|
|
13722
|
+
const ansi = GEM_ANSI[color];
|
|
13723
|
+
for (let i = 0; i < count; i++) {
|
|
13724
|
+
particles.push({
|
|
13725
|
+
x: x + (Math.random() - 0.5) * 2,
|
|
13726
|
+
y: y + (Math.random() - 0.5),
|
|
13727
|
+
char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
|
|
13728
|
+
color: ansi,
|
|
13729
|
+
vx: (Math.random() - 0.5) * 3,
|
|
13730
|
+
vy: (Math.random() - 0.8) * 2,
|
|
13731
|
+
life: 8 + Math.floor(Math.random() * 8),
|
|
13732
|
+
maxLife: 16
|
|
13733
|
+
});
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13736
|
+
function spawnFirework2(x, y, particles) {
|
|
13737
|
+
const colors = ["\x1B[91m", "\x1B[93m", "\x1B[92m", "\x1B[96m", "\x1B[95m"];
|
|
13738
|
+
for (let i = 0; i < 20; i++) {
|
|
13739
|
+
const angle = Math.PI * 2 * i / 20;
|
|
13740
|
+
const speed = 1.5 + Math.random();
|
|
13741
|
+
particles.push({
|
|
13742
|
+
x,
|
|
13743
|
+
y,
|
|
13744
|
+
char: PARTICLE_CHARS[Math.floor(Math.random() * PARTICLE_CHARS.length)],
|
|
13745
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
13746
|
+
vx: Math.cos(angle) * speed,
|
|
13747
|
+
vy: Math.sin(angle) * speed * 0.5,
|
|
13748
|
+
life: 12 + Math.floor(Math.random() * 8),
|
|
13749
|
+
maxLife: 20
|
|
13750
|
+
});
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
function spawnCollapse(startX, startY, width, height, particles) {
|
|
13754
|
+
for (let r = 0; r < height; r++) {
|
|
13755
|
+
for (let c = 0; c < width; c++) {
|
|
13756
|
+
if (Math.random() < 0.4) {
|
|
13757
|
+
particles.push({
|
|
13758
|
+
x: startX + c * 2,
|
|
13759
|
+
y: startY + r,
|
|
13760
|
+
char: "\u2593",
|
|
13761
|
+
color: "\x1B[90m",
|
|
13762
|
+
vx: (Math.random() - 0.5) * 0.5,
|
|
13763
|
+
vy: 0.3 + Math.random() * 0.5,
|
|
13764
|
+
life: 10 + Math.floor(Math.random() * 15),
|
|
13765
|
+
maxLife: 25
|
|
13766
|
+
});
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13770
|
+
}
|
|
13771
|
+
function updateParticles2(particles) {
|
|
13772
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
13773
|
+
const p = particles[i];
|
|
13774
|
+
p.x += p.vx;
|
|
13775
|
+
p.y += p.vy;
|
|
13776
|
+
p.vy += 0.08;
|
|
13777
|
+
p.life--;
|
|
13778
|
+
if (p.life <= 0) {
|
|
13779
|
+
particles.splice(i, 1);
|
|
13780
|
+
}
|
|
13781
|
+
}
|
|
13782
|
+
}
|
|
13783
|
+
function renderParticles(particles, minX, minY, maxX, maxY) {
|
|
13784
|
+
let output = "";
|
|
13785
|
+
for (const p of particles) {
|
|
13786
|
+
const px = Math.round(p.x);
|
|
13787
|
+
const py = Math.round(p.y);
|
|
13788
|
+
if (px < minX || px > maxX || py < minY || py > maxY) continue;
|
|
13789
|
+
const fade = p.life > p.maxLife * 0.5 ? "\x1B[1m" : "\x1B[2m";
|
|
13790
|
+
output += `\x1B[${py};${px}H${fade}${p.color}${p.char}\x1B[0m`;
|
|
13791
|
+
}
|
|
13792
|
+
return output;
|
|
13793
|
+
}
|
|
13794
|
+
var COMBO_MESSAGES = [
|
|
13795
|
+
{ text: "NICE!", color: "\x1B[92m" },
|
|
13796
|
+
{ text: "GREAT!", color: "\x1B[93m" },
|
|
13797
|
+
{ text: "AMAZING!", color: "\x1B[96m" },
|
|
13798
|
+
{ text: "UNSTOPPABLE!", color: "\x1B[95m" },
|
|
13799
|
+
{ text: "GODLIKE!!", color: "\x1B[91m" },
|
|
13800
|
+
{ text: "GODLIKE!!", color: "\x1B[1;91m" }
|
|
13801
|
+
];
|
|
13802
|
+
function spawnComboText(chains, x, y, texts) {
|
|
13803
|
+
const idx = Math.min(chains - 1, COMBO_MESSAGES.length - 1);
|
|
13804
|
+
const msg = COMBO_MESSAGES[idx];
|
|
13805
|
+
texts.push({
|
|
13806
|
+
text: `${msg.text} \xD7${chains}`,
|
|
13807
|
+
x: x - Math.floor(msg.text.length / 2),
|
|
13808
|
+
y,
|
|
13809
|
+
color: msg.color,
|
|
13810
|
+
frames: 20,
|
|
13811
|
+
maxFrames: 20
|
|
13812
|
+
});
|
|
13813
|
+
}
|
|
13814
|
+
function spawnChainCounter(chains, x, y, texts) {
|
|
13815
|
+
texts.push({
|
|
13816
|
+
text: `${chains} CHAIN`,
|
|
13817
|
+
x: x - 3,
|
|
13818
|
+
y: y + 1,
|
|
13819
|
+
color: "\x1B[1;97m",
|
|
13820
|
+
frames: 16,
|
|
13821
|
+
maxFrames: 16
|
|
13822
|
+
});
|
|
13823
|
+
}
|
|
13824
|
+
function updateFloatingTexts(texts) {
|
|
13825
|
+
for (let i = texts.length - 1; i >= 0; i--) {
|
|
13826
|
+
const t = texts[i];
|
|
13827
|
+
t.frames--;
|
|
13828
|
+
if (t.frames % 3 === 0) t.y -= 1;
|
|
13829
|
+
if (t.frames <= 0) {
|
|
13830
|
+
texts.splice(i, 1);
|
|
13831
|
+
}
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
function renderFloatingTexts(texts) {
|
|
13835
|
+
let output = "";
|
|
13836
|
+
for (const t of texts) {
|
|
13837
|
+
if (t.y < 1) continue;
|
|
13838
|
+
const fade = t.frames > t.maxFrames * 0.4 ? "\x1B[1m" : "\x1B[2m";
|
|
13839
|
+
output += `\x1B[${t.y};${Math.max(1, t.x)}H${fade}${t.color}${t.text}\x1B[0m`;
|
|
13840
|
+
}
|
|
13841
|
+
return output;
|
|
13842
|
+
}
|
|
13843
|
+
function spawnProjectile(fromX, toX, y, count, projectiles) {
|
|
13844
|
+
let char;
|
|
13845
|
+
let color;
|
|
13846
|
+
if (count >= 8) {
|
|
13847
|
+
char = "\u25C8";
|
|
13848
|
+
color = "\x1B[1;91m";
|
|
13849
|
+
} else if (count >= 4) {
|
|
13850
|
+
char = "\u25C6";
|
|
13851
|
+
color = "\x1B[1;93m";
|
|
13852
|
+
} else {
|
|
13853
|
+
char = "\u25CF";
|
|
13854
|
+
color = "\x1B[1;97m";
|
|
13855
|
+
}
|
|
13856
|
+
projectiles.push({ fromX, toX, y, progress: 0, char, color, count });
|
|
13857
|
+
}
|
|
13858
|
+
function updateProjectiles(projectiles) {
|
|
13859
|
+
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
13860
|
+
const p = projectiles[i];
|
|
13861
|
+
p.progress += 1 / 7;
|
|
13862
|
+
if (p.progress >= 1) {
|
|
13863
|
+
projectiles.splice(i, 1);
|
|
13864
|
+
}
|
|
13865
|
+
}
|
|
13866
|
+
}
|
|
13867
|
+
function renderProjectiles(projectiles) {
|
|
13868
|
+
let output = "";
|
|
13869
|
+
for (const p of projectiles) {
|
|
13870
|
+
const x = Math.round(p.fromX + (p.toX - p.fromX) * p.progress);
|
|
13871
|
+
if (x < 1) continue;
|
|
13872
|
+
output += `\x1B[${p.y};${x}H${p.color}${p.char}\x1B[0m`;
|
|
13873
|
+
}
|
|
13874
|
+
return output;
|
|
13875
|
+
}
|
|
13876
|
+
function renderPortrait(lines, x, y, color) {
|
|
13877
|
+
let output = "";
|
|
13878
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13879
|
+
output += `\x1B[${y + i};${x}H${color}${lines[i]}\x1B[0m`;
|
|
13880
|
+
}
|
|
13881
|
+
return output;
|
|
13882
|
+
}
|
|
13883
|
+
function triggerShake(shake, intensity, frames) {
|
|
13884
|
+
shake.intensity = Math.max(shake.intensity, intensity);
|
|
13885
|
+
shake.frames = Math.max(shake.frames, frames);
|
|
13886
|
+
}
|
|
13887
|
+
function updateShake(shake) {
|
|
13888
|
+
if (shake.frames <= 0) return { dx: 0, dy: 0 };
|
|
13889
|
+
shake.frames--;
|
|
13890
|
+
if (shake.frames <= 0) {
|
|
13891
|
+
shake.intensity = 0;
|
|
13892
|
+
return { dx: 0, dy: 0 };
|
|
13893
|
+
}
|
|
13894
|
+
return {
|
|
13895
|
+
dx: Math.round((Math.random() - 0.5) * shake.intensity * 2),
|
|
13896
|
+
dy: Math.round((Math.random() - 0.5) * shake.intensity)
|
|
13897
|
+
};
|
|
13898
|
+
}
|
|
13899
|
+
function triggerFlash(flash, color, frames) {
|
|
13900
|
+
flash.color = color;
|
|
13901
|
+
flash.frames = frames;
|
|
13902
|
+
}
|
|
13903
|
+
function updateFlash(flash) {
|
|
13904
|
+
if (flash.frames <= 0) return null;
|
|
13905
|
+
flash.frames--;
|
|
13906
|
+
return flash.color;
|
|
13907
|
+
}
|
|
13908
|
+
function renderEnergyBar(x, y, level, maxLevel) {
|
|
13909
|
+
const barHeight = 6;
|
|
13910
|
+
const filled = Math.min(barHeight, Math.round(level / Math.max(1, maxLevel) * barHeight));
|
|
13911
|
+
let output = "";
|
|
13912
|
+
for (let i = 0; i < barHeight; i++) {
|
|
13913
|
+
const isFilled = i >= barHeight - filled;
|
|
13914
|
+
const char = isFilled ? "\u2588" : "\u2591";
|
|
13915
|
+
const color = isFilled ? filled >= barHeight - 1 ? "\x1B[91m" : filled >= barHeight / 2 ? "\x1B[93m" : "\x1B[92m" : "\x1B[90m";
|
|
13916
|
+
output += `\x1B[${y + i};${x}H${color}${char}\x1B[0m`;
|
|
13917
|
+
}
|
|
13918
|
+
return output;
|
|
13919
|
+
}
|
|
13920
|
+
|
|
13921
|
+
// src/games/hyper-fighter/characters.ts
|
|
13922
|
+
var R = "red";
|
|
13923
|
+
var G = "green";
|
|
13924
|
+
var B = "blue";
|
|
13925
|
+
var Y = "yellow";
|
|
13926
|
+
var ryu = {
|
|
13927
|
+
id: "ryu",
|
|
13928
|
+
name: "Ryu",
|
|
13929
|
+
description: "Vertical columns",
|
|
13930
|
+
damageModifier: 1,
|
|
13931
|
+
dropPattern: [
|
|
13932
|
+
[R, G, B, Y, R, G],
|
|
13933
|
+
[R, G, B, Y, R, G],
|
|
13934
|
+
[R, G, B, Y, R, G],
|
|
13935
|
+
[R, G, B, Y, R, G]
|
|
13936
|
+
],
|
|
13937
|
+
portraits: {
|
|
13938
|
+
idle: [" __ ", " (-_-)", " /| "],
|
|
13939
|
+
attack: [" _\\__ ", " (>o<)", " =|/ "],
|
|
13940
|
+
hit: [" __ ", " (x_x)", " /| "],
|
|
13941
|
+
win: [" \\__/ ", " (^o^)", " /\\ "],
|
|
13942
|
+
lose: [" __ ", " (;_;)", " | "]
|
|
13943
|
+
}
|
|
13944
|
+
};
|
|
13945
|
+
var ken = {
|
|
13946
|
+
id: "ken",
|
|
13947
|
+
name: "Ken",
|
|
13948
|
+
description: "Horizontal rows",
|
|
13949
|
+
damageModifier: 1,
|
|
13950
|
+
dropPattern: [
|
|
13951
|
+
[Y, Y, Y, Y, Y, Y],
|
|
13952
|
+
[G, G, G, G, G, G],
|
|
13953
|
+
[B, B, B, B, B, B],
|
|
13954
|
+
[R, R, R, R, R, R]
|
|
13955
|
+
],
|
|
13956
|
+
portraits: {
|
|
13957
|
+
idle: [" ^^^ ", " [>_>]", " /| "],
|
|
13958
|
+
attack: [" ^^^/ ", " [>o<]", " /=| "],
|
|
13959
|
+
hit: [" ^^^ ", " [x_x]", " /| "],
|
|
13960
|
+
win: [" \\^^/ ", " [^o^]", " /\\ "],
|
|
13961
|
+
lose: [" ^^^ ", " [;_;]", " | "]
|
|
13962
|
+
}
|
|
13963
|
+
};
|
|
13964
|
+
var chunLi = {
|
|
13965
|
+
id: "chunli",
|
|
13966
|
+
name: "Chun-Li",
|
|
13967
|
+
description: "2x2 color blocks",
|
|
13968
|
+
damageModifier: 1.2,
|
|
13969
|
+
dropPattern: [
|
|
13970
|
+
[R, R, G, G, B, B],
|
|
13971
|
+
[R, R, G, G, B, B],
|
|
13972
|
+
[Y, Y, R, R, G, G],
|
|
13973
|
+
[Y, Y, R, R, G, G]
|
|
13974
|
+
],
|
|
13975
|
+
portraits: {
|
|
13976
|
+
idle: [" @ @ ", " {^.^}", " /| "],
|
|
13977
|
+
attack: [" @ @\\", " {>.<}", " /|= "],
|
|
13978
|
+
hit: [" @ @ ", " {x.x}", " /| "],
|
|
13979
|
+
win: ["\\@ @/", " {^o^}", " /\\ "],
|
|
13980
|
+
lose: [" @ @ ", " {;.;}", " | "]
|
|
13981
|
+
}
|
|
13982
|
+
};
|
|
13983
|
+
var sakura = {
|
|
13984
|
+
id: "sakura",
|
|
13985
|
+
name: "Sakura",
|
|
13986
|
+
description: "Fixed edges, alt middle",
|
|
13987
|
+
damageModifier: 1,
|
|
13988
|
+
dropPattern: [
|
|
13989
|
+
[G, R, B, R, B, Y],
|
|
13990
|
+
[G, B, R, B, R, Y],
|
|
13991
|
+
[G, R, B, R, B, Y],
|
|
13992
|
+
[G, B, R, B, R, Y]
|
|
13993
|
+
],
|
|
13994
|
+
portraits: {
|
|
13995
|
+
idle: [" >> ", " <*_*>", " /| "],
|
|
13996
|
+
attack: [" >>/ ", " <*o*>", " /|= "],
|
|
13997
|
+
hit: [" >> ", " <x_x>", " /| "],
|
|
13998
|
+
win: [" \\>>/ ", " <^o^>", " /\\ "],
|
|
13999
|
+
lose: [" >> ", " <;_;>", " | "]
|
|
14000
|
+
}
|
|
14001
|
+
};
|
|
14002
|
+
var morrigan = {
|
|
14003
|
+
id: "morrigan",
|
|
14004
|
+
name: "Morrigan",
|
|
14005
|
+
description: "Symmetric mirrored",
|
|
14006
|
+
damageModifier: 1,
|
|
14007
|
+
dropPattern: [
|
|
14008
|
+
[B, G, R, R, G, B],
|
|
14009
|
+
[G, B, R, R, B, G],
|
|
14010
|
+
[R, G, B, B, G, R],
|
|
14011
|
+
[G, R, B, B, R, G]
|
|
14012
|
+
],
|
|
14013
|
+
portraits: {
|
|
14014
|
+
idle: [" ~ ~ ", " ~^_^~", " /| "],
|
|
14015
|
+
attack: [" ~/~\\ ", " ~>_<~", " /| "],
|
|
14016
|
+
hit: [" ~ ~ ", " ~x_x~", " /| "],
|
|
14017
|
+
win: ["\\~ ~/", " ~^o~ ", " /\\ "],
|
|
14018
|
+
lose: [" ~ ~ ", " ~;_;~", " | "]
|
|
14019
|
+
}
|
|
14020
|
+
};
|
|
14021
|
+
var hsienKo = {
|
|
14022
|
+
id: "hsienKo",
|
|
14023
|
+
name: "Hsien-Ko",
|
|
14024
|
+
description: "Diagonal staircase",
|
|
14025
|
+
damageModifier: 1,
|
|
14026
|
+
dropPattern: [
|
|
14027
|
+
[R, G, B, Y, R, G],
|
|
14028
|
+
[G, B, Y, R, G, B],
|
|
14029
|
+
[B, Y, R, G, B, Y],
|
|
14030
|
+
[Y, R, G, B, Y, R]
|
|
14031
|
+
],
|
|
14032
|
+
portraits: {
|
|
14033
|
+
idle: [" == ", " |o_o|", " /| "],
|
|
14034
|
+
attack: [" ==\\ ", " |o_o|", " /|= "],
|
|
14035
|
+
hit: [" == ", " |x_x|", " /| "],
|
|
14036
|
+
win: [" \\==/ ", " |^o^|", " /\\ "],
|
|
14037
|
+
lose: [" == ", " |;_;|", " | "]
|
|
14038
|
+
}
|
|
14039
|
+
};
|
|
14040
|
+
var felicia = {
|
|
14041
|
+
id: "felicia",
|
|
14042
|
+
name: "Felicia",
|
|
14043
|
+
description: "Fixed edges, swap mid",
|
|
14044
|
+
damageModifier: 1,
|
|
14045
|
+
dropPattern: [
|
|
14046
|
+
[G, R, B, R, B, Y],
|
|
14047
|
+
[G, B, R, B, R, Y],
|
|
14048
|
+
[Y, R, B, R, B, G],
|
|
14049
|
+
[Y, B, R, B, R, G]
|
|
14050
|
+
],
|
|
14051
|
+
portraits: {
|
|
14052
|
+
idle: [" /\\/\\ ", " =^w^=", " /| "],
|
|
14053
|
+
attack: [" /\\/\\\\", " =^o^=", " /|= "],
|
|
14054
|
+
hit: [" /\\/\\ ", " =x_x=", " /| "],
|
|
14055
|
+
win: ["\\/\\/\\/", " =^w^=", " /\\ "],
|
|
14056
|
+
lose: [" /\\/\\ ", " =;w;=", " | "]
|
|
14057
|
+
}
|
|
14058
|
+
};
|
|
14059
|
+
var donovan = {
|
|
14060
|
+
id: "donovan",
|
|
14061
|
+
name: "Donovan",
|
|
14062
|
+
description: "3-col halves + alt base",
|
|
14063
|
+
damageModifier: 1,
|
|
14064
|
+
dropPattern: [
|
|
14065
|
+
[R, R, R, G, G, G],
|
|
14066
|
+
[R, R, R, G, G, G],
|
|
14067
|
+
[B, B, B, Y, Y, Y],
|
|
14068
|
+
[R, G, B, R, G, B]
|
|
14069
|
+
],
|
|
14070
|
+
portraits: {
|
|
14071
|
+
idle: [" || ", " #-_-#", " /| "],
|
|
14072
|
+
attack: [" ||/ ", " #>_<#", " /|= "],
|
|
14073
|
+
hit: [" || ", " #x_x#", " /| "],
|
|
14074
|
+
win: [" \\||/ ", " #^o^#", " /\\ "],
|
|
14075
|
+
lose: [" || ", " #;_;#", " | "]
|
|
14076
|
+
}
|
|
14077
|
+
};
|
|
14078
|
+
var dan = {
|
|
14079
|
+
id: "dan",
|
|
14080
|
+
name: "Dan",
|
|
14081
|
+
description: "ALL RED (joke char)",
|
|
14082
|
+
damageModifier: 1,
|
|
14083
|
+
dropPattern: [
|
|
14084
|
+
[R, R, R, R, R, R],
|
|
14085
|
+
[R, R, R, R, R, R],
|
|
14086
|
+
[R, R, R, R, R, R],
|
|
14087
|
+
[R, R, R, R, R, R]
|
|
14088
|
+
],
|
|
14089
|
+
portraits: {
|
|
14090
|
+
idle: [" ^^ ", " (?_?)", " /| "],
|
|
14091
|
+
attack: [" ^^! ", " (!o!)", " /|~ "],
|
|
14092
|
+
hit: [" ^^ ", " (x_x)", " /| "],
|
|
14093
|
+
win: [" \\^^/ ", " (^o^)", " /\\ "],
|
|
14094
|
+
lose: [" ^^ ", " (T_T)", " | "]
|
|
14095
|
+
}
|
|
14096
|
+
};
|
|
14097
|
+
var akuma = {
|
|
14098
|
+
id: "akuma",
|
|
14099
|
+
name: "Akuma",
|
|
14100
|
+
description: "Diagonal rainbow cycle",
|
|
14101
|
+
damageModifier: 0.7,
|
|
14102
|
+
dropPattern: [
|
|
14103
|
+
[R, Y, B, G, R, Y],
|
|
14104
|
+
[Y, B, G, R, Y, B],
|
|
14105
|
+
[B, G, R, Y, B, G],
|
|
14106
|
+
[G, R, Y, B, G, R]
|
|
14107
|
+
],
|
|
14108
|
+
portraits: {
|
|
14109
|
+
idle: [" /MM\\ ", " !>_<!", " /| "],
|
|
14110
|
+
attack: [" /MM\\|", " !>o<!", " =/| "],
|
|
14111
|
+
hit: [" /MM\\ ", " !x_x!", " /| "],
|
|
14112
|
+
win: ["\\/MM\\/", " !^_^!", " /\\ "],
|
|
14113
|
+
lose: [" /MM\\ ", " !;_;!", " | "]
|
|
14114
|
+
}
|
|
14115
|
+
};
|
|
14116
|
+
var devilotte = {
|
|
14117
|
+
id: "devilotte",
|
|
14118
|
+
name: "Devilotte",
|
|
14119
|
+
description: "Reverse diagonal rainbow",
|
|
14120
|
+
damageModifier: 0.7,
|
|
14121
|
+
dropPattern: [
|
|
14122
|
+
[G, B, Y, R, G, B],
|
|
14123
|
+
[B, Y, R, G, B, Y],
|
|
14124
|
+
[Y, R, G, B, Y, R],
|
|
14125
|
+
[R, G, B, Y, R, G]
|
|
14126
|
+
],
|
|
14127
|
+
portraits: {
|
|
14128
|
+
idle: [" vVv ", " $v_v$", " /| "],
|
|
14129
|
+
attack: [" vVv\\", " $>_<$", " /|= "],
|
|
14130
|
+
hit: [" vVv ", " $x_x$", " /| "],
|
|
14131
|
+
win: [" \\vVv/", " $^_^$", " /\\ "],
|
|
14132
|
+
lose: [" vVv ", " $;_;$", " | "]
|
|
14133
|
+
}
|
|
14134
|
+
};
|
|
14135
|
+
var CHARACTERS = [
|
|
14136
|
+
ryu,
|
|
14137
|
+
ken,
|
|
14138
|
+
chunLi,
|
|
14139
|
+
sakura,
|
|
14140
|
+
morrigan,
|
|
14141
|
+
hsienKo,
|
|
14142
|
+
felicia,
|
|
14143
|
+
donovan,
|
|
14144
|
+
dan,
|
|
14145
|
+
akuma,
|
|
14146
|
+
devilotte
|
|
14147
|
+
];
|
|
14148
|
+
var CHAR_GRID = [
|
|
14149
|
+
[0, 1, 2, 3],
|
|
14150
|
+
[4, 5, 6, 7],
|
|
14151
|
+
[8, 9, 10]
|
|
14152
|
+
];
|
|
14153
|
+
function getRandomCharacter() {
|
|
14154
|
+
return CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
|
|
14155
|
+
}
|
|
14156
|
+
|
|
14157
|
+
// src/games/hyper-fighter/index.ts
|
|
14158
|
+
var TICK_MS = 50;
|
|
14159
|
+
var MIN_COLS = 60;
|
|
14160
|
+
var MIN_ROWS = 36;
|
|
14161
|
+
var VS_COL_WIDTH = 12;
|
|
14162
|
+
var SIDE_PANEL_WIDTH = 14;
|
|
14163
|
+
var HEADER_HEIGHT = 2;
|
|
14164
|
+
var FOOTER_HEIGHT = 2;
|
|
14165
|
+
var MIN_CELL_WIDTH = 3;
|
|
14166
|
+
var MAX_CELL_WIDTH = 6;
|
|
14167
|
+
var MIN_CELL_HEIGHT = 2;
|
|
14168
|
+
var MAX_CELL_HEIGHT = 4;
|
|
14169
|
+
var GEM_COLORS = {
|
|
14170
|
+
red: "\x1B[1;38;5;196m",
|
|
14171
|
+
green: "\x1B[1;38;5;46m",
|
|
14172
|
+
blue: "\x1B[1;38;5;27m",
|
|
14173
|
+
yellow: "\x1B[1;38;5;226m"
|
|
14174
|
+
};
|
|
14175
|
+
var COUNTER_BG_COLORS = {
|
|
14176
|
+
red: "\x1B[48;5;196m",
|
|
14177
|
+
green: "\x1B[48;5;34m",
|
|
14178
|
+
blue: "\x1B[48;5;27m",
|
|
14179
|
+
yellow: "\x1B[48;5;178m"
|
|
14180
|
+
};
|
|
14181
|
+
var BASE_DROP_SPEED = 16;
|
|
14182
|
+
var MIN_DROP_SPEED = 3;
|
|
14183
|
+
var SPEED_RAMP_DROPS = 8;
|
|
14184
|
+
var PHASE_NONE = 0;
|
|
14185
|
+
var PHASE_FLASH = 1;
|
|
14186
|
+
var PHASE_DISSOLVE = 2;
|
|
14187
|
+
var PHASE_GRAVITY = 3;
|
|
14188
|
+
var PHASE_CHECK = 4;
|
|
14189
|
+
var PHASE_GARBAGE = 5;
|
|
14190
|
+
var FLASH_FRAMES = 6;
|
|
14191
|
+
var DISSOLVE_FRAMES = 4;
|
|
14192
|
+
var GRAVITY_FRAMES = 1;
|
|
14193
|
+
var COUNTER_TIMER_NORMAL = 5;
|
|
14194
|
+
var COUNTER_TIMER_DEFENDED = 3;
|
|
14195
|
+
var GARBAGE_DROP_STEP_FRAMES = 2;
|
|
14196
|
+
function runHyperFighterGame(terminal) {
|
|
14197
|
+
const themeColor = getCurrentThemeColor();
|
|
14198
|
+
let running = true;
|
|
14199
|
+
let gameState = "difficulty";
|
|
14200
|
+
let difficultySelection = 1;
|
|
14201
|
+
let pauseMenuSelection = 0;
|
|
14202
|
+
let p1;
|
|
14203
|
+
let p2;
|
|
14204
|
+
let aiState;
|
|
14205
|
+
let selectedDifficulty = DIFFICULTIES2.normal;
|
|
14206
|
+
let p1Character = CHARACTERS[0];
|
|
14207
|
+
let p2Character = getRandomCharacter();
|
|
14208
|
+
let charGridRow = 0;
|
|
14209
|
+
let charGridCol = 0;
|
|
14210
|
+
let p1DropTimer = 0;
|
|
14211
|
+
let p2DropTimer = 0;
|
|
14212
|
+
let p1DropSpeed = BASE_DROP_SPEED;
|
|
14213
|
+
let p2DropSpeed = BASE_DROP_SPEED;
|
|
14214
|
+
let p1Phase = PHASE_NONE;
|
|
14215
|
+
let p2Phase = PHASE_NONE;
|
|
14216
|
+
let p1PhaseTimer = 0;
|
|
14217
|
+
let p2PhaseTimer = 0;
|
|
14218
|
+
let p1ClearedCells = [];
|
|
14219
|
+
let p2ClearedCells = [];
|
|
14220
|
+
let p1ChainCount = 0;
|
|
14221
|
+
let p2ChainCount = 0;
|
|
14222
|
+
let p1TotalCleared = 0;
|
|
14223
|
+
let p2TotalCleared = 0;
|
|
14224
|
+
let p1GarbageDrop = null;
|
|
14225
|
+
let p2GarbageDrop = null;
|
|
14226
|
+
let p1GarbagePatternCursor = 0;
|
|
14227
|
+
let p2GarbagePatternCursor = 0;
|
|
14228
|
+
let p1IsDiamondClear = false;
|
|
14229
|
+
let p2IsDiamondClear = false;
|
|
14230
|
+
let p1AttackAccum = 0;
|
|
14231
|
+
let p2AttackAccum = 0;
|
|
14232
|
+
let particles = [];
|
|
14233
|
+
let floatingTexts = [];
|
|
14234
|
+
let projectiles = [];
|
|
14235
|
+
let p1Shake = { intensity: 0, frames: 0 };
|
|
14236
|
+
let p2Shake = { intensity: 0, frames: 0 };
|
|
14237
|
+
let p1Flash = { color: "", frames: 0 };
|
|
14238
|
+
let p2Flash = { color: "", frames: 0 };
|
|
14239
|
+
let p1Pose = "idle";
|
|
14240
|
+
let p2Pose = "idle";
|
|
14241
|
+
let p1PoseTimer = 0;
|
|
14242
|
+
let p2PoseTimer = 0;
|
|
14243
|
+
let winner = 0;
|
|
14244
|
+
let gameOverTimer = 0;
|
|
14245
|
+
let glitchFrame = 0;
|
|
14246
|
+
let cellWidth = 3;
|
|
14247
|
+
let cellHeight = 2;
|
|
14248
|
+
let boardDisplayWidth = BOARD_COLS * cellWidth + 2;
|
|
14249
|
+
let boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
|
|
14250
|
+
let cellEmpty = " ".repeat(cellWidth);
|
|
14251
|
+
let cellSolid = "\u2588".repeat(cellWidth);
|
|
14252
|
+
let cellPower = "\u2593".repeat(cellWidth);
|
|
14253
|
+
let cellGhost = "\u2591".repeat(cellWidth);
|
|
14254
|
+
let cellCrash = "\u25C6".repeat(cellWidth);
|
|
14255
|
+
let cellDiamond = "\u25C7".repeat(cellWidth);
|
|
14256
|
+
let cellEmptyDot = " ".repeat(Math.floor((cellWidth - 1) / 2)) + "\x1B[38;5;238m\xB7\x1B[0m" + " ".repeat(cellWidth - Math.floor((cellWidth - 1) / 2) - 1);
|
|
14257
|
+
let showSidePanels = false;
|
|
14258
|
+
let sidePanel1X = 0;
|
|
14259
|
+
let sidePanel2X = 0;
|
|
14260
|
+
let boardLeft1 = 2;
|
|
14261
|
+
let boardLeft2 = 28;
|
|
14262
|
+
let vsColX = 16;
|
|
14263
|
+
let boardTop = 3;
|
|
14264
|
+
function recalcCellStrings() {
|
|
14265
|
+
boardDisplayWidth = BOARD_COLS * cellWidth + 2;
|
|
14266
|
+
boardDisplayHeight = BOARD_ROWS * cellHeight + 2;
|
|
14267
|
+
cellEmpty = " ".repeat(cellWidth);
|
|
14268
|
+
cellSolid = "\u2588".repeat(cellWidth);
|
|
14269
|
+
cellPower = "\u2593".repeat(cellWidth);
|
|
14270
|
+
cellGhost = "\u2591".repeat(cellWidth);
|
|
14271
|
+
cellCrash = "\u25C6".repeat(cellWidth);
|
|
14272
|
+
cellDiamond = "\u25C7".repeat(cellWidth);
|
|
14273
|
+
const padLeft = Math.floor((cellWidth - 1) / 2);
|
|
14274
|
+
cellEmptyDot = " ".repeat(padLeft) + "\xB7" + " ".repeat(cellWidth - padLeft - 1);
|
|
14275
|
+
}
|
|
14276
|
+
const controller = {
|
|
14277
|
+
stop: () => {
|
|
14278
|
+
if (!running) return;
|
|
14279
|
+
running = false;
|
|
14280
|
+
},
|
|
14281
|
+
get isRunning() {
|
|
14282
|
+
return running;
|
|
14283
|
+
}
|
|
14284
|
+
};
|
|
14285
|
+
const title = [
|
|
14286
|
+
"\u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2588",
|
|
14287
|
+
"\u2588\u2580\u2588 \u2580\u2584\u2580 \u2588\u2580\u2580 \u2588\u2588\u2584 \u2588\u2580\u2584",
|
|
14288
|
+
"\u2588\u2580\u2580 \u2588 \u2588\u2580\u2580 \u2588 \u2588 \u2580\u2588\u2580 \u2588\u2580\u2580 \u2588\u2580\u2588",
|
|
14289
|
+
"\u2588\u2580 \u2588 \u2588 \u2588 \u2588\u2580\u2588 \u2588 \u2588\u2588\u2584 \u2588\u2580\u2584"
|
|
14290
|
+
];
|
|
14291
|
+
const GLITCH_CHARS2 = "!@#$%^&*\u2591\u2592\u2593\u2588\u2580\u2584";
|
|
14292
|
+
function initGame() {
|
|
14293
|
+
p1 = createPlayerState();
|
|
14294
|
+
p2 = createPlayerState();
|
|
14295
|
+
aiState = createAIState(selectedDifficulty);
|
|
14296
|
+
p1DropTimer = 0;
|
|
14297
|
+
p2DropTimer = 0;
|
|
14298
|
+
p1DropSpeed = BASE_DROP_SPEED;
|
|
14299
|
+
p2DropSpeed = BASE_DROP_SPEED;
|
|
14300
|
+
p1Phase = PHASE_NONE;
|
|
14301
|
+
p2Phase = PHASE_NONE;
|
|
14302
|
+
p1PhaseTimer = 0;
|
|
14303
|
+
p2PhaseTimer = 0;
|
|
14304
|
+
p1ClearedCells = [];
|
|
14305
|
+
p2ClearedCells = [];
|
|
14306
|
+
p1ChainCount = 0;
|
|
14307
|
+
p2ChainCount = 0;
|
|
14308
|
+
p1TotalCleared = 0;
|
|
14309
|
+
p2TotalCleared = 0;
|
|
14310
|
+
p1GarbageDrop = null;
|
|
14311
|
+
p2GarbageDrop = null;
|
|
14312
|
+
p1GarbagePatternCursor = 0;
|
|
14313
|
+
p2GarbagePatternCursor = 0;
|
|
14314
|
+
p1IsDiamondClear = false;
|
|
14315
|
+
p2IsDiamondClear = false;
|
|
14316
|
+
p1AttackAccum = 0;
|
|
14317
|
+
p2AttackAccum = 0;
|
|
14318
|
+
particles = [];
|
|
14319
|
+
floatingTexts = [];
|
|
14320
|
+
projectiles = [];
|
|
14321
|
+
p1Shake = { intensity: 0, frames: 0 };
|
|
14322
|
+
p2Shake = { intensity: 0, frames: 0 };
|
|
14323
|
+
p1Flash = { color: "", frames: 0 };
|
|
14324
|
+
p2Flash = { color: "", frames: 0 };
|
|
14325
|
+
p1Pose = "idle";
|
|
14326
|
+
p2Pose = "idle";
|
|
14327
|
+
p1PoseTimer = 0;
|
|
14328
|
+
p2PoseTimer = 0;
|
|
14329
|
+
winner = 0;
|
|
14330
|
+
gameOverTimer = 0;
|
|
14331
|
+
pauseMenuSelection = 0;
|
|
14332
|
+
spawnPair(p1);
|
|
14333
|
+
spawnPair(p2);
|
|
14334
|
+
}
|
|
14335
|
+
function calculateLayout() {
|
|
14336
|
+
const cols = terminal.cols;
|
|
14337
|
+
const rows = terminal.rows;
|
|
14338
|
+
cellHeight = Math.max(MIN_CELL_HEIGHT, Math.min(
|
|
14339
|
+
MAX_CELL_HEIGHT,
|
|
14340
|
+
Math.floor((rows - HEADER_HEIGHT - FOOTER_HEIGHT - 2 - 2) / BOARD_ROWS)
|
|
14341
|
+
));
|
|
14342
|
+
const availWidthWithPanels = cols - 2 * SIDE_PANEL_WIDTH - VS_COL_WIDTH - 4;
|
|
14343
|
+
const availWidthWithout = cols - VS_COL_WIDTH - 4;
|
|
14344
|
+
const cwWithPanels = Math.floor(availWidthWithPanels / (2 * BOARD_COLS));
|
|
14345
|
+
const cwWithout = Math.floor(availWidthWithout / (2 * BOARD_COLS));
|
|
14346
|
+
if (cwWithPanels >= MIN_CELL_WIDTH) {
|
|
14347
|
+
showSidePanels = true;
|
|
14348
|
+
cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithPanels));
|
|
14349
|
+
} else {
|
|
14350
|
+
showSidePanels = false;
|
|
14351
|
+
cellWidth = Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, cwWithout));
|
|
14352
|
+
}
|
|
14353
|
+
const maxAspectWidth = Math.floor(cellHeight * 1.5);
|
|
14354
|
+
if (cellWidth > maxAspectWidth && maxAspectWidth >= MIN_CELL_WIDTH) {
|
|
14355
|
+
cellWidth = maxAspectWidth;
|
|
14356
|
+
}
|
|
14357
|
+
recalcCellStrings();
|
|
14358
|
+
if (showSidePanels) {
|
|
14359
|
+
const totalWidth = SIDE_PANEL_WIDTH + boardDisplayWidth + VS_COL_WIDTH + boardDisplayWidth + SIDE_PANEL_WIDTH;
|
|
14360
|
+
const startX = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
|
|
14361
|
+
sidePanel1X = startX;
|
|
14362
|
+
boardLeft1 = startX + SIDE_PANEL_WIDTH;
|
|
14363
|
+
vsColX = boardLeft1 + boardDisplayWidth + 1;
|
|
14364
|
+
boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
|
|
14365
|
+
sidePanel2X = boardLeft2 + boardDisplayWidth;
|
|
14366
|
+
} else {
|
|
14367
|
+
const totalWidth = boardDisplayWidth * 2 + VS_COL_WIDTH;
|
|
14368
|
+
boardLeft1 = Math.max(1, Math.floor((cols - totalWidth) / 2) + 1);
|
|
14369
|
+
boardLeft2 = boardLeft1 + boardDisplayWidth + VS_COL_WIDTH;
|
|
14370
|
+
vsColX = boardLeft1 + boardDisplayWidth + 1;
|
|
14371
|
+
sidePanel1X = 0;
|
|
14372
|
+
sidePanel2X = 0;
|
|
14373
|
+
}
|
|
14374
|
+
const availHeight = rows - HEADER_HEIGHT - FOOTER_HEIGHT;
|
|
14375
|
+
boardTop = HEADER_HEIGHT + Math.max(1, Math.floor((availHeight - boardDisplayHeight) / 2));
|
|
14376
|
+
}
|
|
14377
|
+
function getDropSpeed(player) {
|
|
14378
|
+
const ramp = Math.floor(player.totalDrops / SPEED_RAMP_DROPS);
|
|
14379
|
+
return Math.max(MIN_DROP_SPEED, BASE_DROP_SPEED - ramp);
|
|
14380
|
+
}
|
|
14381
|
+
function startResolution(player, isP1) {
|
|
14382
|
+
if (isP1) p1AttackAccum = 0;
|
|
14383
|
+
else p2AttackAccum = 0;
|
|
14384
|
+
const diamondCleared = resolveDiamond(player.board);
|
|
14385
|
+
if (diamondCleared.length > 0) {
|
|
14386
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14387
|
+
if (isP1) p1IsDiamondClear = true;
|
|
14388
|
+
else p2IsDiamondClear = true;
|
|
14389
|
+
} else {
|
|
14390
|
+
if (isP1) p1IsDiamondClear = false;
|
|
14391
|
+
else p2IsDiamondClear = false;
|
|
14392
|
+
}
|
|
14393
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14394
|
+
const { cleared, clearedCells: crashCells, powerGemSizes } = resolveOneStep(player);
|
|
14395
|
+
const clearedCells = [...diamondCleared, ...crashCells];
|
|
14396
|
+
const totalStepCleared = cleared + diamondCleared.length;
|
|
14397
|
+
if (clearedCells.length === 0) {
|
|
14398
|
+
finishResolution(player, isP1);
|
|
14399
|
+
return;
|
|
14400
|
+
}
|
|
14401
|
+
if (isP1) {
|
|
14402
|
+
p1ClearedCells = clearedCells;
|
|
14403
|
+
p1ChainCount++;
|
|
14404
|
+
p1TotalCleared += totalStepCleared;
|
|
14405
|
+
p1Phase = PHASE_FLASH;
|
|
14406
|
+
p1PhaseTimer = FLASH_FRAMES;
|
|
14407
|
+
const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p1ChainCount };
|
|
14408
|
+
p1AttackAccum += calculateStepAttack(stepInfo);
|
|
14409
|
+
} else {
|
|
14410
|
+
p2ClearedCells = clearedCells;
|
|
14411
|
+
p2ChainCount++;
|
|
14412
|
+
p2TotalCleared += totalStepCleared;
|
|
14413
|
+
p2Phase = PHASE_FLASH;
|
|
14414
|
+
p2PhaseTimer = FLASH_FRAMES;
|
|
14415
|
+
const stepInfo = { gemsCleared: totalStepCleared, powerGemSizes, chainStep: p2ChainCount };
|
|
14416
|
+
p2AttackAccum += calculateStepAttack(stepInfo);
|
|
14417
|
+
}
|
|
14418
|
+
const bLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14419
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14420
|
+
if (chainCount >= 1) {
|
|
14421
|
+
spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
|
|
14422
|
+
if (chainCount >= 2) {
|
|
14423
|
+
spawnChainCounter(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 3, floatingTexts);
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14426
|
+
for (const cell of clearedCells) {
|
|
14427
|
+
const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
|
|
14428
|
+
const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
|
|
14429
|
+
spawnClearParticles(px, py, cell.color, 3, particles);
|
|
14430
|
+
}
|
|
14431
|
+
const shakeAmount = Math.min(3, Math.ceil(clearedCells.length / 4));
|
|
14432
|
+
if (isP1) {
|
|
14433
|
+
triggerShake(p1Shake, shakeAmount, 6);
|
|
14434
|
+
triggerFlash(p1Flash, "\x1B[97m", 4);
|
|
14435
|
+
p1Pose = "attack";
|
|
14436
|
+
p1PoseTimer = 12;
|
|
14437
|
+
} else {
|
|
14438
|
+
triggerShake(p2Shake, shakeAmount, 6);
|
|
14439
|
+
triggerFlash(p2Flash, "\x1B[97m", 4);
|
|
14440
|
+
p2Pose = "attack";
|
|
14441
|
+
p2PoseTimer = 12;
|
|
14442
|
+
}
|
|
14443
|
+
}
|
|
14444
|
+
function resolveOneStep(player) {
|
|
14445
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14446
|
+
const { targets, destroyedPowerGemSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14447
|
+
if (targets.length === 0) return { cleared: 0, chains: 0, clearedCells: [], powerGemSizes: [] };
|
|
14448
|
+
for (const t of targets) {
|
|
14449
|
+
player.board[t.row][t.col] = null;
|
|
14450
|
+
}
|
|
14451
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14452
|
+
player.powerGems = [];
|
|
14453
|
+
return { cleared: targets.length, chains: 1, clearedCells: targets, powerGemSizes: destroyedPowerGemSizes };
|
|
14454
|
+
}
|
|
14455
|
+
function buildGarbageDropState(player, defendedCount, normalCount, startCursor, dropPattern) {
|
|
14456
|
+
const occupied = player.board.map((row) => row.map((cell) => cell !== null));
|
|
14457
|
+
const timers = [];
|
|
14458
|
+
for (let i = 0; i < defendedCount; i++) timers.push(COUNTER_TIMER_DEFENDED);
|
|
14459
|
+
for (let i = 0; i < normalCount; i++) timers.push(COUNTER_TIMER_NORMAL);
|
|
14460
|
+
const gems = [];
|
|
14461
|
+
const patternRows = dropPattern.length;
|
|
14462
|
+
const patternCols = dropPattern[0].length;
|
|
14463
|
+
const patternLen = patternRows * patternCols;
|
|
14464
|
+
let placedCount = 0;
|
|
14465
|
+
function findLandingRow(col) {
|
|
14466
|
+
for (let r = BOARD_ROWS - 1; r >= 0; r--) {
|
|
14467
|
+
if (!occupied[r][col]) return r;
|
|
14468
|
+
}
|
|
14469
|
+
return -1;
|
|
14470
|
+
}
|
|
14471
|
+
for (let i = 0; i < timers.length; i++) {
|
|
14472
|
+
const patternIdx = (startCursor + i) % patternLen;
|
|
14473
|
+
const pRow = Math.floor(patternIdx / patternCols);
|
|
14474
|
+
const pCol = patternIdx % patternCols;
|
|
14475
|
+
const color = dropPattern[pRow][pCol];
|
|
14476
|
+
let placeCol = -1;
|
|
14477
|
+
let placeRow = -1;
|
|
14478
|
+
const landing = findLandingRow(pCol);
|
|
14479
|
+
if (landing >= 0) {
|
|
14480
|
+
placeCol = pCol;
|
|
14481
|
+
placeRow = landing;
|
|
14482
|
+
} else {
|
|
14483
|
+
for (let offset = 1; offset < BOARD_COLS; offset++) {
|
|
14484
|
+
for (const dir of [1, -1]) {
|
|
14485
|
+
const adjCol = pCol + offset * dir;
|
|
14486
|
+
if (adjCol >= 0 && adjCol < BOARD_COLS) {
|
|
14487
|
+
const adjLanding = findLandingRow(adjCol);
|
|
14488
|
+
if (adjLanding >= 0) {
|
|
14489
|
+
placeCol = adjCol;
|
|
14490
|
+
placeRow = adjLanding;
|
|
14491
|
+
break;
|
|
14492
|
+
}
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14495
|
+
if (placeRow >= 0) break;
|
|
14496
|
+
}
|
|
14497
|
+
}
|
|
14498
|
+
if (placeRow < 0 || placeCol < 0) continue;
|
|
14499
|
+
occupied[placeRow][placeCol] = true;
|
|
14500
|
+
gems.push({
|
|
14501
|
+
col: placeCol,
|
|
14502
|
+
targetRow: placeRow,
|
|
14503
|
+
currentRow: 0,
|
|
14504
|
+
timer: timers[i],
|
|
14505
|
+
color,
|
|
14506
|
+
delayFrames: i
|
|
14507
|
+
});
|
|
14508
|
+
placedCount++;
|
|
14509
|
+
}
|
|
14510
|
+
if (gems.length === 0) {
|
|
14511
|
+
return { dropState: null, nextCursor: startCursor };
|
|
14512
|
+
}
|
|
14513
|
+
const nextCursor = (startCursor + placedCount) % patternLen;
|
|
14514
|
+
return { dropState: { gems, frameTick: 0 }, nextCursor };
|
|
14515
|
+
}
|
|
14516
|
+
function triggerLoss(isP1) {
|
|
14517
|
+
const loser = isP1 ? p1 : p2;
|
|
14518
|
+
loser.alive = false;
|
|
14519
|
+
gameState = "gameOver";
|
|
14520
|
+
winner = isP1 ? 2 : 1;
|
|
14521
|
+
gameOverTimer = 0;
|
|
14522
|
+
pauseMenuSelection = 0;
|
|
14523
|
+
if (isP1) {
|
|
14524
|
+
p1Pose = "lose";
|
|
14525
|
+
p2Pose = "win";
|
|
14526
|
+
} else {
|
|
14527
|
+
p2Pose = "lose";
|
|
14528
|
+
p1Pose = "win";
|
|
14529
|
+
}
|
|
14530
|
+
const loserLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14531
|
+
const winnerLeft = isP1 ? boardLeft2 : boardLeft1;
|
|
14532
|
+
spawnCollapse(loserLeft, boardTop, boardDisplayWidth, BOARD_ROWS * cellHeight, particles);
|
|
14533
|
+
spawnFirework2(winnerLeft + boardDisplayWidth / 2, boardTop + 3, particles);
|
|
14534
|
+
}
|
|
14535
|
+
function finalizePostResolution(player, isP1) {
|
|
14536
|
+
if (checkGameOver(player.board)) {
|
|
14537
|
+
triggerLoss(isP1);
|
|
14538
|
+
return;
|
|
14539
|
+
}
|
|
14540
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14541
|
+
if (!spawnPair(player)) {
|
|
14542
|
+
triggerLoss(isP1);
|
|
14543
|
+
}
|
|
14544
|
+
}
|
|
14545
|
+
function tickGarbageDrop(player, isP1) {
|
|
14546
|
+
const dropState = isP1 ? p1GarbageDrop : p2GarbageDrop;
|
|
14547
|
+
if (!dropState) {
|
|
14548
|
+
if (isP1) p1Phase = PHASE_NONE;
|
|
14549
|
+
else p2Phase = PHASE_NONE;
|
|
14550
|
+
finalizePostResolution(player, isP1);
|
|
14551
|
+
return;
|
|
14552
|
+
}
|
|
14553
|
+
dropState.frameTick++;
|
|
14554
|
+
if (dropState.frameTick < GARBAGE_DROP_STEP_FRAMES) return;
|
|
14555
|
+
dropState.frameTick = 0;
|
|
14556
|
+
let stillDropping = false;
|
|
14557
|
+
for (const gem of dropState.gems) {
|
|
14558
|
+
if (gem.delayFrames > 0) {
|
|
14559
|
+
gem.delayFrames--;
|
|
14560
|
+
stillDropping = true;
|
|
14561
|
+
continue;
|
|
14562
|
+
}
|
|
14563
|
+
if (gem.currentRow < gem.targetRow) {
|
|
14564
|
+
gem.currentRow++;
|
|
14565
|
+
stillDropping = true;
|
|
14566
|
+
}
|
|
14567
|
+
}
|
|
14568
|
+
if (stillDropping) return;
|
|
14569
|
+
for (const gem of dropState.gems) {
|
|
14570
|
+
if (player.board[gem.targetRow][gem.col] !== null) continue;
|
|
14571
|
+
player.board[gem.targetRow][gem.col] = {
|
|
14572
|
+
color: gem.color,
|
|
14573
|
+
type: "counter",
|
|
14574
|
+
counterTimer: gem.timer
|
|
14575
|
+
};
|
|
14576
|
+
}
|
|
14577
|
+
if (isP1) {
|
|
14578
|
+
p1GarbageDrop = null;
|
|
14579
|
+
p1Phase = PHASE_NONE;
|
|
14580
|
+
} else {
|
|
14581
|
+
p2GarbageDrop = null;
|
|
14582
|
+
p2Phase = PHASE_NONE;
|
|
14583
|
+
}
|
|
14584
|
+
finalizePostResolution(player, isP1);
|
|
14585
|
+
}
|
|
14586
|
+
function tickResolution(isP1) {
|
|
14587
|
+
const phase = isP1 ? p1Phase : p2Phase;
|
|
14588
|
+
const timer = isP1 ? p1PhaseTimer : p2PhaseTimer;
|
|
14589
|
+
const player = isP1 ? p1 : p2;
|
|
14590
|
+
if (phase === PHASE_NONE) return;
|
|
14591
|
+
if (timer > 0) {
|
|
14592
|
+
if (isP1) p1PhaseTimer--;
|
|
14593
|
+
else p2PhaseTimer--;
|
|
14594
|
+
return;
|
|
14595
|
+
}
|
|
14596
|
+
switch (phase) {
|
|
14597
|
+
case PHASE_FLASH:
|
|
14598
|
+
if (isP1) {
|
|
14599
|
+
p1Phase = PHASE_DISSOLVE;
|
|
14600
|
+
p1PhaseTimer = DISSOLVE_FRAMES;
|
|
14601
|
+
} else {
|
|
14602
|
+
p2Phase = PHASE_DISSOLVE;
|
|
14603
|
+
p2PhaseTimer = DISSOLVE_FRAMES;
|
|
14604
|
+
}
|
|
14605
|
+
break;
|
|
14606
|
+
case PHASE_DISSOLVE:
|
|
14607
|
+
if (isP1) {
|
|
14608
|
+
p1Phase = PHASE_GRAVITY;
|
|
14609
|
+
p1PhaseTimer = GRAVITY_FRAMES;
|
|
14610
|
+
} else {
|
|
14611
|
+
p2Phase = PHASE_GRAVITY;
|
|
14612
|
+
p2PhaseTimer = GRAVITY_FRAMES;
|
|
14613
|
+
}
|
|
14614
|
+
break;
|
|
14615
|
+
case PHASE_GRAVITY:
|
|
14616
|
+
if (isP1) {
|
|
14617
|
+
p1Phase = PHASE_CHECK;
|
|
14618
|
+
p1PhaseTimer = 0;
|
|
14619
|
+
} else {
|
|
14620
|
+
p2Phase = PHASE_CHECK;
|
|
14621
|
+
p2PhaseTimer = 0;
|
|
14622
|
+
}
|
|
14623
|
+
break;
|
|
14624
|
+
case PHASE_GARBAGE:
|
|
14625
|
+
tickGarbageDrop(player, isP1);
|
|
14626
|
+
break;
|
|
14627
|
+
case PHASE_CHECK: {
|
|
14628
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14629
|
+
const { targets: more, destroyedPowerGemSizes: pgSizes } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14630
|
+
if (more.length > 0) {
|
|
14631
|
+
for (const t of more) {
|
|
14632
|
+
player.board[t.row][t.col] = null;
|
|
14633
|
+
}
|
|
14634
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14635
|
+
player.powerGems = [];
|
|
14636
|
+
if (isP1) {
|
|
14637
|
+
p1ClearedCells = more;
|
|
14638
|
+
p1ChainCount++;
|
|
14639
|
+
p1TotalCleared += more.length;
|
|
14640
|
+
p1Phase = PHASE_FLASH;
|
|
14641
|
+
p1PhaseTimer = FLASH_FRAMES;
|
|
14642
|
+
const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p1ChainCount };
|
|
14643
|
+
p1AttackAccum += calculateStepAttack(stepInfo);
|
|
14644
|
+
} else {
|
|
14645
|
+
p2ClearedCells = more;
|
|
14646
|
+
p2ChainCount++;
|
|
14647
|
+
p2TotalCleared += more.length;
|
|
14648
|
+
p2Phase = PHASE_FLASH;
|
|
14649
|
+
p2PhaseTimer = FLASH_FRAMES;
|
|
14650
|
+
const stepInfo = { gemsCleared: more.length, powerGemSizes: pgSizes, chainStep: p2ChainCount };
|
|
14651
|
+
p2AttackAccum += calculateStepAttack(stepInfo);
|
|
14652
|
+
}
|
|
14653
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14654
|
+
const bLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14655
|
+
spawnComboText(chainCount, bLeft + boardDisplayWidth / 2, boardTop + 2, floatingTexts);
|
|
14656
|
+
for (const cell of more) {
|
|
14657
|
+
const px = bLeft + 1 + cell.col * cellWidth + Math.floor(cellWidth / 2);
|
|
14658
|
+
const py = boardTop + 1 + cell.row * cellHeight + Math.floor(cellHeight / 2);
|
|
14659
|
+
spawnClearParticles(px, py, cell.color, 4 + chainCount, particles);
|
|
14660
|
+
}
|
|
14661
|
+
triggerShake(isP1 ? p1Shake : p2Shake, Math.min(4, chainCount), 8);
|
|
14662
|
+
} else {
|
|
14663
|
+
const totalCleared = isP1 ? p1TotalCleared : p2TotalCleared;
|
|
14664
|
+
const chainCount = isP1 ? p1ChainCount : p2ChainCount;
|
|
14665
|
+
const dmgMod = isP1 ? p1Character.damageModifier : p2Character.damageModifier;
|
|
14666
|
+
const isDiamond = isP1 ? p1IsDiamondClear : p2IsDiamondClear;
|
|
14667
|
+
const rawAttack = isP1 ? p1AttackAccum : p2AttackAccum;
|
|
14668
|
+
let attack = applyAttackModifiers(rawAttack, dmgMod, isDiamond);
|
|
14669
|
+
if (attack > 0 && player.pendingGarbage > 0) {
|
|
14670
|
+
const defense = resolveCounterAttack(attack, player.pendingGarbage);
|
|
14671
|
+
attack = defense.remainingAttack;
|
|
14672
|
+
player.pendingGarbage = defense.remainingPending;
|
|
14673
|
+
player.pendingCounteredGarbage = defense.pendingStartsAtThree ? defense.remainingPending : 0;
|
|
14674
|
+
if (defense.canceledGems > 0) {
|
|
14675
|
+
const left = isP1 ? boardLeft1 : boardLeft2;
|
|
14676
|
+
floatingTexts.push({
|
|
14677
|
+
text: `DEFENSE -${defense.canceledGems}`,
|
|
14678
|
+
x: left + 2,
|
|
14679
|
+
y: Math.max(1, boardTop - 1),
|
|
14680
|
+
color: "\x1B[96m",
|
|
14681
|
+
frames: 18,
|
|
14682
|
+
maxFrames: 18
|
|
14683
|
+
});
|
|
14684
|
+
}
|
|
14685
|
+
}
|
|
14686
|
+
if (attack > 0) {
|
|
14687
|
+
const opponent = isP1 ? p2 : p1;
|
|
14688
|
+
opponent.pendingGarbage += attack;
|
|
14689
|
+
const fromBLeft = isP1 ? boardLeft1 : boardLeft2;
|
|
14690
|
+
const toBLeft = isP1 ? boardLeft2 : boardLeft1;
|
|
14691
|
+
spawnProjectile(
|
|
14692
|
+
fromBLeft + boardDisplayWidth / 2,
|
|
14693
|
+
toBLeft + boardDisplayWidth / 2,
|
|
14694
|
+
boardTop + Math.floor(boardDisplayHeight / 2),
|
|
14695
|
+
attack,
|
|
14696
|
+
projectiles
|
|
14697
|
+
);
|
|
14698
|
+
if (isP1) {
|
|
14699
|
+
p2Pose = "hit";
|
|
14700
|
+
p2PoseTimer = 10;
|
|
14701
|
+
} else {
|
|
14702
|
+
p1Pose = "hit";
|
|
14703
|
+
p1PoseTimer = 10;
|
|
14704
|
+
}
|
|
14705
|
+
triggerFlash(isP1 ? p2Flash : p1Flash, "\x1B[91m", 6);
|
|
14706
|
+
triggerShake(isP1 ? p2Shake : p1Shake, Math.min(3, Math.ceil(attack / 2)), 6);
|
|
14707
|
+
}
|
|
14708
|
+
player.score += totalCleared * 10 + (chainCount > 1 ? chainCount * 50 : 0);
|
|
14709
|
+
finishResolution(player, isP1);
|
|
14710
|
+
}
|
|
14711
|
+
break;
|
|
14712
|
+
}
|
|
14713
|
+
}
|
|
14714
|
+
}
|
|
14715
|
+
function finishResolution(player, isP1) {
|
|
14716
|
+
if (isP1) {
|
|
14717
|
+
p1Phase = PHASE_NONE;
|
|
14718
|
+
p1PhaseTimer = 0;
|
|
14719
|
+
p1ChainCount = 0;
|
|
14720
|
+
p1TotalCleared = 0;
|
|
14721
|
+
p1ClearedCells = [];
|
|
14722
|
+
p1AttackAccum = 0;
|
|
14723
|
+
} else {
|
|
14724
|
+
p2Phase = PHASE_NONE;
|
|
14725
|
+
p2PhaseTimer = 0;
|
|
14726
|
+
p2ChainCount = 0;
|
|
14727
|
+
p2TotalCleared = 0;
|
|
14728
|
+
p2ClearedCells = [];
|
|
14729
|
+
p2AttackAccum = 0;
|
|
14730
|
+
}
|
|
14731
|
+
decrementCounters(player);
|
|
14732
|
+
player.powerGems = detectPowerGems(player.board);
|
|
14733
|
+
const { targets: postCounterTargets } = findCrashTargetsWithPowerInfo(player.board, player.powerGems);
|
|
14734
|
+
if (postCounterTargets.length > 0) {
|
|
14735
|
+
startResolution(player, isP1);
|
|
14736
|
+
return;
|
|
14737
|
+
}
|
|
14738
|
+
if (player.pendingGarbage > 0) {
|
|
14739
|
+
const incomingCount = player.pendingGarbage;
|
|
14740
|
+
const defendedCount = Math.min(player.pendingCounteredGarbage, player.pendingGarbage);
|
|
14741
|
+
const normalCount = player.pendingGarbage - defendedCount;
|
|
14742
|
+
const cursor = isP1 ? p1GarbagePatternCursor : p2GarbagePatternCursor;
|
|
14743
|
+
const attackerPattern = isP1 ? p2Character.dropPattern : p1Character.dropPattern;
|
|
14744
|
+
const { dropState, nextCursor } = buildGarbageDropState(player, defendedCount, normalCount, cursor, attackerPattern);
|
|
14745
|
+
if (isP1) p1GarbagePatternCursor = nextCursor;
|
|
14746
|
+
else p2GarbagePatternCursor = nextCursor;
|
|
14747
|
+
player.pendingGarbage = 0;
|
|
14748
|
+
player.pendingCounteredGarbage = 0;
|
|
14749
|
+
const left = isP1 ? boardLeft1 : boardLeft2;
|
|
14750
|
+
floatingTexts.push({
|
|
14751
|
+
text: `DROP +${incomingCount}`,
|
|
14752
|
+
x: left + 2,
|
|
14753
|
+
y: Math.max(1, boardTop - 1),
|
|
14754
|
+
color: "\x1B[1;91m",
|
|
14755
|
+
frames: 20,
|
|
14756
|
+
maxFrames: 20
|
|
14757
|
+
});
|
|
14758
|
+
if (dropState) {
|
|
14759
|
+
if (isP1) {
|
|
14760
|
+
p1GarbageDrop = dropState;
|
|
14761
|
+
p1Phase = PHASE_GARBAGE;
|
|
14762
|
+
p1PhaseTimer = 0;
|
|
14763
|
+
} else {
|
|
14764
|
+
p2GarbageDrop = dropState;
|
|
14765
|
+
p2Phase = PHASE_GARBAGE;
|
|
14766
|
+
p2PhaseTimer = 0;
|
|
14767
|
+
}
|
|
14768
|
+
return;
|
|
14769
|
+
}
|
|
14770
|
+
}
|
|
14771
|
+
finalizePostResolution(player, isP1);
|
|
14772
|
+
}
|
|
14773
|
+
function update() {
|
|
14774
|
+
if (gameState !== "running") return;
|
|
14775
|
+
glitchFrame++;
|
|
14776
|
+
updateParticles2(particles);
|
|
14777
|
+
updateFloatingTexts(floatingTexts);
|
|
14778
|
+
updateProjectiles(projectiles);
|
|
14779
|
+
if (p1PoseTimer > 0) {
|
|
14780
|
+
p1PoseTimer--;
|
|
14781
|
+
if (p1PoseTimer <= 0) p1Pose = "idle";
|
|
14782
|
+
}
|
|
14783
|
+
if (p2PoseTimer > 0) {
|
|
14784
|
+
p2PoseTimer--;
|
|
14785
|
+
if (p2PoseTimer <= 0) p2Pose = "idle";
|
|
14786
|
+
}
|
|
14787
|
+
if (p1Phase !== PHASE_NONE) {
|
|
14788
|
+
tickResolution(true);
|
|
14789
|
+
}
|
|
14790
|
+
if (p2Phase !== PHASE_NONE) {
|
|
14791
|
+
tickResolution(false);
|
|
14792
|
+
}
|
|
14793
|
+
if (p1Phase === PHASE_NONE && p1.currentPair && p1.alive) {
|
|
14794
|
+
p1DropSpeed = getDropSpeed(p1);
|
|
14795
|
+
p1DropTimer++;
|
|
14796
|
+
if (p1DropTimer >= p1DropSpeed) {
|
|
14797
|
+
p1DropTimer = 0;
|
|
14798
|
+
if (!dropPair(p1.currentPair, p1.board)) {
|
|
14799
|
+
lockAndResolve(p1, true);
|
|
14800
|
+
}
|
|
14801
|
+
}
|
|
14802
|
+
}
|
|
14803
|
+
if (p2Phase === PHASE_NONE && p2.currentPair && p2.alive) {
|
|
14804
|
+
p2DropSpeed = Math.max(MIN_DROP_SPEED, Math.floor(getDropSpeed(p2) / aiState.difficulty.dropSpeedBoost));
|
|
14805
|
+
const action = aiTick(p2, aiState);
|
|
14806
|
+
switch (action) {
|
|
14807
|
+
case "rotate_cw":
|
|
14808
|
+
rotatePair(p2.currentPair, p2.board, true);
|
|
14809
|
+
break;
|
|
14810
|
+
case "rotate_ccw":
|
|
14811
|
+
rotatePair(p2.currentPair, p2.board, false);
|
|
14812
|
+
break;
|
|
14813
|
+
case "move": {
|
|
14814
|
+
const dir = getAIMoveDirection(p2, aiState);
|
|
14815
|
+
if (dir !== 0) movePair(p2.currentPair, p2.board, dir);
|
|
14816
|
+
break;
|
|
14817
|
+
}
|
|
14818
|
+
case "drop":
|
|
14819
|
+
if (!dropPair(p2.currentPair, p2.board)) {
|
|
14820
|
+
lockAndResolve(p2, false);
|
|
14821
|
+
}
|
|
14822
|
+
break;
|
|
14823
|
+
}
|
|
14824
|
+
p2DropTimer++;
|
|
14825
|
+
if (p2DropTimer >= p2DropSpeed) {
|
|
14826
|
+
p2DropTimer = 0;
|
|
14827
|
+
if (p2.currentPair && !dropPair(p2.currentPair, p2.board)) {
|
|
14828
|
+
lockAndResolve(p2, false);
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
}
|
|
14832
|
+
}
|
|
14833
|
+
function lockAndResolve(player, isP1) {
|
|
14834
|
+
if (!player.currentPair) return;
|
|
14835
|
+
lockPair(player.currentPair, player.board);
|
|
14836
|
+
player.currentPair = null;
|
|
14837
|
+
applyGravityFull(player.board, player.powerGems);
|
|
14838
|
+
if (!isP1) {
|
|
14839
|
+
aiState.decided = false;
|
|
14840
|
+
aiState.thinkTimer = 0;
|
|
14841
|
+
aiState.moveTimer = 0;
|
|
14842
|
+
}
|
|
14843
|
+
startResolution(player, isP1);
|
|
14844
|
+
}
|
|
14845
|
+
function render() {
|
|
14846
|
+
if (!running) return;
|
|
14847
|
+
const cols = terminal.cols;
|
|
14848
|
+
const rows = terminal.rows;
|
|
14849
|
+
if (cols < MIN_COLS || rows < MIN_ROWS) {
|
|
14850
|
+
let output2 = "\x1B[2J\x1B[H";
|
|
14851
|
+
output2 += `\x1B[${Math.floor(rows / 2)};${Math.max(1, Math.floor(cols / 2) - 10)}H`;
|
|
14852
|
+
output2 += `${themeColor}Need ${MIN_COLS}\xD7${MIN_ROWS} (have ${cols}\xD7${rows})\x1B[0m`;
|
|
14853
|
+
terminal.write(output2);
|
|
14854
|
+
return;
|
|
14855
|
+
}
|
|
14856
|
+
calculateLayout();
|
|
14857
|
+
let output = "\x1B[2J\x1B[H";
|
|
14858
|
+
switch (gameState) {
|
|
14859
|
+
case "difficulty":
|
|
14860
|
+
output += renderDifficultyScreen();
|
|
14861
|
+
break;
|
|
14862
|
+
case "characterSelect":
|
|
14863
|
+
output += renderCharacterSelectScreen();
|
|
14864
|
+
break;
|
|
14865
|
+
case "running":
|
|
14866
|
+
output += renderGame();
|
|
14867
|
+
break;
|
|
14868
|
+
case "paused":
|
|
14869
|
+
output += renderGame();
|
|
14870
|
+
output += renderPauseOverlay();
|
|
14871
|
+
break;
|
|
14872
|
+
case "gameOver":
|
|
14873
|
+
output += renderGame({
|
|
14874
|
+
showEffects: false,
|
|
14875
|
+
showHud: false,
|
|
14876
|
+
showControls: false,
|
|
14877
|
+
showVs: false
|
|
14878
|
+
});
|
|
14879
|
+
output += renderGameOverOverlay();
|
|
14880
|
+
break;
|
|
14881
|
+
}
|
|
14882
|
+
terminal.write(output);
|
|
14883
|
+
}
|
|
14884
|
+
function renderDifficultyScreen() {
|
|
14885
|
+
const cols = terminal.cols;
|
|
14886
|
+
const rows = terminal.rows;
|
|
14887
|
+
const centerX = Math.floor(cols / 2);
|
|
14888
|
+
let output = "";
|
|
14889
|
+
const titleY = Math.max(2, Math.floor(rows / 2) - 8);
|
|
14890
|
+
for (let i = 0; i < title.length; i++) {
|
|
14891
|
+
let line = title[i];
|
|
14892
|
+
if (Math.random() < 0.15) {
|
|
14893
|
+
const pos = Math.floor(Math.random() * line.length);
|
|
14894
|
+
const glitchChar = GLITCH_CHARS2[Math.floor(Math.random() * GLITCH_CHARS2.length)];
|
|
14895
|
+
line = line.substring(0, pos) + glitchChar + line.substring(pos + 1);
|
|
14896
|
+
}
|
|
14897
|
+
const x = Math.max(1, centerX - Math.floor(line.length / 2));
|
|
14898
|
+
const color = i < 2 ? themeColor : "\x1B[93m";
|
|
14899
|
+
output += `\x1B[${titleY + i};${x}H${color}\x1B[1m${line}\x1B[0m`;
|
|
14900
|
+
}
|
|
14901
|
+
const subtitle = "\u2554\u2550\u2550 GEM BATTLE VS AI \u2550\u2550\u2557";
|
|
14902
|
+
const subX = Math.max(1, centerX - Math.floor(subtitle.length / 2));
|
|
14903
|
+
output += `\x1B[${titleY + title.length + 1};${subX}H${themeColor}${subtitle}\x1B[0m`;
|
|
14904
|
+
const diffY = titleY + title.length + 4;
|
|
14905
|
+
const diffs = ["easy", "normal", "hard"];
|
|
14906
|
+
const diffLabels = ["EASY", "NORMAL", "HARD"];
|
|
14907
|
+
const diffDescs = ["Relaxed pace, AI makes mistakes", "Balanced challenge", "Fast & ruthless AI"];
|
|
14908
|
+
for (let i = 0; i < diffs.length; i++) {
|
|
14909
|
+
const isSelected = i === difficultySelection;
|
|
14910
|
+
const label = `[${i + 1}] ${diffLabels[i]}`;
|
|
14911
|
+
const text = isSelected ? `\u25BA ${label} \u25C4` : ` ${label} `;
|
|
14912
|
+
const style = isSelected ? "\x1B[1;93m" : `\x1B[2m${themeColor}`;
|
|
14913
|
+
const x = Math.max(1, centerX - Math.floor(text.length / 2));
|
|
14914
|
+
output += `\x1B[${diffY + i * 2};${x}H${style}${text}\x1B[0m`;
|
|
14915
|
+
if (isSelected) {
|
|
14916
|
+
const descX = Math.max(1, centerX - Math.floor(diffDescs[i].length / 2));
|
|
14917
|
+
output += `\x1B[${diffY + i * 2 + 1};${descX}H\x1B[2m${themeColor}${diffDescs[i]}\x1B[0m`;
|
|
14918
|
+
}
|
|
14919
|
+
}
|
|
14920
|
+
const controlsY = diffY + 8;
|
|
14921
|
+
const controls = "\u2191\u2193 Select Enter Confirm Q Quit";
|
|
14922
|
+
const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
|
|
14923
|
+
output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
|
|
14924
|
+
const refY = controlsY + 2;
|
|
14925
|
+
const refLines = [
|
|
14926
|
+
"\u2190\u2192/AD Move \u2191/W Rotate Z Counter-rotate",
|
|
14927
|
+
"\u2193/S Soft drop Space Hard drop ESC Pause"
|
|
14928
|
+
];
|
|
14929
|
+
for (let i = 0; i < refLines.length; i++) {
|
|
14930
|
+
const rx = Math.max(1, centerX - Math.floor(refLines[i].length / 2));
|
|
14931
|
+
output += `\x1B[${refY + i};${rx}H\x1B[2m\x1B[90m${refLines[i]}\x1B[0m`;
|
|
14932
|
+
}
|
|
14933
|
+
return output;
|
|
14934
|
+
}
|
|
14935
|
+
const PATTERN_PREVIEW_COLORS = {
|
|
14936
|
+
red: "\x1B[1;38;5;196m",
|
|
14937
|
+
green: "\x1B[1;38;5;46m",
|
|
14938
|
+
blue: "\x1B[1;38;5;27m",
|
|
14939
|
+
yellow: "\x1B[1;38;5;226m"
|
|
14940
|
+
};
|
|
14941
|
+
function renderCharacterSelectScreen() {
|
|
14942
|
+
const cols = terminal.cols;
|
|
14943
|
+
const rows = terminal.rows;
|
|
14944
|
+
const centerX = Math.floor(cols / 2);
|
|
14945
|
+
let output = "";
|
|
14946
|
+
const titleY = Math.max(2, Math.floor(rows / 2) - 14);
|
|
14947
|
+
const selectTitle = "\u2554\u2550\u2550 SELECT YOUR FIGHTER \u2550\u2550\u2557";
|
|
14948
|
+
const stX = Math.max(1, centerX - Math.floor(selectTitle.length / 2));
|
|
14949
|
+
output += `\x1B[${titleY};${stX}H${themeColor}\x1B[1m${selectTitle}\x1B[0m`;
|
|
14950
|
+
const badge = `[${selectedDifficulty.name}]`;
|
|
14951
|
+
const bX = Math.max(1, centerX - Math.floor(badge.length / 2));
|
|
14952
|
+
output += `\x1B[${titleY + 1};${bX}H\x1B[2m${themeColor}${badge}\x1B[0m`;
|
|
14953
|
+
const gridY = titleY + 3;
|
|
14954
|
+
const cellW = 11;
|
|
14955
|
+
const selectedIdx = CHAR_GRID[charGridRow][charGridCol];
|
|
14956
|
+
for (let row = 0; row < CHAR_GRID.length; row++) {
|
|
14957
|
+
const rowChars = CHAR_GRID[row];
|
|
14958
|
+
const rowWidth = rowChars.length * cellW;
|
|
14959
|
+
const rowStartX = Math.max(1, centerX - Math.floor(rowWidth / 2));
|
|
14960
|
+
for (let col = 0; col < rowChars.length; col++) {
|
|
14961
|
+
const charIdx = rowChars[col];
|
|
14962
|
+
const char2 = CHARACTERS[charIdx];
|
|
14963
|
+
const isSelected = charIdx === selectedIdx;
|
|
14964
|
+
const x = rowStartX + col * cellW;
|
|
14965
|
+
const y = gridY + row * 3;
|
|
14966
|
+
const name = char2.name.slice(0, cellW - 2).padEnd(cellW - 2);
|
|
14967
|
+
if (isSelected) {
|
|
14968
|
+
output += `\x1B[${y};${x}H\x1B[1;93m\u25BA${name}\u25C4\x1B[0m`;
|
|
14969
|
+
} else {
|
|
14970
|
+
output += `\x1B[${y};${x}H${themeColor} ${name} \x1B[0m`;
|
|
14971
|
+
}
|
|
14972
|
+
const dmgStr = char2.damageModifier !== 1 ? `${Math.round(char2.damageModifier * 100)}%` : " ";
|
|
14973
|
+
const dmgColor = char2.damageModifier > 1 ? "\x1B[92m" : char2.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
|
|
14974
|
+
output += `\x1B[${y + 1};${x + 1}H${dmgColor}${dmgStr}\x1B[0m`;
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
const char = CHARACTERS[selectedIdx];
|
|
14978
|
+
const detailY = gridY + CHAR_GRID.length * 3 + 1;
|
|
14979
|
+
const charTitle = `${char.name} \u2014 ${char.description}`;
|
|
14980
|
+
const ctX = Math.max(1, centerX - Math.floor(charTitle.length / 2));
|
|
14981
|
+
output += `\x1B[${detailY};${ctX}H\x1B[1;97m${charTitle}\x1B[0m`;
|
|
14982
|
+
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)`;
|
|
14983
|
+
const dmgLabelColor = char.damageModifier > 1 ? "\x1B[92m" : char.damageModifier < 1 ? "\x1B[96m" : "\x1B[90m";
|
|
14984
|
+
const dlX = Math.max(1, centerX - Math.floor(dmgLabel.length / 2));
|
|
14985
|
+
output += `\x1B[${detailY + 1};${dlX}H${dmgLabelColor}${dmgLabel}\x1B[0m`;
|
|
14986
|
+
const previewWidth = 6 * 2;
|
|
14987
|
+
const previewX = Math.max(1, centerX - Math.floor(previewWidth / 2));
|
|
14988
|
+
const previewY = detailY + 3;
|
|
14989
|
+
output += `\x1B[${previewY - 1};${previewX}H\x1B[2m${themeColor}Drop Pattern:\x1B[0m`;
|
|
14990
|
+
for (let pr = 0; pr < char.dropPattern.length; pr++) {
|
|
14991
|
+
let rowStr = "";
|
|
14992
|
+
for (let pc = 0; pc < char.dropPattern[pr].length; pc++) {
|
|
14993
|
+
const color = char.dropPattern[pr][pc];
|
|
14994
|
+
rowStr += `${PATTERN_PREVIEW_COLORS[color]}\u2588\u2588\x1B[0m`;
|
|
14995
|
+
}
|
|
14996
|
+
output += `\x1B[${previewY + pr};${previewX}H${rowStr}`;
|
|
14997
|
+
}
|
|
14998
|
+
const portraitX = previewX + previewWidth + 3;
|
|
14999
|
+
const portraitLines = char.portraits.idle;
|
|
15000
|
+
output += renderPortrait(portraitLines, portraitX, previewY, themeColor);
|
|
15001
|
+
const controlsY = previewY + char.dropPattern.length + 2;
|
|
15002
|
+
const controls = "\u2190\u2192\u2191\u2193 Select Enter Confirm Esc Back";
|
|
15003
|
+
const cX = Math.max(1, centerX - Math.floor(controls.length / 2));
|
|
15004
|
+
output += `\x1B[${controlsY};${cX}H\x1B[2m${themeColor}${controls}\x1B[0m`;
|
|
15005
|
+
return output;
|
|
15006
|
+
}
|
|
15007
|
+
function renderGame(options) {
|
|
15008
|
+
const config = {
|
|
15009
|
+
showEffects: options?.showEffects ?? true,
|
|
15010
|
+
showHud: options?.showHud ?? true,
|
|
15011
|
+
showControls: options?.showControls ?? true,
|
|
15012
|
+
showVs: options?.showVs ?? true
|
|
15013
|
+
};
|
|
15014
|
+
let output = "";
|
|
15015
|
+
output += renderHeaderBar();
|
|
15016
|
+
const s1 = updateShake(p1Shake);
|
|
15017
|
+
const s2 = updateShake(p2Shake);
|
|
15018
|
+
output += renderBoard(p1, boardLeft1 + s1.dx, boardTop + s1.dy, true, true, p1Phase, p1PhaseTimer, p1ClearedCells, p1Flash);
|
|
15019
|
+
output += renderBoard(p2, boardLeft2 + s2.dx, boardTop + s2.dy, false, false, p2Phase, p2PhaseTimer, p2ClearedCells, p2Flash);
|
|
15020
|
+
if (config.showVs) {
|
|
15021
|
+
output += renderVSColumn();
|
|
15022
|
+
}
|
|
15023
|
+
if (showSidePanels && config.showHud) {
|
|
15024
|
+
output += renderSidePanel(true);
|
|
15025
|
+
output += renderSidePanel(false);
|
|
15026
|
+
}
|
|
15027
|
+
if (!showSidePanels) {
|
|
15028
|
+
output += `\x1B[${boardTop - 1};${boardLeft1 + 2}H${themeColor}${p1Character.name}\x1B[0m`;
|
|
15029
|
+
output += `\x1B[${boardTop - 1};${boardLeft2 + 2}H\x1B[91m${p2Character.name}\x1B[0m`;
|
|
15030
|
+
output += renderNextStrip(p1, boardLeft1, Math.max(1, boardTop - 2), themeColor);
|
|
15031
|
+
output += renderNextStrip(p2, boardLeft2, Math.max(1, boardTop - 2), "\x1B[91m");
|
|
15032
|
+
if (config.showHud) {
|
|
15033
|
+
const panelY = boardTop + boardDisplayHeight + 1;
|
|
15034
|
+
output += renderScorePanel(
|
|
15035
|
+
boardLeft1,
|
|
15036
|
+
panelY,
|
|
15037
|
+
p1.score,
|
|
15038
|
+
p1.pendingGarbage,
|
|
15039
|
+
themeColor,
|
|
15040
|
+
`S${Math.round(BASE_DROP_SPEED / Math.max(1, p1DropSpeed) * 100)}%`
|
|
15041
|
+
);
|
|
15042
|
+
output += renderScorePanel(
|
|
15043
|
+
boardLeft2,
|
|
15044
|
+
panelY,
|
|
15045
|
+
p2.score,
|
|
15046
|
+
p2.pendingGarbage,
|
|
15047
|
+
"\x1B[1;38;5;203m",
|
|
15048
|
+
"AI"
|
|
15049
|
+
);
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
if (config.showEffects) {
|
|
15053
|
+
output += renderParticles(particles, 1, 1, terminal.cols, terminal.rows);
|
|
15054
|
+
output += renderFloatingTexts(floatingTexts);
|
|
15055
|
+
output += renderProjectiles(projectiles);
|
|
15056
|
+
}
|
|
15057
|
+
if (config.showControls) {
|
|
15058
|
+
output += renderFooterBar();
|
|
15059
|
+
}
|
|
15060
|
+
return output;
|
|
15061
|
+
}
|
|
15062
|
+
function renderNextStrip(player, left, y, accent) {
|
|
15063
|
+
if (!player.nextPair) return "";
|
|
15064
|
+
let output = "";
|
|
15065
|
+
output += `\x1B[${y};${left}H${accent}\x1B[1mNEXT\x1B[0m `;
|
|
15066
|
+
output += renderGemCell(player.nextPair.primary, false, false);
|
|
15067
|
+
output += renderGemCell(player.nextPair.secondary, false, false);
|
|
15068
|
+
output += ` \x1B[2m${accent}\u25CF\u25CF\u25CF\x1B[0m`;
|
|
15069
|
+
return output;
|
|
15070
|
+
}
|
|
15071
|
+
function renderScorePanel(left, y, score, pending, accent, tag) {
|
|
15072
|
+
const { levelLabel, levelColor } = getIncomingThreatLevel(pending);
|
|
15073
|
+
const meterWidth = 5;
|
|
15074
|
+
const filled = Math.min(meterWidth, pending);
|
|
15075
|
+
const meter = `${"\u25A0".repeat(filled)}${"\xB7".repeat(meterWidth - filled)}`;
|
|
15076
|
+
const scoreLine = `SCORE ${score.toString().padStart(6, "0")} ${tag}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
|
|
15077
|
+
const statusLine = `IN${pending.toString().padStart(2, "0")} ${meter} ${levelLabel}`.slice(0, boardDisplayWidth).padEnd(boardDisplayWidth, " ");
|
|
15078
|
+
let output = "";
|
|
15079
|
+
output += `\x1B[${y};${left}H${accent}\x1B[48;5;237m${scoreLine}\x1B[0m`;
|
|
15080
|
+
output += `\x1B[${y + 1};${left}H${levelColor}\x1B[48;5;235m${statusLine}\x1B[0m`;
|
|
15081
|
+
return output;
|
|
15082
|
+
}
|
|
15083
|
+
function getIncomingThreatLevel(pending) {
|
|
15084
|
+
if (pending <= 0) return { levelLabel: "CLEAR", levelColor: "\x1B[92m" };
|
|
15085
|
+
if (pending <= 2) return { levelLabel: "LOW", levelColor: "\x1B[93m" };
|
|
15086
|
+
if (pending <= 5) return { levelLabel: "HIGH", levelColor: "\x1B[91m" };
|
|
15087
|
+
return { levelLabel: "DANGER", levelColor: "\x1B[1;91m" };
|
|
15088
|
+
}
|
|
15089
|
+
function renderBoard(player, left, top, isP1Board, showGhost, phase, phaseTimer, clearedCells, flash) {
|
|
15090
|
+
let output = "";
|
|
15091
|
+
const board = player.board;
|
|
15092
|
+
const pair = player.currentPair;
|
|
15093
|
+
const flashColor = updateFlash(flash);
|
|
15094
|
+
const borderColor = flashColor || themeColor;
|
|
15095
|
+
output += `\x1B[${top};${left}H${borderColor}\u2554${"\u2550".repeat(BOARD_COLS * cellWidth)}\u2557\x1B[0m`;
|
|
15096
|
+
const clearedSet = new Set(clearedCells.map((c) => `${c.row},${c.col}`));
|
|
15097
|
+
let ghost = null;
|
|
15098
|
+
if (showGhost && pair) {
|
|
15099
|
+
ghost = getGhostPosition(pair, board);
|
|
15100
|
+
}
|
|
15101
|
+
for (let r = 0; r < BOARD_ROWS; r++) {
|
|
15102
|
+
let rowContent0 = "";
|
|
15103
|
+
let rowContentN = "";
|
|
15104
|
+
for (let c = 0; c < BOARD_COLS; c++) {
|
|
15105
|
+
const gem = board[r][c];
|
|
15106
|
+
const isCleared = clearedSet.has(`${r},${c}`);
|
|
15107
|
+
let pairGem = null;
|
|
15108
|
+
if (pair) {
|
|
15109
|
+
if (pair.row === r && pair.col === c) pairGem = pair.primary;
|
|
15110
|
+
const sec = getSecondaryPos(pair);
|
|
15111
|
+
if (sec.row === r && sec.col === c) pairGem = pair.secondary;
|
|
15112
|
+
}
|
|
15113
|
+
if (pairGem) {
|
|
15114
|
+
const cell = renderGemCell(pairGem, false, false);
|
|
15115
|
+
rowContent0 += cell;
|
|
15116
|
+
rowContentN += cell;
|
|
15117
|
+
} else if (isCleared && phase === PHASE_FLASH) {
|
|
15118
|
+
const cell = clearedCells.find((cc) => cc.row === r && cc.col === c);
|
|
15119
|
+
let s;
|
|
15120
|
+
if (cell && phaseTimer % 2 === 0) {
|
|
15121
|
+
s = `\x1B[1;97m${cellSolid}\x1B[0m`;
|
|
15122
|
+
} else {
|
|
15123
|
+
s = `${GEM_COLORS[cell?.color || "red"]}${cellSolid}\x1B[0m`;
|
|
15124
|
+
}
|
|
15125
|
+
rowContent0 += s;
|
|
15126
|
+
rowContentN += s;
|
|
15127
|
+
} else if (isCleared && phase === PHASE_DISSOLVE) {
|
|
15128
|
+
const stage = DISSOLVE_FRAMES - phaseTimer;
|
|
15129
|
+
let s;
|
|
15130
|
+
if (stage === 0) s = `\x1B[2m${cellPower}\x1B[0m`;
|
|
15131
|
+
else if (stage === 1) s = `\x1B[2m${cellGhost}\x1B[0m`;
|
|
15132
|
+
else s = cellEmpty;
|
|
15133
|
+
rowContent0 += s;
|
|
15134
|
+
rowContentN += s;
|
|
15135
|
+
} else if (gem) {
|
|
15136
|
+
const inPowerGem = gem.powerGemId !== void 0;
|
|
15137
|
+
const cell = renderGemCell(gem, inPowerGem, false);
|
|
15138
|
+
rowContent0 += cell;
|
|
15139
|
+
rowContentN += cell;
|
|
15140
|
+
} else {
|
|
15141
|
+
let isGhost = false;
|
|
15142
|
+
if (ghost) {
|
|
15143
|
+
if (ghost.primaryRow === r && ghost.primaryCol === c || ghost.secondaryRow === r && ghost.secondaryCol === c) {
|
|
15144
|
+
isGhost = true;
|
|
15145
|
+
}
|
|
15146
|
+
}
|
|
15147
|
+
if (isGhost) {
|
|
15148
|
+
const g = `\x1B[2;90m${cellGhost}\x1B[0m`;
|
|
15149
|
+
rowContent0 += g;
|
|
15150
|
+
rowContentN += g;
|
|
15151
|
+
} else {
|
|
15152
|
+
rowContent0 += `\x1B[38;5;238m${cellEmptyDot}\x1B[0m`;
|
|
15153
|
+
rowContentN += cellEmpty;
|
|
15154
|
+
}
|
|
15155
|
+
}
|
|
15156
|
+
}
|
|
15157
|
+
for (let h = 0; h < cellHeight; h++) {
|
|
15158
|
+
const rowY = top + 1 + r * cellHeight + h;
|
|
15159
|
+
output += `\x1B[${rowY};${left}H${borderColor}\u2551\x1B[0m`;
|
|
15160
|
+
output += h === 0 ? rowContent0 : rowContentN;
|
|
15161
|
+
output += `${borderColor}\u2551\x1B[0m`;
|
|
15162
|
+
}
|
|
15163
|
+
}
|
|
15164
|
+
output += `\x1B[${top + boardDisplayHeight - 1};${left}H${borderColor}\u255A${"\u2550".repeat(BOARD_COLS * cellWidth)}\u255D\x1B[0m`;
|
|
15165
|
+
const garbageDrop = isP1Board ? p1GarbageDrop : p2GarbageDrop;
|
|
15166
|
+
if (garbageDrop) {
|
|
15167
|
+
for (const g of garbageDrop.gems) {
|
|
15168
|
+
if (g.delayFrames > 0) continue;
|
|
15169
|
+
const row = Math.max(0, Math.min(BOARD_ROWS - 1, g.currentRow));
|
|
15170
|
+
const cellStr = renderGemCell({ color: g.color, type: "counter", counterTimer: g.timer }, false, false);
|
|
15171
|
+
for (let h = 0; h < cellHeight; h++) {
|
|
15172
|
+
const y = top + 1 + row * cellHeight + h;
|
|
15173
|
+
const x = left + 1 + g.col * cellWidth;
|
|
15174
|
+
output += `\x1B[${y};${x}H${cellStr}`;
|
|
15175
|
+
}
|
|
15176
|
+
}
|
|
15177
|
+
}
|
|
15178
|
+
return output;
|
|
15179
|
+
}
|
|
15180
|
+
function renderGemCell(gem, isPowerGem, _dimmed) {
|
|
15181
|
+
const color = GEM_COLORS[gem.color];
|
|
15182
|
+
switch (gem.type) {
|
|
15183
|
+
case "normal":
|
|
15184
|
+
if (isPowerGem) {
|
|
15185
|
+
return `${color}${cellPower}\x1B[0m`;
|
|
15186
|
+
}
|
|
15187
|
+
return `${color}${cellSolid}\x1B[0m`;
|
|
15188
|
+
case "crash":
|
|
15189
|
+
return `\x1B[1m${color}${cellCrash}\x1B[0m`;
|
|
15190
|
+
case "counter": {
|
|
15191
|
+
const bg = COUNTER_BG_COLORS[gem.color];
|
|
15192
|
+
if (gem.counterTimer !== void 0) {
|
|
15193
|
+
const timerStr = gem.counterTimer.toString().padStart(2, " ");
|
|
15194
|
+
return `\x1B[1;97m${bg}${timerStr.padEnd(cellWidth, " ")}\x1B[0m`;
|
|
15195
|
+
}
|
|
15196
|
+
return `\x1B[1;97m${bg}${" ?".padEnd(cellWidth, " ")}\x1B[0m`;
|
|
15197
|
+
}
|
|
15198
|
+
case "diamond":
|
|
15199
|
+
return `\x1B[1;97m${cellDiamond}\x1B[0m`;
|
|
15200
|
+
default:
|
|
15201
|
+
return `${color}${cellSolid}\x1B[0m`;
|
|
15202
|
+
}
|
|
15203
|
+
}
|
|
15204
|
+
function renderVSColumn() {
|
|
15205
|
+
let output = "";
|
|
15206
|
+
const x = vsColX;
|
|
15207
|
+
const y = boardTop + Math.max(2, Math.floor(boardDisplayHeight / 2) - 5);
|
|
15208
|
+
output += `\x1B[${Math.max(1, y - 2)};${x + 1}H\x1B[1;96mHYPER\x1B[0m`;
|
|
15209
|
+
output += `\x1B[${Math.max(1, y - 1)};${x + 1}H\x1B[1;95mFIGHT\x1B[0m`;
|
|
15210
|
+
output += `\x1B[${y};${x + 2}H\x1B[1;93mVS\x1B[0m`;
|
|
15211
|
+
const p1Lines = p1Character.portraits[p1Pose];
|
|
15212
|
+
output += renderPortrait(p1Lines, x, y + 2, themeColor);
|
|
15213
|
+
const p2Lines = p2Character.portraits[p2Pose];
|
|
15214
|
+
output += renderPortrait(p2Lines, x, y + 6, "\x1B[91m");
|
|
15215
|
+
const maxEnergy = 10;
|
|
15216
|
+
const p1Energy = p1ChainCount * 3 + p1TotalCleared;
|
|
15217
|
+
output += renderEnergyBar(x + 8, y + 2, p1Energy, maxEnergy);
|
|
15218
|
+
return output;
|
|
15219
|
+
}
|
|
15220
|
+
function renderHeaderBar() {
|
|
15221
|
+
const cols = terminal.cols;
|
|
15222
|
+
let output = "";
|
|
15223
|
+
const leftText = " HYPER FIGHTER";
|
|
15224
|
+
const rightText = `${p1Character.name} vs ${p2Character.name} [${selectedDifficulty.name}] `;
|
|
15225
|
+
const padLen = Math.max(0, cols - leftText.length - rightText.length);
|
|
15226
|
+
const row1 = leftText + " ".repeat(padLen) + rightText;
|
|
15227
|
+
output += `\x1B[1;1H\x1B[1;97m\x1B[48;5;236m${row1.slice(0, cols).padEnd(cols, " ")}\x1B[0m`;
|
|
15228
|
+
output += `\x1B[2;1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
|
|
15229
|
+
return output;
|
|
15230
|
+
}
|
|
15231
|
+
function renderFooterBar() {
|
|
15232
|
+
const cols = terminal.cols;
|
|
15233
|
+
const rows = terminal.rows;
|
|
15234
|
+
let output = "";
|
|
15235
|
+
output += `\x1B[${rows - 1};1H\x1B[38;5;240m${"\u2500".repeat(cols)}\x1B[0m`;
|
|
15236
|
+
const controls = "\u2190\u2192 Move \u2191/W Rotate Z CCW \u2193/S Soft Space Drop ESC Pause";
|
|
15237
|
+
const cx = Math.max(1, Math.floor((cols - controls.length) / 2) + 1);
|
|
15238
|
+
output += `\x1B[${rows};${cx}H\x1B[2m\x1B[90m${controls}\x1B[0m`;
|
|
15239
|
+
return output;
|
|
15240
|
+
}
|
|
15241
|
+
function renderMiniBar(value, max, width, fillColor) {
|
|
15242
|
+
const filled = Math.min(width, Math.round(value / Math.max(1, max) * width));
|
|
15243
|
+
const empty = width - filled;
|
|
15244
|
+
return `${fillColor}${"\u25A0".repeat(filled)}\x1B[38;5;238m${"\xB7".repeat(empty)}\x1B[0m`;
|
|
15245
|
+
}
|
|
15246
|
+
function renderSidePanel(isLeft) {
|
|
15247
|
+
const player = isLeft ? p1 : p2;
|
|
15248
|
+
const x = isLeft ? sidePanel1X : sidePanel2X;
|
|
15249
|
+
const accent = isLeft ? themeColor : "\x1B[1;38;5;203m";
|
|
15250
|
+
const dropSpeed = isLeft ? p1DropSpeed : p2DropSpeed;
|
|
15251
|
+
const chainCount = isLeft ? p1ChainCount : p2ChainCount;
|
|
15252
|
+
let output = "";
|
|
15253
|
+
let y = boardTop + 1;
|
|
15254
|
+
output += `\x1B[${y};${x}H${accent}\x1B[1mNEXT\x1B[0m`;
|
|
15255
|
+
y++;
|
|
15256
|
+
if (player.nextPair) {
|
|
15257
|
+
output += `\x1B[${y};${x}H`;
|
|
15258
|
+
output += renderGemCell(player.nextPair.primary, false, false);
|
|
15259
|
+
output += renderGemCell(player.nextPair.secondary, false, false);
|
|
15260
|
+
}
|
|
15261
|
+
y += 2;
|
|
15262
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mSCORE\x1B[0m`;
|
|
15263
|
+
y++;
|
|
15264
|
+
output += `\x1B[${y};${x}H${accent}${player.score.toString().padStart(7, "0")}\x1B[0m`;
|
|
15265
|
+
y += 2;
|
|
15266
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mSPEED\x1B[0m`;
|
|
15267
|
+
y++;
|
|
15268
|
+
const speedPct = Math.round(BASE_DROP_SPEED / Math.max(1, dropSpeed) * 100);
|
|
15269
|
+
output += `\x1B[${y};${x}H${speedPct >= 200 ? "\x1B[91m" : speedPct >= 150 ? "\x1B[93m" : "\x1B[92m"}${speedPct}%\x1B[0m `;
|
|
15270
|
+
output += renderMiniBar(speedPct, 300, 6, speedPct >= 200 ? "\x1B[91m" : "\x1B[92m");
|
|
15271
|
+
y += 2;
|
|
15272
|
+
if (chainCount > 0) {
|
|
15273
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mCHAIN\x1B[0m`;
|
|
15274
|
+
y++;
|
|
15275
|
+
output += `\x1B[${y};${x}H\x1B[1;93m${chainCount}\x1B[0m`;
|
|
15276
|
+
y += 2;
|
|
15277
|
+
} else {
|
|
15278
|
+
y += 3;
|
|
15279
|
+
}
|
|
15280
|
+
const { levelLabel, levelColor } = getIncomingThreatLevel(player.pendingGarbage);
|
|
15281
|
+
output += `\x1B[${y};${x}H\x1B[38;5;245mINCOMING\x1B[0m`;
|
|
15282
|
+
y++;
|
|
15283
|
+
output += `\x1B[${y};${x}H${levelColor}${player.pendingGarbage.toString().padStart(2, "0")}\x1B[0m `;
|
|
15284
|
+
output += renderMiniBar(player.pendingGarbage, 10, 6, levelColor);
|
|
15285
|
+
y++;
|
|
15286
|
+
output += `\x1B[${y};${x}H${levelColor}${levelLabel}\x1B[0m`;
|
|
15287
|
+
return output;
|
|
15288
|
+
}
|
|
15289
|
+
function renderPauseOverlay() {
|
|
15290
|
+
const cols = terminal.cols;
|
|
15291
|
+
const rows = terminal.rows;
|
|
15292
|
+
const centerX = Math.floor(cols / 2);
|
|
15293
|
+
const centerY = Math.floor(rows / 2);
|
|
15294
|
+
let output = "";
|
|
15295
|
+
output += `\x1B[${centerY - 3};${centerX - 4}H\x1B[1;5m${themeColor}\u23F8 PAUSED\x1B[0m`;
|
|
15296
|
+
output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
|
|
15297
|
+
centerX,
|
|
15298
|
+
startY: centerY - 1,
|
|
15299
|
+
showShortcuts: true
|
|
15300
|
+
});
|
|
15301
|
+
return output;
|
|
15302
|
+
}
|
|
15303
|
+
function renderGameOverOverlay() {
|
|
15304
|
+
const cols = terminal.cols;
|
|
15305
|
+
const rows = terminal.rows;
|
|
15306
|
+
const centerX = Math.floor(cols / 2);
|
|
15307
|
+
const centerY = Math.floor(rows / 2);
|
|
15308
|
+
let output = "";
|
|
15309
|
+
gameOverTimer++;
|
|
15310
|
+
const panelWidth = Math.min(44, Math.max(34, cols - 8));
|
|
15311
|
+
const panelHeight = 12;
|
|
15312
|
+
const panelLeft = Math.max(2, centerX - Math.floor(panelWidth / 2));
|
|
15313
|
+
const panelTop = Math.max(2, centerY - Math.floor(panelHeight / 2));
|
|
15314
|
+
for (let y = 0; y < panelHeight; y++) {
|
|
15315
|
+
output += `\x1B[${panelTop + y};${panelLeft}H\x1B[40m${" ".repeat(panelWidth)}\x1B[0m`;
|
|
15316
|
+
}
|
|
15317
|
+
output += `\x1B[${panelTop};${panelLeft}H${themeColor}\u2554${"\u2550".repeat(panelWidth - 2)}\u2557\x1B[0m`;
|
|
15318
|
+
for (let y = 1; y < panelHeight - 1; y++) {
|
|
15319
|
+
output += `\x1B[${panelTop + y};${panelLeft}H${themeColor}\u2551\x1B[0m`;
|
|
15320
|
+
output += `\x1B[${panelTop + y};${panelLeft + panelWidth - 1}H${themeColor}\u2551\x1B[0m`;
|
|
15321
|
+
}
|
|
15322
|
+
output += `\x1B[${panelTop + panelHeight - 1};${panelLeft}H${themeColor}\u255A${"\u2550".repeat(panelWidth - 2)}\u255D\x1B[0m`;
|
|
15323
|
+
const winnerChar = winner === 1 ? p1Character : p2Character;
|
|
15324
|
+
const winText = winner === 1 ? `${winnerChar.name} WINS!` : `${winnerChar.name} WINS!`;
|
|
15325
|
+
const winColor = winner === 1 ? "\x1B[1;92m" : "\x1B[1;91m";
|
|
15326
|
+
const winX = Math.max(1, centerX - Math.floor(winText.length / 2));
|
|
15327
|
+
output += `\x1B[${panelTop + 2};${winX}H${winColor}${winText}\x1B[0m`;
|
|
15328
|
+
const scoreLine = `${p1Character.name}: ${p1.score} | ${p2Character.name}: ${p2.score}`;
|
|
15329
|
+
const sX = Math.max(1, centerX - Math.floor(scoreLine.length / 2));
|
|
15330
|
+
output += `\x1B[${panelTop + 4};${sX}H${themeColor}${scoreLine}\x1B[0m`;
|
|
15331
|
+
const menuItems = [
|
|
15332
|
+
{ label: "RESTART", shortcut: "R" },
|
|
15333
|
+
{ label: "QUIT", shortcut: "Q" },
|
|
15334
|
+
{ label: "LIST GAMES", shortcut: "L" },
|
|
15335
|
+
{ label: "NEXT GAME", shortcut: "N" }
|
|
15336
|
+
];
|
|
15337
|
+
output += renderSimpleMenu(menuItems, pauseMenuSelection, {
|
|
15338
|
+
centerX,
|
|
15339
|
+
startY: panelTop + 6,
|
|
15340
|
+
showShortcuts: true
|
|
15341
|
+
});
|
|
15342
|
+
return output;
|
|
15343
|
+
}
|
|
15344
|
+
const keyListener = terminal.onKey(({ domEvent }) => {
|
|
15345
|
+
if (!running) return;
|
|
15346
|
+
domEvent.preventDefault();
|
|
15347
|
+
domEvent.stopPropagation();
|
|
15348
|
+
const key = domEvent.key.toLowerCase();
|
|
15349
|
+
switch (gameState) {
|
|
15350
|
+
case "difficulty":
|
|
15351
|
+
handleDifficultyInput(key, domEvent);
|
|
15352
|
+
break;
|
|
15353
|
+
case "characterSelect":
|
|
15354
|
+
handleCharacterSelectInput(key, domEvent);
|
|
15355
|
+
break;
|
|
15356
|
+
case "running":
|
|
15357
|
+
handleGameInput(key, domEvent);
|
|
15358
|
+
break;
|
|
15359
|
+
case "paused":
|
|
15360
|
+
handlePauseInput(key, domEvent);
|
|
15361
|
+
break;
|
|
15362
|
+
case "gameOver":
|
|
15363
|
+
handleGameOverInput(key, domEvent);
|
|
15364
|
+
break;
|
|
15365
|
+
}
|
|
15366
|
+
});
|
|
15367
|
+
function handleDifficultyInput(key, domEvent) {
|
|
15368
|
+
if (domEvent.key === "ArrowUp" || key === "w") {
|
|
15369
|
+
difficultySelection = (difficultySelection - 1 + 3) % 3;
|
|
15370
|
+
} else if (domEvent.key === "ArrowDown" || key === "s") {
|
|
15371
|
+
difficultySelection = (difficultySelection + 1) % 3;
|
|
15372
|
+
} else if (domEvent.key === "Enter" || domEvent.key === " ") {
|
|
15373
|
+
const diffs = ["easy", "normal", "hard"];
|
|
15374
|
+
selectedDifficulty = DIFFICULTIES2[diffs[difficultySelection]];
|
|
15375
|
+
enterCharacterSelect();
|
|
15376
|
+
} else if (key === "1") {
|
|
15377
|
+
selectedDifficulty = DIFFICULTIES2.easy;
|
|
15378
|
+
difficultySelection = 0;
|
|
15379
|
+
enterCharacterSelect();
|
|
15380
|
+
} else if (key === "2") {
|
|
15381
|
+
selectedDifficulty = DIFFICULTIES2.normal;
|
|
15382
|
+
difficultySelection = 1;
|
|
15383
|
+
enterCharacterSelect();
|
|
15384
|
+
} else if (key === "3") {
|
|
15385
|
+
selectedDifficulty = DIFFICULTIES2.hard;
|
|
15386
|
+
difficultySelection = 2;
|
|
15387
|
+
enterCharacterSelect();
|
|
15388
|
+
} else if (key === "q") {
|
|
15389
|
+
cleanup();
|
|
15390
|
+
dispatchGameQuit(terminal);
|
|
15391
|
+
}
|
|
15392
|
+
}
|
|
15393
|
+
function enterCharacterSelect() {
|
|
15394
|
+
gameState = "characterSelect";
|
|
15395
|
+
charGridRow = 0;
|
|
15396
|
+
charGridCol = 0;
|
|
15397
|
+
}
|
|
15398
|
+
function handleCharacterSelectInput(key, domEvent) {
|
|
15399
|
+
if (key === "q" || key === "escape") {
|
|
15400
|
+
gameState = "difficulty";
|
|
15401
|
+
return;
|
|
15402
|
+
}
|
|
15403
|
+
if (domEvent.key === "ArrowLeft" || key === "a") {
|
|
15404
|
+
const row = CHAR_GRID[charGridRow];
|
|
15405
|
+
charGridCol = (charGridCol - 1 + row.length) % row.length;
|
|
15406
|
+
} else if (domEvent.key === "ArrowRight" || key === "d") {
|
|
15407
|
+
const row = CHAR_GRID[charGridRow];
|
|
15408
|
+
charGridCol = (charGridCol + 1) % row.length;
|
|
15409
|
+
} else if (domEvent.key === "ArrowUp" || key === "w") {
|
|
15410
|
+
charGridRow = (charGridRow - 1 + CHAR_GRID.length) % CHAR_GRID.length;
|
|
15411
|
+
charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
|
|
15412
|
+
} else if (domEvent.key === "ArrowDown" || key === "s") {
|
|
15413
|
+
charGridRow = (charGridRow + 1) % CHAR_GRID.length;
|
|
15414
|
+
charGridCol = Math.min(charGridCol, CHAR_GRID[charGridRow].length - 1);
|
|
15415
|
+
} else if (domEvent.key === "Enter" || domEvent.key === " ") {
|
|
15416
|
+
const idx = CHAR_GRID[charGridRow][charGridCol];
|
|
15417
|
+
p1Character = CHARACTERS[idx];
|
|
15418
|
+
p2Character = getRandomCharacter();
|
|
15419
|
+
gameState = "running";
|
|
15420
|
+
initGame();
|
|
15421
|
+
}
|
|
15422
|
+
}
|
|
15423
|
+
function handleGameInput(key, domEvent) {
|
|
15424
|
+
if (key === "escape") {
|
|
15425
|
+
gameState = "paused";
|
|
15426
|
+
pauseMenuSelection = 0;
|
|
15427
|
+
return;
|
|
15428
|
+
}
|
|
15429
|
+
if (!p1.currentPair || p1Phase !== PHASE_NONE) return;
|
|
15430
|
+
switch (domEvent.key) {
|
|
15431
|
+
case "ArrowLeft":
|
|
15432
|
+
case "a":
|
|
15433
|
+
movePair(p1.currentPair, p1.board, -1);
|
|
15434
|
+
break;
|
|
15435
|
+
case "ArrowRight":
|
|
15436
|
+
case "d":
|
|
15437
|
+
movePair(p1.currentPair, p1.board, 1);
|
|
15438
|
+
break;
|
|
15439
|
+
case "ArrowUp":
|
|
15440
|
+
case "w":
|
|
15441
|
+
rotatePair(p1.currentPair, p1.board, true);
|
|
15442
|
+
break;
|
|
15443
|
+
case "z":
|
|
15444
|
+
rotatePair(p1.currentPair, p1.board, false);
|
|
15445
|
+
break;
|
|
15446
|
+
case "ArrowDown":
|
|
15447
|
+
case "s":
|
|
15448
|
+
if (dropPair(p1.currentPair, p1.board)) {
|
|
15449
|
+
p1DropTimer = 0;
|
|
15450
|
+
} else {
|
|
15451
|
+
lockAndResolve(p1, true);
|
|
15452
|
+
}
|
|
15453
|
+
break;
|
|
15454
|
+
case " ":
|
|
15455
|
+
hardDrop(p1.currentPair, p1.board);
|
|
15456
|
+
lockAndResolve(p1, true);
|
|
15457
|
+
break;
|
|
15458
|
+
}
|
|
15459
|
+
}
|
|
15460
|
+
function handlePauseInput(key, domEvent) {
|
|
15461
|
+
if (key === "escape") {
|
|
15462
|
+
gameState = "running";
|
|
15463
|
+
return;
|
|
15464
|
+
}
|
|
15465
|
+
const { newSelection, confirmed } = navigateMenu(
|
|
15466
|
+
pauseMenuSelection,
|
|
15467
|
+
PAUSE_MENU_ITEMS.length,
|
|
15468
|
+
key,
|
|
15469
|
+
domEvent
|
|
15470
|
+
);
|
|
15471
|
+
if (newSelection !== pauseMenuSelection) {
|
|
15472
|
+
pauseMenuSelection = newSelection;
|
|
15473
|
+
}
|
|
15474
|
+
const shortcutIdx = checkShortcut(PAUSE_MENU_ITEMS, key);
|
|
15475
|
+
if (confirmed || shortcutIdx >= 0) {
|
|
15476
|
+
const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
|
|
15477
|
+
const item = PAUSE_MENU_ITEMS[idx];
|
|
15478
|
+
switch (item.label) {
|
|
15479
|
+
case "RESUME":
|
|
15480
|
+
gameState = "running";
|
|
15481
|
+
break;
|
|
15482
|
+
case "RESTART":
|
|
15483
|
+
gameState = "running";
|
|
15484
|
+
initGame();
|
|
15485
|
+
break;
|
|
15486
|
+
case "QUIT":
|
|
15487
|
+
cleanup();
|
|
15488
|
+
dispatchGameQuit(terminal);
|
|
15489
|
+
break;
|
|
15490
|
+
case "LIST GAMES":
|
|
15491
|
+
cleanup();
|
|
15492
|
+
dispatchGamesMenu(terminal);
|
|
15493
|
+
break;
|
|
15494
|
+
case "NEXT GAME":
|
|
15495
|
+
cleanup();
|
|
15496
|
+
dispatchGameSwitch(terminal);
|
|
15497
|
+
break;
|
|
15498
|
+
}
|
|
15499
|
+
}
|
|
15500
|
+
}
|
|
15501
|
+
function handleGameOverInput(key, domEvent) {
|
|
15502
|
+
const menuItems = [
|
|
15503
|
+
{ label: "RESTART", shortcut: "R" },
|
|
15504
|
+
{ label: "QUIT", shortcut: "Q" },
|
|
15505
|
+
{ label: "LIST GAMES", shortcut: "L" },
|
|
15506
|
+
{ label: "NEXT GAME", shortcut: "N" }
|
|
15507
|
+
];
|
|
15508
|
+
const { newSelection, confirmed } = navigateMenu(
|
|
15509
|
+
pauseMenuSelection,
|
|
15510
|
+
menuItems.length,
|
|
15511
|
+
key,
|
|
15512
|
+
domEvent
|
|
15513
|
+
);
|
|
15514
|
+
if (newSelection !== pauseMenuSelection) {
|
|
15515
|
+
pauseMenuSelection = newSelection;
|
|
15516
|
+
}
|
|
15517
|
+
const shortcutIdx = checkShortcut(menuItems, key);
|
|
15518
|
+
if (confirmed || shortcutIdx >= 0) {
|
|
15519
|
+
const idx = shortcutIdx >= 0 ? shortcutIdx : pauseMenuSelection;
|
|
15520
|
+
switch (menuItems[idx].label) {
|
|
15521
|
+
case "RESTART":
|
|
15522
|
+
gameState = "difficulty";
|
|
15523
|
+
pauseMenuSelection = 0;
|
|
15524
|
+
break;
|
|
15525
|
+
case "QUIT":
|
|
15526
|
+
cleanup();
|
|
15527
|
+
dispatchGameQuit(terminal);
|
|
15528
|
+
break;
|
|
15529
|
+
case "LIST GAMES":
|
|
15530
|
+
cleanup();
|
|
15531
|
+
dispatchGamesMenu(terminal);
|
|
15532
|
+
break;
|
|
15533
|
+
case "NEXT GAME":
|
|
15534
|
+
cleanup();
|
|
15535
|
+
dispatchGameSwitch(terminal);
|
|
15536
|
+
break;
|
|
15537
|
+
}
|
|
15538
|
+
}
|
|
15539
|
+
}
|
|
15540
|
+
const resizeListener = terminal.onResize(() => {
|
|
15541
|
+
calculateLayout();
|
|
15542
|
+
});
|
|
15543
|
+
function cleanup() {
|
|
15544
|
+
running = false;
|
|
15545
|
+
clearInterval(gameLoop);
|
|
15546
|
+
keyListener.dispose();
|
|
15547
|
+
resizeListener.dispose();
|
|
15548
|
+
}
|
|
15549
|
+
enterAlternateBuffer(terminal, "hyper-fighter");
|
|
15550
|
+
const gameLoop = setInterval(() => {
|
|
15551
|
+
if (!running) {
|
|
15552
|
+
clearInterval(gameLoop);
|
|
15553
|
+
return;
|
|
15554
|
+
}
|
|
15555
|
+
update();
|
|
15556
|
+
render();
|
|
15557
|
+
}, TICK_MS);
|
|
15558
|
+
const originalStop = controller.stop;
|
|
15559
|
+
controller.stop = () => {
|
|
15560
|
+
cleanup();
|
|
15561
|
+
exitAlternateBuffer(terminal, "hyper-fighter");
|
|
15562
|
+
originalStop();
|
|
15563
|
+
};
|
|
15564
|
+
return controller;
|
|
15565
|
+
}
|
|
15566
|
+
|
|
15567
|
+
// src/games/gamesMenu.ts
|
|
15568
|
+
function showGamesMenu(terminal, optionsOrCallback) {
|
|
15569
|
+
const options = typeof optionsOrCallback === "function" ? { onGameSelect: optionsOrCallback } : optionsOrCallback || {};
|
|
15570
|
+
const { onGameSelect, onQuit } = options;
|
|
15571
|
+
const themeColor = getCurrentThemeColor();
|
|
15572
|
+
const lightTheme = isLightTheme2();
|
|
15573
|
+
let running = true;
|
|
15574
|
+
let selectedIndex = 0;
|
|
15575
|
+
let scrollOffset = 0;
|
|
15576
|
+
const controller = {
|
|
15577
|
+
stop: () => {
|
|
15578
|
+
if (!running) return;
|
|
15579
|
+
running = false;
|
|
15580
|
+
},
|
|
15581
|
+
get isRunning() {
|
|
15582
|
+
return running;
|
|
15583
|
+
}
|
|
15584
|
+
};
|
|
15585
|
+
const title = [
|
|
15586
|
+
"\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",
|
|
15587
|
+
"\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"
|
|
15588
|
+
];
|
|
15589
|
+
function render() {
|
|
15590
|
+
let output = "";
|
|
15591
|
+
output += "\x1B[2J\x1B[H";
|
|
15592
|
+
const cols = terminal.cols;
|
|
15593
|
+
const rows = terminal.rows;
|
|
15594
|
+
const titleX = Math.floor((cols - title[0].length) / 2);
|
|
15595
|
+
output += `\x1B[2;${titleX}H${themeColor}\x1B[1m${title[0]}\x1B[0m`;
|
|
15596
|
+
output += `\x1B[3;${titleX}H${themeColor}\x1B[1m${title[1]}\x1B[0m`;
|
|
15597
|
+
const subtitle = `Select a game to play (${games.length} games)`;
|
|
15598
|
+
output += `\x1B[5;${Math.floor((cols - subtitle.length) / 2)}H\x1B[2m${subtitle}\x1B[0m`;
|
|
15599
|
+
const boxWidth = 38;
|
|
15600
|
+
const columnGap = 2;
|
|
15601
|
+
const twoColumnWidth = boxWidth * 2 + columnGap;
|
|
15602
|
+
const useTwoColumns = cols >= twoColumnWidth + 8;
|
|
15603
|
+
const listStartY = 6;
|
|
15604
|
+
const availableRows = rows - listStartY - 4;
|
|
15605
|
+
const maxVisibleGames = Math.max(1, Math.floor(availableRows / 2));
|
|
15606
|
+
if (useTwoColumns) {
|
|
15607
|
+
renderTwoColumns(output, cols, rows, boxWidth, columnGap, listStartY, maxVisibleGames);
|
|
15608
|
+
} else {
|
|
15609
|
+
renderSingleColumn(output, cols, rows, boxWidth, listStartY, maxVisibleGames);
|
|
15610
|
+
}
|
|
15611
|
+
}
|
|
15612
|
+
function renderSingleColumn(baseOutput, cols, rows, boxWidth, listStartY, maxVisibleGames) {
|
|
15613
|
+
let output = baseOutput;
|
|
15614
|
+
const boxX = Math.floor((cols - boxWidth) / 2);
|
|
15615
|
+
const visibleGames = Math.min(maxVisibleGames, games.length);
|
|
15616
|
+
if (selectedIndex < scrollOffset) {
|
|
15617
|
+
scrollOffset = selectedIndex;
|
|
15618
|
+
} else if (selectedIndex >= scrollOffset + visibleGames) {
|
|
15619
|
+
scrollOffset = selectedIndex - visibleGames + 1;
|
|
15620
|
+
}
|
|
15621
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset, games.length - visibleGames));
|
|
15622
|
+
const hasScrollUp = scrollOffset > 0;
|
|
15623
|
+
const hasScrollDown = scrollOffset + visibleGames < games.length;
|
|
15624
|
+
const topScrollIndicator = hasScrollUp ? " \u25B2 more " : "\u2550".repeat(8);
|
|
15625
|
+
const topBorderWidth = boxWidth - 2 - topScrollIndicator.length;
|
|
15626
|
+
const topLeftPad = Math.floor(topBorderWidth / 2);
|
|
15627
|
+
const topRightPad = topBorderWidth - topLeftPad;
|
|
15628
|
+
output += `\x1B[${listStartY};${boxX}H${themeColor}\u2554${"\u2550".repeat(topLeftPad)}${hasScrollUp ? "\x1B[33m" : ""}${topScrollIndicator}${hasScrollUp ? themeColor : ""}${"\u2550".repeat(topRightPad)}\u2557\x1B[0m`;
|
|
15629
|
+
for (let vi = 0; vi < visibleGames; vi++) {
|
|
15630
|
+
const i = scrollOffset + vi;
|
|
15631
|
+
const game = games[i];
|
|
15632
|
+
const y = listStartY + 1 + vi * 2;
|
|
15633
|
+
const isSelected = i === selectedIndex;
|
|
15634
|
+
output += renderGameEntry(game, i, isSelected, y, boxX, boxWidth);
|
|
15635
|
+
}
|
|
15636
|
+
const bottomY = listStartY + 1 + visibleGames * 2;
|
|
15637
|
+
const bottomScrollIndicator = hasScrollDown ? " \u25BC more " : "\u2550".repeat(8);
|
|
15638
|
+
const bottomBorderWidth = boxWidth - 2 - bottomScrollIndicator.length;
|
|
15639
|
+
const bottomLeftPad = Math.floor(bottomBorderWidth / 2);
|
|
15640
|
+
const bottomRightPad = bottomBorderWidth - bottomLeftPad;
|
|
15641
|
+
output += `\x1B[${bottomY};${boxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeftPad)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRightPad)}\u255D\x1B[0m`;
|
|
15642
|
+
const controls = `\u2191\u2193 Navigate | ENTER Select | 1-9 Quick | Q Quit`;
|
|
15643
|
+
output += `\x1B[${rows - 1};${Math.floor((cols - controls.length) / 2)}H\x1B[2m${controls}\x1B[0m`;
|
|
15644
|
+
terminal.write(output);
|
|
15645
|
+
}
|
|
15646
|
+
function renderTwoColumns(baseOutput, cols, rows, boxWidth, columnGap, listStartY, maxVisibleGames) {
|
|
15647
|
+
let output = baseOutput;
|
|
15648
|
+
const gamesPerColumn = Math.min(maxVisibleGames, Math.ceil(games.length / 2));
|
|
15649
|
+
const selectedRow = Math.floor(selectedIndex / 2);
|
|
15650
|
+
if (selectedRow < scrollOffset) {
|
|
15651
|
+
scrollOffset = selectedRow;
|
|
15652
|
+
} else if (selectedRow >= scrollOffset + gamesPerColumn) {
|
|
15653
|
+
scrollOffset = selectedRow - gamesPerColumn + 1;
|
|
15654
|
+
}
|
|
15655
|
+
const maxScroll = Math.max(0, Math.ceil(games.length / 2) - gamesPerColumn);
|
|
15656
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset, maxScroll));
|
|
15657
|
+
const hasScrollUp = scrollOffset > 0;
|
|
15658
|
+
const hasScrollDown = (scrollOffset + gamesPerColumn) * 2 < games.length;
|
|
15659
|
+
const totalWidth = boxWidth * 2 + columnGap;
|
|
15660
|
+
const startX = Math.floor((cols - totalWidth) / 2);
|
|
15661
|
+
const leftBoxX = startX;
|
|
15662
|
+
const rightBoxX = startX + boxWidth + columnGap;
|
|
15663
|
+
const topScrollIndicator = hasScrollUp ? " \u25B2 " : "\u2550\u2550\u2550";
|
|
15664
|
+
const topBorderContent = boxWidth - 2 - topScrollIndicator.length;
|
|
15665
|
+
const topLeft = Math.floor(topBorderContent / 2);
|
|
15666
|
+
const topRight = topBorderContent - topLeft;
|
|
15667
|
+
output += `\x1B[${listStartY};${leftBoxX}H${themeColor}\u2554${"\u2550".repeat(topLeft)}${hasScrollUp ? "\x1B[33m" : ""}${topScrollIndicator}${hasScrollUp ? themeColor : ""}${"\u2550".repeat(topRight)}\u2557\x1B[0m`;
|
|
15668
|
+
output += `\x1B[${listStartY};${rightBoxX}H${themeColor}\u2554${"\u2550".repeat(topLeft)}${hasScrollUp ? "\x1B[33m" : ""}${topScrollIndicator}${hasScrollUp ? themeColor : ""}${"\u2550".repeat(topRight)}\u2557\x1B[0m`;
|
|
15669
|
+
for (let row = 0; row < gamesPerColumn; row++) {
|
|
15670
|
+
const leftIdx = (scrollOffset + row) * 2;
|
|
15671
|
+
const rightIdx = leftIdx + 1;
|
|
15672
|
+
const y = listStartY + 1 + row * 2;
|
|
15673
|
+
if (leftIdx < games.length) {
|
|
15674
|
+
output += renderGameEntry(games[leftIdx], leftIdx, leftIdx === selectedIndex, y, leftBoxX, boxWidth);
|
|
15675
|
+
} else {
|
|
15676
|
+
output += `\x1B[${y};${leftBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
15677
|
+
output += `\x1B[${y + 1};${leftBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
15678
|
+
}
|
|
15679
|
+
if (rightIdx < games.length) {
|
|
15680
|
+
output += renderGameEntry(games[rightIdx], rightIdx, rightIdx === selectedIndex, y, rightBoxX, boxWidth);
|
|
15681
|
+
} else {
|
|
15682
|
+
output += `\x1B[${y};${rightBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
15683
|
+
output += `\x1B[${y + 1};${rightBoxX}H${themeColor}\u2551${" ".repeat(boxWidth - 2)}\u2551\x1B[0m`;
|
|
15684
|
+
}
|
|
15685
|
+
}
|
|
15686
|
+
const bottomY = listStartY + 1 + gamesPerColumn * 2;
|
|
15687
|
+
const bottomScrollIndicator = hasScrollDown ? " \u25BC " : "\u2550\u2550\u2550";
|
|
15688
|
+
const bottomBorderContent = boxWidth - 2 - bottomScrollIndicator.length;
|
|
15689
|
+
const bottomLeft = Math.floor(bottomBorderContent / 2);
|
|
15690
|
+
const bottomRight = bottomBorderContent - bottomLeft;
|
|
15691
|
+
output += `\x1B[${bottomY};${leftBoxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeft)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRight)}\u255D\x1B[0m`;
|
|
15692
|
+
output += `\x1B[${bottomY};${rightBoxX}H${themeColor}\u255A${"\u2550".repeat(bottomLeft)}${hasScrollDown ? "\x1B[33m" : ""}${bottomScrollIndicator}${hasScrollDown ? themeColor : ""}${"\u2550".repeat(bottomRight)}\u255D\x1B[0m`;
|
|
15693
|
+
const controls = `\u2191\u2193\u2190\u2192 Navigate | ENTER Select | 1-9 Quick | Q Quit`;
|
|
15694
|
+
output += `\x1B[${rows - 1};${Math.floor((cols - controls.length) / 2)}H\x1B[2m${controls}\x1B[0m`;
|
|
15695
|
+
terminal.write(output);
|
|
15696
|
+
}
|
|
15697
|
+
function renderGameEntry(game, index, isSelected, y, boxX, boxWidth) {
|
|
15698
|
+
let output = "";
|
|
15699
|
+
const contentWidth = boxWidth - 2;
|
|
15700
|
+
const prefix = isSelected ? "\u25B6" : " ";
|
|
15701
|
+
const highlight = isSelected ? lightTheme ? "\x1B[1;7;97m" : "\x1B[1;7m" : "";
|
|
15702
|
+
const keyColor = isSelected ? "" : "\x1B[33m";
|
|
15703
|
+
const keyNum = index + 1;
|
|
15704
|
+
const keyDisplay = keyNum <= 9 ? `${keyNum}` : " ";
|
|
15705
|
+
const lineContent = `${prefix} [${keyDisplay}] ${game.name}`;
|
|
15706
|
+
const padding = Math.max(0, contentWidth - lineContent.length);
|
|
15707
|
+
output += `\x1B[${y};${boxX}H${themeColor}\u2551\x1B[0m${highlight}${keyColor}${lineContent}${" ".repeat(padding)}\x1B[0m${themeColor}\u2551\x1B[0m`;
|
|
15708
|
+
const descContent = ` ${game.description}`;
|
|
15709
|
+
const descPadding = Math.max(0, contentWidth - descContent.length);
|
|
15710
|
+
const descColor = isSelected ? lightTheme ? "\x1B[2;7;97m" : "\x1B[2;7m" : "\x1B[2m";
|
|
15711
|
+
output += `\x1B[${y + 1};${boxX}H${themeColor}\u2551\x1B[0m${descColor}${descContent}${" ".repeat(descPadding)}\x1B[0m${themeColor}\u2551\x1B[0m`;
|
|
15712
|
+
return output;
|
|
15713
|
+
}
|
|
15714
|
+
setTimeout(() => {
|
|
15715
|
+
if (!running) return;
|
|
15716
|
+
if (!isTerminalValid(terminal)) {
|
|
15717
|
+
console.warn("[GamesMenu] Terminal became invalid before menu could start");
|
|
15718
|
+
running = false;
|
|
15719
|
+
return;
|
|
15720
|
+
}
|
|
15721
|
+
let keyListener = null;
|
|
15722
|
+
try {
|
|
15723
|
+
enterAlternateBuffer(terminal, "games-menu");
|
|
15724
|
+
render();
|
|
15725
|
+
const resizeListener = terminal.onResize(() => {
|
|
15726
|
+
if (running) render();
|
|
15727
|
+
});
|
|
13239
15728
|
keyListener = terminal.onKey(({ domEvent }) => {
|
|
13240
15729
|
if (!running) {
|
|
13241
15730
|
keyListener?.dispose();
|
|
@@ -13329,6 +15818,7 @@ function showGamesMenu(terminal, optionsOrCallback) {
|
|
|
13329
15818
|
const originalStop = controller.stop;
|
|
13330
15819
|
controller.stop = () => {
|
|
13331
15820
|
keyListener?.dispose();
|
|
15821
|
+
resizeListener.dispose();
|
|
13332
15822
|
exitAlternateBuffer(terminal, "games-menu-stop");
|
|
13333
15823
|
originalStop();
|
|
13334
15824
|
};
|
|
@@ -13662,7 +16152,7 @@ function runRebootEffect(terminal) {
|
|
|
13662
16152
|
|
|
13663
16153
|
// src/games/shared/effects.ts
|
|
13664
16154
|
var MAX_PARTICLES2 = 100;
|
|
13665
|
-
var
|
|
16155
|
+
var PARTICLE_CHARS2 = {
|
|
13666
16156
|
explosion: ["\u2717", "\xD7", "\xB7", "\u25CB", "\u2592", "\u2591"],
|
|
13667
16157
|
success: ["\u2726", "\u2605", "\u25C6", "\u25CF", "\u2666"],
|
|
13668
16158
|
fire: ["\u2593", "\u2592", "\u2591", "\u25CF", "\u25C6"],
|
|
@@ -13695,8 +16185,8 @@ function spawnParticles2(particles, x, y, count, color, chars = ["\u2726", "\u26
|
|
|
13695
16185
|
});
|
|
13696
16186
|
}
|
|
13697
16187
|
}
|
|
13698
|
-
function
|
|
13699
|
-
const chars =
|
|
16188
|
+
function spawnFirework3(particles, x, y, intensity = 1) {
|
|
16189
|
+
const chars = PARTICLE_CHARS2.firework;
|
|
13700
16190
|
for (let i = 0; i < 12 * intensity; i++) {
|
|
13701
16191
|
const angle = Math.PI * 2 * i / (12 * intensity);
|
|
13702
16192
|
const speed = 0.4 + Math.random() * 0.4;
|
|
@@ -13731,7 +16221,7 @@ function spawnSparkleTrail2(particles, x, y, count = 6, color = "\x1B[1;93m") {
|
|
|
13731
16221
|
particles.push({
|
|
13732
16222
|
x: x + (Math.random() - 0.5) * 4,
|
|
13733
16223
|
y,
|
|
13734
|
-
char:
|
|
16224
|
+
char: PARTICLE_CHARS2.sparkle[Math.floor(Math.random() * PARTICLE_CHARS2.sparkle.length)],
|
|
13735
16225
|
color,
|
|
13736
16226
|
vx: (Math.random() - 0.5) * 0.2,
|
|
13737
16227
|
vy: -0.3 - Math.random() * 0.3,
|
|
@@ -13739,7 +16229,7 @@ function spawnSparkleTrail2(particles, x, y, count = 6, color = "\x1B[1;93m") {
|
|
|
13739
16229
|
});
|
|
13740
16230
|
}
|
|
13741
16231
|
}
|
|
13742
|
-
function
|
|
16232
|
+
function updateParticles3(particles, gravityMult = 1) {
|
|
13743
16233
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
13744
16234
|
const p = particles[i];
|
|
13745
16235
|
p.x += p.vx;
|
|
@@ -13763,7 +16253,7 @@ function updatePopups2(popups) {
|
|
|
13763
16253
|
function createShakeState() {
|
|
13764
16254
|
return { frames: 0, intensity: 0 };
|
|
13765
16255
|
}
|
|
13766
|
-
function
|
|
16256
|
+
function triggerShake2(state, frames, intensity) {
|
|
13767
16257
|
state.frames = frames;
|
|
13768
16258
|
state.intensity = intensity;
|
|
13769
16259
|
}
|
|
@@ -13780,10 +16270,10 @@ function applyShake(state) {
|
|
|
13780
16270
|
function createFlashState() {
|
|
13781
16271
|
return { frames: 0 };
|
|
13782
16272
|
}
|
|
13783
|
-
function
|
|
16273
|
+
function triggerFlash2(state, frames) {
|
|
13784
16274
|
state.frames = frames;
|
|
13785
16275
|
}
|
|
13786
|
-
function
|
|
16276
|
+
function updateFlash2(state) {
|
|
13787
16277
|
if (state.frames > 0) {
|
|
13788
16278
|
state.frames--;
|
|
13789
16279
|
return true;
|
|
@@ -13814,7 +16304,8 @@ var games = [
|
|
|
13814
16304
|
{ id: "typingtest", name: "Typing Test", description: "Test your speed", run: runTypingTest },
|
|
13815
16305
|
{ id: "tron", name: "Tron", description: "Light cycle battle", run: runTronGame },
|
|
13816
16306
|
{ id: "crack", name: "Crack", description: "Hack the system", run: runCrackGame },
|
|
13817
|
-
{ id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame }
|
|
16307
|
+
{ id: "chopper", name: "Chopper", description: "Deliver passengers", run: runCourierGame },
|
|
16308
|
+
{ id: "hyper-fighter", name: "Hyper Fighter", description: "Gem battle vs AI", run: runHyperFighterGame }
|
|
13818
16309
|
];
|
|
13819
16310
|
function getGame(id) {
|
|
13820
16311
|
return games.find((g) => g.id === id);
|
|
@@ -13831,7 +16322,7 @@ export {
|
|
|
13831
16322
|
GAME_EVENTS,
|
|
13832
16323
|
MAX_PARTICLES2 as MAX_PARTICLES,
|
|
13833
16324
|
MODE_SELECT_ITEMS,
|
|
13834
|
-
PARTICLE_CHARS,
|
|
16325
|
+
PARTICLE_CHARS2 as PARTICLE_CHARS,
|
|
13835
16326
|
PAUSE_MENU_ITEMS,
|
|
13836
16327
|
addScorePopup,
|
|
13837
16328
|
applyShake,
|
|
@@ -13901,14 +16392,14 @@ export {
|
|
|
13901
16392
|
runWordleGame,
|
|
13902
16393
|
setTheme,
|
|
13903
16394
|
showGamesMenu,
|
|
13904
|
-
|
|
16395
|
+
spawnFirework3 as spawnFirework,
|
|
13905
16396
|
spawnParticles2 as spawnParticles,
|
|
13906
16397
|
spawnSparkleTrail2 as spawnSparkleTrail,
|
|
13907
16398
|
startMatrixRain,
|
|
13908
|
-
triggerFlash,
|
|
13909
|
-
triggerShake,
|
|
13910
|
-
updateFlash,
|
|
13911
|
-
|
|
16399
|
+
triggerFlash2 as triggerFlash,
|
|
16400
|
+
triggerShake2 as triggerShake,
|
|
16401
|
+
updateFlash2 as updateFlash,
|
|
16402
|
+
updateParticles3 as updateParticles,
|
|
13912
16403
|
updatePopups2 as updatePopups
|
|
13913
16404
|
};
|
|
13914
16405
|
//# sourceMappingURL=index.js.map
|