Загрузить файлы в «3d Runner»

This commit is contained in:
2026-02-23 21:14:27 +03:00
commit 1954252cd0
5 changed files with 750 additions and 0 deletions

106
3d Runner/SPEC.md Normal file
View File

@@ -0,0 +1,106 @@
# 2.5D Browser Game Specification
## Project Overview
- **Project Name**: Neon Runner 2.5D
- **Type**: Browser-based 2.5D endless runner/collector game
- **Core Functionality**: Player controls a character running on a 3D track, collecting coins while avoiding obstacles
- **Target Users**: Casual gamers looking for quick, engaging browser gameplay
## Technical Approach
- **Rendering**: Three.js for 3D graphics with fixed isometric-style camera angle
- **Perspective**: 2.5D - 3D graphics but gameplay confined to 2D plane (left/right movement)
- **Camera**: Fixed angle (~45°) looking down at the track, creating depth illusion
## Visual & Rendering Specification
### Scene Setup
- **Camera**: PerspectiveCamera at 45° angle, positioned above and behind player
- **Camera Position**: (0, 8, 12) looking at (0, 0, -5)
- **Lighting**:
- Ambient light: soft purple (#6644aa) at intensity 0.4
- Directional light: cyan (#00ffff) at intensity 0.8, position (5, 10, 5)
- Point light: magenta (#ff00ff) following player for glow effect
### Environment
- **Background**: Dark gradient (deep purple to black)
- **Fog**: Linear fog, color #1a0a2e, near 20, far 80
- **Ground**: Infinite scrolling neon grid track
### Materials & Effects
- **Track**: Emissive grid material with cyan glow lines
- **Player**: Glowing cube/sphere with magenta emissive material + pulsing animation
- **Obstacles**: Red/orange glowing geometric shapes (cubes, pyramids)
- **Coins**: Yellow/gold rotating torus with golden emissive glow
- **Post-processing**: Bloom effect for neon glow aesthetic
### Color Palette
- Primary: Cyan (#00ffff)
- Secondary: Magenta (#ff00ff)
- Accent: Gold (#ffd700)
- Danger: Red (#ff3333)
- Background: Deep Purple (#1a0a2e)
## Game Mechanics Specification
### Player Controls
- **Left/Right Movement**: A/D or Arrow Keys or Mouse click (left/right side of screen)
- **Movement Range**: -3 to +3 units on X-axis
- **Movement Speed**: Smooth interpolation, 0.15 units per frame
### Gameplay Elements
- **Track Width**: 7 units total (-3.5 to +3.5)
- **Player Position**: Fixed Z at 0, moves only on X-axis
- **Speed**: Starts at 0.3 units/frame, increases 0.001 per frame (max 0.8)
### Obstacles
- **Types**:
- Low barrier (must dodge)
- Tall pillar (full lane blocker)
- **Spawn Rate**: Every 30-50 frames, random lane
- **Colors**: Red/orange emissive, slight pulsing
### Coins
- **Shape**: Torus (ring)
- **Spawn Rate**: Every 20-40 frames, random lane
- **Rotation**: Continuous Y-axis rotation
- **Collection**: Player proximity triggers collection (+10 points)
### Scoring
- **Distance Score**: +1 point per 10 frames survived
- **Coin Bonus**: +10 points per coin collected
- **High Score**: Stored in localStorage
### Game States
- **Start Screen**: "Click to Start" overlay
- **Playing**: Active gameplay
- **Game Over**: Show score, "Click to Restart" overlay
## UI Specification
### HUD Elements
- **Score Display**: Top-left, large neon font
- **High Score**: Top-right, smaller font
- **Style**: Cyberpunk/retro-futuristic with text-shadow glow
### Overlays
- **Start Screen**: Semi-transparent dark overlay with game title and instructions
- **Game Over**: Score display, high score, restart prompt
## Audio (Optional Enhancement)
- No audio required for MVP
## Performance Targets
- **Target FPS**: 60fps
- **Object Pooling**: Reuse obstacles and coins to prevent garbage collection
## Acceptance Criteria
1. Game loads and displays 3D scene with neon aesthetic
2. Player can move left/right using keyboard or mouse
3. Obstacles spawn and scroll toward player
4. Coins spawn and can be collected for points
5. Collision with obstacle ends game
6. Score displays and updates in real-time
7. Game can be restarted after game over
8. High score persists between sessions
9. Smooth 60fps performance
10. Responsive to window resize

253
3d Runner/background.js Normal file
View File

@@ -0,0 +1,253 @@
/**
* Enhanced background - Space with floating geometric city silhouette shapes and
*/
class BackgroundShapes {
constructor(scene) {
this.scene = scene;
this.shapes = [];
this.stars = [];
this.cityLights = [];
this.frameCount = 0;
this.createStars();
this.createCityscape();
this.createShapes();
}
createStars() {
// Starfield
const starGeometry = new THREE.BufferGeometry();
const starCount = 500;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
const sizes = new Float32Array(starCount);
for (let i = 0; i < starCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = Math.random() * 40 + 5;
positions[i * 3 + 2] = -40 - Math.random() * 60;
// Random star colors (white, cyan, magenta, yellow)
const colorChoice = Math.random();
if (colorChoice < 0.7) {
colors[i * 3] = 1; colors[i * 3 + 1] = 1; colors[i * 3 + 2] = 1;
} else if (colorChoice < 0.85) {
colors[i * 3] = 0; colors[i * 3 + 1] = 1; colors[i * 3 + 2] = 1;
} else {
colors[i * 3] = 1; colors[i * 3 + 1] = 0; colors[i * 3 + 2] = 1;
}
sizes[i] = Math.random() * 2 + 0.5;
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
starGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const starMaterial = new THREE.PointsMaterial({
size: 0.3,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
});
this.starField = new THREE.Points(starGeometry, starMaterial);
scene.add(this.starField);
// Shooting stars
this.shootingStars = [];
}
createCityscape() {
// Create neon city silhouette in background
const buildingCount = 30;
const buildingMaterial = new THREE.MeshBasicMaterial({
color: 0x0a0515,
transparent: true,
opacity: 0.9
});
for (let i = 0; i < buildingCount; i++) {
const width = 1 + Math.random() * 2;
const height = 3 + Math.random() * 12;
const depth = 0.5;
const geometry = new THREE.BoxGeometry(width, height, depth);
const building = new THREE.Mesh(geometry, buildingMaterial);
building.position.set(
-25 + i * 1.8 + Math.random() * 0.5,
height / 2 - 1,
-45 - Math.random() * 10
);
// Add window lights
const windowRows = Math.floor(height / 1.5);
const windowCols = Math.floor(width / 0.8);
for (let row = 0; row < windowRows; row++) {
for (let col = 0; col < windowCols; col++) {
if (Math.random() > 0.4) {
const windowGeom = new THREE.PlaneGeometry(0.3, 0.4);
const windowColor = Math.random() > 0.5 ? 0x00ffff : 0xff00ff;
const windowMat = new THREE.MeshBasicMaterial({
color: windowColor,
transparent: true,
opacity: 0.3 + Math.random() * 0.4
});
const windowMesh = new THREE.Mesh(windowGeom, windowMat);
windowMesh.position.set(
-width/2 + 0.4 + col * 0.7,
-height/2 + 1 + row * 1.2,
depth/2 + 0.01
);
building.add(windowMesh);
}
}
}
this.cityLights.push(building);
scene.add(building);
}
// Ground reflection plane
const reflectionGeom = new THREE.PlaneGeometry(60, 100);
const reflectionMat = new THREE.MeshBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.05,
side: THREE.DoubleSide
});
const reflection = new THREE.Mesh(reflectionGeom, reflectionMat);
reflection.rotation.x = -Math.PI / 2;
reflection.position.y = -0.48;
reflection.position.z = -20;
scene.add(reflection);
}
createShapes() {
// Floating neon shapes
const colors = [0x00ffff, 0xff00ff, 0x6644aa, 0xffd700];
const types = ['cube', 'octahedron', 'tetrahedron', 'torus'];
for (let i = 0; i < 20; i++) {
let geometry;
const type = types[Math.floor(Math.random() * types.length)];
switch(type) {
case 'cube':
geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
break;
case 'octahedron':
geometry = new THREE.OctahedronGeometry(0.6);
break;
case 'tetrahedron':
geometry = new THREE.TetrahedronGeometry(0.6);
break;
case 'torus':
geometry = new THREE.TorusGeometry(0.4, 0.15, 8, 16);
break;
}
const material = new THREE.MeshBasicMaterial({
color: colors[Math.floor(Math.random() * colors.length)],
transparent: true,
opacity: 0.2,
wireframe: true
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
(Math.random() - 0.5) * 30,
Math.random() * 12 + 3,
-25 - Math.random() * 35
);
mesh.userData.rotationSpeed = {
x: (Math.random() - 0.5) * 0.02,
y: (Math.random() - 0.5) * 0.02,
z: (Math.random() - 0.5) * 0.02
};
mesh.userData.floatSpeed = 0.003 + Math.random() * 0.008;
mesh.userData.baseY = mesh.position.y;
mesh.userData.floatAmount = 1 + Math.random() * 2;
this.shapes.push(mesh);
scene.add(mesh);
}
}
update(frameCount) {
this.frameCount = frameCount;
// Animate starfield
if (this.starField) {
this.starField.rotation.y = frameCount * 0.0001;
this.starField.position.z = Math.sin(frameCount * 0.001) * 2;
}
// Animate shapes
this.shapes.forEach(mesh => {
mesh.rotation.x += mesh.userData.rotationSpeed.x;
mesh.rotation.y += mesh.userData.rotationSpeed.y;
mesh.rotation.z += mesh.userData.rotationSpeed.z;
mesh.position.y = mesh.userData.baseY +
Math.sin(frameCount * mesh.userData.floatSpeed) * mesh.userData.floatAmount;
});
// Twinkle city lights
this.cityLights.forEach((building, idx) => {
building.children.forEach((window, wIdx) => {
if (window.material) {
const twinkle = Math.sin(frameCount * 0.05 + idx * 0.5 + wIdx * 0.1);
window.material.opacity = 0.3 + twinkle * 0.2;
}
});
});
// Random shooting star
if (frameCount % 200 === 0 && Math.random() > 0.5) {
this.createShootingStar();
}
// Update shooting stars
for (let i = this.shootingStars.length - 1; i >= 0; i--) {
const ss = this.shootingStars[i];
ss.position.add(ss.userData.velocity);
ss.material.opacity -= 0.02;
if (ss.material.opacity <= 0) {
this.scene.remove(ss);
this.shootingStars.splice(i, 1);
}
}
}
createShootingStar() {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, -2, 1)
]);
const material = new THREE.LineBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 1
});
const shootingStar = new THREE.Line(geometry, material);
shootingStar.position.set(
Math.random() * 40 - 20,
25 + Math.random() * 10,
-30 - Math.random() * 20
);
shootingStar.userData.velocity = new THREE.Vector3(-0.3, -0.5, 0.5);
this.shootingStars.push(shootingStar);
this.scene.add(shootingStar);
}
}

113
3d Runner/coin.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* Coin class - manages collectible coins with enhanced visuals
*/
class Coin {
constructor(scene, lane) {
this.scene = scene;
this.mesh = null;
this.glowMesh = null;
this.createMesh(lane);
}
createMesh(lane) {
// Create procedural texture for coin
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// Gold gradient background
const gradient = ctx.createLinearGradient(0, 0, 128, 64);
gradient.addColorStop(0, '#ffd700');
gradient.addColorStop(0.3, '#ffec8b');
gradient.addColorStop(0.5, '#ffd700');
gradient.addColorStop(0.7, '#ffec8b');
gradient.addColorStop(1, '#daa520');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 128, 64);
// Inner ring
ctx.strokeStyle = '#b8860b';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.ellipse(64, 32, 45, 20, 0, 0, Math.PI * 2);
ctx.stroke();
// Star symbol in center
ctx.fillStyle = '#b8860b';
ctx.beginPath();
const cx = 64, cy = 32;
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI / 5) - Math.PI / 2;
const x = cx + Math.cos(angle) * 15;
const y = cy + Math.sin(angle) * 15;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
const texture = new THREE.CanvasTexture(canvas);
const geometry = new THREE.CylinderGeometry(0.5, 0.5, 0.15, 24);
const material = new THREE.MeshStandardMaterial({
map: texture,
color: 0xffd700,
emissive: 0xffd700,
emissiveIntensity: 0.6,
metalness: 0.9,
roughness: 0.1
});
const LANE_WIDTH = 3.5;
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.position.set(lane * LANE_WIDTH, 1, -60);
this.mesh.rotation.x = Math.PI / 2;
// Outer glow ring
const glowGeometry = new THREE.TorusGeometry(0.6, 0.12, 8, 32);
const glowMaterial = new THREE.MeshBasicMaterial({
color: 0xffd700,
transparent: true,
opacity: 0.4
});
this.glowMesh = new THREE.Mesh(glowGeometry, glowMaterial);
this.glowMesh.rotation.x = Math.PI / 2;
this.mesh.add(this.glowMesh);
// Add point light for glow
const coinLight = new THREE.PointLight(0xffd700, 0.4, 3);
this.mesh.add(coinLight);
this.scene.add(this.mesh);
}
update(gameSpeed) {
this.mesh.position.z += gameSpeed;
// Rotate coin
this.mesh.rotation.z += 0.05;
this.glowMesh.rotation.z -= 0.03;
// Bobbing motion
this.mesh.position.y = 1 + Math.sin(Date.now() * 0.005) * 0.15;
}
isPastCamera() {
return this.mesh.position.z > 15;
}
checkCollection(playerPosition) {
const dx = Math.abs(this.mesh.position.x - playerPosition.x);
const dz = Math.abs(this.mesh.position.z - playerPosition.z);
return dx < 1 && dz < 1;
}
dispose() {
this.scene.remove(this.mesh);
}
}

41
3d Runner/index.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Neon Runner 2.5D</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="score">0</div>
<div id="highScore">HIGH: 0</div>
<div id="startScreen" class="overlay">
<h1>NEON RUNNER</h1>
<p>2.5D ENDLESS RUNNER</p>
<button class="start-btn" onclick="startGame()">START</button>
<div class="controls-hint">A/D or ←/→ or SWIPE to move</div>
</div>
<div id="gameOverScreen" class="overlay hidden">
<h2>GAME OVER</h2>
<div class="final-score">SCORE: <span id="finalScore">0</span></div>
<div class="final-high">HIGH SCORE: <span id="finalHigh">0</span></div>
<button class="start-btn" onclick="restartGame()">RETRY</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="js/player.js"></script>
<script src="js/obstacle.js"></script>
<script src="js/coin.js"></script>
<script src="js/particles.js"></script>
<script src="js/trail.js"></script>
<script src="js/speedlines.js"></script>
<script src="js/background.js"></script>
<script src="js/game.js"></script>
</body>
</html>

237
3d Runner/style.css Normal file
View File

@@ -0,0 +1,237 @@
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #1a0a2e;
font-family: 'Orbitron', monospace;
touch-action: none;
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
}
#ui {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
}
#score {
position: absolute;
top: 20px;
left: 30px;
color: #00ffff;
font-size: 32px;
font-weight: 700;
text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff, 0 0 40px #00ffff;
letter-spacing: 2px;
animation: glow-pulse 2s ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% { text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff, 0 0 40px #00ffff; }
50% { text-shadow: 0 0 15px #00ffff, 0 0 30px #00ffff, 0 0 60px #00ffff, 0 0 80px #00ffff; }
}
#highScore {
position: absolute;
top: 25px;
right: 30px;
color: #ff00ff;
font-size: 18px;
font-weight: 400;
text-shadow: 0 0 8px #ff00ff, 0 0 16px #ff00ff;
letter-spacing: 1px;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(26, 10, 46, 0.92);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: auto;
transition: opacity 0.4s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
#startScreen h1 {
font-size: clamp(36px, 10vw, 72px);
font-weight: 900;
color: #00ffff;
text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff, 0 0 80px #00ffff;
margin-bottom: 15px;
letter-spacing: 4px;
animation: title-glow 2s ease-in-out infinite;
}
@keyframes title-glow {
0%, 100% {
transform: scale(1);
text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff, 0 0 80px #00ffff;
}
50% {
transform: scale(1.03);
text-shadow: 0 0 30px #00ffff, 0 0 60px #00ffff, 0 0 100px #00ffff, 0 0 120px #00ffff;
}
}
#startScreen p {
font-size: clamp(14px, 3vw, 20px);
color: #ff00ff;
text-shadow: 0 0 10px #ff00ff;
margin-bottom: 35px;
letter-spacing: 3px;
}
.start-btn {
padding: 18px 50px;
font-size: clamp(18px, 4vw, 26px);
font-family: 'Orbitron', monospace;
font-weight: 700;
color: #1a0a2e;
background: linear-gradient(135deg, #00ffff, #ff00ff);
border: none;
cursor: pointer;
letter-spacing: 3px;
transition: all 0.3s ease;
box-shadow: 0 0 20px #00ffff, 0 0 40px #ff00ff;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.start-btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
transform: rotate(45deg) translateX(-100%);
transition: transform 0.6s ease;
}
.start-btn:hover {
transform: scale(1.08);
box-shadow: 0 0 30px #00ffff, 0 0 60px #ff00ff;
}
.start-btn:hover::before {
transform: rotate(45deg) translateX(100%);
}
.start-btn:active {
transform: scale(0.98);
}
#gameOverScreen h2 {
font-size: clamp(32px, 8vw, 56px);
font-weight: 900;
color: #ff3333;
text-shadow: 0 0 20px #ff3333, 0 0 40px #ff3333;
margin-bottom: 25px;
letter-spacing: 4px;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-10px); }
40% { transform: translateX(10px); }
60% { transform: translateX(-10px); }
80% { transform: translateX(10px); }
}
#gameOverScreen .final-score {
font-size: clamp(24px, 6vw, 40px);
color: #00ffff;
text-shadow: 0 0 15px #00ffff;
margin-bottom: 12px;
}
#gameOverScreen .final-high {
font-size: clamp(16px, 3vw, 24px);
color: #ffd700;
text-shadow: 0 0 10px #ffd700;
margin-bottom: 35px;
}
.controls-hint {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
color: #888;
font-size: 14px;
letter-spacing: 1px;
text-align: center;
}
/* Mobile optimizations */
@media (max-width: 600px) {
#score {
top: 15px;
left: 20px;
font-size: 24px;
}
#highScore {
top: 18px;
right: 20px;
font-size: 14px;
}
.controls-hint {
font-size: 12px;
padding: 0 20px;
}
}
/* Scanline effect overlay */
#ui::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.03) 2px,
rgba(0, 0, 0, 0.03) 4px
);
pointer-events: none;
}