anycoder-86e51e3d / index.html
InfernalDread's picture
Upload folder using huggingface_hub
f3d2a89 verified
<!DOCTYPE html>
<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">&#8593;</div>
<div class="key">&#8592;</div>
<div class="key">&#8595;</div>
<div class="key">&#8594;</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">&#9650;</button>
<button class="mobile-btn left" data-dir="left">&#9664;</button>
<button class="mobile-btn right" data-dir="right">&#9654;</button>
<button class="mobile-btn down" data-dir="down">&#9660;</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