Batfly commited on
Commit
d663d1e
·
verified ·
1 Parent(s): 7c1de69

Create a fully responsive HTML5, CSS, and JavaScript FPS game optimized for high frame rates (60fps+), with the visual and gameplay style inspired by classic Doom (retro pixelated graphics, fast-paced movement, tight corridors, and enemies). Core Requirements: Rendering & Performance: Use WebGL or Canvas 2D for rendering. Optimize for low-latency input and smooth animations. Implement efficient sprite batching and level culling to avoid frame drops. Game Mechanics: Player can move (WASD), strafe, jump, and shoot with mouse or keyboard controls. Include hit detection and enemy AI (simple chase and attack behavior). Add health, ammo, and armor pickups. Basic HUD showing health, ammo, and minimap. Level Design: Generate maze-like maps using procedural generation or tiled layouts. Include doors, keys, and trigger zones for gameplay progression. Walls and floors use pixelated textures for a retro feel. Visual & Audio Style: Retro textures and low-res sprite enemies (pixel-art style). Add dynamic lighting (fake light cones or simple shading). Include looping heavy metal soundtrack and retro weapon sound effects. Weapons: Start with a pistol and shotgun, each with distinct fire rates and damage. Animated muzzle flash and reload animations. Optimizations: Ensure mobile and desktop compatibility. Use requestAnimationFrame for smooth rendering. Preload textures, sounds, and maps to minimize lag. Scalability: Code should be modular (separate files: index.html, style.css, main.js, enemies.js, weapons.js, levels.js). Include comments explaining performance tricks and architecture. Allow future expansions (more weapons, multiplayer). Styling & Immersion: Fullscreen support with responsive UI scaling. Retro CRT shader/filter (optional) for authentic Doom-like visuals. Smooth transition effects when changing levels or dying. Deliverables: Provide all files separately (HTML, CSS, JS, assets folder). Include a readme.md with instructions to run locally or deploy. Ensure clean, commented, and optimized code for production use. - Follow Up Deployment

Browse files
Files changed (1) hide show
  1. index.html +859 -956
index.html CHANGED
@@ -3,1102 +3,1005 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>GirlyX - Stylish Puzzle Adventure</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
- @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Rubik:wght@400;600;700&display=swap');
10
-
11
- :root {
12
- --neon-pink: #ff2a6d;
13
- --neon-blue: #05d9e8;
14
- --neon-purple: #d300c5;
15
- --neon-green: #00ff87;
16
- --dark-bg: #0d0221;
17
- }
18
-
19
  body {
20
- font-family: 'Rubik', sans-serif;
21
- background-color: var(--dark-bg);
22
- color: white;
23
  overflow: hidden;
 
 
24
  touch-action: none;
 
25
  }
26
 
27
- .pixel-font {
28
- font-family: 'Press Start 2P', cursive;
29
- }
30
-
31
- .neon-text {
32
- text-shadow: 0 0 5px var(--neon-pink), 0 0 10px var(--neon-pink), 0 0 20px var(--neon-pink);
33
  }
34
 
35
- .neon-border {
36
- box-shadow: 0 0 5px var(--neon-blue), 0 0 10px var(--neon-blue), inset 0 0 5px var(--neon-blue);
37
- border: 2px solid var(--neon-blue);
 
 
38
  }
39
 
40
- .neon-button {
41
- background: var(--dark-bg);
42
- color: var(--neon-green);
43
- border: 2px solid var(--neon-green);
44
- box-shadow: 0 0 10px var(--neon-green), 0 0 20px var(--neon-green);
45
- transition: all 0.3s;
 
 
 
 
 
 
46
  }
47
 
48
- .neon-button:hover {
49
- background: var(--neon-green);
50
- color: var(--dark-bg);
51
- box-shadow: 0 0 20px var(--neon-green), 0 0 30px var(--neon-green);
 
 
 
 
52
  }
53
 
54
- #gameCanvas {
55
- display: block;
56
- background-color: #1a0933;
57
- touch-action: none;
 
 
 
 
58
  }
59
 
60
- .level-select {
61
- display: grid;
62
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
63
- gap: 15px;
64
  }
65
 
66
- .level-card {
67
- background: rgba(13, 2, 33, 0.7);
68
- border: 2px solid var(--neon-purple);
69
- border-radius: 8px;
70
- overflow: hidden;
71
- cursor: pointer;
72
- transition: all 0.3s;
73
  }
74
 
75
- .level-card:hover {
76
- transform: scale(1.05);
77
- box-shadow: 0 0 15px var(--neon-purple);
 
 
78
  }
79
 
80
- .level-thumbnail {
 
 
 
81
  width: 100%;
82
- height: 80px;
83
- background-size: cover;
84
- background-position: center;
 
 
 
 
 
85
  }
86
 
87
- .progress-bar {
88
- height: 10px;
89
- background: rgba(255, 255, 255, 0.1);
90
- border-radius: 5px;
91
- overflow: hidden;
92
  }
93
 
94
- .progress-fill {
95
- height: 100%;
96
- background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple));
97
- transition: width 0.3s;
 
 
 
 
 
 
98
  }
99
 
100
- /* Animation for enemy collision */
101
- @keyframes shake {
102
- 0%, 100% { transform: translateX(0); }
103
- 25% { transform: translateX(-5px); }
104
- 75% { transform: translateX(5px); }
105
  }
106
 
107
- .shake {
108
- animation: shake 0.2s linear 2;
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
- /* Fill animation */
112
- @keyframes fillPulse {
113
- 0% { opacity: 0.7; }
114
- 50% { opacity: 1; }
115
- 100% { opacity: 0.7; }
 
 
 
 
 
 
 
116
  }
117
 
118
- .filling {
119
- animation: fillPulse 1s infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
  </style>
122
  </head>
123
- <body class="h-screen flex flex-col">
124
- <!-- Main Menu -->
125
- <div id="mainMenu" class="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 transition-opacity duration-500">
126
- <h1 class="text-5xl md:text-6xl font-bold mb-8 neon-text pixel-font">GirlyX</h1>
127
- <p class="text-xl mb-12 text-center max-w-md px-4">Reveal the hidden artwork by drawing shapes!<br>Avoid the enemies and complete each level.</p>
128
 
129
- <div class="flex flex-col gap-4 w-full max-w-xs">
130
- <button id="startGame" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font">START GAME</button>
131
- <button id="levelSelect" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font">LEVEL SELECT</button>
132
- <button id="howToPlay" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font">HOW TO PLAY</button>
 
133
  </div>
134
 
135
- <div class="mt-12 text-sm opacity-70">
136
- <p>Use mouse/touch to draw shapes</p>
137
- <p>Enclosed areas will reveal the artwork</p>
138
  </div>
139
- </div>
140
-
141
- <!-- Level Select Screen -->
142
- <div id="levelSelectScreen" class="fixed inset-0 bg-black bg-opacity-90 z-40 hidden flex-col items-center p-6 overflow-y-auto">
143
- <h2 class="text-3xl font-bold mb-8 neon-text pixel-font">SELECT LEVEL</h2>
144
 
145
- <div class="level-select w-full max-w-4xl mb-8">
146
- <!-- Level cards will be generated by JavaScript -->
147
- </div>
148
 
149
- <button id="backToMenu" class="neon-button py-2 px-6 rounded-lg text-md font-bold pixel-font">BACK TO MENU</button>
150
- </div>
151
-
152
- <!-- How to Play Screen -->
153
- <div id="howToPlayScreen" class="fixed inset-0 bg-black bg-opacity-90 z-40 hidden flex-col items-center p-6 overflow-y-auto">
154
- <h2 class="text-3xl font-bold mb-8 neon-text pixel-font">HOW TO PLAY</h2>
155
-
156
- <div class="max-w-2xl bg-gray-900 bg-opacity-50 p-6 rounded-lg neon-border mb-8">
157
- <div class="flex items-start mb-6">
158
- <div class="bg-blue-500 rounded-full w-8 h-8 flex items-center justify-center mr-4 flex-shrink-0">1</div>
159
- <div>
160
- <h3 class="text-xl font-bold text-blue-400 mb-2">Drawing Shapes</h3>
161
- <p>Click and drag to draw lines. Release to complete the shape. Enclosed areas will automatically fill and reveal parts of the hidden image.</p>
162
- </div>
163
- </div>
164
-
165
- <div class="flex items-start mb-6">
166
- <div class="bg-purple-500 rounded-full w-8 h-8 flex items-center justify-center mr-4 flex-shrink-0">2</div>
167
- <div>
168
- <h3 class="text-xl font-bold text-purple-400 mb-2">Avoid Enemies</h3>
169
- <p>Floating enemies will move around the screen. If you touch them while drawing, you'll lose a life. Different enemies have different movement patterns.</p>
170
- </div>
171
- </div>
172
-
173
- <div class="flex items-start mb-6">
174
- <div class="bg-green-500 rounded-full w-8 h-8 flex items-center justify-center mr-4 flex-shrink-0">3</div>
175
- <div>
176
- <h3 class="text-xl font-bold text-green-400 mb-2">Complete Levels</h3>
177
- <p>Reveal at least 70% of the hidden image to complete the level. You get bonus points for completing quickly and for creating large enclosed areas.</p>
178
- </div>
179
- </div>
180
-
181
- <div class="flex items-start">
182
- <div class="bg-pink-500 rounded-full w-8 h-8 flex items-center justify-center mr-4 flex-shrink-0">4</div>
183
- <div>
184
- <h3 class="text-xl font-bold text-pink-400 mb-2">Lives & Game Over</h3>
185
- <p>You start with 3 lives. If you lose all lives, it's game over! Your high scores are saved locally.</p>
186
- </div>
187
  </div>
188
  </div>
189
 
190
- <button id="backToMenuFromHelp" class="neon-button py-2 px-6 rounded-lg text-md font-bold pixel-font">BACK TO MENU</button>
191
- </div>
192
-
193
- <!-- Game UI -->
194
- <div id="gameUI" class="hidden">
195
- <div class="flex justify-between items-center p-4">
196
- <div class="flex items-center">
197
- <div class="mr-6">
198
- <div class="text-sm opacity-80">LEVEL</div>
199
- <div class="text-2xl font-bold neon-text" id="levelDisplay">1</div>
200
- </div>
201
-
202
- <div class="mr-6">
203
- <div class="text-sm opacity-80">SCORE</div>
204
- <div class="text-2xl font-bold neon-text" id="scoreDisplay">0</div>
205
- </div>
206
-
207
- <div>
208
- <div class="text-sm opacity-80">LIVES</div>
209
- <div class="flex" id="livesDisplay">
210
- <!-- Lives will be added here -->
211
- </div>
212
- </div>
213
- </div>
214
-
215
- <div class="flex gap-2">
216
- <button id="pauseButton" class="neon-button py-1 px-3 rounded text-sm font-bold pixel-font">PAUSE</button>
217
- <button id="restartButton" class="neon-button py-1 px-3 rounded text-sm font-bold pixel-font">RESTART</button>
218
- </div>
219
  </div>
220
 
221
- <div class="px-4 mb-2">
222
- <div class="flex justify-between text-sm mb-1">
223
- <span>Progress</span>
224
- <span id="progressPercent">0%</span>
225
- </div>
226
- <div class="progress-bar">
227
- <div class="progress-fill" id="progressBar" style="width: 0%"></div>
228
  </div>
 
229
  </div>
230
  </div>
231
-
232
- <!-- Game Canvas -->
233
- <canvas id="gameCanvas" class="flex-grow mx-auto w-full max-w-4xl"></canvas>
234
-
235
- <!-- Pause Menu -->
236
- <div id="pauseMenu" class="fixed inset-0 bg-black bg-opacity-70 z-30 hidden flex-col items-center justify-center">
237
- <h2 class="text-4xl font-bold mb-8 neon-text pixel-font">PAUSED</h2>
238
- <div class="flex flex-col gap-4 w-full max-w-xs">
239
- <button id="resumeGame" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font">RESUME</button>
240
- <button id="quitToMenu" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font">QUIT TO MENU</button>
241
- </div>
242
- </div>
243
-
244
- <!-- Level Complete Screen -->
245
- <div id="levelCompleteScreen" class="fixed inset-0 bg-black bg-opacity-70 z-30 hidden flex-col items-center justify-center">
246
- <h2 class="text-4xl font-bold mb-4 neon-text pixel-font">LEVEL COMPLETE!</h2>
247
- <div class="text-xl mb-8" id="levelCompleteStats">
248
- <!-- Stats will be added here -->
249
- </div>
250
- <button id="nextLevel" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font mb-4">NEXT LEVEL</button>
251
- <button id="backToMenuFromComplete" class="text-sm opacity-70 hover:opacity-100">Back to menu</button>
252
- </div>
253
-
254
- <!-- Game Over Screen -->
255
- <div id="gameOverScreen" class="fixed inset-0 bg-black bg-opacity-70 z-30 hidden flex-col items-center justify-center">
256
- <h2 class="text-4xl font-bold mb-4 neon-text pixel-font">GAME OVER</h2>
257
- <div class="text-xl mb-8" id="gameOverStats">
258
- <!-- Stats will be added here -->
259
- </div>
260
- <button id="restartFromGameOver" class="neon-button py-3 px-6 rounded-lg text-lg font-bold pixel-font mb-4">TRY AGAIN</button>
261
- <button id="backToMenuFromGameOver" class="text-sm opacity-70 hover:opacity-100">Back to menu</button>
262
- </div>
263
-
264
  <script>
265
- // Game Configuration
266
- const config = {
267
- canvas: {
268
- width: 800,
269
- height: 600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  },
271
- game: {
272
- lives: 3,
273
- revealThreshold: 70, // Percentage needed to complete level
274
- baseScorePerPixel: 0.1,
275
- timeBonusMultiplier: 10,
276
- areaBonusMultiplier: 0.5
 
 
 
 
 
 
 
277
  },
278
- drawing: {
279
- lineColor: '#05d9e8',
280
- lineWidth: 3,
281
- fillColor: 'rgba(255, 42, 109, 0.5)',
282
- glowColor: 'rgba(255, 42, 109, 0.3)',
283
- glowBlur: 15
284
  },
285
- enemies: {
286
- baseSpeed: 1,
287
- types: [
288
- { speed: 1, color: '#ff2a6d', size: 12, score: 100 },
289
- { speed: 2, color: '#d300c5', size: 10, score: 150 },
290
- { speed: 0.5, color: '#00ff87', size: 15, score: 50, zigzag: true }
291
- ]
292
  },
293
- levels: [
294
- {
295
- id: 1,
296
- name: "Cute Cat Girl",
297
- image: "https://i.imgur.com/JQJjZYX.png",
298
- thumbnail: "https://i.imgur.com/JQJjZYX.png",
299
- enemyCount: 3,
300
- timeLimit: 120
301
- },
302
- {
303
- id: 2,
304
- name: "Magical Girl",
305
- image: "https://i.imgur.com/8X8X8X8.png",
306
- thumbnail: "https://i.imgur.com/8X8X8X8.png",
307
- enemyCount: 5,
308
- timeLimit: 100
309
- },
310
- {
311
- id: 3,
312
- name: "Pixel Art Fox",
313
- image: "https://i.imgur.com/9Y9Y9Y9.png",
314
- thumbnail: "https://i.imgur.com/9Y9Y9Y9.png",
315
- enemyCount: 7,
316
- timeLimit: 90
317
- },
318
- {
319
- id: 4,
320
- name: "Abstract Shapes",
321
- image: "https://i.imgur.com/7Z7Z7Z7.png",
322
- thumbnail: "https://i.imgur.com/7Z7Z7Z7.png",
323
- enemyCount: 9,
324
- timeLimit: 80
325
- },
326
- {
327
- id: 5,
328
- name: "Cyber Bunny",
329
- image: "https://i.imgur.com/6A6A6A6.png",
330
- thumbnail: "https://i.imgur.com/6A6A6A6.png",
331
- enemyCount: 12,
332
- timeLimit: 70
333
- }
334
- ]
335
- };
336
-
337
- // Game State
338
- const state = {
339
- currentLevel: 0,
340
- score: 0,
341
- lives: config.game.lives,
342
- isDrawing: false,
343
- isPaused: false,
344
- gameStarted: false,
345
- currentPath: [],
346
- revealedPixels: 0,
347
- totalPixels: 0,
348
- startTime: 0,
349
- elapsedTime: 0,
350
- highScores: JSON.parse(localStorage.getItem('girlyxHighScores')) || {}
351
  };
352
-
353
- // DOM Elements
354
- const elements = {
355
- mainMenu: document.getElementById('mainMenu'),
356
- levelSelectScreen: document.getElementById('levelSelectScreen'),
357
- howToPlayScreen: document.getElementById('howToPlayScreen'),
358
- gameUI: document.getElementById('gameUI'),
359
- gameCanvas: document.getElementById('gameCanvas'),
360
- pauseMenu: document.getElementById('pauseMenu'),
361
- levelCompleteScreen: document.getElementById('levelCompleteScreen'),
362
- gameOverScreen: document.getElementById('gameOverScreen'),
363
- levelDisplay: document.getElementById('levelDisplay'),
364
- scoreDisplay: document.getElementById('scoreDisplay'),
365
- livesDisplay: document.getElementById('livesDisplay'),
366
- progressBar: document.getElementById('progressBar'),
367
- progressPercent: document.getElementById('progressPercent'),
368
- levelCompleteStats: document.getElementById('levelCompleteStats'),
369
- gameOverStats: document.getElementById('gameOverStats')
370
  };
371
-
372
- // Buttons
373
- const buttons = {
374
- startGame: document.getElementById('startGame'),
375
- levelSelect: document.getElementById('levelSelect'),
376
- howToPlay: document.getElementById('howToPlay'),
377
- backToMenu: document.getElementById('backToMenu'),
378
- backToMenuFromHelp: document.getElementById('backToMenuFromHelp'),
379
- pauseButton: document.getElementById('pauseButton'),
380
- restartButton: document.getElementById('restartButton'),
381
- resumeGame: document.getElementById('resumeGame'),
382
- quitToMenu: document.getElementById('quitToMenu'),
383
- nextLevel: document.getElementById('nextLevel'),
384
- backToMenuFromComplete: document.getElementById('backToMenuFromComplete'),
385
- restartFromGameOver: document.getElementById('restartFromGameOver'),
386
- backToMenuFromGameOver: document.getElementById('backToMenuFromGameOver')
387
  };
388
-
389
- // Canvas Context
390
- const ctx = elements.gameCanvas.getContext('2d');
391
-
392
- // Game Objects
393
- let enemies = [];
394
- let hiddenImage = new Image();
395
- let hiddenImageData = null;
396
- let pixelData = null;
397
- let levelSelectCards = [];
398
-
399
- // Initialize the game
400
- function init() {
401
- setupCanvas();
402
- setupEventListeners();
403
- generateLevelSelectCards();
404
- updateUI();
405
- }
406
-
407
- // Set up canvas dimensions
408
- function setupCanvas() {
409
- // Adjust canvas size based on window size
410
- const maxWidth = Math.min(window.innerWidth - 40, config.canvas.width);
411
- const maxHeight = Math.min(window.innerHeight - 200, config.canvas.height);
412
-
413
- elements.gameCanvas.width = maxWidth;
414
- elements.gameCanvas.height = maxHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  }
416
-
417
- // Set up event listeners
418
- function setupEventListeners() {
419
- // Mouse events
420
- elements.gameCanvas.addEventListener('mousedown', startDrawing);
421
- elements.gameCanvas.addEventListener('mousemove', draw);
422
- elements.gameCanvas.addEventListener('mouseup', endDrawing);
423
- elements.gameCanvas.addEventListener('mouseleave', endDrawing);
424
-
425
- // Touch events
426
- elements.gameCanvas.addEventListener('touchstart', handleTouchStart);
427
- elements.gameCanvas.addEventListener('touchmove', handleTouchMove);
428
- elements.gameCanvas.addEventListener('touchend', handleTouchEnd);
 
 
 
 
429
 
430
- // Button events
431
- buttons.startGame.addEventListener('click', () => {
432
- state.currentLevel = 0;
433
- startGame();
434
- });
435
 
436
- buttons.levelSelect.addEventListener('click', showLevelSelect);
437
- buttons.howToPlay.addEventListener('click', showHowToPlay);
438
- buttons.backToMenu.addEventListener('click', backToMenu);
439
- buttons.backToMenuFromHelp.addEventListener('click', backToMenu);
 
 
 
 
 
 
 
440
 
441
- buttons.pauseButton.addEventListener('click', togglePause);
442
- buttons.restartButton.addEventListener('click', restartLevel);
443
- buttons.resumeGame.addEventListener('click', togglePause);
444
- buttons.quitToMenu.addEventListener('click', quitToMenu);
445
 
446
- buttons.nextLevel.addEventListener('click', nextLevel);
447
- buttons.backToMenuFromComplete.addEventListener('click', backToMenu);
448
- buttons.restartFromGameOver.addEventListener('click', restartLevel);
449
- buttons.backToMenuFromGameOver.addEventListener('click', backToMenu);
450
 
451
- // Window resize
452
- window.addEventListener('resize', () => {
453
- if (state.gameStarted) {
454
- setupCanvas();
455
- drawGame();
456
- }
457
- });
458
- }
459
-
460
- // Generate level select cards
461
- function generateLevelSelectCards() {
462
- const container = document.querySelector('.level-select');
463
- container.innerHTML = '';
464
 
465
- config.levels.forEach((level, index) => {
466
- const card = document.createElement('div');
467
- card.className = 'level-card';
468
- card.dataset.level = index;
469
-
470
- const thumbnail = document.createElement('div');
471
- thumbnail.className = 'level-thumbnail';
472
- thumbnail.style.backgroundImage = `url(${level.thumbnail})`;
473
-
474
- const info = document.createElement('div');
475
- info.className = 'p-2';
476
-
477
- const name = document.createElement('div');
478
- name.className = 'font-bold text-center truncate';
479
- name.textContent = level.name;
480
-
481
- const highScore = document.createElement('div');
482
- highScore.className = 'text-xs text-center opacity-70';
483
- highScore.textContent = state.highScores[level.id] ? `High Score: ${state.highScores[level.id]}` : 'Not played yet';
484
-
485
- info.appendChild(name);
486
- info.appendChild(highScore);
487
- card.appendChild(thumbnail);
488
- card.appendChild(info);
489
-
490
- card.addEventListener('click', () => {
491
- state.currentLevel = index;
492
- startGame();
493
- });
494
-
495
- container.appendChild(card);
496
- levelSelectCards.push(card);
497
- });
498
  }
499
-
500
- // Start the game
501
  function startGame() {
502
- state.gameStarted = true;
503
- state.score = 0;
504
- state.lives = config.game.lives;
505
-
506
- elements.mainMenu.style.display = 'none';
507
- elements.levelSelectScreen.style.display = 'none';
508
- elements.howToPlayScreen.style.display = 'none';
509
- elements.gameUI.style.display = 'block';
510
-
511
- loadLevel(config.levels[state.currentLevel]);
512
- }
513
-
514
- // Load a level
515
- function loadLevel(level) {
516
- state.currentPath = [];
517
- state.isDrawing = false;
518
- state.revealedPixels = 0;
519
- state.startTime = Date.now();
520
- state.elapsedTime = 0;
521
 
522
- // Update UI
523
- elements.levelDisplay.textContent = level.id;
524
- updateScoreDisplay();
525
- updateLivesDisplay();
526
- updateProgressDisplay();
 
 
 
527
 
528
- // Load hidden image
529
- hiddenImage = new Image();
530
- hiddenImage.src = level.image;
531
- hiddenImage.onload = () => {
532
- // Create pixel data for collision detection
533
- createPixelData();
534
-
535
- // Create enemies
536
- createEnemies(level.enemyCount);
537
-
538
- // Start game loop
539
- if (!state.isPaused) {
540
- requestAnimationFrame(gameLoop);
541
- }
542
-
543
- drawGame();
544
- };
545
- }
546
-
547
- // Create pixel data for collision detection
548
- function createPixelData() {
549
- // Create a temporary canvas to get pixel data
550
- const tempCanvas = document.createElement('canvas');
551
- tempCanvas.width = elements.gameCanvas.width;
552
- tempCanvas.height = elements.gameCanvas.height;
553
- const tempCtx = tempCanvas.getContext('2d');
554
 
555
- // Draw the image to the temp canvas
556
- tempCtx.drawImage(
557
- hiddenImage,
558
- 0, 0, hiddenImage.width, hiddenImage.height,
559
- 0, 0, tempCanvas.width, tempCanvas.height
560
- );
561
 
562
- // Get image data
563
- hiddenImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
564
 
565
- // Create a 2D array to track revealed pixels
566
- pixelData = new Array(tempCanvas.width);
567
- for (let x = 0; x < tempCanvas.width; x++) {
568
- pixelData[x] = new Array(tempCanvas.height).fill(false);
569
- }
570
 
571
- // Calculate total non-transparent pixels
572
- state.totalPixels = 0;
573
- for (let y = 0; y < tempCanvas.height; y++) {
574
- for (let x = 0; x < tempCanvas.width; x++) {
575
- const alpha = hiddenImageData.data[(y * tempCanvas.width + x) * 4 + 3];
576
- if (alpha > 0) {
577
- state.totalPixels++;
 
 
 
 
 
 
 
 
 
 
 
 
578
  }
579
  }
580
  }
581
- }
582
-
583
- // Create enemies for the level
584
- function createEnemies(count) {
585
- enemies = [];
586
 
587
- for (let i = 0; i < count; i++) {
588
- const typeIndex = Math.floor(Math.random() * config.enemies.types.length);
589
- const enemyType = config.enemies.types[typeIndex];
590
-
591
- const enemy = {
592
- x: Math.random() * elements.gameCanvas.width,
593
- y: Math.random() * elements.gameCanvas.height,
594
- size: enemyType.size,
595
- speed: enemyType.speed * config.enemies.baseSpeed,
596
- color: enemyType.color,
597
- dx: Math.random() > 0.5 ? 1 : -1,
598
- dy: Math.random() > 0.5 ? 1 : -1,
599
- zigzag: enemyType.zigzag || false,
600
- zigzagCounter: 0,
601
- scoreValue: enemyType.score
602
- };
603
-
604
- enemies.push(enemy);
605
- }
606
- }
607
-
608
- // Game loop
609
- function gameLoop() {
610
- if (state.isPaused || !state.gameStarted) return;
611
 
612
- // Update game state
613
- update();
 
 
 
 
614
 
615
- // Draw game
616
- drawGame();
617
-
618
- // Continue loop
619
- requestAnimationFrame(gameLoop);
620
- }
621
-
622
- // Update game state
623
- function update() {
624
- // Update elapsed time
625
- state.elapsedTime = (Date.now() - state.startTime) / 1000;
626
 
627
- // Update enemies
628
- updateEnemies();
629
  }
630
-
631
- // Update enemy positions
632
- function updateEnemies() {
633
- enemies.forEach(enemy => {
634
- // Move enemy
635
- enemy.x += enemy.dx * enemy.speed;
636
- enemy.y += enemy.dy * enemy.speed;
637
-
638
- // Zigzag movement
639
- if (enemy.zigzag) {
640
- enemy.zigzagCounter++;
641
- if (enemy.zigzagCounter % 30 === 0) {
642
- enemy.dx = -enemy.dx;
643
- }
644
- if (enemy.zigzagCounter % 45 === 0) {
645
- enemy.dy = -enemy.dy;
646
- }
647
- }
648
-
649
- // Bounce off walls
650
- if (enemy.x <= 0 || enemy.x >= elements.gameCanvas.width - enemy.size) {
651
- enemy.dx = -enemy.dx;
652
- }
653
 
654
- if (enemy.y <= 0 || enemy.y >= elements.gameCanvas.height - enemy.size) {
655
- enemy.dy = -enemy.dy;
656
- }
 
 
 
 
 
 
 
 
 
 
 
 
657
 
658
- // Keep within bounds
659
- enemy.x = Math.max(0, Math.min(elements.gameCanvas.width - enemy.size, enemy.x));
660
- enemy.y = Math.max(0, Math.min(elements.gameCanvas.height - enemy.size, enemy.y));
661
- });
 
 
 
 
 
 
 
662
  }
663
-
664
- // Draw the game
665
- function drawGame() {
666
- // Clear canvas
667
- ctx.clearRect(0, 0, elements.gameCanvas.width, elements.gameCanvas.height);
668
 
669
- // Draw hidden image (only revealed parts)
670
- drawRevealedImage();
671
 
672
- // Draw current path if drawing
673
- if (state.isDrawing && state.currentPath.length > 0) {
674
- drawPath();
675
- }
676
 
677
- // Draw enemies
678
- drawEnemies();
679
- }
680
-
681
- // Draw revealed parts of the hidden image
682
- function drawRevealedImage() {
683
- if (!hiddenImageData) return;
684
 
685
- // Create a pattern with the hidden image
686
- ctx.save();
687
 
688
- // Create a clipping path from the revealed pixels
689
- ctx.beginPath();
690
- for (let y = 0; y < elements.gameCanvas.height; y++) {
691
- for (let x = 0; x < elements.gameCanvas.width; x++) {
692
- if (pixelData[x] && pixelData[x][y]) {
693
- ctx.rect(x, y, 1, 1);
694
- }
695
- }
696
  }
697
- ctx.clip();
698
 
699
- // Draw the image within the clipped area
700
- ctx.drawImage(
701
- hiddenImage,
702
- 0, 0, hiddenImage.width, hiddenImage.height,
703
- 0, 0, elements.gameCanvas.width, elements.gameCanvas.height
704
- );
705
 
706
- ctx.restore();
 
 
 
 
 
 
707
  }
708
-
709
- // Draw the current path
710
- function drawPath() {
711
- ctx.save();
 
 
 
 
 
 
 
 
 
 
712
 
713
- // Draw glow effect
714
- ctx.shadowColor = config.drawing.glowColor;
715
- ctx.shadowBlur = config.drawing.glowBlur;
716
 
717
- // Draw line
718
- ctx.beginPath();
719
- ctx.moveTo(state.currentPath[0].x, state.currentPath[0].y);
 
 
 
 
 
720
 
721
- for (let i = 1; i < state.currentPath.length; i++) {
722
- ctx.lineTo(state.currentPath[i].x, state.currentPath[i].y);
 
 
 
 
 
 
723
  }
724
 
725
- ctx.strokeStyle = config.drawing.lineColor;
726
- ctx.lineWidth = config.drawing.lineWidth;
727
- ctx.lineJoin = 'round';
728
- ctx.lineCap = 'round';
729
- ctx.stroke();
730
 
731
- ctx.restore();
732
- }
733
-
734
- // Draw enemies
735
- function drawEnemies() {
736
- enemies.forEach(enemy => {
737
- ctx.save();
738
-
739
- // Glow effect
740
- ctx.shadowColor = enemy.color;
741
- ctx.shadowBlur = 10;
742
-
743
- // Draw enemy
744
- ctx.fillStyle = enemy.color;
745
- ctx.beginPath();
746
- ctx.arc(enemy.x + enemy.size/2, enemy.y + enemy.size/2, enemy.size/2, 0, Math.PI * 2);
747
- ctx.fill();
748
-
749
- // Draw eyes for cuteness
750
- ctx.fillStyle = 'white';
751
- ctx.beginPath();
752
- ctx.arc(enemy.x + enemy.size/3, enemy.y + enemy.size/3, enemy.size/6, 0, Math.PI * 2);
753
- ctx.arc(enemy.x + enemy.size*2/3, enemy.y + enemy.size/3, enemy.size/6, 0, Math.PI * 2);
754
- ctx.fill();
755
-
756
- ctx.fillStyle = 'black';
757
- ctx.beginPath();
758
- ctx.arc(enemy.x + enemy.size/3, enemy.y + enemy.size/3, enemy.size/12, 0, Math.PI * 2);
759
- ctx.arc(enemy.x + enemy.size*2/3, enemy.y + enemy.size/3, enemy.size/12, 0, Math.PI * 2);
760
- ctx.fill();
761
-
762
- ctx.restore();
763
- });
764
- }
765
-
766
- // Start drawing
767
- function startDrawing(e) {
768
- if (state.isPaused) return;
769
 
770
- const pos = getMousePos(e);
771
- state.isDrawing = true;
772
- state.currentPath = [{ x: pos.x, y: pos.y }];
773
 
774
- // Check if starting on an enemy
775
- if (checkEnemyCollision(pos.x, pos.y)) {
776
- endDrawing();
777
- return;
 
 
778
  }
779
  }
780
-
781
- // Handle touch start
782
- function handleTouchStart(e) {
783
- e.preventDefault();
784
- startDrawing(e.touches[0]);
785
- }
786
-
787
- // Draw
788
- function draw(e) {
789
- if (!state.isDrawing || state.isPaused) return;
790
-
791
- const pos = getMousePos(e);
792
- state.currentPath.push({ x: pos.x, y: pos.y });
793
 
794
- // Check for enemy collision along the path
795
- if (checkEnemyCollision(pos.x, pos.y)) {
796
- endDrawing();
797
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  }
799
 
800
- drawGame();
801
- }
802
-
803
- // Handle touch move
804
- function handleTouchMove(e) {
805
- e.preventDefault();
806
- draw(e.touches[0]);
 
 
 
807
  }
808
-
809
- // End drawing
810
- function endDrawing(e) {
811
- if (!state.isDrawing || state.isPaused) return;
812
 
813
- if (e) {
814
- const pos = getMousePos(e);
815
- state.currentPath.push({ x: pos.x, y: pos.y });
816
 
817
- // Check for enemy collision
818
- if (checkEnemyCollision(pos.x, pos.y)) {
819
- state.isDrawing = false;
820
- state.currentPath = [];
821
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
  }
823
  }
824
-
825
- // Only process if path has enough points
826
- if (state.currentPath.length > 2) {
827
- processPath();
828
- }
829
-
830
- state.isDrawing = false;
831
- state.currentPath = [];
832
- drawGame();
833
- }
834
-
835
- // Handle touch end
836
- function handleTouchEnd(e) {
837
- e.preventDefault();
838
- endDrawing(e.changedTouches[0]);
839
- }
840
-
841
- // Get mouse position
842
- function getMousePos(e) {
843
- const rect = elements.gameCanvas.getBoundingClientRect();
844
- return {
845
- x: e.clientX - rect.left,
846
- y: e.clientY - rect.top
847
- };
848
  }
849
-
850
- // Check for enemy collision
851
- function checkEnemyCollision(x, y) {
852
- for (const enemy of enemies) {
853
- const distance = Math.sqrt(
854
- Math.pow(x - (enemy.x + enemy.size/2), 2) +
855
- Math.pow(y - (enemy.y + enemy.size/2), 2)
856
- );
857
 
858
- if (distance < enemy.size/2) {
859
- // Collision detected
860
- handleEnemyCollision(enemy);
861
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  }
863
  }
864
- return false;
865
  }
866
-
867
- // Handle enemy collision
868
- function handleEnemyCollision(enemy) {
869
- // Remove the enemy
870
- enemies = enemies.filter(e => e !== enemy);
871
-
872
- // Add score for enemy
873
- state.score += enemy.scoreValue;
874
- updateScoreDisplay();
875
-
876
- // Lose a life
877
- state.lives--;
878
- updateLivesDisplay();
879
-
880
- // Shake effect
881
- elements.gameCanvas.classList.add('shake');
882
- setTimeout(() => {
883
- elements.gameCanvas.classList.remove('shake');
884
- }, 500);
885
-
886
- // Game over check
887
- if (state.lives <= 0) {
888
- gameOver();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  }
890
  }
891
-
892
- // Process the drawn path
893
- function processPath() {
894
- // Create a temporary canvas to detect enclosed areas
895
- const tempCanvas = document.createElement('canvas');
896
- tempCanvas.width = elements.gameCanvas.width;
897
- tempCanvas.height = elements.gameCanvas.height;
898
- const tempCtx = tempCanvas.getContext('2d');
899
-
900
- // Draw the path
901
- tempCtx.beginPath();
902
- tempCtx.moveTo(state.currentPath[0].x, state.currentPath[0].y);
903
-
904
- for (let i = 1; i < state.currentPath.length; i++) {
905
- tempCtx.lineTo(state.currentPath[i].x, state.currentPath[i].y);
906
- }
907
-
908
- tempCtx.closePath();
909
- tempCtx.fillStyle = 'white';
910
- tempCtx.fill();
911
-
912
- // Get the pixel data
913
- const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
914
-
915
- // Find enclosed areas and reveal pixels
916
- let revealedCount = 0;
917
- let totalArea = 0;
918
 
919
- for (let y = 0; y < tempCanvas.height; y++) {
920
- for (let x = 0; x < tempCanvas.width; x++) {
921
- const index = (y * tempCanvas.width + x) * 4;
 
 
 
 
922
 
923
- // If pixel is white (inside the shape) and not already revealed
924
- if (imageData.data[index] === 255 && !pixelData[x][y]) {
925
- // Check if this pixel is part of the hidden image
926
- const hiddenAlpha = hiddenImageData.data[(y * tempCanvas.width + x) * 4 + 3];
927
-
928
- if (hiddenAlpha > 0) {
929
- pixelData[x][y] = true;
930
- revealedCount++;
931
- totalArea++;
932
- }
 
 
 
933
  }
 
 
 
 
934
  }
935
  }
936
-
937
- // Update revealed pixels count
938
- state.revealedPixels += revealedCount;
939
-
940
- // Calculate score
941
- const baseScore = revealedCount * config.game.baseScorePerPixel;
942
- const areaBonus = totalArea * config.game.areaBonusMultiplier;
943
- state.score += Math.floor(baseScore + areaBonus);
944
-
945
- updateScoreDisplay();
946
- updateProgressDisplay();
947
-
948
- // Check if level is complete
949
- const percentRevealed = (state.revealedPixels / state.totalPixels) * 100;
950
- if (percentRevealed >= config.game.revealThreshold) {
951
- levelComplete();
952
- }
953
  }
954
-
955
- // Update score display
956
- function updateScoreDisplay() {
957
- elements.scoreDisplay.textContent = state.score;
958
- }
959
-
960
- // Update lives display
961
- function updateLivesDisplay() {
962
- elements.livesDisplay.innerHTML = '';
963
-
964
- for (let i = 0; i < state.lives; i++) {
965
- const heart = document.createElement('div');
966
- heart.className = 'text-2xl text-red-500 mx-1';
967
- heart.innerHTML = '♥';
968
- elements.livesDisplay.appendChild(heart);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
  }
970
  }
971
-
972
- // Update progress display
973
- function updateProgressDisplay() {
974
- const percentRevealed = (state.revealedPixels / state.totalPixels) * 100;
975
- elements.progressBar.style.width = `${percentRevealed}%`;
976
- elements.progressPercent.textContent = `${Math.floor(percentRevealed)}%`;
977
- }
978
-
979
- // Level complete
980
- function levelComplete() {
981
- state.isPaused = true;
982
-
983
- // Calculate time bonus
984
- const levelTimeLimit = config.levels[state.currentLevel].timeLimit;
985
- const timeLeft = Math.max(0, levelTimeLimit - state.elapsedTime);
986
- const timeBonus = Math.floor(timeLeft * config.game.timeBonusMultiplier);
987
-
988
- // Add to score
989
- state.score += timeBonus;
990
- updateScoreDisplay();
991
 
992
- // Update high score if needed
993
- const levelId = config.levels[state.currentLevel].id;
994
- if (!state.highScores[levelId] || state.score > state.highScores[levelId]) {
995
- state.highScores[levelId] = state.score;
996
- localStorage.setItem('girlyxHighScores', JSON.stringify(state.highScores));
997
- }
998
 
999
- // Show level complete screen
1000
- elements.levelCompleteScreen.style.display = 'flex';
1001
 
1002
- // Set stats
1003
- elements.levelCompleteStats.innerHTML = `
1004
- <div class="mb-2">Score: <span class="text-green-400">${state.score}</span></div>
1005
- <div class="mb-2">Time: <span class="text-blue-400">${Math.floor(state.elapsedTime)}s</span></div>
1006
- <div class="mb-2">Time Bonus: <span class="text-purple-400">+${timeBonus}</span></div>
1007
- <div>Revealed: <span class="text-pink-400">${Math.floor((state.revealedPixels / state.totalPixels) * 100)}%</span></div>
1008
- `;
1009
- }
1010
-
1011
- // Game over
1012
- function gameOver() {
1013
- state.isPaused = true;
1014
 
1015
- // Show game over screen
1016
- elements.gameOverScreen.style.display = 'flex';
 
1017
 
1018
- // Set stats
1019
- elements.gameOverStats.innerHTML = `
1020
- <div class="mb-2">Level: <span class="text-blue-400">${config.levels[state.currentLevel].name}</span></div>
1021
- <div class="mb-2">Score: <span class="text-green-400">${state.score}</span></div>
1022
- <div class="mb-2">Revealed: <span class="text-pink-400">${Math.floor((state.revealedPixels / state.totalPixels) * 100)}%</span></div>
1023
- `;
1024
- }
1025
-
1026
- // Next level
1027
- function nextLevel() {
1028
- elements.levelCompleteScreen.style.display = 'none';
1029
 
1030
- if (state.currentLevel < config.levels.length - 1) {
1031
- state.currentLevel++;
1032
- loadLevel(config.levels[state.currentLevel]);
1033
- } else {
1034
- // Game completed
1035
- backToMenu();
1036
- }
1037
- }
1038
-
1039
- // Restart level
1040
- function restartLevel() {
1041
- elements.pauseMenu.style.display = 'none';
1042
- elements.gameOverScreen.style.display = 'none';
1043
 
1044
- state.isPaused = false;
1045
- loadLevel(config.levels[state.currentLevel]);
1046
- }
1047
-
1048
- // Toggle pause
1049
- function togglePause() {
1050
- state.isPaused = !state.isPaused;
1051
 
1052
- if (state.isPaused) {
1053
- elements.pauseMenu.style.display = 'flex';
1054
- } else {
1055
- elements.pauseMenu.style.display = 'none';
1056
- requestAnimationFrame(gameLoop);
1057
  }
1058
- }
1059
-
1060
- // Quit to menu
1061
- function quitToMenu() {
1062
- state.gameStarted = false;
1063
- state.isPaused = false;
1064
 
1065
- elements.pauseMenu.style.display = 'none';
1066
- elements.gameUI.style.display = 'none';
1067
- elements.mainMenu.style.display = 'flex';
1068
  }
1069
-
1070
- // Back to menu
1071
- function backToMenu() {
1072
- state.gameStarted = false;
1073
- state.isPaused = false;
1074
 
1075
- elements.levelCompleteScreen.style.display = 'none';
1076
- elements.gameOverScreen.style.display = 'none';
1077
- elements.gameUI.style.display = 'none';
1078
- elements.mainMenu.style.display = 'flex';
1079
- }
1080
-
1081
- // Show level select
1082
- function showLevelSelect() {
1083
- elements.mainMenu.style.display = 'none';
1084
- elements.levelSelectScreen.style.display = 'flex';
1085
- }
1086
-
1087
- // Show how to play
1088
- function showHowToPlay() {
1089
- elements.mainMenu.style.display = 'none';
1090
- elements.howToPlayScreen.style.display = 'flex';
1091
- }
1092
-
1093
- // Update UI
1094
- function updateUI() {
1095
- updateScoreDisplay();
1096
- updateLivesDisplay();
1097
- updateProgressDisplay();
1098
- }
1099
-
1100
- // Initialize the game when the page loads
1101
- window.addEventListener('load', init);
1102
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1103
  <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Batfly/girlyx" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1104
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Retro FPS - Doom Style</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
+ /* Custom CSS for CRT effect and game-specific styling */
 
 
 
 
 
 
 
 
 
10
  body {
11
+ margin: 0;
12
+ padding: 0;
 
13
  overflow: hidden;
14
+ background-color: #000;
15
+ font-family: 'Courier New', monospace;
16
  touch-action: none;
17
+ user-select: none;
18
  }
19
 
20
+ #game-container {
21
+ position: relative;
22
+ width: 100vw;
23
+ height: 100vh;
24
+ overflow: hidden;
 
25
  }
26
 
27
+ #game-canvas {
28
+ display: block;
29
+ width: 100%;
30
+ height: 100%;
31
+ image-rendering: pixelated;
32
  }
33
 
34
+ #hud {
35
+ position: absolute;
36
+ bottom: 0;
37
+ left: 0;
38
+ width: 100%;
39
+ padding: 1rem;
40
+ color: #ff0;
41
+ text-shadow: 2px 2px 0 #f00;
42
+ font-size: 1.5rem;
43
+ display: flex;
44
+ justify-content: space-between;
45
+ pointer-events: none;
46
  }
47
 
48
+ #weapon-display {
49
+ position: absolute;
50
+ bottom: 0;
51
+ left: 50%;
52
+ transform: translateX(-50%);
53
+ width: 300px;
54
+ height: 200px;
55
+ pointer-events: none;
56
  }
57
 
58
+ #crosshair {
59
+ position: absolute;
60
+ top: 50%;
61
+ left: 50%;
62
+ transform: translate(-50%, -50%);
63
+ width: 20px;
64
+ height: 20px;
65
+ pointer-events: none;
66
  }
67
 
68
+ #crosshair::before, #crosshair::after {
69
+ content: '';
70
+ position: absolute;
71
+ background-color: #ff0;
72
  }
73
 
74
+ #crosshair::before {
75
+ width: 2px;
76
+ height: 10px;
77
+ left: 9px;
78
+ top: 5px;
 
 
79
  }
80
 
81
+ #crosshair::after {
82
+ width: 10px;
83
+ height: 2px;
84
+ left: 5px;
85
+ top: 9px;
86
  }
87
 
88
+ #start-screen, #game-over-screen {
89
+ position: absolute;
90
+ top: 0;
91
+ left: 0;
92
  width: 100%;
93
+ height: 100%;
94
+ display: flex;
95
+ flex-direction: column;
96
+ justify-content: center;
97
+ align-items: center;
98
+ background-color: rgba(0, 0, 0, 0.8);
99
+ color: #ff0;
100
+ z-index: 100;
101
  }
102
 
103
+ .title {
104
+ font-size: 4rem;
105
+ margin-bottom: 2rem;
106
+ text-shadow: 4px 4px 0 #f00;
107
+ letter-spacing: 2px;
108
  }
109
 
110
+ .btn {
111
+ padding: 1rem 2rem;
112
+ background-color: #333;
113
+ color: #ff0;
114
+ border: 2px solid #ff0;
115
+ font-size: 1.5rem;
116
+ cursor: pointer;
117
+ margin: 0.5rem;
118
+ text-transform: uppercase;
119
+ letter-spacing: 1px;
120
  }
121
 
122
+ .btn:hover {
123
+ background-color: #ff0;
124
+ color: #000;
 
 
125
  }
126
 
127
+ /* CRT effect */
128
+ .crt::after {
129
+ content: " ";
130
+ display: block;
131
+ position: absolute;
132
+ top: 0;
133
+ left: 0;
134
+ bottom: 0;
135
+ right: 0;
136
+ background: rgba(18, 16, 16, 0.1);
137
+ opacity: 0.15;
138
+ z-index: 2;
139
+ pointer-events: none;
140
  }
141
 
142
+ .crt::before {
143
+ content: " ";
144
+ display: block;
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ bottom: 0;
149
+ right: 0;
150
+ background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
151
+ z-index: 2;
152
+ background-size: 100% 2px, 3px 100%;
153
+ pointer-events: none;
154
  }
155
 
156
+ @media (max-width: 768px) {
157
+ #hud {
158
+ font-size: 1rem;
159
+ padding: 0.5rem;
160
+ }
161
+
162
+ .title {
163
+ font-size: 2rem;
164
+ }
165
+
166
+ .btn {
167
+ padding: 0.5rem 1rem;
168
+ font-size: 1rem;
169
+ }
170
+
171
+ #mobile-controls {
172
+ position: absolute;
173
+ bottom: 100px;
174
+ width: 100%;
175
+ display: flex;
176
+ justify-content: space-between;
177
+ padding: 1rem;
178
+ z-index: 10;
179
+ }
180
+
181
+ .mobile-btn {
182
+ width: 60px;
183
+ height: 60px;
184
+ background-color: rgba(255, 255, 0, 0.3);
185
+ border-radius: 50%;
186
+ display: flex;
187
+ justify-content: center;
188
+ align-items: center;
189
+ color: white;
190
+ font-size: 1.5rem;
191
+ user-select: none;
192
+ }
193
+
194
+ #joystick-area {
195
+ position: absolute;
196
+ left: 20px;
197
+ bottom: 20px;
198
+ width: 120px;
199
+ height: 120px;
200
+ background-color: rgba(0, 0, 0, 0.3);
201
+ border-radius: 50%;
202
+ }
203
+
204
+ #joystick {
205
+ position: absolute;
206
+ width: 50px;
207
+ height: 50px;
208
+ background-color: rgba(255, 255, 0, 0.5);
209
+ border-radius: 50%;
210
+ top: 35px;
211
+ left: 35px;
212
+ }
213
  }
214
  </style>
215
  </head>
216
+ <body class="crt">
217
+ <div id="game-container">
218
+ <canvas id="game-canvas"></canvas>
 
 
219
 
220
+ <div id="hud">
221
+ <div id="health">HEALTH: 100</div>
222
+ <div id="ammo">AMMO: 50</div>
223
+ <div id="armor">ARMOR: 0</div>
224
+ <div id="score">SCORE: 0</div>
225
  </div>
226
 
227
+ <div id="weapon-display">
228
+ <canvas id="weapon-canvas" width="300" height="200"></canvas>
 
229
  </div>
 
 
 
 
 
230
 
231
+ <div id="crosshair"></div>
 
 
232
 
233
+ <div id="start-screen">
234
+ <h1 class="title">RETRO FPS</h1>
235
+ <button id="start-btn" class="btn">START GAME</button>
236
+ <button id="fullscreen-btn" class="btn">FULLSCREEN</button>
237
+ <div class="mt-8 text-center">
238
+ <p>WASD to move</p>
239
+ <p>Mouse to aim and shoot</p>
240
+ <p>Find the exit to win!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </div>
242
  </div>
243
 
244
+ <div id="game-over-screen" style="display: none;">
245
+ <h1 class="title" id="game-over-title">GAME OVER</h1>
246
+ <div id="final-score" class="text-2xl mb-8">SCORE: 0</div>
247
+ <button id="restart-btn" class="btn">PLAY AGAIN</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  </div>
249
 
250
+ <!-- Mobile controls (hidden on desktop) -->
251
+ <div id="mobile-controls" style="display: none;">
252
+ <div id="joystick-area">
253
+ <div id="joystick"></div>
 
 
 
254
  </div>
255
+ <div class="mobile-btn" id="shoot-btn">FIRE</div>
256
  </div>
257
  </div>
258
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  <script>
260
+ // Game constants
261
+ const CELL_SIZE = 64;
262
+ const PLAYER_HEIGHT = 32;
263
+ const PLAYER_SPEED = 5;
264
+ const ROTATION_SPEED = 0.05;
265
+ const FOV = Math.PI / 3; // 60 degrees
266
+ const HALF_FOV = FOV / 2;
267
+ const NUM_RAYS = 320;
268
+ const MAX_DEPTH = 16;
269
+ const WALL_HEIGHT = 100;
270
+
271
+ // Game state
272
+ const gameState = {
273
+ player: {
274
+ x: CELL_SIZE * 1.5,
275
+ y: CELL_SIZE * 1.5,
276
+ angle: 0,
277
+ health: 100,
278
+ armor: 0,
279
+ ammo: 50,
280
+ score: 0,
281
+ weapons: ['pistol'],
282
+ currentWeapon: 0,
283
+ isShooting: false,
284
+ lastShot: 0,
285
+ reloading: false
286
  },
287
+ enemies: [],
288
+ projectiles: [],
289
+ map: [],
290
+ level: 1,
291
+ gameOver: false,
292
+ victory: false,
293
+ started: false,
294
+ keys: {
295
+ w: false,
296
+ a: false,
297
+ s: false,
298
+ d: false,
299
+ space: false
300
  },
301
+ mouse: {
302
+ x: 0,
303
+ y: 0,
304
+ down: false
 
 
305
  },
306
+ touch: {
307
+ x: 0,
308
+ y: 0,
309
+ active: false,
310
+ startX: 0,
311
+ startY: 0
 
312
  },
313
+ lastTime: 0,
314
+ fps: 0,
315
+ frameCount: 0,
316
+ lastFpsUpdate: 0,
317
+ screenShake: 0,
318
+ damageFlash: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  };
320
+
321
+ // Textures and sprites
322
+ const textures = {
323
+ wall1: createTexture('#8B0000', '#800000'), // Dark red brick
324
+ wall2: createTexture('#006400', '#004d00'), // Dark green brick
325
+ wall3: createTexture('#00008B', '#000080'), // Dark blue brick
326
+ wall4: createTexture('#4B0082', '#3d0066'), // Indigo brick
327
+ floor: createTexture('#333333', '#222222'), // Dark floor
328
+ ceiling: createTexture('#111111', '#000000'), // Black ceiling
329
+ enemy: createEnemySprite(),
330
+ pistol: createPistolSprite(),
331
+ shotgun: createShotgunSprite(),
332
+ bulletHole: createBulletHoleSprite(),
333
+ health: createPickupSprite('#FF0000'),
334
+ ammo: createPickupSprite('#FFFF00'),
335
+ armor: createPickupSprite('#00FFFF'),
336
+ exit: createPickupSprite('#00FF00')
 
337
  };
338
+
339
+ // Audio
340
+ const audio = {
341
+ pistolShot: createSound(800, 0.2, 0.02, 'square'),
342
+ shotgunShot: createSound([400, 300, 200], 0.3, 0.1, 'sawtooth'),
343
+ enemyHit: createSound(200, 0.1, 0.01, 'square'),
344
+ playerHit: createSound(100, 0.2, 0.05, 'sine'),
345
+ pickup: createSound(600, 0.1, 0.01, 'sine'),
346
+ emptyGun: createSound(100, 0.1, 0.1, 'sine'),
347
+ reload: createSound([300, 400], 0.2, 0.1, 'sine'),
348
+ metalTrack: null // Would be loaded from file in production
 
 
 
 
 
349
  };
350
+
351
+ // Canvas setup
352
+ const canvas = document.getElementById('game-canvas');
353
+ const ctx = canvas.getContext('2d');
354
+ const weaponCanvas = document.getElementById('weapon-canvas');
355
+ const weaponCtx = weaponCanvas.getContext('2d');
356
+
357
+ // UI elements
358
+ const healthDisplay = document.getElementById('health');
359
+ const ammoDisplay = document.getElementById('ammo');
360
+ const armorDisplay = document.getElementById('armor');
361
+ const scoreDisplay = document.getElementById('score');
362
+ const startScreen = document.getElementById('start-screen');
363
+ const gameOverScreen = document.getElementById('game-over-screen');
364
+ const gameOverTitle = document.getElementById('game-over-title');
365
+ const finalScoreDisplay = document.getElementById('final-score');
366
+ const startBtn = document.getElementById('start-btn');
367
+ const restartBtn = document.getElementById('restart-btn');
368
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
369
+ const mobileControls = document.getElementById('mobile-controls');
370
+ const joystick = document.getElementById('joystick');
371
+ const shootBtn = document.getElementById('shoot-btn');
372
+
373
+ // Event listeners
374
+ startBtn.addEventListener('click', startGame);
375
+ restartBtn.addEventListener('click', startGame);
376
+ fullscreenBtn.addEventListener('click', toggleFullscreen);
377
+ document.addEventListener('keydown', handleKeyDown);
378
+ document.addEventListener('keyup', handleKeyUp);
379
+ document.addEventListener('mousemove', handleMouseMove);
380
+ document.addEventListener('mousedown', handleMouseDown);
381
+ document.addEventListener('mouseup', handleMouseUp);
382
+ document.addEventListener('touchstart', handleTouchStart, { passive: false });
383
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
384
+ document.addEventListener('touchend', handleTouchEnd);
385
+ shootBtn.addEventListener('touchstart', handleShootTouch);
386
+
387
+ // Check if mobile
388
+ if (/Mobi|Android/i.test(navigator.userAgent)) {
389
+ mobileControls.style.display = 'flex';
390
  }
391
+
392
+ // Initialize game
393
+ initGame();
394
+
395
+ // Main game loop
396
+ function gameLoop(timestamp) {
397
+ // Calculate delta time for smooth movement
398
+ const deltaTime = (timestamp - gameState.lastTime) / 1000;
399
+ gameState.lastTime = timestamp;
400
+
401
+ // Update FPS counter
402
+ updateFpsCounter(timestamp);
403
+
404
+ // Only update if game is running
405
+ if (gameState.started && !gameState.gameOver && !gameState.victory) {
406
+ update(deltaTime);
407
+ }
408
 
409
+ render();
 
 
 
 
410
 
411
+ requestAnimationFrame(gameLoop);
412
+ }
413
+
414
+ // Start the game loop
415
+ requestAnimationFrame(gameLoop);
416
+
417
+ // Game functions
418
+ function initGame() {
419
+ // Set canvas size
420
+ resizeCanvas();
421
+ window.addEventListener('resize', resizeCanvas);
422
 
423
+ // Generate level
424
+ generateLevel();
 
 
425
 
426
+ // Create some enemies
427
+ spawnEnemies(3 + gameState.level);
 
 
428
 
429
+ // Set initial weapon
430
+ gameState.player.currentWeapon = 0;
 
 
 
 
 
 
 
 
 
 
 
431
 
432
+ // Update HUD
433
+ updateHud();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  }
435
+
 
436
  function startGame() {
437
+ // Reset game state
438
+ gameState.player = {
439
+ x: CELL_SIZE * 1.5,
440
+ y: CELL_SIZE * 1.5,
441
+ angle: 0,
442
+ health: 100,
443
+ armor: 0,
444
+ ammo: 50,
445
+ score: 0,
446
+ weapons: ['pistol'],
447
+ currentWeapon: 0,
448
+ isShooting: false,
449
+ lastShot: 0,
450
+ reloading: false
451
+ };
 
 
 
 
452
 
453
+ gameState.enemies = [];
454
+ gameState.projectiles = [];
455
+ gameState.level = 1;
456
+ gameState.gameOver = false;
457
+ gameState.victory = false;
458
+ gameState.started = true;
459
+ gameState.screenShake = 0;
460
+ gameState.damageFlash = 0;
461
 
462
+ // Hide screens
463
+ startScreen.style.display = 'none';
464
+ gameOverScreen.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
+ // Generate level
467
+ generateLevel();
 
 
 
 
468
 
469
+ // Spawn enemies
470
+ spawnEnemies(3 + gameState.level);
471
 
472
+ // Update HUD
473
+ updateHud();
 
 
 
474
 
475
+ // Play music (would be loaded from file in production)
476
+ // audio.metalTrack.play();
477
+ }
478
+
479
+ function generateLevel() {
480
+ // Simple maze generation
481
+ const size = 10 + gameState.level;
482
+ gameState.map = [];
483
+
484
+ // Create empty map
485
+ for (let y = 0; y < size; y++) {
486
+ gameState.map[y] = [];
487
+ for (let x = 0; x < size; x++) {
488
+ // Border walls
489
+ if (x === 0 || y === 0 || x === size - 1 || y === size - 1) {
490
+ gameState.map[y][x] = 1;
491
+ } else {
492
+ // Random walls (25% chance)
493
+ gameState.map[y][x] = Math.random() < 0.25 ? 1 : 0;
494
  }
495
  }
496
  }
 
 
 
 
 
497
 
498
+ // Ensure player start is clear
499
+ const startX = Math.floor(gameState.player.x / CELL_SIZE);
500
+ const startY = Math.floor(gameState.player.y / CELL_SIZE);
501
+ gameState.map[startY][startX] = 0;
502
+ gameState.map[startY][startX + 1] = 0;
503
+ gameState.map[startY + 1][startX] = 0;
504
+ gameState.map[startY + 1][startX + 1] = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
 
506
+ // Place exit
507
+ let exitX, exitY;
508
+ do {
509
+ exitX = Math.floor(Math.random() * (size - 2)) + 1;
510
+ exitY = Math.floor(Math.random() * (size - 2)) + 1;
511
+ } while (Math.abs(exitX - startX) < 3 || Math.abs(exitY - startY) < 3);
512
 
513
+ gameState.map[exitY][exitX] = 5; // Exit
 
 
 
 
 
 
 
 
 
 
514
 
515
+ // Place pickups
516
+ placePickups(size, 3);
517
  }
518
+
519
+ function placePickups(size, count) {
520
+ for (let i = 0; i < count; i++) {
521
+ let x, y;
522
+ do {
523
+ x = Math.floor(Math.random() * (size - 2)) + 1;
524
+ y = Math.floor(Math.random() * (size - 2)) + 1;
525
+ } while (gameState.map[y][x] !== 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
+ // Random pickup type (2: health, 3: ammo, 4: armor)
528
+ gameState.map[y][x] = Math.floor(Math.random() * 3) + 2;
529
+ }
530
+ }
531
+
532
+ function spawnEnemies(count) {
533
+ for (let i = 0; i < count; i++) {
534
+ let x, y;
535
+ do {
536
+ x = Math.floor(Math.random() * (gameState.map[0].length - 2)) + 1;
537
+ y = Math.floor(Math.random() * (gameState.map.length - 2)) + 1;
538
+ } while (
539
+ gameState.map[y][x] !== 0 ||
540
+ distance(gameState.player.x, gameState.player.y, x * CELL_SIZE, y * CELL_SIZE) < CELL_SIZE * 3
541
+ );
542
 
543
+ gameState.enemies.push({
544
+ x: x * CELL_SIZE + CELL_SIZE / 2,
545
+ y: y * CELL_SIZE + CELL_SIZE / 2,
546
+ health: 30,
547
+ speed: 1 + Math.random() * 0.5,
548
+ damage: 10,
549
+ attackCooldown: 0,
550
+ size: 20,
551
+ type: 'demon'
552
+ });
553
+ }
554
  }
555
+
556
+ function update(deltaTime) {
557
+ // Player movement
558
+ handleMovement(deltaTime);
 
559
 
560
+ // Handle shooting
561
+ handleShooting(deltaTime);
562
 
563
+ // Update enemies
564
+ updateEnemies(deltaTime);
 
 
565
 
566
+ // Update projectiles
567
+ updateProjectiles(deltaTime);
 
 
 
 
 
568
 
569
+ // Check for pickups
570
+ checkPickups();
571
 
572
+ // Update screen shake
573
+ if (gameState.screenShake > 0) {
574
+ gameState.screenShake -= deltaTime * 10;
575
+ if (gameState.screenShake < 0) gameState.screenShake = 0;
 
 
 
 
576
  }
 
577
 
578
+ // Update damage flash
579
+ if (gameState.damageFlash > 0) {
580
+ gameState.damageFlash -= deltaTime * 10;
581
+ if (gameState.damageFlash < 0) gameState.damageFlash = 0;
582
+ }
 
583
 
584
+ // Check for game over
585
+ if (gameState.player.health <= 0) {
586
+ gameState.gameOver = true;
587
+ gameOverTitle.textContent = 'GAME OVER';
588
+ finalScoreDisplay.textContent = `SCORE: ${gameState.player.score}`;
589
+ gameOverScreen.style.display = 'flex';
590
+ }
591
  }
592
+
593
+ function handleMovement(deltaTime) {
594
+ const moveSpeed = PLAYER_SPEED * deltaTime;
595
+ const rotSpeed = ROTATION_SPEED * deltaTime;
596
+
597
+ // Rotation
598
+ if (gameState.touch.active) {
599
+ // Mobile joystick rotation
600
+ const dx = gameState.touch.x - gameState.touch.startX;
601
+ gameState.player.angle += dx * 0.01;
602
+ } else if (gameState.mouse.down) {
603
+ // Mouse look
604
+ gameState.player.angle += gameState.mouse.x * 0.005;
605
+ }
606
 
607
+ // Forward/backward movement
608
+ let moveX = 0;
609
+ let moveY = 0;
610
 
611
+ if (gameState.keys.w) {
612
+ moveX += Math.cos(gameState.player.angle) * moveSpeed;
613
+ moveY += Math.sin(gameState.player.angle) * moveSpeed;
614
+ }
615
+ if (gameState.keys.s) {
616
+ moveX -= Math.cos(gameState.player.angle) * moveSpeed;
617
+ moveY -= Math.sin(gameState.player.angle) * moveSpeed;
618
+ }
619
 
620
+ // Strafe movement
621
+ if (gameState.keys.a) {
622
+ moveX += Math.cos(gameState.player.angle - Math.PI / 2) * moveSpeed;
623
+ moveY += Math.sin(gameState.player.angle - Math.PI / 2) * moveSpeed;
624
+ }
625
+ if (gameState.keys.d) {
626
+ moveX += Math.cos(gameState.player.angle + Math.PI / 2) * moveSpeed;
627
+ moveY += Math.sin(gameState.player.angle + Math.PI / 2) * moveSpeed;
628
  }
629
 
630
+ // Jump (space)
631
+ if (gameState.keys.space) {
632
+ // Simple jump effect (would be more complex in a full game)
633
+ gameState.player.z = Math.sin(Date.now() * 0.01) * 5;
634
+ }
635
 
636
+ // Collision detection
637
+ const newX = gameState.player.x + moveX;
638
+ const newY = gameState.player.y + moveY;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
+ const cellX = Math.floor(newX / CELL_SIZE);
641
+ const cellY = Math.floor(newY / CELL_SIZE);
 
642
 
643
+ if (cellX >= 0 && cellX < gameState.map[0].length &&
644
+ cellY >= 0 && cellY < gameState.map.length) {
645
+ if (gameState.map[cellY][cellX] === 0 || gameState.map[cellY][cellX] >= 2) {
646
+ gameState.player.x = newX;
647
+ gameState.player.y = newY;
648
+ }
649
  }
650
  }
651
+
652
+ function handleShooting(deltaTime) {
653
+ const now = Date.now();
 
 
 
 
 
 
 
 
 
 
654
 
655
+ // Check if player is shooting
656
+ if ((gameState.mouse.down || gameState.touch.shooting) && !gameState.player.reloading) {
657
+ const weapon = gameState.player.weapons[gameState.player.currentWeapon];
658
+ const fireRate = weapon === 'pistol' ? 500 : 1000; // ms between shots
659
+
660
+ if (now - gameState.player.lastShot > fireRate) {
661
+ if (gameState.player.ammo > 0) {
662
+ // Fire weapon
663
+ gameState.player.lastShot = now;
664
+ gameState.player.ammo--;
665
+ gameState.screenShake = weapon === 'shotgun' ? 5 : 2;
666
+
667
+ // Play sound
668
+ if (weapon === 'pistol') {
669
+ audio.pistolShot.play();
670
+ } else {
671
+ audio.shotgunShot.play();
672
+ }
673
+
674
+ // Create projectile
675
+ const spread = weapon === 'pistol' ? 0.02 : 0.1;
676
+ const numProjectiles = weapon === 'pistol' ? 1 : 5;
677
+
678
+ for (let i = 0; i < numProjectiles; i++) {
679
+ const angle = gameState.player.angle + (Math.random() * spread - spread / 2);
680
+ gameState.projectiles.push({
681
+ x: gameState.player.x,
682
+ y: gameState.player.y,
683
+ angle: angle,
684
+ speed: 20,
685
+ range: 500,
686
+ damage: weapon === 'pistol' ? 15 : 10,
687
+ owner: 'player'
688
+ });
689
+ }
690
+
691
+ // Check for hits
692
+ checkHits(weapon === 'pistol' ? 1 : 5);
693
+
694
+ // Update HUD
695
+ updateHud();
696
+ } else {
697
+ // Out of ammo
698
+ if (now - gameState.player.lastShot > 1000) {
699
+ audio.emptyGun.play();
700
+ gameState.player.lastShot = now;
701
+ }
702
+ }
703
+ }
704
  }
705
 
706
+ // Reload if out of ammo
707
+ if (gameState.player.ammo <= 0 && !gameState.player.reloading) {
708
+ gameState.player.reloading = true;
709
+ setTimeout(() => {
710
+ gameState.player.ammo = gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 12 : 6;
711
+ gameState.player.reloading = false;
712
+ audio.reload.play();
713
+ updateHud();
714
+ }, 1000);
715
+ }
716
  }
717
+
718
+ function checkHits(numRays) {
719
+ const hitEnemies = new Set();
 
720
 
721
+ for (let i = 0; i < numRays; i++) {
722
+ const spread = 0.05;
723
+ const angle = gameState.player.angle + (Math.random() * spread - spread / 2);
724
 
725
+ // Check for enemy hits
726
+ for (const enemy of gameState.enemies) {
727
+ if (hitEnemies.has(enemy)) continue;
728
+
729
+ const dx = enemy.x - gameState.player.x;
730
+ const dy = enemy.y - gameState.player.y;
731
+ const dist = Math.sqrt(dx * dx + dy * dy);
732
+ const enemyAngle = Math.atan2(dy, dx);
733
+
734
+ // Normalize angles
735
+ const angleDiff = normalizeAngle(enemyAngle - angle);
736
+
737
+ // Check if enemy is in front of player and within FOV
738
+ if (Math.abs(angleDiff) < HALF_FOV && dist < 300) {
739
+ // Check if there's a wall between player and enemy
740
+ const steps = dist / 5;
741
+ let hitWall = false;
742
+
743
+ for (let j = 1; j <= steps; j++) {
744
+ const checkX = gameState.player.x + (dx / steps) * j;
745
+ const checkY = gameState.player.y + (dy / steps) * j;
746
+
747
+ const cellX = Math.floor(checkX / CELL_SIZE);
748
+ const cellY = Math.floor(checkY / CELL_SIZE);
749
+
750
+ if (cellX >= 0 && cellX < gameState.map[0].length &&
751
+ cellY >= 0 && cellY < gameState.map.length) {
752
+ if (gameState.map[cellY][cellX] === 1) {
753
+ hitWall = true;
754
+ break;
755
+ }
756
+ }
757
+ }
758
+
759
+ if (!hitWall) {
760
+ // Hit the enemy
761
+ enemy.health -= gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 15 : 10;
762
+ audio.enemyHit.play();
763
+ hitEnemies.add(enemy);
764
+
765
+ if (enemy.health <= 0) {
766
+ // Enemy died
767
+ gameState.player.score += 100;
768
+ updateHud();
769
+ }
770
+ }
771
+ }
772
  }
773
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774
  }
775
+
776
+ function updateEnemies(deltaTime) {
777
+ for (let i = gameState.enemies.length - 1; i >= 0; i--) {
778
+ const enemy = gameState.enemies[i];
 
 
 
 
779
 
780
+ // Remove dead enemies
781
+ if (enemy.health <= 0) {
782
+ gameState.enemies.splice(i, 1);
783
+ continue;
784
+ }
785
+
786
+ // Move toward player
787
+ const dx = gameState.player.x - enemy.x;
788
+ const dy = gameState.player.y - enemy.y;
789
+ const dist = Math.sqrt(dx * dx + dy * dy);
790
+
791
+ if (dist > 0) {
792
+ enemy.x += (dx / dist) * enemy.speed * deltaTime;
793
+ enemy.y += (dy / dist) * enemy.speed * deltaTime;
794
+ }
795
+
796
+ // Attack if close enough
797
+ if (dist < 40 && enemy.attackCooldown <= 0) {
798
+ gameState.player.health -= enemy.damage;
799
+ gameState.damageFlash = 1;
800
+ audio.playerHit.play();
801
+ enemy.attackCooldown = 1;
802
+ updateHud();
803
+ }
804
+
805
+ if (enemy.attackCooldown > 0) {
806
+ enemy.attackCooldown -= deltaTime;
807
  }
808
  }
 
809
  }
810
+
811
+ function updateProjectiles(deltaTime) {
812
+ for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
813
+ const proj = gameState.projectiles[i];
814
+
815
+ // Move projectile
816
+ proj.x += Math.cos(proj.angle) * proj.speed;
817
+ proj.y += Math.sin(proj.angle) * proj.speed;
818
+ proj.range -= proj.speed;
819
+
820
+ // Check if projectile is out of range
821
+ if (proj.range <= 0) {
822
+ gameState.projectiles.splice(i, 1);
823
+ continue;
824
+ }
825
+
826
+ // Check for wall collision
827
+ const cellX = Math.floor(proj.x / CELL_SIZE);
828
+ const cellY = Math.floor(proj.y / CELL_SIZE);
829
+
830
+ if (cellX >= 0 && cellX < gameState.map[0].length &&
831
+ cellY >= 0 && cellY < gameState.map.length) {
832
+ if (gameState.map[cellY][cellX] === 1) {
833
+ gameState.projectiles.splice(i, 1);
834
+ continue;
835
+ }
836
+ }
837
+
838
+ // Check for enemy hits (if projectile is from player)
839
+ if (proj.owner === 'player') {
840
+ for (const enemy of gameState.enemies) {
841
+ const dist = distance(proj.x, proj.y, enemy.x, enemy.y);
842
+ if (dist < enemy.size) {
843
+ enemy.health -= proj.damage;
844
+ audio.enemyHit.play();
845
+ gameState.projectiles.splice(i, 1);
846
+
847
+ if (enemy.health <= 0) {
848
+ gameState.player.score += 100;
849
+ updateHud();
850
+ }
851
+ break;
852
+ }
853
+ }
854
+ }
855
  }
856
  }
857
+
858
+ function checkPickups() {
859
+ const cellX = Math.floor(gameState.player.x / CELL_SIZE);
860
+ const cellY = Math.floor(gameState.player.y / CELL_SIZE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
 
862
+ if (cellX >= 0 && cellX < gameState.map[0].length &&
863
+ cellY >= 0 && cellY < gameState.map.length) {
864
+ const cellValue = gameState.map[cellY][cellX];
865
+
866
+ if (cellValue >= 2) {
867
+ // Pickup item
868
+ audio.pickup.play();
869
 
870
+ switch (cellValue) {
871
+ case 2: // Health
872
+ gameState.player.health = Math.min(100, gameState.player.health + 25);
873
+ break;
874
+ case 3: // Ammo
875
+ gameState.player.ammo += gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 12 : 6;
876
+ break;
877
+ case 4: // Armor
878
+ gameState.player.armor = Math.min(100, gameState.player.armor + 25);
879
+ break;
880
+ case 5: // Exit
881
+ nextLevel();
882
+ return;
883
  }
884
+
885
+ // Remove pickup from map
886
+ gameState.map[cellY][cellX] = 0;
887
+ updateHud();
888
  }
889
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  }
891
+
892
+ function nextLevel() {
893
+ gameState.level++;
894
+ gameState.player.score += 1000;
895
+
896
+ if (gameState.level > 5) {
897
+ // Victory!
898
+ gameState.victory = true;
899
+ gameOverTitle.textContent = 'VICTORY!';
900
+ finalScoreDisplay.textContent = `FINAL SCORE: ${gameState.player.score}`;
901
+ gameOverScreen.style.display = 'flex';
902
+ } else {
903
+ // Generate next level
904
+ generateLevel();
905
+
906
+ // Spawn more enemies
907
+ spawnEnemies(3 + gameState.level);
908
+
909
+ // Add shotgun on level 2
910
+ if (gameState.level === 2 && !gameState.player.weapons.includes('shotgun')) {
911
+ gameState.player.weapons.push('shotgun');
912
+ }
913
+
914
+ // Reset player position
915
+ gameState.player.x = CELL_SIZE * 1.5;
916
+ gameState.player.y = CELL_SIZE * 1.5;
917
+ gameState.player.angle = 0;
918
+
919
+ // Update HUD
920
+ updateHud();
921
  }
922
  }
923
+
924
+ function render() {
925
+ // Clear canvas
926
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
 
928
+ // Apply screen shake effect
929
+ const shakeX = gameState.screenShake > 0 ? (Math.random() * gameState.screenShake - gameState.screenShake / 2) : 0;
930
+ const shakeY = gameState.screenShake > 0 ? (Math.random() * gameState.screenShake - gameState.screenShake / 2) : 0;
 
 
 
931
 
932
+ ctx.save();
933
+ ctx.translate(shakeX, shakeY);
934
 
935
+ // Draw sky (top half)
936
+ ctx.fillStyle = '#1a1a2e';
937
+ ctx.fillRect(0, 0, canvas.width, canvas.height / 2);
 
 
 
 
 
 
 
 
 
938
 
939
+ // Draw floor (bottom half)
940
+ ctx.fillStyle = '#333333';
941
+ ctx.fillRect(0, canvas.height / 2, canvas.width, canvas.height / 2);
942
 
943
+ // Raycasting to draw walls
944
+ castRays();
 
 
 
 
 
 
 
 
 
945
 
946
+ // Draw enemies
947
+ drawEnemies();
 
 
 
 
 
 
 
 
 
 
 
948
 
949
+ // Draw weapon
950
+ drawWeapon();
 
 
 
 
 
951
 
952
+ // Apply damage flash if player was hit
953
+ if (gameState.damageFlash > 0) {
954
+ ctx.fillStyle = `rgba(255, 0, 0, ${gameState.damageFlash * 0.3})`;
955
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
 
956
  }
 
 
 
 
 
 
957
 
958
+ ctx.restore();
 
 
959
  }
960
+
961
+ function castRays() {
962
+ const halfHeight = canvas.height / 2;
 
 
963
 
964
+ for (let i = 0; i < NUM_RAYS; i++) {
965
+ const rayAngle = gameState.player.angle - HALF_FOV + (i / NUM_RAYS) * FOV;
966
+ const rayCos = Math.cos(rayAngle);
967
+ const raySin = Math.sin(rayAngle);
968
+
969
+ let distToWall = 0;
970
+ let hitWall = false;
971
+ let wallX, wallY;
972
+ let wallType;
973
+
974
+ // Incrementally check for wall hits
975
+ while (!hitWall && distToWall < MAX_DEPTH * CELL_SIZE) {
976
+ distToWall += 2;
977
+ wallX = gameState.player.x + rayCos * distToWall;
978
+ wallY = gameState.player.y + raySin * distToWall;
979
+
980
+ const mapX = Math.floor(wallX / CELL_SIZE);
981
+ const mapY = Math.floor(wallY / CELL_SIZE);
982
+
983
+ // Check if ray is out of bounds
984
+ if (mapX < 0 || mapX >= gameState.map[0].length ||
985
+ mapY < 0 || mapY >= gameState.map.length) {
986
+ hitWall = true;
987
+ distToWall = MAX_DEPTH * CELL_SIZE;
988
+ wallType = 1;
989
+ }
990
+ // Check if ray hit a wall
991
+ else if (gameState.map[mapY][mapX] > 0) {
992
+ hitWall = true;
993
+ wallType = gameState.map[mapY][mapX];
994
+ }
995
+ }
996
+
997
+ // Calculate distance to wall (correcting for fish-eye effect)
998
+ const correctedDist = distToWall * Math.cos(rayAngle - gameState.player.angle);
999
+
1000
+ // Calculate wall height
1001
+ const wallHeight = (CELL_SIZE / correctedDist) * ((canvas.width / 2) / Math.tan(HALF_FOV));
1002
+
1003
+ // Draw wall slice
1004
+ if (hitWall && wallType !== undefined) {
1005
+ const wallTop = Math.max(0, halfHeight{"ok":false,"message":"terminated"}
1006
  <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Batfly/girlyx" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1007
  </html>