diff --git a/src/App.svelte b/src/App.svelte index 0e84bc0..347839e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -9,70 +9,83 @@ const CONFIG = { lane: 2.5, jump: 0.35, grav: 0.015, - speed: 0.3, - gameOverDelay: 1400, - playerScale: 1.25 + speed: 55, // Keeping your faster speed + playerScale: 1.7 // Increased size significantly }; -let score = 0; -let isPlaying = false; -let gameOver = false; -let startScreen = true; -let showGameOverModal = false; +let score = 0, isPlaying = false, gameOver = false, startScreen = true; +let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0; +let container, canvas, scene, camera, renderer; +let worldObjects = [], animationFrame; +let isDying = false, hitFlash = false; -let lane = 0; -let currX = 0; -let isJumping = false; -let jumpV = 0; -let playerY = 0; +let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0; -let container, canvas, scene, camera, renderer, floor; -let worldObjects = []; -let resizeObserver; -let animationFrame; - -let isDying = false; -let hitFlash = false; -let shake = 0; - -let playerAnchor; -let currentModel = null; -let currentMixer = null; -let currentAction = null; -let currentState = "run"; -let swapToken = 0; +// Treadmill System +const CHUNKS = []; +const CHUNK_COUNT = 3; +const CHUNK_SIZE = 140; +let uTime = { value: 0 }; const loader = new GLTFLoader(); const clock = new THREE.Clock(); const glbCache = new Map(); -function normalizeModel(root) { - root.scale.setScalar(CONFIG.playerScale); - root.rotation.y = Math.PI; +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); + } +`; - root.traverse((obj) => { - if (obj.isMesh || obj.isSkinnedMesh) { - obj.castShadow = true; - obj.receiveShadow = true; - obj.frustumCulled = false; - } - if (obj.isBone) { - obj.frustumCulled = false; - } +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); + } +`; + +function 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; // Adjusted for slightly larger floor + 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 }); - - root.updateMatrixWorld(true); - - const box = new THREE.Box3().setFromObject(root); - const center = new THREE.Vector3(); - box.getCenter(center); - - root.position.x -= center.x; - root.position.z -= center.z; - root.position.y -= box.min.y; - - root.updateMatrixWorld(true); - return root; + + 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; + let z = (Math.random() - 0.5) * CHUNK_SIZE; + dummy.position.set(x, 0, z); + 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) { @@ -83,345 +96,180 @@ async function getCachedGLTF(file) { return glbCache.get(file); } -function disposeCurrentCharacter() { - if (currentMixer) { - currentMixer.stopAllAction(); - } - - if (currentModel && playerAnchor) { - playerAnchor.remove(currentModel); - } - - currentModel = null; - currentMixer = null; - currentAction = null; -} - -async function swapCharacter(file, stateName, { loop = true } = {}) { +async function swapCharacter(file, isDeathAnimation = false) { const myToken = ++swapToken; - const source = await getCachedGLTF(file); if (myToken !== swapToken) return; - - const model = normalizeModel(cloneSkeleton(source.scene)); + + const model = cloneSkeleton(source.scene); + model.scale.setScalar(CONFIG.playerScale); + model.rotation.y = Math.PI; + const mixer = new THREE.AnimationMixer(model); - - let action = null; - if (source.animations && source.animations.length > 0) { - action = mixer.clipAction(source.animations[0]); - if (loop) { - action.setLoop(THREE.LoopRepeat, Infinity); - } else { + if (source.animations?.length) { + const action = mixer.clipAction(source.animations[0]); + + if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); - action.clampWhenFinished = true; + action.clampWhenFinished = true; // STOPS AT THE LAST FRAME + action.play(); + } else { + action.play(); } - action.reset(); - action.play(); } - - disposeCurrentCharacter(); - - currentModel = model; - currentMixer = mixer; - currentAction = action; - currentState = stateName; - + + if (currentModel) playerAnchor.remove(currentModel); + currentModel = model; currentMixer = mixer; playerAnchor.add(currentModel); } -function showRun() { - return swapCharacter("Running.glb", "run", { loop: true }); -} - -function showJump() { - return swapCharacter("Jumping.glb", "jump", { loop: false }); -} - -function showDeath() { - return swapCharacter("Falling Back Death.glb", "death", { loop: false }); -} - -function triggerGameOver() { - if (isDying || gameOver) return; - - isPlaying = false; - gameOver = true; - isDying = true; - hitFlash = true; - shake = 0.35; - - showDeath(); - - setTimeout(() => { - hitFlash = false; - }, 150); - - setTimeout(() => { - showGameOverModal = true; - }, CONFIG.gameOverDelay); -} - function init() { scene = new THREE.Scene(); + const skyColor = 0xa4c3b2; + scene.background = new THREE.Color(skyColor); + scene.fog = new THREE.Fog(skyColor, 35, 150); camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000); - camera.position.set(0, 5, 10); - camera.lookAt(0, 0, -5); + camera.position.set(0, 4.5, 13); // Lowered slightly to match bigger player + camera.lookAt(0, 1, -5); renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); - renderer.setPixelRatio(window.devicePixelRatio); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); - scene.add(new THREE.AmbientLight(0xffffff, 0.8)); + scene.add(new THREE.AmbientLight(0xffffff, 1.2)); - const sun = new THREE.DirectionalLight(0xffffff, 1); - sun.position.set(5, 10, 7); - scene.add(sun); - - const floorGeo = new THREE.PlaneGeometry(100, 2000); - floor = new THREE.Mesh( - floorGeo, - new THREE.MeshStandardMaterial({ color: 0x222222 }) - ); - floor.rotation.x = -Math.PI / 2; - scene.add(floor); + for (let i = 0; i < CHUNK_COUNT; i++) { + const chunk = createWorldChunk(-i * CHUNK_SIZE); + CHUNKS.push(chunk); + scene.add(chunk); + } playerAnchor = new THREE.Group(); scene.add(playerAnchor); - resizeObserver = new ResizeObserver(() => { + new ResizeObserver(() => { const { width, height } = container.getBoundingClientRect(); renderer.setSize(width, height); camera.aspect = width / height; camera.updateProjectionMatrix(); - }); - - resizeObserver.observe(container); + }).observe(container); } function spawn() { const l = Math.floor(Math.random() * 5) - 2; const mesh = new THREE.Mesh( - new THREE.BoxGeometry(1.5, 1.5, 1.5), - new THREE.MeshStandardMaterial({ color: 0x00fff2 }) + new THREE.BoxGeometry(1.8, 1.8, 1.8), + new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 }) ); - mesh.position.set(l * CONFIG.lane, 0.75, -100); + mesh.position.set(l * CONFIG.lane, 0.9, -130); scene.add(mesh); worldObjects.push({ mesh, lane: l }); } function update() { const delta = clock.getDelta(); + uTime.value = clock.getElapsedTime(); - if (currentMixer) { - currentMixer.update(delta); - } - - if (shake > 0.001) { - shake *= 0.9; - camera.position.x = (Math.random() - 0.5) * shake; - } else { - shake = 0; - camera.position.x += (0 - camera.position.x) * 0.2; - } - - if (playerAnchor) { - playerAnchor.position.set(currX, playerY, 0); - } - + if (currentMixer) currentMixer.update(delta); if (!isPlaying) return; + const moveStep = CONFIG.speed * delta; score++; - currX += (lane * CONFIG.lane - currX) * 0.15; + + 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) showRun(); + playerY = 0; + isJumping = false; + if (!isDying) swapCharacter("Running.glb"); } } + playerAnchor.position.y = playerY; - worldObjects = worldObjects - .map((obj) => { - obj.mesh.position.z += CONFIG.speed; - - if ( - Math.abs(obj.mesh.position.z) < 0.8 && - obj.lane === lane && - playerY < 1 - ) { - triggerGameOver(); - } - - return obj; - }) - .filter((obj) => { - const keep = obj.mesh.position.z <= 15; - if (!keep) scene.remove(obj.mesh); - return keep; - }); + worldObjects = worldObjects.map(obj => { + obj.mesh.position.z += moveStep; + // Collision box adjusted for larger character + if (Math.abs(obj.mesh.position.z) < 1.3 && obj.lane === lane && playerY < 1.5) triggerGameOver(); + return obj; + }).filter(obj => { + const keep = obj.mesh.position.z < 25; + if (!keep) scene.remove(obj.mesh); + return keep; + }); if (score % 30 === 0) spawn(); } +function triggerGameOver() { + isPlaying = false; gameOver = true; isDying = true; hitFlash = true; + swapCharacter("Falling Back Death.glb", true); // Pass 'true' for death logic + setTimeout(() => hitFlash = false, 150); +} + async function startGame() { - worldObjects.forEach((obj) => scene.remove(obj.mesh)); + worldObjects.forEach(obj => scene.remove(obj.mesh)); worldObjects = []; - - score = 0; - isPlaying = true; - gameOver = false; - startScreen = false; - showGameOverModal = false; - - lane = 0; - currX = 0; - isJumping = false; - jumpV = 0; - playerY = 0; - - isDying = false; - hitFlash = false; - shake = 0; - - if (playerAnchor) { - playerAnchor.position.set(0, 0, 0); - playerAnchor.rotation.set(0, 0, 0); - } - - await showRun(); + score = 0; isPlaying = true; gameOver = false; startScreen = false; + 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"); } function handleKeyDown(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; - showJump(); + isJumping = true; jumpV = CONFIG.jump; swapCharacter("Jumping.glb"); } } onMount(() => { init(); - - const loop = () => { - animationFrame = requestAnimationFrame(loop); - update(); - renderer.render(scene, camera); - }; - + const loop = () => { animationFrame = requestAnimationFrame(loop); update(); renderer.render(scene, camera); }; loop(); window.addEventListener("keydown", handleKeyDown); - - return () => { - cancelAnimationFrame(animationFrame); - window.removeEventListener("keydown", handleKeyDown); - resizeObserver?.disconnect(); - - if (currentMixer) currentMixer.stopAllAction(); - renderer?.dispose(); - }; + return () => { cancelAnimationFrame(animationFrame); window.removeEventListener("keydown", handleKeyDown); }; });
- - {#if hitFlash} -
- {/if} - + {#if hitFlash}
{/if}
{score}
- - {#if startScreen || showGameOverModal} + {#if startScreen || gameOver} {/if}