Files
overload/src/cool.svelte

748 lines
22 KiB
Svelte

<script>
// @ts-nocheck
import LandingPage from './LandingPage.svelte';
import { onMount, tick } from "svelte";
import * as THREE from "three";
import p5 from "p5";
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 = {
lane: 2.5,
jump: 0.35,
grav: 0.015,
playerScale: 1.7,
START_SPEED: 45, // Initial slow speed
MAX_SPEED: 95, // The "chaos" threshold
ACCELERATION: 1 // Speed added per second
};
let currentSpeed = CONFIG.START_SPEED;
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
let attentiveness = 100;
let lives = 5;
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
let container, canvas, scene, camera, renderer, p5Container;
let worldObjects = [], animationFrame, p5Instance;
let isDying = false, hitFlash = false;
let spawnDistanceTracker = 0;
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
// 2D Game Logic
let gamePhase = "START";
let instructionTimer = 3;
let targetType = "STRAWBERRY";
let targets = [];
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
let CHUNKS = [];
const CHUNK_COUNT = 3;
const CHUNK_SIZE = 140;
let uTime = { value: 0 };
const loader = new GLTFLoader();
const glbCache = new Map();
let textures = {};
const sketch = (p) => {
// --- FOOLPROOF IMAGE LOADER ---
p.setup = async () => {
const w = container?.clientWidth || p.windowWidth;
const h = container?.clientHeight || p.windowHeight;
p.createCanvas(w, h);
const loadImg = (path) => new Promise(resolve => {
p.loadImage(path, img => resolve(img), () => resolve(null));
});
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() {
if (!canvas || !container) return; // Safety check
scene = new THREE.Scene();
const skyColor = 0x87CEFA;
scene.background = new THREE.Color(skyColor);
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.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.setSize(container.clientWidth, container.clientHeight);
const lights = [
new THREE.AmbientLight(0xffffff, 1.8),
new THREE.DirectionalLight(0xffffff, 1.2)
];
lights[1].position.set(0, 50, 0);
scene.add(...lights);
const cloudGroup = new THREE.Group();
createClouds(cloudGroup);
scene.add(cloudGroup);
CHUNKS = Array.from({ length: CHUNK_COUNT }).map((_, i) => {
const chunk = createWorldChunk(-i * CHUNK_SIZE);
scene.add(chunk);
return chunk;
});
playerAnchor = new THREE.Group();
scene.add(playerAnchor);
const ro = new ResizeObserver(() => {
if (!container || !renderer) return;
const { width, height } = container.getBoundingClientRect();
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
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() {
const now = performance.now();
const delta = (now - lastTime) / 1000;
lastTime = now;
uTime.value += delta;
if (currentMixer) currentMixer.update(delta);
if (!isPlaying) return;
if (gamePhase === "INSTRUCTIONS") {
instructionTimer -= delta;
if (instructionTimer <= 0) gamePhase = "PLAYING";
return;
}
if (currentSpeed < CONFIG.MAX_SPEED) {
currentSpeed += CONFIG.ACCELERATION * delta;
}
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;
});
currX += (lane * CONFIG.lane - currX) * 0.18;
playerAnchor.position.x = currX;
if (isJumping) {
jumpV -= CONFIG.grav;
playerY += jumpV;
if (playerY <= 0) {
playerY = 0;
isJumping = false;
if (!isDying) swapCharacter("Running.glb");
}
}
playerAnchor.position.y = playerY;
worldObjects = worldObjects.map(obj => {
obj.mesh.position.z += moveStep;
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) triggerGameOver();
return obj;
/*
if (Math.abs(obj.mesh.position.z) < 1.5 && obj.lane === lane && playerY < 1.5) {
lives--;
hitFlash = true;
setTimeout(() => hitFlash = false, 150);
// Remove hit object to prevent multi-hits
scene.remove(obj.mesh);
return null;
}
return obj;
*/
}).filter(obj => {
const active = obj.mesh.position.z < 25;
if (!active) scene.remove(obj.mesh);
return active;
});
// Normal Obstacle Spawning
spawnDistanceTracker += moveStep;
if (spawnDistanceTracker >= SPAWN_INTERVAL) {
spawn();
spawnDistanceTracker = 0;
}
}
function triggerGameOver() {
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
hasSubmitted = false; // Allow a new submission for this game over
loadLeaderboard(); // Refresh board to show latest rankings
swapCharacter("Falling Back Death.glb", true);
setTimeout(() => hitFlash = false, 150);
}
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 = [];
targets = [];
spawnDistanceTracker = 0;
score = 0; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives
gamePhase = "INSTRUCTIONS"; instructionTimer = 3;
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
CHUNKS.forEach((chunk, i) => { chunk.position.z = -i * CHUNK_SIZE; });
await swapCharacter("Running.glb");
}
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(() => {
loadLeaderboard();
window.addEventListener("keydown", handleKeyDown);
return () => {
cancelAnimationFrame(animationFrame);
window.removeEventListener("keydown", handleKeyDown);
if (p5Instance) p5Instance.remove();
if (renderer) renderer.dispose();
};
});
</script>
{#if showLanding}
<LandingPage onStart={handleStart} />
{:else}
<div id="wrapper" bind:this={container}>
<!-- The 3D Game World -->
<canvas bind:this={canvas}></canvas>
<!-- The 2D P5.js Overlay (Hearts, Hammer, Fruit) -->
<div class="p5-hud" bind:this={p5Container}></div>
{#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">
<!-- Live Score -->
<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}
<button class="retry-btn" on:click={startGame}>PLAY AGAIN</button>
</div>
{/if}
</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>
// function update() {
// const now = performance.now();
// const delta = (now - lastTime) / 1000;
// lastTime = now;
// uTime.value += delta;
// if (currentMixer) currentMixer.update(delta);
// if (!isPlaying) return;
// // --- MULTIPLIER COUNTDOWN ---
// if (multiplierTimer > 0) {
// multiplierTimer -= delta;
// if (multiplierTimer <= 0) {
// multiplierTimer = 0;
// scoreMultiplier = 1;
// }
// }
// // --- NEW MODULAR ENVIRONMENT CALL ---
// updateEnvironment(uTime.value, scene,
// { ambientLight, sunLight, headLight },
// { sun, moon }
// );
// if (gamePhase === "INSTRUCTIONS") {
// instructionTimer -= delta;
// if (instructionTimer <= 0) gamePhase = "PLAYING";
// return;
// }
// if (currentSpeed < CONFIG.MAX_SPEED) {
// currentSpeed += CONFIG.ACCELERATION * delta;
// }
// const moveStep = currentSpeed * delta;
// // --- BOOSTED DISTANCE SCORE ---
// // We apply the multiplier to the floor calculation
// score += Math.floor((currentSpeed / 40) * scoreMultiplier);
// if (cloudGroup) {
// // 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();
// CHUNKS.forEach(chunk => {
// chunk.position.z += moveStep;
// if (chunk.position.z > CHUNK_SIZE) chunk.position.z -= CHUNK_SIZE * CHUNK_COUNT;
// });
// currX += (lane * CONFIG.lane - currX) * 0.18;
// playerAnchor.position.x = currX;
// if (isJumping) {
// jumpV -= CONFIG.grav;
// playerY += jumpV;
// if (playerY <= 0) {
// playerY = 0;
// isJumping = false;
// if (!isDying) swapCharacter("Running.glb");
// }
// }
// playerAnchor.position.y = playerY;
// // Update object positions
// worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; });
// // Handle Collisions using the module
// worldObjects = handleCollisions(worldObjects, lane, playerY, triggerGameOver);
// // Filter out-of-bounds objects
// worldObjects = worldObjects.filter(obj => {
// const active = obj.mesh.position.z < 25;
// if (!active) scene.remove(obj.mesh);
// return active;
// });
// // Normal Obstacle Spawning
// spawnDistanceTracker += moveStep;
// if (spawnDistanceTracker >= SPAWN_INTERVAL) {
// spawn();
// spawnDistanceTracker = 0;
// }
// }