fixed jump and cloud issues, added changing day/night cycle
This commit is contained in:
116
src/App.svelte
116
src/App.svelte
@@ -80,7 +80,8 @@ const CONFIG = {
|
|||||||
playerScale: 1.7,
|
playerScale: 1.7,
|
||||||
START_SPEED: 45, // Initial slow speed
|
START_SPEED: 45, // Initial slow speed
|
||||||
MAX_SPEED: 95, // The "chaos" threshold
|
MAX_SPEED: 95, // The "chaos" threshold
|
||||||
ACCELERATION: 1 // Speed added per second
|
ACCELERATION: 1, // Speed added per second
|
||||||
|
CYCLE_INTERVAL: 7000
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentSpeed = CONFIG.START_SPEED;
|
let currentSpeed = CONFIG.START_SPEED;
|
||||||
@@ -94,6 +95,12 @@ let isDying = false, hitFlash = false;
|
|||||||
|
|
||||||
let spawnDistanceTracker = 0;
|
let spawnDistanceTracker = 0;
|
||||||
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
|
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
|
||||||
|
let cloudGroup;
|
||||||
|
let skyColors = {
|
||||||
|
day: new THREE.Color(0x87CEFA),
|
||||||
|
night: new THREE.Color(0x02050a)
|
||||||
|
};
|
||||||
|
let sun, moon, ambientLight, sunLight, headLight;
|
||||||
|
|
||||||
// 2D Game Logic
|
// 2D Game Logic
|
||||||
let gamePhase = "START";
|
let gamePhase = "START";
|
||||||
@@ -119,8 +126,6 @@ const sketch = (p) => {
|
|||||||
const h = container?.clientHeight || p.windowHeight;
|
const h = container?.clientHeight || p.windowHeight;
|
||||||
p.createCanvas(w, h);
|
p.createCanvas(w, h);
|
||||||
|
|
||||||
// Load images and store them in the textures object
|
|
||||||
// Using p.loadImage with a promise-like approach
|
|
||||||
const loadImg = (path) => new Promise(resolve => {
|
const loadImg = (path) => new Promise(resolve => {
|
||||||
p.loadImage(path, img => resolve(img), () => resolve(null));
|
p.loadImage(path, img => resolve(img), () => resolve(null));
|
||||||
});
|
});
|
||||||
@@ -130,7 +135,6 @@ const sketch = (p) => {
|
|||||||
textures.BLUEBERRY = await loadImg('blubb.png');
|
textures.BLUEBERRY = await loadImg('blubb.png');
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- NEW: Function to draw a pixelated heart ---
|
|
||||||
const drawHeart = (x, y, size, active) => {
|
const drawHeart = (x, y, size, active) => {
|
||||||
p.push();
|
p.push();
|
||||||
p.noStroke();
|
p.noStroke();
|
||||||
@@ -148,7 +152,6 @@ const sketch = (p) => {
|
|||||||
p.draw = () => {
|
p.draw = () => {
|
||||||
p.clear();
|
p.clear();
|
||||||
if (!isPlaying) return;
|
if (!isPlaying) return;
|
||||||
|
|
||||||
if (gamePhase === "INSTRUCTIONS") {
|
if (gamePhase === "INSTRUCTIONS") {
|
||||||
p.fill(0, 200); // Darken background
|
p.fill(0, 200); // Darken background
|
||||||
p.rect(0, 0, p.width, p.height);
|
p.rect(0, 0, p.width, p.height);
|
||||||
@@ -184,7 +187,7 @@ const sketch = (p) => {
|
|||||||
drawHeart(20 + (i * 35), 20, 25, i < lives);
|
drawHeart(20 + (i * 35), 20, 25, i < lives);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawning Logic (keeping your random chance)
|
|
||||||
if (p.random(1) < 0.004) {
|
if (p.random(1) < 0.004) {
|
||||||
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
||||||
targets.push({
|
targets.push({
|
||||||
@@ -246,9 +249,7 @@ const sketch = (p) => {
|
|||||||
if (p.dist(p.mouseX, p.mouseY, t.x, t.y) < 40) {
|
if (p.dist(p.mouseX, p.mouseY, t.x, t.y) < 40) {
|
||||||
if (t.type === targetType) {
|
if (t.type === targetType) {
|
||||||
score += 100;
|
score += 100;
|
||||||
attentiveness = Math.min(100, attentiveness + 15);
|
|
||||||
} else {
|
} else {
|
||||||
attentiveness -= 20;
|
|
||||||
if (lives > 0) lives--;
|
if (lives > 0) lives--;
|
||||||
}
|
}
|
||||||
targets.splice(i, 1);
|
targets.splice(i, 1);
|
||||||
@@ -277,6 +278,7 @@ const grassFragment = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const createClouds = (group) => {
|
const createClouds = (group) => {
|
||||||
const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
|
const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
|
||||||
const thickness = 2;
|
const thickness = 2;
|
||||||
@@ -284,7 +286,8 @@ const createClouds = (group) => {
|
|||||||
const w = 10 + Math.random() * 20;
|
const w = 10 + Math.random() * 20;
|
||||||
const d = 10 + Math.random() * 20;
|
const d = 10 + Math.random() * 20;
|
||||||
const cloud = new THREE.Mesh(new THREE.BoxGeometry(w, thickness, d), cloudMaterial);
|
const cloud = new THREE.Mesh(new THREE.BoxGeometry(w, thickness, d), cloudMaterial);
|
||||||
cloud.position.set((Math.random() - 0.5) * 280, 35, (Math.random() - 0.5) * 300);
|
const y = 30 + Math.random() * 25;
|
||||||
|
cloud.position.set((Math.random() - 0.5) * 280, y, (Math.random() - 0.5) * 300);
|
||||||
group.add(cloud);
|
group.add(cloud);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -363,7 +366,7 @@ function init() {
|
|||||||
lights[1].position.set(0, 50, 0);
|
lights[1].position.set(0, 50, 0);
|
||||||
scene.add(...lights);
|
scene.add(...lights);
|
||||||
|
|
||||||
const cloudGroup = new THREE.Group();
|
cloudGroup = new THREE.Group();
|
||||||
createClouds(cloudGroup);
|
createClouds(cloudGroup);
|
||||||
scene.add(cloudGroup);
|
scene.add(cloudGroup);
|
||||||
|
|
||||||
@@ -384,6 +387,29 @@ function init() {
|
|||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
});
|
});
|
||||||
ro.observe(container);
|
ro.observe(container);
|
||||||
|
|
||||||
|
// 1. Update Fog to use the day color
|
||||||
|
scene.fog = new THREE.Fog(skyColors.day, 150, 300);
|
||||||
|
|
||||||
|
// 2. Setup Lights
|
||||||
|
ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
|
||||||
|
sunLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
sunLight.position.set(0, 50, -50);
|
||||||
|
|
||||||
|
// 3. Add the "Headlight" to the camera (stays off during day)
|
||||||
|
headLight = new THREE.PointLight(0x00d2ff, 0, 40);
|
||||||
|
camera.add(headLight);
|
||||||
|
scene.add(camera, ambientLight, sunLight);
|
||||||
|
|
||||||
|
// 4. Create Low-Poly Sun and Moon
|
||||||
|
const lowPolyGeo = new THREE.IcosahedronGeometry(10, 1);
|
||||||
|
sun = new THREE.Mesh(lowPolyGeo, new THREE.MeshBasicMaterial({ color: 0xffffcc }));
|
||||||
|
moon = new THREE.Mesh(lowPolyGeo, new THREE.MeshBasicMaterial({ color: 0x94b0ff }));
|
||||||
|
|
||||||
|
// Position them at opposite poles
|
||||||
|
sun.position.set(60, 100, -250);
|
||||||
|
moon.position.set(60, -100, -250);
|
||||||
|
scene.add(sun, moon);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function spawn() {
|
async function spawn() {
|
||||||
@@ -400,6 +426,7 @@ async function spawn() {
|
|||||||
worldObjects = [...worldObjects, { mesh: pivot, lane: l }];
|
worldObjects = [...worldObjects, { mesh: pivot, lane: l }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const delta = (now - lastTime) / 1000;
|
const delta = (now - lastTime) / 1000;
|
||||||
@@ -409,13 +436,34 @@ function update() {
|
|||||||
if (currentMixer) currentMixer.update(delta);
|
if (currentMixer) currentMixer.update(delta);
|
||||||
if (!isPlaying) return;
|
if (!isPlaying) return;
|
||||||
|
|
||||||
|
// --- DAY/NIGHT CYCLE LOGIC START ---
|
||||||
|
|
||||||
|
// Calculate alpha (0 = Day, 1 = Night) using a sine wave based on score
|
||||||
|
let cycleProgress = (score % (CONFIG.CYCLE_INTERVAL * 2)) / (CONFIG.CYCLE_INTERVAL * 2);
|
||||||
|
let nightAlpha = Math.pow(Math.sin(cycleProgress * Math.PI), 2);
|
||||||
|
|
||||||
|
// Interpolate Background and Fog colors
|
||||||
|
const currentSky = skyColors.day.clone().lerp(skyColors.night, nightAlpha);
|
||||||
|
scene.background.copy(currentSky);
|
||||||
|
scene.fog.color.copy(currentSky);
|
||||||
|
|
||||||
|
// Adjust Light Intensities
|
||||||
|
ambientLight.intensity = THREE.MathUtils.lerp(1.5, 0.2, nightAlpha);
|
||||||
|
sunLight.intensity = THREE.MathUtils.lerp(1.0, 0.1, nightAlpha);
|
||||||
|
headLight.intensity = THREE.MathUtils.lerp(0, 2.5, nightAlpha);
|
||||||
|
|
||||||
|
// Move Sun and Moon (Sun goes down, Moon comes up)
|
||||||
|
sun.position.y = THREE.MathUtils.lerp(100, -100, nightAlpha);
|
||||||
|
moon.position.y = THREE.MathUtils.lerp(-100, 100, nightAlpha);
|
||||||
|
|
||||||
|
// --- DAY/NIGHT CYCLE LOGIC END ---
|
||||||
|
|
||||||
if (gamePhase === "INSTRUCTIONS") {
|
if (gamePhase === "INSTRUCTIONS") {
|
||||||
instructionTimer -= delta;
|
instructionTimer -= delta;
|
||||||
if (instructionTimer <= 0) gamePhase = "PLAYING";
|
if (instructionTimer <= 0) gamePhase = "PLAYING";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase speed over time, but cap it at MAX_SPEED
|
|
||||||
if (currentSpeed < CONFIG.MAX_SPEED) {
|
if (currentSpeed < CONFIG.MAX_SPEED) {
|
||||||
currentSpeed += CONFIG.ACCELERATION * delta;
|
currentSpeed += CONFIG.ACCELERATION * delta;
|
||||||
}
|
}
|
||||||
@@ -423,8 +471,19 @@ function update() {
|
|||||||
const moveStep = currentSpeed * delta;
|
const moveStep = currentSpeed * delta;
|
||||||
score += Math.floor(currentSpeed / 40);
|
score += Math.floor(currentSpeed / 40);
|
||||||
|
|
||||||
attentiveness -= 0.05;
|
if (cloudGroup) {
|
||||||
// --- NEW: Trigger game over if hearts run out or attention hits zero ---
|
// Moving at 40% speed (moveStep * 0.4) creates a nice parallax depth
|
||||||
|
cloudGroup.children.forEach(cloud => {
|
||||||
|
cloud.position.z += moveStep * 0.4;
|
||||||
|
|
||||||
|
// Reset cloud position if it goes too far behind the camera
|
||||||
|
if (cloud.position.z > 50) {
|
||||||
|
cloud.position.z = -250;
|
||||||
|
cloud.position.x = (Math.random() - 0.5) * 280; // Randomize X again for variety
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (lives <= 0) triggerGameOver();
|
if (lives <= 0) triggerGameOver();
|
||||||
|
|
||||||
CHUNKS.forEach(chunk => {
|
CHUNKS.forEach(chunk => {
|
||||||
@@ -438,7 +497,11 @@ function update() {
|
|||||||
if (isJumping) {
|
if (isJumping) {
|
||||||
jumpV -= CONFIG.grav;
|
jumpV -= CONFIG.grav;
|
||||||
playerY += jumpV;
|
playerY += jumpV;
|
||||||
if (playerY <= 0) { playerY = 0; isJumping = false; if (!isDying) swapCharacter("Running.glb"); }
|
if (playerY <= 0) {
|
||||||
|
playerY = 0;
|
||||||
|
isJumping = false;
|
||||||
|
if (!isDying) swapCharacter("Running.glb");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
playerAnchor.position.y = playerY;
|
playerAnchor.position.y = playerY;
|
||||||
|
|
||||||
@@ -448,21 +511,20 @@ function update() {
|
|||||||
return obj;
|
return obj;
|
||||||
}).filter(obj => {
|
}).filter(obj => {
|
||||||
const active = obj.mesh.position.z < 25;
|
const active = obj.mesh.position.z < 25;
|
||||||
if (!active) {
|
if (!active) scene.remove(obj.mesh);
|
||||||
scene.remove(obj.mesh);
|
|
||||||
attentiveness = Math.min(100, attentiveness + 2);
|
|
||||||
}
|
|
||||||
return active;
|
return active;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- ROBUST SPAWN LOGIC ---
|
// Normal Obstacle Spawning
|
||||||
spawnDistanceTracker += moveStep;
|
spawnDistanceTracker += moveStep;
|
||||||
if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
||||||
spawn();
|
spawn();
|
||||||
spawnDistanceTracker = 0; // Reset the tracker
|
spawnDistanceTracker = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function triggerGameOver() {
|
function triggerGameOver() {
|
||||||
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
|
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
|
||||||
hasSubmitted = false; // Allow a new submission for this game over
|
hasSubmitted = false; // Allow a new submission for this game over
|
||||||
@@ -481,7 +543,7 @@ async function startGame() {
|
|||||||
worldObjects = [];
|
worldObjects = [];
|
||||||
targets = [];
|
targets = [];
|
||||||
spawnDistanceTracker = 0;
|
spawnDistanceTracker = 0;
|
||||||
score = 0; attentiveness = 100; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives
|
score = 0; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives
|
||||||
gamePhase = "INSTRUCTIONS"; instructionTimer = 3;
|
gamePhase = "INSTRUCTIONS"; instructionTimer = 3;
|
||||||
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
|
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
|
||||||
CHUNKS.forEach((chunk, i) => { chunk.position.z = -i * CHUNK_SIZE; });
|
CHUNKS.forEach((chunk, i) => { chunk.position.z = -i * CHUNK_SIZE; });
|
||||||
@@ -493,10 +555,10 @@ const handleKeyDown = (e) => {
|
|||||||
const actions = {
|
const actions = {
|
||||||
ArrowLeft: () => lane > -2 && lane--, a: () => lane > -2 && lane--, A: () => lane > -2 && lane--,
|
ArrowLeft: () => lane > -2 && lane--, a: () => lane > -2 && lane--, A: () => lane > -2 && lane--,
|
||||||
ArrowRight: () => lane < 2 && lane++, d: () => lane < 2 && lane++, D: () => lane < 2 && lane++,
|
ArrowRight: () => lane < 2 && lane++, d: () => lane < 2 && lane++, D: () => lane < 2 && lane++,
|
||||||
" ": () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
" ": () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")),
|
||||||
ArrowUp: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
ArrowUp: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")),
|
||||||
w: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
w: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")),
|
||||||
W: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb"))
|
W: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb"))
|
||||||
};
|
};
|
||||||
actions[e.key]?.();
|
actions[e.key]?.();
|
||||||
};
|
};
|
||||||
@@ -525,7 +587,7 @@ onMount(() => {
|
|||||||
|
|
||||||
{#if hitFlash} <div class="flash"></div> {/if}
|
{#if hitFlash} <div class="flash"></div> {/if}
|
||||||
|
|
||||||
<!-- 1. NEW TOP-RIGHT LEADERBOARD -->
|
<!-- TOP-RIGHT LEADERBOARD -->
|
||||||
<div class="side-hud">
|
<div class="side-hud">
|
||||||
<div class="leaderboard-view">
|
<div class="leaderboard-view">
|
||||||
<h3>TOP RUNNERS</h3>
|
<h3>TOP RUNNERS</h3>
|
||||||
@@ -584,9 +646,7 @@ onMount(() => {
|
|||||||
.p5-hud { position: absolute; top: 0; left: 0; pointer-events: auto; z-index: 10; width: 100%; height: 100%; }
|
.p5-hud { position: absolute; top: 0; left: 0; pointer-events: auto; z-index: 10; width: 100%; height: 100%; }
|
||||||
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: 'Segoe UI', sans-serif; z-index: 11 }
|
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: 'Segoe UI', sans-serif; z-index: 11 }
|
||||||
.score { font-size: 2.5rem; margin-top: 60px; font-weight: 800; text-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
.score { font-size: 2.5rem; margin-top: 60px; font-weight: 800; text-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
||||||
|
|
||||||
.modal { pointer-events: auto; background: rgba(255, 255, 255, 0.98); padding: 40px; border-radius: 30px; margin-top: 5vh; color: #1e2b21; display: inline-block; box-shadow: 0 25px 60px rgba(0,0,0,0.15); width: 320px; }
|
.modal { pointer-events: auto; background: rgba(255, 255, 255, 0.98); padding: 40px; border-radius: 30px; margin-top: 5vh; color: #1e2b21; display: inline-block; box-shadow: 0 25px 60px rgba(0,0,0,0.15); width: 320px; }
|
||||||
|
|
||||||
.leaderboard-entry { margin: 20px 0; }
|
.leaderboard-entry { margin: 20px 0; }
|
||||||
input { width: 80%; padding: 12px; border: 2px solid #eee; border-radius: 10px; font-family: inherit; font-weight: bold; text-align: center; margin-bottom: 10px; }
|
input { width: 80%; padding: 12px; border: 2px solid #eee; border-radius: 10px; font-family: inherit; font-weight: bold; text-align: center; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
|||||||
764
src/cool.svelte
764
src/cool.svelte
@@ -1,285 +1,651 @@
|
|||||||
<script>
|
<script>
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { onMount, onDestroy } from "svelte";
|
import LandingPage from './LandingPage.svelte';
|
||||||
|
import { onMount, tick } from "svelte";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import p5 from "p5";
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||||
|
|
||||||
|
let showLanding = true;
|
||||||
|
let lastTime = performance.now();
|
||||||
|
|
||||||
|
// --- LEADERBOARD STATE ---
|
||||||
|
let leaderboard = [];
|
||||||
|
let playerName = "";
|
||||||
|
let hasSubmitted = false;
|
||||||
|
|
||||||
|
async function handleStart() {
|
||||||
|
showLanding = false;
|
||||||
|
// Wait for Svelte to render the #wrapper and canvas
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
init();
|
||||||
|
p5Instance = new p5(sketch, p5Container);
|
||||||
|
|
||||||
|
// Start the game logic
|
||||||
|
startGame();
|
||||||
|
|
||||||
|
// Start the render loop
|
||||||
|
const loop = () => {
|
||||||
|
animationFrame = requestAnimationFrame(loop);
|
||||||
|
update();
|
||||||
|
if (renderer && scene && camera) {
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LEADERBOARD LOGIC ---
|
||||||
|
function saveScore() {
|
||||||
|
if (hasSubmitted) return;
|
||||||
|
|
||||||
|
const name = playerName.trim() || "Anonymous";
|
||||||
|
let currentBoard = JSON.parse(localStorage.getItem("neuro_leaderboard") || "[]");
|
||||||
|
|
||||||
|
// Check if this player already has a record (case-insensitive)
|
||||||
|
const existingIndex = currentBoard.findIndex(
|
||||||
|
entry => entry.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Only update if the new score is actually higher
|
||||||
|
if (score > currentBoard[existingIndex].score) {
|
||||||
|
currentBoard[existingIndex].score = score;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New player, just add them
|
||||||
|
currentBoard.push({ name: name, score: score });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by highest score first and keep only top 5
|
||||||
|
currentBoard.sort((a, b) => b.score - a.score);
|
||||||
|
currentBoard = currentBoard.slice(0, 5);
|
||||||
|
|
||||||
|
localStorage.setItem("neuro_leaderboard", JSON.stringify(currentBoard));
|
||||||
|
leaderboard = currentBoard;
|
||||||
|
hasSubmitted = true;
|
||||||
|
playerName = ""; // Reset for next time
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLeaderboard() {
|
||||||
|
leaderboard = JSON.parse(localStorage.getItem("neuro_leaderboard") || "[]");
|
||||||
|
}
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
lane: 2.5,
|
lane: 2.5,
|
||||||
jump: 0.35,
|
jump: 0.35,
|
||||||
grav: 0.015,
|
grav: 0.015,
|
||||||
speed: 0.3
|
playerScale: 1.7,
|
||||||
|
START_SPEED: 45, // Initial slow speed
|
||||||
|
MAX_SPEED: 95, // The "chaos" threshold
|
||||||
|
ACCELERATION: 1 // Speed added per second
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI State
|
let currentSpeed = CONFIG.START_SPEED;
|
||||||
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
|
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
|
||||||
let hitFlash = false;
|
let attentiveness = 100;
|
||||||
|
let lives = 5;
|
||||||
// Physics/Logic State
|
|
||||||
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
|
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
|
||||||
let isDying = false;
|
let container, canvas, scene, camera, renderer, p5Container;
|
||||||
let shake = 0;
|
let worldObjects = [], animationFrame, p5Instance;
|
||||||
let fade = 1;
|
let isDying = false, hitFlash = false;
|
||||||
|
|
||||||
let container, canvas, scene, camera, renderer;
|
let spawnDistanceTracker = 0;
|
||||||
let worldObjects = [];
|
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
|
||||||
let resizeObserver;
|
|
||||||
|
|
||||||
// Character Refs
|
// 2D Game Logic
|
||||||
let playerGroup, mixer, currentAction, animations = [];
|
let gamePhase = "START";
|
||||||
let clock = new THREE.Clock();
|
let instructionTimer = 3;
|
||||||
|
let targetType = "STRAWBERRY";
|
||||||
|
let targets = [];
|
||||||
|
|
||||||
function playAnimation(name, loop = true) {
|
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
|
||||||
if (!mixer || !animations.length) return;
|
let CHUNKS = [];
|
||||||
const clip = animations.find(a => a.name === name);
|
const CHUNK_COUNT = 3;
|
||||||
if (!clip) return;
|
const CHUNK_SIZE = 140;
|
||||||
|
|
||||||
const newAction = mixer.clipAction(clip);
|
let uTime = { value: 0 };
|
||||||
if (currentAction === newAction) return;
|
const loader = new GLTFLoader();
|
||||||
|
const glbCache = new Map();
|
||||||
|
|
||||||
if (currentAction) currentAction.fadeOut(0.2);
|
let textures = {};
|
||||||
|
|
||||||
newAction.reset()
|
const sketch = (p) => {
|
||||||
.setEffectiveTimeScale(1)
|
// --- FOOLPROOF IMAGE LOADER ---
|
||||||
.setEffectiveWeight(1)
|
p.setup = async () => {
|
||||||
.fadeIn(0.2)
|
const w = container?.clientWidth || p.windowWidth;
|
||||||
.play();
|
const h = container?.clientHeight || p.windowHeight;
|
||||||
|
p.createCanvas(w, h);
|
||||||
|
|
||||||
newAction.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce);
|
const loadImg = (path) => new Promise(resolve => {
|
||||||
newAction.clampWhenFinished = !loop;
|
p.loadImage(path, img => resolve(img), () => resolve(null));
|
||||||
currentAction = newAction;
|
});
|
||||||
|
|
||||||
|
textures.STRAWBERRY = await loadImg('strawberry.png');
|
||||||
|
textures.BANANA = await loadImg('banana.png');
|
||||||
|
textures.BLUEBERRY = await loadImg('blubb.png');
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawHeart = (x, y, size, active) => {
|
||||||
|
p.push();
|
||||||
|
p.noStroke();
|
||||||
|
// Use red if active, grey if dead
|
||||||
|
p.fill(active ? [255, 50, 50] : [100, 100, 100, 150]);
|
||||||
|
const s = size / 5;
|
||||||
|
p.rect(x + s, y, s, s); p.rect(x + 3 * s, y, s, s);
|
||||||
|
p.rect(x, y + s, 5 * s, s);
|
||||||
|
p.rect(x, y + 2 * s, 5 * s, s);
|
||||||
|
p.rect(x + s, y + 3 * s, 3 * s, s);
|
||||||
|
p.rect(x + 2 * s, y + 4 * s, s, s);
|
||||||
|
p.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
p.draw = () => {
|
||||||
|
p.clear();
|
||||||
|
if (!isPlaying) return;
|
||||||
|
if (gamePhase === "INSTRUCTIONS") {
|
||||||
|
p.fill(0, 200); // Darken background
|
||||||
|
p.rect(0, 0, p.width, p.height);
|
||||||
|
|
||||||
|
p.fill(255);
|
||||||
|
p.textAlign(p.CENTER);
|
||||||
|
p.textFont('Segoe UI');
|
||||||
|
p.textStyle(p.BOLD);
|
||||||
|
|
||||||
|
// The Mission Text
|
||||||
|
p.textSize(28);
|
||||||
|
p.text(`MISSION: COLLECT`, p.width / 2, p.height / 2 - 100);
|
||||||
|
|
||||||
|
// Draw the target icon to collect
|
||||||
|
const targetImg = textures[targetType];
|
||||||
|
if (targetImg) {
|
||||||
|
p.imageMode(p.CENTER);
|
||||||
|
p.image(targetImg, p.width / 2, p.height / 2 - 20, 80, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.textSize(32);
|
||||||
|
p.fill(0, 255, 200); // Cyan color for the target name
|
||||||
|
p.text(targetType, p.width / 2, p.height / 2 + 60);
|
||||||
|
|
||||||
|
// Countdown
|
||||||
|
p.fill(255);
|
||||||
|
p.textSize(80);
|
||||||
|
p.text(Math.ceil(instructionTimer), p.width / 2, p.height / 2 + 160);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// --- RENDER HEARTS ---
|
||||||
|
for (let i = 0; i < 5; i++) { // Always run 5 times
|
||||||
|
drawHeart(20 + (i * 35), 20, 25, i < lives);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (p.random(1) < 0.004) {
|
||||||
|
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
||||||
|
targets.push({
|
||||||
|
x: p.random(p.width * 0.2, p.width * 0.8),
|
||||||
|
y: -50,
|
||||||
|
type: types[p.floor(p.random(types.length))],
|
||||||
|
speed: p.random(1.5, 3),
|
||||||
|
rot: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RENDER IMAGES ---
|
||||||
|
for (let i = targets.length - 1; i >= 0; i--) {
|
||||||
|
let t = targets[i];
|
||||||
|
t.y += t.speed;
|
||||||
|
t.rot += 0.02;
|
||||||
|
|
||||||
|
p.push();
|
||||||
|
p.translate(t.x, t.y);
|
||||||
|
p.rotate(t.rot);
|
||||||
|
p.imageMode(p.CENTER);
|
||||||
|
|
||||||
|
// Check if the texture exists before trying to draw it
|
||||||
|
const img = textures[t.type];
|
||||||
|
if (img) {
|
||||||
|
// Draw the image. Scale it to 40x40 pixels (adjust as needed)
|
||||||
|
p.image(img, 0, 0, 60, 60);
|
||||||
|
} else {
|
||||||
|
// Fallback: draw a small circle if image fails to load
|
||||||
|
p.fill(255);
|
||||||
|
p.ellipse(0, 0, 10);
|
||||||
|
}
|
||||||
|
p.pop();
|
||||||
|
|
||||||
|
if (t.y > p.height + 50) {
|
||||||
|
// If the one we missed was the target, lose a life
|
||||||
|
if (t.type === targetType && lives > 0) {
|
||||||
|
lives--;
|
||||||
|
}
|
||||||
|
targets.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2D HAMMER ---
|
||||||
|
p.push();
|
||||||
|
p.translate(p.mouseX, p.mouseY);
|
||||||
|
p.rotate(-0.4);
|
||||||
|
p.fill(120, 80, 50); p.noStroke();
|
||||||
|
p.rect(-5, 0, 10, 40, 2);
|
||||||
|
p.fill(100);
|
||||||
|
p.rect(-20, -10, 40, 20, 4);
|
||||||
|
p.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
p.mousePressed = () => {
|
||||||
|
if (gamePhase !== "PLAYING") return;
|
||||||
|
for (let i = targets.length - 1; i >= 0; i--) {
|
||||||
|
let t = targets[i];
|
||||||
|
if (p.dist(p.mouseX, p.mouseY, t.x, t.y) < 40) {
|
||||||
|
if (t.type === targetType) {
|
||||||
|
score += 100;
|
||||||
|
} else {
|
||||||
|
if (lives > 0) lives--;
|
||||||
|
}
|
||||||
|
targets.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const grassVertex = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform float uTime;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
vec3 pos = position;
|
||||||
|
float sway = sin(uTime * 2.0 + (instanceMatrix[3][0] * 0.5) + (instanceMatrix[3][2] * 0.5)) * 0.15 * uv.y;
|
||||||
|
pos.x += sway;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const grassFragment = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(mix(vec3(0.12, 0.28, 0.18), vec3(0.5, 0.72, 0.4), vUv.y), 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const createClouds = (group) => {
|
||||||
|
const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
|
||||||
|
const thickness = 2;
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const w = 10 + Math.random() * 20;
|
||||||
|
const d = 10 + Math.random() * 20;
|
||||||
|
const cloud = new THREE.Mesh(new THREE.BoxGeometry(w, thickness, d), cloudMaterial);
|
||||||
|
cloud.position.set((Math.random() - 0.5) * 280, 35, (Math.random() - 0.5) * 300);
|
||||||
|
group.add(cloud);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWorldChunk = (zOffset) => {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.z = zOffset;
|
||||||
|
const floor = new THREE.Mesh(new THREE.PlaneGeometry(160, CHUNK_SIZE + 0.1), new THREE.MeshStandardMaterial({ color: 0x1e2b21 }));
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
|
group.add(floor);
|
||||||
|
|
||||||
|
const count = 7000;
|
||||||
|
const geo = new THREE.PlaneGeometry(0.4, 0.9, 1, 2);
|
||||||
|
geo.translate(0, 0.45, 0);
|
||||||
|
const mat = new THREE.ShaderMaterial({
|
||||||
|
uniforms: { uTime }, vertexShader: grassVertex, fragmentShader: grassFragment,
|
||||||
|
side: THREE.DoubleSide, alphaToCoverage: true
|
||||||
|
});
|
||||||
|
const mesh = new THREE.InstancedMesh(geo, mat, count);
|
||||||
|
const dummy = new THREE.Object3D();
|
||||||
|
for(let i=0; i<count; i++) {
|
||||||
|
let x = (Math.random() - 0.5) * 120;
|
||||||
|
if (x > -10 && x < 10) x += (x > 0) ? 10 : -10;
|
||||||
|
dummy.position.set(x, 0, (Math.random() - 0.5) * CHUNK_SIZE);
|
||||||
|
dummy.rotation.y = Math.random() * Math.PI;
|
||||||
|
dummy.scale.setScalar(0.7 + Math.random() * 1.6);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
mesh.setMatrixAt(i, dummy.matrix);
|
||||||
|
}
|
||||||
|
group.add(mesh);
|
||||||
|
return group;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getCachedGLTF(file) {
|
||||||
|
if (!glbCache.has(file)) glbCache.set(file, await loader.loadAsync(file));
|
||||||
|
return glbCache.get(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function swapCharacter(file, isDeathAnimation = false) {
|
||||||
|
const myToken = ++swapToken;
|
||||||
|
const source = await getCachedGLTF(file);
|
||||||
|
if (myToken !== swapToken) return;
|
||||||
|
const model = cloneSkeleton(source.scene);
|
||||||
|
model.scale.setScalar(CONFIG.playerScale);
|
||||||
|
model.rotation.y = Math.PI;
|
||||||
|
const mixer = new THREE.AnimationMixer(model);
|
||||||
|
if (source.animations?.length) {
|
||||||
|
const action = mixer.clipAction(source.animations[0]);
|
||||||
|
if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; }
|
||||||
|
action.play();
|
||||||
|
}
|
||||||
|
if (currentModel) playerAnchor.remove(currentModel);
|
||||||
|
currentModel = model; currentMixer = mixer;
|
||||||
|
playerAnchor.add(currentModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
if (!canvas || !container) return; // Safety check
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
|
const skyColor = 0x87CEFA;
|
||||||
camera.position.set(0, 5, 10);
|
scene.background = new THREE.Color(skyColor);
|
||||||
camera.lookAt(0, 0, -5);
|
scene.fog = new THREE.Fog(skyColor, 150, 350);
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
|
||||||
|
camera.position.set(0, 4.5, 13);
|
||||||
|
camera.lookAt(0, 1, -5);
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
|
||||||
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
const lights = [
|
||||||
const sun = new THREE.DirectionalLight(0x00FFD1, 1.2);
|
new THREE.AmbientLight(0xffffff, 1.8),
|
||||||
sun.position.set(5, 10, 7);
|
new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
scene.add(sun);
|
];
|
||||||
|
lights[1].position.set(0, 50, 0);
|
||||||
|
scene.add(...lights);
|
||||||
|
|
||||||
const floorGeo = new THREE.PlaneGeometry(100, 2000);
|
const cloudGroup = new THREE.Group();
|
||||||
const floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2, metalness: 0.5 }));
|
createClouds(cloudGroup);
|
||||||
floor.rotation.x = -Math.PI / 2;
|
scene.add(cloudGroup);
|
||||||
scene.add(floor);
|
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
CHUNKS = Array.from({ length: CHUNK_COUNT }).map((_, i) => {
|
||||||
// Using the RobotExpressive model from your logic reference
|
const chunk = createWorldChunk(-i * CHUNK_SIZE);
|
||||||
loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', (gltf) => {
|
scene.add(chunk);
|
||||||
const model = gltf.scene;
|
return chunk;
|
||||||
model.scale.set(0.4, 0.4, 0.4);
|
|
||||||
model.rotation.y = Math.PI;
|
|
||||||
|
|
||||||
playerGroup = new THREE.Group();
|
|
||||||
playerGroup.add(model);
|
|
||||||
scene.add(playerGroup);
|
|
||||||
|
|
||||||
animations = gltf.animations;
|
|
||||||
mixer = new THREE.AnimationMixer(model);
|
|
||||||
playAnimation('Running');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
playerAnchor = new THREE.Group();
|
||||||
|
scene.add(playerAnchor);
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
if (!container || !renderer) return;
|
||||||
const { width, height } = container.getBoundingClientRect();
|
const { width, height } = container.getBoundingClientRect();
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
camera.aspect = width / height;
|
camera.aspect = width / height;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
});
|
});
|
||||||
resizeObserver.observe(container);
|
ro.observe(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function spawn() {
|
||||||
|
const l = Math.floor(Math.random() * 5) - 2;
|
||||||
|
const source = await getCachedGLTF("Simple computer.glb");
|
||||||
|
const model = cloneSkeleton(source.scene);
|
||||||
|
const pivot = new THREE.Group();
|
||||||
|
pivot.position.set(l * CONFIG.lane, 0, -130);
|
||||||
|
model.position.set(0, 0.6, 0);
|
||||||
|
model.rotation.y = Math.PI;
|
||||||
|
model.scale.setScalar(5.5);
|
||||||
|
pivot.add(model);
|
||||||
|
scene.add(pivot);
|
||||||
|
worldObjects = [...worldObjects, { mesh: pivot, lane: l }];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const delta = clock.getDelta();
|
const now = performance.now();
|
||||||
if (mixer) mixer.update(delta);
|
const delta = (now - lastTime) / 1000;
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
// 1. Camera Shake Logic
|
uTime.value += delta;
|
||||||
if (shake > 0) {
|
if (currentMixer) currentMixer.update(delta);
|
||||||
shake -= 0.05;
|
if (!isPlaying) return;
|
||||||
camera.position.x = (Math.random() - 0.5) * shake;
|
|
||||||
camera.position.y = 5 + (Math.random() - 0.5) * shake;
|
if (gamePhase === "INSTRUCTIONS") {
|
||||||
} else {
|
instructionTimer -= delta;
|
||||||
camera.position.x = 0;
|
if (instructionTimer <= 0) gamePhase = "PLAYING";
|
||||||
camera.position.y = 5;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Death Fade Logic
|
if (currentSpeed < CONFIG.MAX_SPEED) {
|
||||||
if (isDying && fade > 0) {
|
currentSpeed += CONFIG.ACCELERATION * delta;
|
||||||
fade -= 0.005;
|
|
||||||
playerGroup?.traverse((child) => {
|
|
||||||
if (child.isMesh) {
|
|
||||||
child.material.transparent = true;
|
|
||||||
child.material.opacity = fade;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moveStep = currentSpeed * delta;
|
||||||
|
score += Math.floor(currentSpeed / 40);
|
||||||
|
|
||||||
|
if (lives <= 0) triggerGameOver();
|
||||||
|
|
||||||
|
CHUNKS.forEach(chunk => {
|
||||||
|
chunk.position.z += moveStep;
|
||||||
|
if (chunk.position.z > CHUNK_SIZE) chunk.position.z -= CHUNK_SIZE * CHUNK_COUNT;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Stop physics if not playing or dying
|
currX += (lane * CONFIG.lane - currX) * 0.18;
|
||||||
if (!isPlaying || isDying || gameOver) return;
|
playerAnchor.position.x = currX;
|
||||||
|
|
||||||
score++;
|
|
||||||
|
|
||||||
// Smooth movement
|
|
||||||
currX += (lane * CONFIG.lane - currX) * 0.15;
|
|
||||||
if (playerGroup) playerGroup.position.x = currX;
|
|
||||||
|
|
||||||
// Jump Physics
|
|
||||||
if (isJumping) {
|
if (isJumping) {
|
||||||
jumpV -= CONFIG.grav;
|
jumpV -= CONFIG.grav;
|
||||||
playerY += jumpV;
|
playerY += jumpV;
|
||||||
if (playerY <= 0) {
|
if (playerY <= 0) {
|
||||||
playerY = 0;
|
playerY = 0;
|
||||||
isJumping = false;
|
isJumping = false;
|
||||||
|
if (!isDying) swapCharacter("Running.glb");
|
||||||
}
|
}
|
||||||
if (playerGroup) playerGroup.position.y = playerY;
|
|
||||||
}
|
}
|
||||||
|
playerAnchor.position.y = playerY;
|
||||||
|
|
||||||
// Obstacles & Collision
|
|
||||||
worldObjects = worldObjects.map(obj => {
|
worldObjects = worldObjects.map(obj => {
|
||||||
obj.mesh.position.z += CONFIG.speed;
|
obj.mesh.position.z += moveStep;
|
||||||
|
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) triggerGameOver();
|
||||||
if (Math.abs(obj.mesh.position.z) < 0.8 && obj.lane === lane && playerY < 1) {
|
return obj;
|
||||||
// --- TRIGGER DEATH SEQUENCE ---
|
/*
|
||||||
isDying = true;
|
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) {
|
||||||
shake = 0.5;
|
lives--;
|
||||||
hitFlash = true;
|
hitFlash = true;
|
||||||
playAnimation('Death', false);
|
setTimeout(() => hitFlash = false, 150);
|
||||||
|
// Remove hit object to prevent multi-hits
|
||||||
setTimeout(() => { hitFlash = false; }, 150);
|
scene.remove(obj.mesh);
|
||||||
|
return null;
|
||||||
setTimeout(() => {
|
|
||||||
isPlaying = false;
|
|
||||||
gameOver = true;
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
|
*/
|
||||||
}).filter(obj => {
|
}).filter(obj => {
|
||||||
const keep = obj.mesh.position.z <= 15;
|
const active = obj.mesh.position.z < 25;
|
||||||
if (!keep) scene.remove(obj.mesh);
|
if (!active) scene.remove(obj.mesh);
|
||||||
return keep;
|
return active;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (score % 30 === 0) spawn();
|
// Normal Obstacle Spawning
|
||||||
|
spawnDistanceTracker += moveStep;
|
||||||
|
if (spawnDistanceTracker >= SPAWN_INTERVAL) {
|
||||||
|
spawn();
|
||||||
|
spawnDistanceTracker = 0;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawn() {
|
|
||||||
const l = Math.floor(Math.random() * 5) - 2;
|
function triggerGameOver() {
|
||||||
const mesh = new THREE.Mesh(
|
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
|
||||||
new THREE.BoxGeometry(1.5, 1.5, 1.5),
|
hasSubmitted = false; // Allow a new submission for this game over
|
||||||
new THREE.MeshStandardMaterial({
|
loadLeaderboard(); // Refresh board to show latest rankings
|
||||||
color: 0xFF0055,
|
swapCharacter("Falling Back Death.glb", true);
|
||||||
emissive: 0xFF0055,
|
setTimeout(() => hitFlash = false, 150);
|
||||||
emissiveIntensity: 0.5
|
|
||||||
})
|
|
||||||
);
|
|
||||||
mesh.position.set(l * CONFIG.lane, 0.75, -100);
|
|
||||||
scene.add(mesh);
|
|
||||||
worldObjects.push({ mesh, lane: l });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGame() {
|
async function startGame() {
|
||||||
|
if (!scene) return;
|
||||||
|
currentSpeed = CONFIG.START_SPEED;
|
||||||
|
// Choose a random target type for this mission
|
||||||
|
const types = ["STRAWBERRY", "BANANA", "BLUEBERRY"];
|
||||||
|
targetType = types[Math.floor(Math.random() * types.length)];
|
||||||
worldObjects.forEach(obj => scene.remove(obj.mesh));
|
worldObjects.forEach(obj => scene.remove(obj.mesh));
|
||||||
worldObjects = [];
|
worldObjects = [];
|
||||||
|
targets = [];
|
||||||
score = 0; lane = 0; playerY = 0; currX = 0;
|
spawnDistanceTracker = 0;
|
||||||
isJumping = false; isDying = false; gameOver = false;
|
score = 0; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives
|
||||||
fade = 1; shake = 0; startScreen = false; isPlaying = true;
|
gamePhase = "INSTRUCTIONS"; instructionTimer = 3;
|
||||||
|
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
|
||||||
if (playerGroup) {
|
CHUNKS.forEach((chunk, i) => { chunk.position.z = -i * CHUNK_SIZE; });
|
||||||
playerGroup.position.set(0, 0, 0);
|
await swapCharacter("Running.glb");
|
||||||
playerGroup.traverse(child => {
|
|
||||||
if (child.isMesh) child.material.opacity = 1;
|
|
||||||
});
|
|
||||||
playAnimation('Running');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (!isPlaying || isDying) return;
|
||||||
|
const actions = {
|
||||||
|
ArrowLeft: () => lane > -2 && lane--, a: () => lane > -2 && lane--, A: () => lane > -2 && lane--,
|
||||||
|
ArrowRight: () => lane < 2 && lane++, d: () => lane < 2 && lane++, D: () => lane < 2 && lane++,
|
||||||
|
" ": () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||||
|
ArrowUp: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||||
|
w: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb")),
|
||||||
|
W: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jumping.glb"))
|
||||||
|
};
|
||||||
|
actions[e.key]?.();
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
init();
|
loadLeaderboard();
|
||||||
const loop = () => {
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
requestAnimationFrame(loop);
|
return () => {
|
||||||
update();
|
cancelAnimationFrame(animationFrame);
|
||||||
renderer.render(scene, camera);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
if (p5Instance) p5Instance.remove();
|
||||||
|
if (renderer) renderer.dispose();
|
||||||
};
|
};
|
||||||
loop();
|
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
|
||||||
if (!isPlaying || isDying) return;
|
|
||||||
if (e.key === "ArrowLeft" && lane > -2) lane--;
|
|
||||||
if (e.key === "ArrowRight" && lane < 2) lane++;
|
|
||||||
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
|
|
||||||
isJumping = true;
|
|
||||||
jumpV = CONFIG.jump;
|
|
||||||
playAnimation('Jump', false);
|
|
||||||
// Switch back to running after jump time (approx 800ms)
|
|
||||||
setTimeout(() => { if(!isDying) playAnimation('Running'); }, 800);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => resizeObserver?.disconnect());
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
{#if showLanding}
|
||||||
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #050505; }
|
<LandingPage onStart={handleStart} />
|
||||||
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
{:else}
|
||||||
canvas { width: 100% !important; height: 100% !important; display: block; }
|
|
||||||
|
|
||||||
.flash { position: absolute; inset: 0; background: white; z-index: 100; pointer-events: none; }
|
|
||||||
|
|
||||||
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: sans-serif; z-index: 50; }
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
pointer-events: auto;
|
|
||||||
background: rgba(0,0,0,0.95);
|
|
||||||
padding: 60px;
|
|
||||||
border: 1px solid #FF0055;
|
|
||||||
box-shadow: 0 0 50px rgba(255, 0, 85, 0.2);
|
|
||||||
margin-top: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-hud { font-size: 5rem; font-weight: 900; color: #00FFD1; margin-top: 20px; font-style: italic; }
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 20px 60px;
|
|
||||||
background: #FF0055;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 900;
|
|
||||||
color: white;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="wrapper" bind:this={container}>
|
<div id="wrapper" bind:this={container}>
|
||||||
|
<!-- The 3D Game World -->
|
||||||
<canvas bind:this={canvas}></canvas>
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
|
||||||
{#if hitFlash}
|
<!-- The 2D P5.js Overlay (Hearts, Hammer, Fruit) -->
|
||||||
<div class="flash"></div>
|
<div class="p5-hud" bind:this={p5Container}></div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
{#if hitFlash} <div class="flash"></div> {/if}
|
||||||
|
|
||||||
|
<!-- TOP-RIGHT LEADERBOARD -->
|
||||||
|
<div class="side-hud">
|
||||||
|
<div class="leaderboard-view">
|
||||||
|
<h3>TOP RUNNERS</h3>
|
||||||
|
<ul>
|
||||||
|
{#if leaderboard.length > 0}
|
||||||
|
{#each leaderboard as entry, i}
|
||||||
|
<li>
|
||||||
|
<span>{i + 1}. {entry.name}</span>
|
||||||
|
<span>{entry.score}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<li style="opacity: 0.5; justify-content: center;">No scores yet</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. GAME UI OVERLAY -->
|
||||||
<div class="ui">
|
<div class="ui">
|
||||||
{#if isPlaying && !gameOver}
|
<!-- Live Score -->
|
||||||
<div class="score-hud">{score}</div>
|
<div class="score">{score}</div>
|
||||||
|
|
||||||
|
<!-- Game Over Modal -->
|
||||||
|
{#if gameOver}
|
||||||
|
<div class="modal">
|
||||||
|
<h1>YOU LOST</h1>
|
||||||
|
<p>Final Score: <strong>{score}</strong></p>
|
||||||
|
|
||||||
|
<!-- Only show the save input if they haven't submitted yet -->
|
||||||
|
{#if !hasSubmitted}
|
||||||
|
<div class="leaderboard-entry">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={playerName}
|
||||||
|
placeholder="ENTER NAME"
|
||||||
|
maxlength="10"
|
||||||
|
/>
|
||||||
|
<button class="save-btn" on:click={saveScore}>SAVE TO BOARD</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="saved-msg">SCORE SAVED!</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if startScreen || gameOver}
|
<button class="retry-btn" on:click={startGame}>PLAY AGAIN</button>
|
||||||
<div class="modal">
|
|
||||||
<h1 style="font-size: 4rem; font-style: italic; font-weight: 900;">
|
|
||||||
{gameOver ? 'SYSTEM FAILURE' : 'CYBER RUN'}
|
|
||||||
</h1>
|
|
||||||
<p style="letter-spacing: 5px; opacity: 0.6; margin-bottom: 40px;">
|
|
||||||
{gameOver ? 'FINAL SCORE: ' + score : 'NEURAL LINK READY'}
|
|
||||||
</p>
|
|
||||||
<button on:click={startGame}>{gameOver ? 'RE-INITIATE' : 'START SIMULATION'}</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #8cd0f8; cursor: none; }
|
||||||
|
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; }
|
||||||
|
canvas { width: 100% !important; height: 100% !important; display: block; }
|
||||||
|
.p5-hud { position: absolute; top: 0; left: 0; pointer-events: auto; z-index: 10; width: 100%; height: 100%; }
|
||||||
|
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: 'Segoe UI', sans-serif; z-index: 11 }
|
||||||
|
.score { font-size: 2.5rem; margin-top: 60px; font-weight: 800; text-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
||||||
|
|
||||||
|
.modal { pointer-events: auto; background: rgba(255, 255, 255, 0.98); padding: 40px; border-radius: 30px; margin-top: 5vh; color: #1e2b21; display: inline-block; box-shadow: 0 25px 60px rgba(0,0,0,0.15); width: 320px; }
|
||||||
|
|
||||||
|
.leaderboard-entry { margin: 20px 0; }
|
||||||
|
input { width: 80%; padding: 12px; border: 2px solid #eee; border-radius: 10px; font-family: inherit; font-weight: bold; text-align: center; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
/* New Sidebar Container */
|
||||||
|
.side-hud {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 220px;
|
||||||
|
z-index: 15;
|
||||||
|
pointer-events: none; /* Let clicks pass through to the game if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Updated Leaderboard View */
|
||||||
|
.leaderboard-view {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure buttons and inputs still work */
|
||||||
|
input, button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaderboard-view h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #00d2ff; /* Change color to cyan to match your theme */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1); /* Lighter border */
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
ul { list-style: none; padding: 0; margin: 0; }
|
||||||
|
|
||||||
|
button { width: 100%; padding: 15px; border: none; border-radius: 12px; cursor: pointer; font-weight: bold; font-size: 1rem; transition: all 0.2s; }
|
||||||
|
.save-btn { background: #00d2ff; color: white; margin-bottom: 5px; }
|
||||||
|
.retry-btn { background: #1e2b21; color: white; margin-top: 10px; }
|
||||||
|
button:hover { transform: translateY(-2px); filter: brightness(1.1); }
|
||||||
|
|
||||||
|
.flash { position: absolute; inset: 0; background: white; z-index: 20; pointer-events: none; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user