index.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  6. <title>Neon Infiltrator</title>
  7. <style>
  8. * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; }
  9. html, body { overflow: hidden; }
  10. body { background: #000; color: #eee; font-family: monospace; display: flex; flex-direction: column; align-items: center; height: 100vh; }
  11. #topbar { width: 100%; padding: 8px 16px; display: flex; align-items: center; gap: 16px; background: #111; border-bottom: 1px solid #333; }
  12. #topbar a { color: #FFA500; text-decoration: none; font-size: 14px; }
  13. #ui { display: flex; gap: 16px; padding: 6px 8px; font-size: 14px; color: #FFA500; flex-wrap: wrap; justify-content: center; width: 100%; }
  14. canvas { display: block; width: 100%; height: auto; max-width: 700px; border: 1px solid #333; image-rendering: crisp-edges; }
  15. #msg { font-size: 13px; color: #FFA500; margin: 3px; text-align: center; min-height: 18px; }
  16. #dpad { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 4px; }
  17. .dpad-row { display: flex; gap: 4px; }
  18. #dpad button { width: 50px; height: 50px; background: #1a0d00; border: 1px solid #FF6600; color: #FF6600; font-size: 20px; cursor: pointer; border-radius: 4px; }
  19. #dpad button:active { background: #3a1a00; }
  20. #controls { color: #666; font-size: 12px; text-align: center; margin-bottom: 3px; }
  21. #scoreSubmit { text-align: center; margin: 6px; }
  22. #scoreSubmit button { background: #1a3a1a; border: 1px solid #FFA500; color: #FFA500; padding: 6px 16px; cursor: pointer; font-family: monospace; font-size: 14px; }
  23. @media (min-width: 768px) { #dpad, #controls { display: none; } }
  24. </style>
  25. </head>
  26. <body>
  27. <div id="topbar">
  28. <a href="/games" target="_top">&#8592; Back to Games</a>
  29. <span style="color:#FFA500;font-weight:bold">NEON INFILTRATOR</span>
  30. </div>
  31. <div id="ui">
  32. <span>DATA: <b id="dataEl">0/0</b></span>
  33. <span>LEVEL: <b id="levelEl">1</b></span>
  34. <span>SCORE: <b id="scoreEl">0</b></span>
  35. <span>BEST: <b id="bestEl">-</b></span>
  36. <span>ENEMIES: <b id="enemyEl">0</b></span>
  37. </div>
  38. <canvas id="c"></canvas>
  39. <div id="msg">Press any arrow key or tap to start</div>
  40. <div id="dpad">
  41. <div class="dpad-row"><button id="btn-up">&#8593;</button></div>
  42. <div class="dpad-row">
  43. <button id="btn-left">&#8592;</button>
  44. <button id="btn-down">&#8595;</button>
  45. <button id="btn-right">&#8594;</button>
  46. </div>
  47. </div>
  48. <div id="controls">Arrow keys / WASD &nbsp;|&nbsp; SPACE — new game</div>
  49. <div id="scoreSubmit" style="display:none">
  50. <form method="POST" action="/games/submit-score" target="_top">
  51. <input type="hidden" name="game" value="neoninfiltrator">
  52. <input type="hidden" id="scoreInput" name="score" value="0">
  53. <button type="submit">Submit Score to Hall of Fame</button>
  54. </form>
  55. </div>
  56. <script>
  57. const canvas = document.getElementById('c');
  58. const ctx = canvas.getContext('2d');
  59. const CELL = 28;
  60. let MAP_W = 0, MAP_H = 0;
  61. let walls = [], enemies = [], dataItems = [], exitPos = { x: -1, y: -1 };
  62. let player = { x: 1, y: 1 };
  63. let level = 1, dataCollected = 0, totalData = 0;
  64. let score = 0;
  65. let best = parseInt(localStorage.getItem('ni_best') || '-1');
  66. let gameState = 'idle';
  67. let lastPlayerMove = 0, lastEnemyStep = 0;
  68. const MOVE_DELAY = 140, ENEMY_INTERVAL = 500;
  69. const keys = {};
  70. function setMsg(txt) { document.getElementById('msg').textContent = txt; }
  71. function updateUI() {
  72. document.getElementById('dataEl').textContent = dataCollected + '/' + totalData;
  73. document.getElementById('levelEl').textContent = level;
  74. document.getElementById('scoreEl').textContent = score;
  75. document.getElementById('bestEl').textContent = best < 0 ? '-' : best;
  76. document.getElementById('enemyEl').textContent = enemies.length;
  77. }
  78. function resizeCanvas() {
  79. let bw = 18, bh = 14;
  80. if (level >= 5) { bw = 22; bh = 18; }
  81. else if (level >= 3) { bw = 20; bh = 16; }
  82. MAP_W = bw; MAP_H = bh;
  83. canvas.width = MAP_W * CELL;
  84. canvas.height = MAP_H * CELL;
  85. }
  86. function generateLevel() {
  87. resizeCanvas();
  88. walls = Array(MAP_H).fill(null).map(() => Array(MAP_W).fill(false));
  89. for (let i = 0; i < MAP_W; i++) { walls[0][i] = true; walls[MAP_H - 1][i] = true; }
  90. for (let i = 0; i < MAP_H; i++) { walls[i][0] = true; walls[i][MAP_W - 1] = true; }
  91. for (let i = 0; i < Math.floor(MAP_W * MAP_H / 7); i++) {
  92. const x = 2 + Math.floor(Math.random() * (MAP_W - 4));
  93. const y = 2 + Math.floor(Math.random() * (MAP_H - 4));
  94. if (!walls[y][x] && !(Math.abs(x - player.x) < 2 && Math.abs(y - player.y) < 2)) {
  95. if (Math.random() < 0.55) walls[y][x] = true;
  96. }
  97. }
  98. for (let i = 0; i < MAP_W * 2; i++) {
  99. const x = 1 + Math.floor(Math.random() * (MAP_W - 2));
  100. const y = 1 + Math.floor(Math.random() * (MAP_H - 2));
  101. if (walls[y][x] && x > 1 && x < MAP_W - 2 && y > 1 && y < MAP_H - 2) {
  102. if (Math.random() < 0.5) walls[y][x] = false;
  103. }
  104. }
  105. player.x = 1; player.y = 1;
  106. if (walls[player.y][player.x]) {
  107. outer: for (let y = 1; y < MAP_H - 1; y++) {
  108. for (let x = 1; x < MAP_W - 1; x++) {
  109. if (!walls[y][x]) { player.x = x; player.y = y; break outer; }
  110. }
  111. }
  112. }
  113. exitPos = { x: MAP_W - 2, y: MAP_H - 2 };
  114. for (let t = 0; t < 200; t++) {
  115. const ex = MAP_W - 2 - Math.floor(Math.random() * 3);
  116. const ey = MAP_H - 2 - Math.floor(Math.random() * 3);
  117. if (ex >= 1 && ey >= 1 && !walls[ey][ex] && (Math.abs(ex - player.x) + Math.abs(ey - player.y)) > 6) {
  118. exitPos = { x: ex, y: ey }; break;
  119. }
  120. }
  121. totalData = Math.min(12, 3 + Math.floor(level / 1.5));
  122. dataItems = [];
  123. for (let i = 0; i < totalData; i++) {
  124. let placed = false;
  125. for (let t = 0; t < 100; t++) {
  126. const dx = 1 + Math.floor(Math.random() * (MAP_W - 2));
  127. const dy = 1 + Math.floor(Math.random() * (MAP_H - 2));
  128. if (!walls[dy][dx] && !(dx === player.x && dy === player.y) && !(dx === exitPos.x && dy === exitPos.y) && !dataItems.some(d => d.x === dx && d.y === dy)) {
  129. dataItems.push({ x: dx, y: dy, collected: false }); placed = true; break;
  130. }
  131. }
  132. if (!placed) {
  133. outer2: for (let y = 1; y < MAP_H - 1; y++) {
  134. for (let x = 1; x < MAP_W - 1; x++) {
  135. if (!walls[y][x] && !(x === player.x && y === player.y) && !(x === exitPos.x && y === exitPos.y) && !dataItems.some(d => d.x === x && d.y === y)) {
  136. dataItems.push({ x, y, collected: false }); break outer2;
  137. }
  138. }
  139. }
  140. }
  141. }
  142. dataCollected = 0;
  143. const enemyCount = Math.min(6, 1 + Math.floor(level / 2));
  144. enemies = [];
  145. for (let i = 0; i < enemyCount; i++) {
  146. let placed = false;
  147. for (let t = 0; t < 150; t++) {
  148. const ex = 2 + Math.floor(Math.random() * (MAP_W - 4));
  149. const ey = 2 + Math.floor(Math.random() * (MAP_H - 4));
  150. if (!walls[ey][ex] && (Math.abs(ex - player.x) + Math.abs(ey - player.y)) > 4 && !(ex === exitPos.x && ey === exitPos.y) && !dataItems.some(d => d.x === ex && d.y === ey)) {
  151. enemies.push({ x: ex, y: ey }); placed = true; break;
  152. }
  153. }
  154. if (!placed) {
  155. outer3: for (let y = 2; y < MAP_H - 2; y++) {
  156. for (let x = 2; x < MAP_W - 2; x++) {
  157. if (!walls[y][x] && (Math.abs(x - player.x) + Math.abs(y - player.y)) > 3 && !enemies.some(e => e.x === x && e.y === y)) {
  158. enemies.push({ x, y }); break outer3;
  159. }
  160. }
  161. }
  162. }
  163. }
  164. updateUI();
  165. setMsg(`Level ${level} — Collect ${totalData} data items and reach the exit`);
  166. }
  167. function checkEnemyCollision() {
  168. for (const e of enemies) {
  169. if (e.x === player.x && e.y === player.y) { endGame(); return true; }
  170. }
  171. return false;
  172. }
  173. function tryMove(dx, dy) {
  174. if (gameState !== 'play') return;
  175. const nx = player.x + dx, ny = player.y + dy;
  176. if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H || walls[ny][nx]) return;
  177. player.x = nx; player.y = ny;
  178. for (const d of dataItems) {
  179. if (!d.collected && d.x === player.x && d.y === player.y) {
  180. d.collected = true; dataCollected++; score += 100;
  181. updateUI(); setMsg(`Data collected! (${dataCollected}/${totalData})`); break;
  182. }
  183. }
  184. if (dataCollected === totalData && exitPos.x === player.x && exitPos.y === player.y) {
  185. const bonus = level * 50;
  186. score += bonus;
  187. updateUI();
  188. setMsg(`Level ${level} complete! +${bonus} bonus. Advancing...`);
  189. gameState = 'transit';
  190. setTimeout(() => { level++; generateLevel(); gameState = 'play'; lastPlayerMove = performance.now(); lastEnemyStep = performance.now(); }, 1400);
  191. return;
  192. }
  193. checkEnemyCollision();
  194. }
  195. function moveEnemies() {
  196. if (gameState !== 'play') return;
  197. for (const e of enemies) {
  198. let dx = 0, dy = 0;
  199. if (Math.random() < 0.6) {
  200. if (Math.abs(e.x - player.x) > Math.abs(e.y - player.y)) dx = e.x > player.x ? -1 : 1;
  201. else dy = e.y > player.y ? -1 : 1;
  202. } else {
  203. const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  204. [dx, dy] = dirs[Math.floor(Math.random() * 4)];
  205. }
  206. const nx = e.x + dx, ny = e.y + dy;
  207. if (nx >= 0 && nx < MAP_W && ny >= 0 && ny < MAP_H && !walls[ny][nx] && !(nx === exitPos.x && ny === exitPos.y)) {
  208. e.x = nx; e.y = ny;
  209. } else {
  210. for (const [sx, sy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
  211. const bx = e.x + sx, by = e.y + sy;
  212. if (bx >= 0 && bx < MAP_W && by >= 0 && by < MAP_H && !walls[by][bx] && !(bx === exitPos.x && by === exitPos.y)) {
  213. e.x = bx; e.y = by; break;
  214. }
  215. }
  216. }
  217. }
  218. checkEnemyCollision();
  219. updateUI();
  220. }
  221. function endGame() {
  222. gameState = 'over';
  223. if (best < 0 || score > best) { best = score; localStorage.setItem('ni_best', best); }
  224. updateUI();
  225. setMsg(`DETECTED! Score: ${score}. Press SPACE for new game.`);
  226. document.getElementById('scoreInput').value = score;
  227. document.getElementById('scoreSubmit').style.display = 'block';
  228. }
  229. function newGame() {
  230. score = 0; level = 1;
  231. document.getElementById('scoreSubmit').style.display = 'none';
  232. generateLevel();
  233. gameState = 'play';
  234. lastPlayerMove = performance.now();
  235. lastEnemyStep = performance.now();
  236. }
  237. function draw() {
  238. ctx.clearRect(0, 0, canvas.width, canvas.height);
  239. ctx.fillStyle = '#050505';
  240. ctx.fillRect(0, 0, canvas.width, canvas.height);
  241. ctx.strokeStyle = '#1a0d00'; ctx.lineWidth = 0.6;
  242. for (let i = 0; i <= MAP_W; i++) {
  243. ctx.beginPath(); ctx.moveTo(i * CELL, 0); ctx.lineTo(i * CELL, canvas.height); ctx.stroke();
  244. }
  245. for (let i = 0; i <= MAP_H; i++) {
  246. ctx.beginPath(); ctx.moveTo(0, i * CELL); ctx.lineTo(canvas.width, i * CELL); ctx.stroke();
  247. }
  248. for (let y = 0; y < MAP_H; y++) {
  249. for (let x = 0; x < MAP_W; x++) {
  250. if (walls[y][x]) {
  251. ctx.fillStyle = '#1a0a05';
  252. ctx.fillRect(x * CELL, y * CELL, CELL - 0.5, CELL - 0.5);
  253. ctx.strokeStyle = '#FF6600'; ctx.lineWidth = 1;
  254. ctx.strokeRect(x * CELL, y * CELL, CELL - 0.5, CELL - 0.5);
  255. }
  256. }
  257. }
  258. for (const d of dataItems) {
  259. if (!d.collected) {
  260. ctx.fillStyle = '#FFB347'; ctx.shadowBlur = 6; ctx.shadowColor = '#FF6600';
  261. ctx.beginPath(); ctx.arc(d.x * CELL + CELL / 2, d.y * CELL + CELL / 2, CELL * 0.25, 0, Math.PI * 2); ctx.fill();
  262. ctx.fillStyle = '#FF884D';
  263. ctx.beginPath(); ctx.arc(d.x * CELL + CELL / 2, d.y * CELL + CELL / 2, CELL * 0.12, 0, Math.PI * 2); ctx.fill();
  264. ctx.shadowBlur = 0;
  265. }
  266. }
  267. ctx.fillStyle = '#FFA500'; ctx.globalAlpha = 0.7;
  268. ctx.fillRect(exitPos.x * CELL + 4, exitPos.y * CELL + 4, CELL - 8, CELL - 8);
  269. ctx.globalAlpha = 1; ctx.fillStyle = '#FFA500';
  270. ctx.font = `${CELL - 6}px monospace`;
  271. ctx.fillText('\u25C8', exitPos.x * CELL + CELL * 0.28, exitPos.y * CELL + CELL * 0.78);
  272. if (dataCollected === totalData && gameState === 'play') {
  273. ctx.fillStyle = '#FFB347'; ctx.shadowBlur = 4; ctx.shadowColor = '#FF6600';
  274. ctx.font = 'bold 14px monospace'; ctx.textAlign = 'center';
  275. ctx.fillText('\u2192 EXIT AVAILABLE \u2190', canvas.width / 2, 20);
  276. ctx.shadowBlur = 0; ctx.textAlign = 'left';
  277. }
  278. for (const e of enemies) {
  279. ctx.fillStyle = '#FF4400'; ctx.shadowBlur = 6; ctx.shadowColor = '#FF2200';
  280. ctx.fillRect(e.x * CELL + 4, e.y * CELL + 4, CELL - 8, CELL - 8);
  281. ctx.shadowBlur = 0;
  282. }
  283. ctx.fillStyle = '#FF6600'; ctx.shadowBlur = 8; ctx.shadowColor = '#FF6600';
  284. ctx.fillRect(player.x * CELL + 6, player.y * CELL + 6, CELL - 12, CELL - 12);
  285. ctx.fillStyle = '#FFB347';
  286. ctx.fillRect(player.x * CELL + 10, player.y * CELL + 10, 5, 5);
  287. ctx.shadowBlur = 0;
  288. }
  289. function gameLoop(now) {
  290. if (gameState === 'play') {
  291. let dx = 0, dy = 0;
  292. if (keys.ArrowUp || keys.w) dy = -1;
  293. if (keys.ArrowDown || keys.s) dy = 1;
  294. if (keys.ArrowLeft || keys.a) dx = -1;
  295. if (keys.ArrowRight || keys.d) dx = 1;
  296. if ((dx !== 0 || dy !== 0) && now - lastPlayerMove >= MOVE_DELAY) {
  297. tryMove(dx, dy); lastPlayerMove = now;
  298. }
  299. if (now - lastEnemyStep >= ENEMY_INTERVAL) { moveEnemies(); lastEnemyStep = now; }
  300. }
  301. draw();
  302. requestAnimationFrame(gameLoop);
  303. }
  304. document.addEventListener('keydown', e => {
  305. const key = e.key;
  306. if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 's', 'a', 'd', ' '].includes(key)) e.preventDefault();
  307. if (key === ' ') { if (gameState === 'idle' || gameState === 'over') { newGame(); } return; }
  308. keys[key] = true;
  309. if (gameState === 'idle') newGame();
  310. });
  311. document.addEventListener('keyup', e => { keys[e.key] = false; });
  312. const dpadStart = () => { if (gameState === 'idle' || gameState === 'over') newGame(); };
  313. document.getElementById('btn-up').addEventListener('click', () => { dpadStart(); tryMove(0, -1); });
  314. document.getElementById('btn-down').addEventListener('click', () => { dpadStart(); tryMove(0, 1); });
  315. document.getElementById('btn-left').addEventListener('click', () => { dpadStart(); tryMove(-1, 0); });
  316. document.getElementById('btn-right').addEventListener('click', () => { dpadStart(); tryMove(1, 0); });
  317. generateLevel();
  318. requestAnimationFrame(gameLoop);
  319. </script>
  320. </body>
  321. </html>