Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Pac-Man</title> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0a12; | |
| --fg: #ffffff; | |
| --accent: #ffcc00; | |
| --ghost-red: #ff0000; | |
| --ghost-pink: #ffb8ff; | |
| --ghost-cyan: #00ffff; | |
| --ghost-orange: #ffb852; | |
| --wall: #1a1aff; | |
| --pellet: #ffcc00; | |
| --power: #ff00ff; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Orbitron', sans-serif; | |
| background: var(--bg); | |
| color: var(--fg); | |
| overflow: hidden; | |
| min-height: 100vh; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| .ui-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| pointer-events: none; | |
| z-index: 100; | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 20px 30px; | |
| background: linear-gradient(180deg, rgba(0,0,0,0.8) 0%, transparent 100%); | |
| } | |
| .logo { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(14px, 3vw, 24px); | |
| color: var(--accent); | |
| text-shadow: | |
| 0 0 10px var(--accent), | |
| 0 0 20px var(--accent), | |
| 0 0 40px var(--accent); | |
| letter-spacing: 2px; | |
| } | |
| .logo span { | |
| color: #ff3333; | |
| text-shadow: | |
| 0 0 10px #ff3333, | |
| 0 0 20px #ff3333; | |
| } | |
| .built-with { | |
| font-size: 10px; | |
| color: #666; | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| pointer-events: auto; | |
| } | |
| .built-with:hover { | |
| color: var(--accent); | |
| } | |
| .stats { | |
| display: flex; | |
| gap: 30px; | |
| } | |
| .stat { | |
| text-align: center; | |
| } | |
| .stat-label { | |
| font-size: 10px; | |
| color: #888; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| margin-bottom: 5px; | |
| } | |
| .stat-value { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(16px, 2.5vw, 24px); | |
| color: var(--accent); | |
| text-shadow: 0 0 10px var(--accent); | |
| } | |
| .lives-container { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .life { | |
| width: 24px; | |
| height: 24px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px var(--accent); | |
| position: relative; | |
| } | |
| .life::before { | |
| content: ''; | |
| position: absolute; | |
| right: 0; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 0; | |
| height: 0; | |
| border-left: 8px solid var(--bg); | |
| border-top: 6px solid transparent; | |
| border-bottom: 6px solid transparent; | |
| } | |
| .screen-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(0,0,0,0.9); | |
| z-index: 200; | |
| pointer-events: auto; | |
| transition: opacity 0.5s; | |
| } | |
| .screen-overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .title { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(28px, 6vw, 64px); | |
| color: var(--accent); | |
| text-shadow: | |
| 0 0 20px var(--accent), | |
| 0 0 40px var(--accent), | |
| 0 0 60px var(--accent); | |
| margin-bottom: 20px; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .subtitle { | |
| font-size: clamp(12px, 2vw, 18px); | |
| color: #888; | |
| margin-bottom: 40px; | |
| text-align: center; | |
| padding: 0 20px; | |
| } | |
| .start-btn { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(12px, 2vw, 18px); | |
| padding: 20px 40px; | |
| background: transparent; | |
| border: 3px solid var(--accent); | |
| color: var(--accent); | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .start-btn:hover { | |
| background: var(--accent); | |
| color: var(--bg); | |
| box-shadow: 0 0 30px var(--accent); | |
| transform: scale(1.05); | |
| } | |
| .controls-info { | |
| margin-top: 40px; | |
| text-align: center; | |
| } | |
| .controls-info h3 { | |
| font-size: 14px; | |
| color: #666; | |
| margin-bottom: 15px; | |
| } | |
| .keys { | |
| display: flex; | |
| gap: 10px; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| } | |
| .key { | |
| width: 40px; | |
| height: 40px; | |
| background: #222; | |
| border: 2px solid #444; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| color: #888; | |
| } | |
| .game-over-text { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(20px, 4vw, 40px); | |
| color: #ff3333; | |
| text-shadow: 0 0 20px #ff3333; | |
| margin-bottom: 20px; | |
| } | |
| .win-text { | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(20px, 4vw, 40px); | |
| color: #00ff00; | |
| text-shadow: 0 0 20px #00ff00; | |
| margin-bottom: 20px; | |
| } | |
| .final-score { | |
| font-size: 18px; | |
| color: var(--accent); | |
| margin-bottom: 30px; | |
| } | |
| .mobile-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: none; | |
| z-index: 150; | |
| pointer-events: auto; | |
| } | |
| @media (max-width: 768px) { | |
| .mobile-controls { | |
| display: grid; | |
| grid-template-columns: repeat(3, 60px); | |
| grid-template-rows: repeat(3, 60px); | |
| gap: 5px; | |
| } | |
| } | |
| .mobile-btn { | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(255, 204, 0, 0.2); | |
| border: 2px solid var(--accent); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--accent); | |
| font-size: 24px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .mobile-btn:active { | |
| background: var(--accent); | |
| color: var(--bg); | |
| } | |
| .mobile-btn.up { grid-column: 2; grid-row: 1; } | |
| .mobile-btn.left { grid-column: 1; grid-row: 2; } | |
| .mobile-btn.right { grid-column: 3; grid-row: 2; } | |
| .mobile-btn.down { grid-column: 2; grid-row: 3; } | |
| .power-indicator { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(16px, 3vw, 28px); | |
| color: var(--power); | |
| text-shadow: 0 0 20px var(--power); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| pointer-events: none; | |
| z-index: 150; | |
| } | |
| .power-indicator.active { | |
| opacity: 1; | |
| animation: flash 0.3s infinite; | |
| } | |
| @keyframes flash { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .level-indicator { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: clamp(24px, 5vw, 48px); | |
| color: var(--accent); | |
| text-shadow: 0 0 30px var(--accent); | |
| opacity: 0; | |
| pointer-events: none; | |
| z-index: 150; | |
| } | |
| .level-indicator.active { | |
| animation: levelUp 2s forwards; | |
| } | |
| @keyframes levelUp { | |
| 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } | |
| 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); } | |
| 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } | |
| 100% { opacity: 0; transform: translate(-50%, -50%) scale(1); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div class="ui-overlay"> | |
| <div class="header"> | |
| <div class="logo">PAC<span>-MAN</span> 3D</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="built-with" target="_blank">Built with anycoder</a> | |
| <div class="stats"> | |
| <div class="stat"> | |
| <div class="stat-label">Score</div> | |
| <div class="stat-value" id="score">0</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-label">High Score</div> | |
| <div class="stat-value" id="high-score">0</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-label">Lives</div> | |
| <div class="lives-container" id="lives"> | |
| <div class="life"></div> | |
| <div class="life"></div> | |
| <div class="life"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="power-indicator" id="power-indicator">POWER UP!</div> | |
| <div class="level-indicator" id="level-indicator">LEVEL 1</div> | |
| <div class="screen-overlay" id="start-screen"> | |
| <div class="title">PAC-MAN 3D</div> | |
| <div class="subtitle">Navigate the maze, eat all pellets, avoid the ghosts!</div> | |
| <button class="start-btn" id="start-btn">START GAME</button> | |
| <div class="controls-info"> | |
| <h3>Controls</h3> | |
| <div class="keys"> | |
| <div class="key">W</div> | |
| <div class="key">A</div> | |
| <div class="key">S</div> | |
| <div class="key">D</div> | |
| <span style="color: #666; margin: 0 10px;">or</span> | |
| <div class="key">↑</div> | |
| <div class="key">←</div> | |
| <div class="key">↓</div> | |
| <div class="key">→</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="screen-overlay hidden" id="game-over-screen"> | |
| <div class="game-over-text">GAME OVER</div> | |
| <div class="final-score">Final Score: <span id="final-score">0</span></div> | |
| <button class="start-btn" id="restart-btn">PLAY AGAIN</button> | |
| </div> | |
| <div class="screen-overlay hidden" id="win-screen"> | |
| <div class="win-text">LEVEL COMPLETE!</div> | |
| <div class="final-score">Score: <span id="level-score">0</span></div> | |
| <button class="start-btn" id="next-level-btn">NEXT LEVEL</button> | |
| </div> | |
| <div class="mobile-controls"> | |
| <button class="mobile-btn up" data-dir="up">▲</button> | |
| <button class="mobile-btn left" data-dir="left">◀</button> | |
| <button class="mobile-btn right" data-dir="right">▶</button> | |
| <button class="mobile-btn down" data-dir="down">▼</button> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| // Game Constants | |
| const CELL_SIZE = 1; | |
| const PACMAN_SPEED = 0.08; | |
| const GHOST_SPEED = 0.05; | |
| const POWER_DURATION = 8000; | |
| // Maze Layout (1 = wall, 0 = path, 2 = pellet, 3 = power pellet, 4 = ghost spawn) | |
| const MAZE_TEMPLATE = [ | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], | |
| [1,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,1], | |
| [1,3,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,3,1], | |
| [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], | |
| [1,2,1,1,2,1,2,1,1,1,1,1,2,1,2,1,1,2,1], | |
| [1,2,2,2,2,1,2,2,2,1,2,2,2,1,2,2,2,2,1], | |
| [1,1,1,1,2,1,1,1,0,1,0,1,1,1,2,1,1,1,1], | |
| [0,0,0,1,2,1,0,0,0,0,0,0,0,1,2,1,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,4,1,1,0,1,2,1,1,1,1], | |
| [0,0,0,0,2,0,0,1,4,4,4,1,0,0,2,0,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,1,1,1,0,1,2,1,1,1,1], | |
| [0,0,0,1,2,1,0,0,0,0,0,0,0,1,2,1,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,1,1,1,0,1,2,1,1,1,1], | |
| [1,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,1], | |
| [1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1], | |
| [1,3,2,1,2,2,2,2,2,0,2,2,2,2,2,1,2,3,1], | |
| [1,1,2,1,2,1,2,1,1,1,1,1,2,1,2,1,2,1,1], | |
| [1,2,2,2,2,1,2,2,2,1,2,2,2,1,2,2,2,2,1], | |
| [1,2,1,1,1,1,1,1,2,1,2,1,1,1,1,1,1,2,1], | |
| [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] | |
| ]; | |
| // Game State | |
| let scene, camera, renderer; | |
| let pacman, pacmanMouth; | |
| let ghosts = []; | |
| let pellets = []; | |
| let powerPellets = []; | |
| let walls = []; | |
| let maze = []; | |
| let score = 0; | |
| let highScore = parseInt(localStorage.getItem('pacmanHighScore') || '0'); | |
| let lives = 3; | |
| let gameRunning = false; | |
| let powerMode = false; | |
| let powerTimer = null; | |
| let currentDirection = null; | |
| let nextDirection = null; | |
| let level = 1; | |
| let totalPellets = 0; | |
| let pelletsEaten = 0; | |
| // Ghost colors | |
| const GHOST_COLORS = [ | |
| 0xff0000, // Blinky (Red) | |
| 0xffb8ff, // Pinky (Pink) | |
| 0x00ffff, // Inky (Cyan) | |
| 0xffb852 // Clyde (Orange) | |
| ]; | |
| // Initialize Three.js | |
| function initThree() { | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0a0a12); | |
| scene.fog = new THREE.Fog(0x0a0a12, 10, 30); | |
| camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); | |
| camera.position.set(9, 15, 12); | |
| camera.lookAt(9, 0, 10); | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document.getElementById('game-container').insertBefore(renderer.domElement, document.querySelector('.ui-overlay')); | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0x404080, 0.5); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(10, 20, 10); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| directionalLight.shadow.camera.near = 0.5; | |
| directionalLight.shadow.camera.far = 50; | |
| directionalLight.shadow.camera.left = -20; | |
| directionalLight.shadow.camera.right = 20; | |
| directionalLight.shadow.camera.top = 20; | |
| directionalLight.shadow.camera.bottom = -20; | |
| scene.add(directionalLight); | |
| // Point lights for atmosphere | |
| const pointLight1 = new THREE.PointLight(0x0000ff, 0.5, 20); | |
| pointLight1.position.set(0, 5, 0); | |
| scene.add(pointLight1); | |
| const pointLight2 = new THREE.PointLight(0xffcc00, 0.5, 20); | |
| pointLight2.position.set(18, 5, 20); | |
| scene.add(pointLight2); | |
| // Floor | |
| const floorGeometry = new THREE.PlaneGeometry(30, 30); | |
| const floorMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x0a0a20, | |
| roughness: 0.9, | |
| metalness: 0.1 | |
| }); | |
| const floor = new THREE.Mesh(floorGeometry, floorMaterial); | |
| floor.rotation.x = -Math.PI / 2; | |
| floor.position.set(9, -0.5, 10); | |
| floor.receiveShadow = true; | |
| scene.add(floor); | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // Create Pac-Man | |
| function createPacman() { | |
| const group = new THREE.Group(); | |
| // Body | |
| const bodyGeometry = new THREE.SphereGeometry(0.4, 32, 32); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xffcc00, | |
| emissive: 0xffcc00, | |
| emissiveIntensity: 0.3, | |
| roughness: 0.3, | |
| metalness: 0.5 | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.castShadow = true; | |
| group.add(body); | |
| // Mouth (using a wedge shape) | |
| const mouthGeometry = new THREE.ConeGeometry(0.45, 0.5, 8, 1, true, 0, Math.PI * 0.4); | |
| const mouthMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x0a0a12, | |
| side: THREE.DoubleSide | |
| }); | |
| pacmanMouth = new THREE.Mesh(mouthGeometry, mouthMaterial); | |
| pacmanMouth.rotation.z = Math.PI / 2; | |
| pacmanMouth.position.x = 0.1; | |
| group.add(pacmanMouth); | |
| // Eyes | |
| const eyeGeometry = new THREE.SphereGeometry(0.08, 16, 16); | |
| const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 }); | |
| const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial); | |
| leftEye.position.set(0.1, 0.15, 0.3); | |
| group.add(leftEye); | |
| const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial); | |
| rightEye.position.set(0.1, 0.15, -0.3); | |
| group.add(rightEye); | |
| // Starting position | |
| group.position.set(9, 0, 15); | |
| group.userData = { gridX: 9, gridZ: 15, targetX: 9, targetZ: 15 }; | |
| scene.add(group); | |
| return group; | |
| } | |
| // Create Ghost | |
| function createGhost(color, x, z) { | |
| const group = new THREE.Group(); | |
| // Body (dome top + wavy bottom) | |
| const bodyGeometry = new THREE.CylinderGeometry(0.35, 0.4, 0.6, 16, 1, false, 0, Math.PI * 2); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| emissive: color, | |
| emissiveIntensity: 0.2, | |
| roughness: 0.4, | |
| metalness: 0.3 | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.position.y = 0.3; | |
| body.castShadow = true; | |
| group.add(body); | |
| // Head (sphere top) | |
| const headGeometry = new THREE.SphereGeometry(0.4, 16, 16, 0, Math.PI * 2, 0, Math.PI / 2); | |
| const head = new THREE.Mesh(headGeometry, bodyMaterial); | |
| head.position.y = 0.6; | |
| head.castShadow = true; | |
| group.add(head); | |
| // Wavy bottom | |
| for (let i = 0; i < 6; i++) { | |
| const waveGeometry = new THREE.SphereGeometry(0.12, 8, 8); | |
| const wave = new THREE.Mesh(waveGeometry, bodyMaterial); | |
| const angle = (i / 6) * Math.PI * 2; | |
| wave.position.set(Math.cos(angle) * 0.28, 0, Math.sin(angle) * 0.28); | |
| group.add(wave); | |
| } | |
| // Eyes | |
| const eyeWhiteGeometry = new THREE.SphereGeometry(0.12, 16, 16); | |
| const eyeWhiteMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); | |
| const leftEyeWhite = new THREE.Mesh(eyeWhiteGeometry, eyeWhiteMaterial); | |
| leftEyeWhite.position.set(0.2, 0.65, 0.18); | |
| group.add(leftEyeWhite); | |
| const rightEyeWhite = new THREE.Mesh(eyeWhiteGeometry, eyeWhiteMaterial); | |
| rightEyeWhite.position.set(0.2, 0.65, -0.18); | |
| group.add(rightEyeWhite); | |
| // Pupils | |
| const pupilGeometry = new THREE.SphereGeometry(0.06, 16, 16); | |
| const pupilMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff }); | |
| const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial); | |
| leftPupil.position.set(0.3, 0.65, 0.18); | |
| group.add(leftPupil); | |
| const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial); | |
| rightPupil.position.set(0.3, 0.65, -0.18); | |
| group.add(rightPupil); | |
| group.position.set(x, 0, z); | |
| group.userData = { | |
| gridX: x, | |
| gridZ: z, | |
| targetX: x, | |
| targetZ: z, | |
| originalColor: color, | |
| direction: Math.floor(Math.random() * 4), | |
| scared: false | |
| }; | |
| scene.add(group); | |
| return group; | |
| } | |
| // Create Wall | |
| function createWall(x, z) { | |
| const geometry = new THREE.BoxGeometry(CELL_SIZE, 0.8, CELL_SIZE); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: 0x1a1aff, | |
| emissive: 0x0000aa, | |
| emissiveIntensity: 0.3, | |
| roughness: 0.2, | |
| metalness: 0.8 | |
| }); | |
| const wall = new THREE.Mesh(geometry, material); | |
| wall.position.set(x, 0.4, z); | |
| wall.castShadow = true; | |
| wall.receiveShadow = true; | |
| scene.add(wall); | |
| return wall; | |
| } | |
| // Create Pellet | |
| function createPellet(x, z) { | |
| const geometry = new THREE.SphereGeometry(0.1, 16, 16); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: 0xffcc00, | |
| emissive: 0xffcc00, | |
| emissiveIntensity: 0.5, | |
| roughness: 0.2, | |
| metalness: 0.5 | |
| }); | |
| const pellet = new THREE.Mesh(geometry, material); | |
| pellet.position.set(x, 0.2, z); | |
| pellet.userData = { gridX: x, gridZ: z }; | |
| scene.add(pellet); | |
| return pellet; | |
| } | |
| // Create Power Pellet | |
| function createPowerPellet(x, z) { | |
| const geometry = new THREE.SphereGeometry(0.2, 16, 16); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: 0xff00ff, | |
| emissive: 0xff00ff, | |
| emissiveIntensity: 0.8, | |
| roughness: 0.1, | |
| metalness: 0.5 | |
| }); | |
| const pellet = new THREE.Mesh(geometry, material); | |
| pellet.position.set(x, 0.3, z); | |
| pellet.userData = { gridX: x, gridZ: z, isPower: true }; | |
| scene.add(pellet); | |
| return pellet; | |
| } | |
| // Build Maze | |
| function buildMaze() { | |
| // Clear existing | |
| walls.forEach(w => scene.remove(w)); | |
| pellets.forEach(p => scene.remove(p)); | |
| powerPellets.forEach(p => scene.remove(p)); | |
| walls = []; | |
| pellets = []; | |
| powerPellets = []; | |
| totalPellets = 0; | |
| pelletsEaten = 0; | |
| // Deep copy maze template | |
| maze = MAZE_TEMPLATE.map(row => [...row]); | |
| for (let z = 0; z < maze.length; z++) { | |
| for (let x = 0; x < maze[z].length; x++) { | |
| const cell = maze[z][x]; | |
| if (cell === 1) { | |
| walls.push(createWall(x, z)); | |
| } else if (cell === 2) { | |
| pellets.push(createPellet(x, z)); | |
| totalPellets++; | |
| } else if (cell === 3) { | |
| powerPellets.push(createPowerPellet(x, z)); | |
| totalPellets++; | |
| } | |
| } | |
| } | |
| } | |
| // Initialize Game | |
| function initGame() { | |
| if (pacman) scene.remove(pacman); | |
| ghosts.forEach(g => scene.remove(g)); | |
| ghosts = []; | |
| buildMaze(); | |
| // Create Pac-Man | |
| pacman = createPacman(); | |
| // Create Ghosts | |
| const ghostPositions = [ | |
| { x: 8, z: 9 }, | |
| { x: 9, z: 9 }, | |
| { x: 10, z: 9 }, | |
| { x: 9, z: 10 } | |
| ]; | |
| ghostPositions.forEach((pos, i) => { | |
| ghosts.push(createGhost(GHOST_COLORS[i], pos.x, pos.z)); | |
| }); | |
| score = 0; | |
| lives = 3; | |
| powerMode = false; | |
| currentDirection = null; | |
| nextDirection = null; | |
| updateUI(); | |
| } | |
| // Update UI | |
| function updateUI() { | |
| document.getElementById('score').textContent = score; | |
| document.getElementById('high-score').textContent = highScore; | |
| const livesContainer = document.getElementById('lives'); | |
| livesContainer.innerHTML = ''; | |
| for (let i = 0; i < lives; i++) { | |
| const life = document.createElement('div'); | |
| life.className = 'life'; | |
| livesContainer.appendChild(life); | |
| } | |
| } | |
| // Check if position is valid | |
| function isValidPosition(x, z) { | |
| const gridX = Math.round(x); | |
| const gridZ = Math.round(z); | |
| if (gridZ < 0 || gridZ >= maze.length) return true; // Allow tunnel | |
| if (gridX < 0 || gridX >= maze[0].length) return true; // Allow tunnel | |
| return maze[gridZ][gridX] !== 1; | |
| } | |
| // Move Pac-Man | |
| function movePacman() { | |
| if (!gameRunning) return; | |
| const pos = pacman.userData; | |
| let dx = 0, dz = 0; | |
| // Try next direction first | |
| if (nextDirection) { | |
| switch (nextDirection) { | |
| case 'up': dz = -1; break; | |
| case 'down': dz = 1; break; | |
| case 'left': dx = -1; break; | |
| case 'right': dx = 1; break; | |
| } | |
| const nextX = pos.gridX + dx; | |
| const nextZ = pos.gridZ + dz; | |
| if (isValidPosition(nextX, nextZ)) { | |
| currentDirection = nextDirection; | |
| nextDirection = null; | |
| } | |
| } | |
| // Move in current direction | |
| dx = 0; dz = 0; | |
| if (currentDirection) { | |
| switch (currentDirection) { | |
| case 'up': dz = -1; break; | |
| case 'down': dz = 1; break; | |
| case 'left': dx = -1; break; | |
| case 'right': dx = 1; break; | |
| } | |
| const nextX = pos.gridX + dx; | |
| const nextZ = pos.gridZ + dz; | |
| if (isValidPosition(nextX, nextZ)) { | |
| pos.targetX = nextX; | |
| pos.targetZ = nextZ; | |
| } | |
| } | |
| // Smooth movement | |
| const speed = PACMAN_SPEED * (1 + level * 0.1); | |
| if (Math.abs(pacman.position.x - pos.targetX) > 0.01) { | |
| pacman.position.x += (pos.targetX - pacman.position.x) * speed * 2; | |
| } else { | |
| pacman.position.x = pos.targetX; | |
| } | |
| if (Math.abs(pacman.position.z - pos.targetZ) > 0.01) { | |
| pacman.position.z += (pos.targetZ - pacman.position.z) * speed * 2; | |
| } else { | |
| pacman.position.z = pos.targetZ; | |
| } | |
| // Update grid position | |
| pos.gridX = Math.round(pacman.position.x); | |
| pos.gridZ = Math.round(pacman.position.z); | |
| // Tunnel wrap | |
| if (pacman.position.x < -0.5) { | |
| pacman.position.x = maze[0].length - 0.5; | |
| pos.gridX = maze[0].length - 1; | |
| pos.targetX = pos.gridX; | |
| } else if (pacman.position.x > maze[0].length - 0.5) { | |
| pacman.position.x = 0.5; | |
| pos.gridX = 0; | |
| pos.targetX = 0; | |
| } | |
| // Rotation based on direction | |
| if (currentDirection) { | |
| let targetRotation = 0; | |
| switch (currentDirection) { | |
| case 'right': targetRotation = 0; break; | |
| case 'down': targetRotation = Math.PI / 2; break; | |
| case 'left': targetRotation = Math.PI; break; | |
| case 'up': targetRotation = -Math.PI / 2; break; | |
| } | |
| pacman.rotation.y = targetRotation; | |
| } | |
| // Animate mouth | |
| const time = Date.now() * 0.01; | |
| pacmanMouth.rotation.y = Math.sin(time) * 0.3; | |
| // Check pellet collision | |
| checkPelletCollision(); | |
| // Check ghost collision | |
| checkGhostCollision(); | |
| } | |
| // Move Ghosts | |
| function moveGhosts() { | |
| if (!gameRunning) return; | |
| const speed = GHOST_SPEED * (1 + level * 0.05); | |
| ghosts.forEach((ghost, index) => { | |
| const pos = ghost.userData; | |
| // Get valid directions | |
| const directions = []; | |
| const testDirs = [ | |
| { dir: 0, dx: 1, dz: 0 }, // right | |
| { dir: 1, dx: 0, dz: 1 }, // down | |
| { dir: 2, dx: -1, dz: 0 }, // left | |
| { dir: 3, dx: 0, dz: -1 } // up | |
| ]; | |
| testDirs.forEach(d => { | |
| if (isValidPosition(pos.gridX + d.dx, pos.gridZ + d.dz)) { | |
| directions.push(d); | |
| } | |
| }); | |
| // Choose direction (chase or scatter) | |
| if (directions.length > 0) { | |
| let chosenDir; | |
| if (powerMode && ghost.userData.scared) { | |
| // Run away from Pac-Man | |
| let maxDist = -1; | |
| directions.forEach(d => { | |
| const dist = Math.hypot( | |
| (pos.gridX + d.dx) - pacman.userData.gridX, | |
| (pos.gridZ + d.dz) - pacman.userData.gridZ | |
| ); | |
| if (dist > maxDist) { | |
| maxDist = dist; | |
| chosenDir = d; | |
| } | |
| }); | |
| } else { | |
| // Chase Pac-Man (with some randomness based on ghost type) | |
| if (Math.random() < 0.7 + index * 0.05) { | |
| let minDist = Infinity; | |
| directions.forEach(d => { | |
| const dist = Math.hypot( | |
| (pos.gridX + d.dx) - pacman.userData.gridX, | |
| (pos.gridZ + d.dz) - pacman.userData.gridZ | |
| ); | |
| if (dist < minDist) { | |
| minDist = dist; | |
| chosenDir = d; | |
| } | |
| }); | |
| } else { | |
| chosenDir = directions[Math.floor(Math.random() * directions.length)]; | |
| } | |
| } | |
| if (chosenDir) { | |
| pos.targetX = pos.gridX + chosenDir.dx; | |
| pos.targetZ = pos.gridZ + chosenDir.dz; | |
| ghost.userData.direction = chosenDir.dir; | |
| } | |
| } | |
| // Smooth movement | |
| if (Math.abs(ghost.position.x - pos.targetX) > 0.01) { | |
| ghost.position.x += (pos.targetX - ghost.position.x) * speed * 2; | |
| } else { | |
| ghost.position.x = pos.targetX; | |
| } | |
| if (Math.abs(ghost.position.z - pos.targetZ) > 0.01) { | |
| ghost.position.z += (pos.targetZ - ghost.position.z) * speed * 2; | |
| } else { | |
| ghost.position.z = pos.targetZ; | |
| } | |
| pos.gridX = Math.round(ghost.position.x); | |
| pos.gridZ = Math.round(ghost.position.z); | |
| // Tunnel wrap | |
| if (ghost.position.x < -0.5) { | |
| ghost.position.x = maze[0].length - 0.5; | |
| pos.gridX = maze[0].length - 1; | |
| pos.targetX = pos.gridX; | |
| } else if (ghost.position.x > maze[0].length - 0.5) { | |
| ghost.position.x = 0.5; | |
| pos.gridX = 0; | |
| pos.targetX = 0; | |
| } | |
| // Rotation | |
| ghost.rotation.y = ghost.userData.direction * Math.PI / 2; | |
| // Bobbing animation | |
| ghost.position.y = Math.sin(Date.now() * 0.005 + index) * 0.05; | |
| }); | |
| } | |
| // Check Pellet Collision | |
| function checkPelletCollision() { | |
| const px = Math.round(pacman.position.x); | |
| const pz = Math.round(pacman.position.z); | |
| // Regular pellets | |
| for (let i = pellets.length - 1; i >= 0; i--) { | |
| const pellet = pellets[i]; | |
| if (Math.round(pellet.position.x) === px && Math.round(pellet.position.z) === pz) { | |
| scene.remove(pellet); | |
| pellets.splice(i, 1); | |
| score += 10; | |
| pelletsEaten++; | |
| updateUI(); | |
| } | |
| } | |
| // Power pellets | |
| for (let i = powerPellets.length - 1; i >= 0; i--) { | |
| const pellet = powerPellets[i]; | |
| if (Math.round(pellet.position.x) === px && Math.round(pellet.position.z) === pz) { | |
| scene.remove(pellet); | |
| powerPellets.splice(i, 1); | |
| score += 50; | |
| pelletsEaten++; | |
| activatePowerMode(); | |
| updateUI(); | |
| } | |
| } | |
| // Check win condition | |
| if (pelletsEaten >= totalPellets) { | |
| winLevel(); | |
| } | |
| } | |
| // Check Ghost Collision | |
| function checkGhostCollision() { | |
| const px = pacman.position.x; | |
| const pz = pacman.position.z; | |
| ghosts.forEach((ghost, index) => { | |
| const dist = Math.hypot(ghost.position.x - px, ghost.position.z - pz); | |
| if (dist < 0.6) { | |
| if (powerMode && ghost.userData.scared) { | |
| // Eat ghost | |
| score += 200; | |
| resetGhostPosition(ghost, index); | |
| updateUI(); | |
| } else if (!ghost.userData.scared) { | |
| // Lose life | |
| loseLife(); | |
| } | |
| } | |
| }); | |
| } | |
| // Reset Ghost Position | |
| function resetGhostPosition(ghost, index) { | |
| const positions = [ | |
| { x: 8, z: 9 }, | |
| { x: 9, z: 9 }, | |
| { x: 10, z: 9 }, | |
| { x: 9, z: 10 } | |
| ]; | |
| ghost.position.set(positions[index].x, 0, positions[index].z); | |
| ghost.userData.gridX = positions[index].x; | |
| ghost.userData.gridZ = positions[index].z; | |
| ghost.userData.targetX = positions[index].x; | |
| ghost.userData.targetZ = positions[index].z; | |
| ghost.userData.scared = false; | |
| // Reset color | |
| ghost.children.forEach(child => { | |
| if (child.material && child.material.emissive) { | |
| child.material.color.setHex(ghost.userData.originalColor); | |
| child.material.emiss |