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- 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>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<style>
|
| 9 |
-
|
| 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 |
-
|
| 21 |
-
|
| 22 |
-
color: white;
|
| 23 |
overflow: hidden;
|
|
|
|
|
|
|
| 24 |
touch-action: none;
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
text-shadow: 0 0 5px var(--neon-pink), 0 0 10px var(--neon-pink), 0 0 20px var(--neon-pink);
|
| 33 |
}
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
-
#
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
}
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
cursor: pointer;
|
| 72 |
-
transition: all 0.3s;
|
| 73 |
}
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
width: 100%;
|
| 82 |
-
height:
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
-
.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
}
|
| 93 |
|
| 94 |
-
.
|
| 95 |
-
|
| 96 |
-
background:
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
25% { transform: translateX(-5px); }
|
| 104 |
-
75% { transform: translateX(5px); }
|
| 105 |
}
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
</style>
|
| 122 |
</head>
|
| 123 |
-
<body class="
|
| 124 |
-
<
|
| 125 |
-
|
| 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
|
| 130 |
-
<
|
| 131 |
-
<
|
| 132 |
-
<
|
|
|
|
| 133 |
</div>
|
| 134 |
|
| 135 |
-
<div
|
| 136 |
-
<
|
| 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
|
| 146 |
-
<!-- Level cards will be generated by JavaScript -->
|
| 147 |
-
</div>
|
| 148 |
|
| 149 |
-
<
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 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 |
-
<
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 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 |
-
<
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
<
|
| 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
|
| 266 |
-
const
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
},
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
},
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
glowColor: 'rgba(255, 42, 109, 0.3)',
|
| 283 |
-
glowBlur: 15
|
| 284 |
},
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
]
|
| 292 |
},
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 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 |
-
//
|
| 354 |
-
const
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
gameOverStats: document.getElementById('gameOverStats')
|
| 370 |
};
|
| 371 |
-
|
| 372 |
-
//
|
| 373 |
-
const
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 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
|
| 390 |
-
const
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
}
|
| 416 |
-
|
| 417 |
-
//
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
|
| 430 |
-
|
| 431 |
-
buttons.startGame.addEventListener('click', () => {
|
| 432 |
-
state.currentLevel = 0;
|
| 433 |
-
startGame();
|
| 434 |
-
});
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
buttons.resumeGame.addEventListener('click', togglePause);
|
| 444 |
-
buttons.quitToMenu.addEventListener('click', quitToMenu);
|
| 445 |
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
buttons.restartFromGameOver.addEventListener('click', restartLevel);
|
| 449 |
-
buttons.backToMenuFromGameOver.addEventListener('click', backToMenu);
|
| 450 |
|
| 451 |
-
//
|
| 452 |
-
|
| 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 |
-
|
| 466 |
-
|
| 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 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
state.isDrawing = false;
|
| 518 |
-
state.revealedPixels = 0;
|
| 519 |
-
state.startTime = Date.now();
|
| 520 |
-
state.elapsedTime = 0;
|
| 521 |
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
-
//
|
| 529 |
-
|
| 530 |
-
|
| 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 |
-
//
|
| 556 |
-
|
| 557 |
-
hiddenImage,
|
| 558 |
-
0, 0, hiddenImage.width, hiddenImage.height,
|
| 559 |
-
0, 0, tempCanvas.width, tempCanvas.height
|
| 560 |
-
);
|
| 561 |
|
| 562 |
-
//
|
| 563 |
-
|
| 564 |
|
| 565 |
-
//
|
| 566 |
-
|
| 567 |
-
for (let x = 0; x < tempCanvas.width; x++) {
|
| 568 |
-
pixelData[x] = new Array(tempCanvas.height).fill(false);
|
| 569 |
-
}
|
| 570 |
|
| 571 |
-
//
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
}
|
| 579 |
}
|
| 580 |
}
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
// Create enemies for the level
|
| 584 |
-
function createEnemies(count) {
|
| 585 |
-
enemies = [];
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 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 |
-
//
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
|
| 615 |
-
//
|
| 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 |
-
//
|
| 628 |
-
|
| 629 |
}
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 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 |
-
|
| 655 |
-
|
| 656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
}
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
ctx.clearRect(0, 0, elements.gameCanvas.width, elements.gameCanvas.height);
|
| 668 |
|
| 669 |
-
//
|
| 670 |
-
|
| 671 |
|
| 672 |
-
//
|
| 673 |
-
|
| 674 |
-
drawPath();
|
| 675 |
-
}
|
| 676 |
|
| 677 |
-
//
|
| 678 |
-
|
| 679 |
-
}
|
| 680 |
-
|
| 681 |
-
// Draw revealed parts of the hidden image
|
| 682 |
-
function drawRevealedImage() {
|
| 683 |
-
if (!hiddenImageData) return;
|
| 684 |
|
| 685 |
-
//
|
| 686 |
-
|
| 687 |
|
| 688 |
-
//
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
if (pixelData[x] && pixelData[x][y]) {
|
| 693 |
-
ctx.rect(x, y, 1, 1);
|
| 694 |
-
}
|
| 695 |
-
}
|
| 696 |
}
|
| 697 |
-
ctx.clip();
|
| 698 |
|
| 699 |
-
//
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
);
|
| 705 |
|
| 706 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
}
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
-
//
|
| 714 |
-
|
| 715 |
-
|
| 716 |
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
-
|
| 722 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
}
|
| 724 |
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
|
| 731 |
-
|
| 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
|
| 771 |
-
|
| 772 |
-
state.currentPath = [{ x: pos.x, y: pos.y }];
|
| 773 |
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
|
|
|
|
|
|
| 778 |
}
|
| 779 |
}
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 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
|
| 795 |
-
if (
|
| 796 |
-
|
| 797 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
}
|
| 799 |
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
|
|
|
|
|
|
|
|
|
| 807 |
}
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
if (!state.isDrawing || state.isPaused) return;
|
| 812 |
|
| 813 |
-
|
| 814 |
-
const
|
| 815 |
-
|
| 816 |
|
| 817 |
-
// Check for enemy
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 851 |
-
|
| 852 |
-
|
| 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 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
}
|
| 863 |
}
|
| 864 |
-
return false;
|
| 865 |
}
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
}
|
| 890 |
}
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 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 |
-
|
| 920 |
-
|
| 921 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 922 |
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 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 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
}
|
| 970 |
}
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 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 |
-
//
|
| 993 |
-
const
|
| 994 |
-
|
| 995 |
-
state.highScores[levelId] = state.score;
|
| 996 |
-
localStorage.setItem('girlyxHighScores', JSON.stringify(state.highScores));
|
| 997 |
-
}
|
| 998 |
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
|
| 1002 |
-
//
|
| 1003 |
-
|
| 1004 |
-
|
| 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 |
-
//
|
| 1016 |
-
|
|
|
|
| 1017 |
|
| 1018 |
-
//
|
| 1019 |
-
|
| 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 |
-
|
| 1031 |
-
|
| 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 |
-
|
| 1045 |
-
|
| 1046 |
-
}
|
| 1047 |
-
|
| 1048 |
-
// Toggle pause
|
| 1049 |
-
function togglePause() {
|
| 1050 |
-
state.isPaused = !state.isPaused;
|
| 1051 |
|
| 1052 |
-
if
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
requestAnimationFrame(gameLoop);
|
| 1057 |
}
|
| 1058 |
-
}
|
| 1059 |
-
|
| 1060 |
-
// Quit to menu
|
| 1061 |
-
function quitToMenu() {
|
| 1062 |
-
state.gameStarted = false;
|
| 1063 |
-
state.isPaused = false;
|
| 1064 |
|
| 1065 |
-
|
| 1066 |
-
elements.gameUI.style.display = 'none';
|
| 1067 |
-
elements.mainMenu.style.display = 'flex';
|
| 1068 |
}
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
state.gameStarted = false;
|
| 1073 |
-
state.isPaused = false;
|
| 1074 |
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|