From 1954252cd04df1f0512a0157416f147b60df267d Mon Sep 17 00:00:00 2001 From: "maxim.dolgolyov" Date: Mon, 23 Feb 2026 21:14:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?3d=20Runner=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3d Runner/SPEC.md | 106 +++++++++++++++++ 3d Runner/background.js | 253 ++++++++++++++++++++++++++++++++++++++++ 3d Runner/coin.js | 113 ++++++++++++++++++ 3d Runner/index.html | 41 +++++++ 3d Runner/style.css | 237 +++++++++++++++++++++++++++++++++++++ 5 files changed, 750 insertions(+) create mode 100644 3d Runner/SPEC.md create mode 100644 3d Runner/background.js create mode 100644 3d Runner/coin.js create mode 100644 3d Runner/index.html create mode 100644 3d Runner/style.css diff --git a/3d Runner/SPEC.md b/3d Runner/SPEC.md new file mode 100644 index 0000000..c33d3d7 --- /dev/null +++ b/3d Runner/SPEC.md @@ -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 diff --git a/3d Runner/background.js b/3d Runner/background.js new file mode 100644 index 0000000..2802fa3 --- /dev/null +++ b/3d Runner/background.js @@ -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); + } +} diff --git a/3d Runner/coin.js b/3d Runner/coin.js new file mode 100644 index 0000000..d5c9533 --- /dev/null +++ b/3d Runner/coin.js @@ -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); + } +} diff --git a/3d Runner/index.html b/3d Runner/index.html new file mode 100644 index 0000000..ef51761 --- /dev/null +++ b/3d Runner/index.html @@ -0,0 +1,41 @@ + + + + + + Neon Runner 2.5D + + + + + +
+
0
+
HIGH: 0
+ +
+

NEON RUNNER

+

2.5D ENDLESS RUNNER

+ +
A/D or ←/→ or SWIPE to move
+
+ + +
+ + + + + + + + + + + + diff --git a/3d Runner/style.css b/3d Runner/style.css new file mode 100644 index 0000000..2ae0ec7 --- /dev/null +++ b/3d Runner/style.css @@ -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; +}