diff --git a/Falling Back Death.glb b/3dmodels/Falling Back Death.glb similarity index 100% rename from Falling Back Death.glb rename to 3dmodels/Falling Back Death.glb diff --git a/Jump.glb b/3dmodels/Jump.glb similarity index 100% rename from Jump.glb rename to 3dmodels/Jump.glb diff --git a/Running.glb b/3dmodels/Running.glb similarity index 100% rename from Running.glb rename to 3dmodels/Running.glb diff --git a/Simple computer.glb b/3dmodels/Simple computer.glb similarity index 100% rename from Simple computer.glb rename to 3dmodels/Simple computer.glb diff --git a/bird_in_a_claw_machine.glb b/3dmodels/bird_in_a_claw_machine.glb similarity index 100% rename from bird_in_a_claw_machine.glb rename to 3dmodels/bird_in_a_claw_machine.glb diff --git a/hiphop.glb b/3dmodels/hiphop.glb similarity index 100% rename from hiphop.glb rename to 3dmodels/hiphop.glb diff --git a/monitor.glb b/3dmodels/monitor.glb similarity index 100% rename from monitor.glb rename to 3dmodels/monitor.glb diff --git a/banana.png b/images/banana.png similarity index 100% rename from banana.png rename to images/banana.png diff --git a/bggame.png b/images/bggame.png similarity index 100% rename from bggame.png rename to images/bggame.png diff --git a/blubb.png b/images/blubb.png similarity index 100% rename from blubb.png rename to images/blubb.png diff --git a/overload_trans.png b/images/overload_trans.png similarity index 100% rename from overload_trans.png rename to images/overload_trans.png diff --git a/star.png b/images/star.png similarity index 100% rename from star.png rename to images/star.png diff --git a/src/assets/strawberry.png b/images/strawberry.png similarity index 100% rename from src/assets/strawberry.png rename to images/strawberry.png diff --git a/watermelon.png b/images/watermelon.png similarity index 100% rename from watermelon.png rename to images/watermelon.png diff --git a/src/App.svelte b/src/App.svelte index 11b7314..8e1d81a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -11,48 +11,59 @@ import { loadLeaderboard, saveScore, playerName, leaderboard , hasSubmitted } fr import { updateEnvironment, skyColors } from './environment.js'; import { createObstacle, handleCollisions } from './obstacles.js'; import { createSketch } from './p5overlay.js'; +import { + handleInput, + processInteraction, + updatePhysics, + updateGameFlow // Add this +} from './GameController.js'; + +import { + createWorldChunk, + createClouds, + moveWorld, + animateClouds // Added this +} from './WorldScene.js'; +import { ObstacleFactory } from './ObstacleFactory.js'; +import { GameManager } from './GameManager.js'; let showLanding = true; let lastTime = performance.now(); async function handleStart() { showLanding = false; - // 1. Wait for Svelte to render the div so p5Container is NOT null await tick(); - - // 2. YOU FORGOT THIS: Initialize Three.js scene init(); // 3. Setup the Bridge Object const gameState = { get isPlaying() { return isPlaying; }, get score() { return score; }, - set score(v) { score = v; }, // Allows p5 to update the score variable here + set score(v) { score = v; }, get lives() { return lives; }, - set lives(v) { lives = v; }, // Allows p5 to update the lives variable here + set lives(v) { lives = v; }, get multiplierTimer() { return multiplierTimer; }, get targetType() { return targetType; }, get instructionTimer() { return instructionTimer; }, + set instructionTimer(v) { instructionTimer = v; }, get gamePhase() { return gamePhase; }, + set gamePhase(v) { gamePhase = v; }, get lastStarScore() { return lastStarScore; }, set lastStarScore(v) { lastStarScore = v; }, - targets, - scorePopups, + + // FIX: Use getters for the arrays so they don't get stale! + get targets() { return targets; }, + get scorePopups() { return scorePopups; }, + onHit: (t) => { - // This logic runs in App.svelte scope when p5 calls it - if (t.type === targetType) { - const gain = 100 * scoreMultiplier; - score += gain; - scorePopups.push({ x: t.x, y: t.y, opacity: 255, life: 1, val: `+${gain}` }); - } else if (t.type === "STAR") { - scoreMultiplier = 2; - multiplierTimer = BOOST_DURATION; - scorePopups.push({ x: t.x, y: t.y, opacity: 255, life: 1, val: "X2 BOOST!" }); - } else { - if (lives > 0) lives--; - } + // MVC: Delegate the 'Decision' to the Controller + processInteraction(t, gameState, scoreMultiplier, BOOST_DURATION); + }, + onActivateBoost: (mult, dur) => { + scoreMultiplier = mult; + multiplierTimer = dur; } - }; + }; // 4. Initialize p5 now that p5Container exists p5Instance = new p5(createSketch(gameState, textures), p5Container); @@ -120,69 +131,6 @@ let multiplierTimer = 0; // Remaining seconds of boost let lastStarScore = 0; // Add this line to prevent the crash const BOOST_DURATION = 20; // 20 seconds - -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); - const y = 30 + Math.random() * 25; - cloud.position.set((Math.random() - 0.5) * 280, y, (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 -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); @@ -190,7 +138,7 @@ async function getCachedGLTF(file) { async function swapCharacter(file, isDeathAnimation = false) { const myToken = ++swapToken; - const source = await getCachedGLTF(file); + const source = await getCachedGLTF(`3dmodels/${file}`); if (myToken !== swapToken) return; const model = cloneSkeleton(source.scene); model.scale.setScalar(CONFIG.playerScale); @@ -233,7 +181,7 @@ function init() { scene.add(cloudGroup); CHUNKS = Array.from({ length: CHUNK_COUNT }).map((_, i) => { - const chunk = createWorldChunk(-i * CHUNK_SIZE); + const chunk = createWorldChunk(-i * CHUNK_SIZE, uTime); scene.add(chunk); return chunk; }); @@ -286,7 +234,6 @@ async function spawn() { worldObjects = [...worldObjects, obstacleData]; } - function update() { const now = performance.now(); const delta = (now - lastTime) / 1000; @@ -296,92 +243,76 @@ function update() { if (currentMixer) currentMixer.update(delta); if (!isPlaying) return; - // --- MULTIPLIER COUNTDOWN --- - if (multiplierTimer > 0) { - multiplierTimer -= delta; - if (multiplierTimer <= 0) { - multiplierTimer = 0; - scoreMultiplier = 1; - } - } + // --- ADD THIS LINE HERE --- + // This updates the instruction timer and switches the phase + updateGameFlow({ + get gamePhase() { return gamePhase; }, + set gamePhase(v) { gamePhase = v; }, + get instructionTimer() { return instructionTimer; }, + set instructionTimer(v) { instructionTimer = v; } + }, delta); - // --- 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; - } + // If we are still in instructions, stop the rest of the game logic + // (like movement and spawning) so the player doesn't die while reading. + if (gamePhase === "INSTRUCTIONS") return; const moveStep = currentSpeed * delta; - // --- BOOSTED DISTANCE SCORE --- - // We apply the multiplier to the floor calculation - score += Math.floor((currentSpeed / 40) * scoreMultiplier); - + // 1. Environment Controller + updateEnvironment(uTime.value, scene, { ambientLight, sunLight, headLight }, { sun, moon }); - if (cloudGroup) { - // Moving at 40% speed (moveStep * 0.4) creates a nice parallax depth - cloudGroup.children.forEach(cloud => { - cloud.position.z += moveStep * 0.4; + // 2. World Controller + moveWorld(CHUNKS, moveStep, CHUNK_SIZE, CHUNK_COUNT); + animateClouds(cloudGroup, moveStep); - // 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 - } - }); - } + // 3. Player Physics Controller + updatePhysics( + { + get lane() { return lane; }, + get currX() { return currX; }, set currX(v) { currX = v; }, + get playerY() { return playerY; }, set playerY(v) { playerY = v; }, + get isJumping() { return isJumping; }, set isJumping(v) { isJumping = v; }, + get jumpV() { return jumpV; }, set jumpV(v) { jumpV = v; }, + isDying + }, + CONFIG, delta, swapCharacter + ); - 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; + // Apply visual positions to the actual 3D objects 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 + // 4. Obstacle Controller 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; - + // 5. Scoring & Spawning Logic + if (multiplierTimer > 0) { + multiplierTimer -= delta; + if (multiplierTimer <= 0) { multiplierTimer = 0; scoreMultiplier = 1; } } + + score += Math.floor((currentSpeed / 40) * scoreMultiplier); + if (currentSpeed < CONFIG.MAX_SPEED) currentSpeed += CONFIG.ACCELERATION * delta; + + // 1. Ask the Static Manager if we should spawn + if (GameManager.shouldSpawn(moveStep)) { + // 2. Pass 'loader' so the factory can fetch models if needed + ObstacleFactory.spawnRandom(glbCache, loader, CONFIG.lane).then(obstacle => { + scene.add(obstacle.mesh); + worldObjects = [...worldObjects, obstacle]; + }); + } + // spawnDistanceTracker += moveStep; + // if (spawnDistanceTracker >= SPAWN_INTERVAL) { + // spawn(); + // spawnDistanceTracker = 0; + // } } @@ -400,8 +331,9 @@ async function startGame() { const types = ["STRAWBERRY", "WATERMELON", "BLUEBERRY"]; targetType = types[Math.floor(Math.random() * types.length)]; worldObjects.forEach(obj => scene.remove(obj.mesh)); + targets.length = 0; + scorePopups.length = 0; worldObjects = []; - targets = []; scorePopups = []; spawnDistanceTracker = 0; score = 0; isPlaying = true; gameOver = false; startScreen = false; lives = 5; // Reset lives gamePhase = "INSTRUCTIONS"; instructionTimer = 3; @@ -411,16 +343,22 @@ async function startGame() { } 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("Jump.glb")), - ArrowUp: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")), - w: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")), - W: () => !isJumping && (isJumping = true, jumpV = CONFIG.jump, swapCharacter("Jump.glb")) - }; - actions[e.key]?.(); + // MVC: Delegate keyboard input to the Controller + handleInput(e, + { + isPlaying, + isDying, + // Use getters/setters so the Controller can actually change the Model + get lane() { return lane; }, + set lane(v) { lane = v; }, + get isJumping() { return isJumping; }, + set isJumping(v) { isJumping = v; }, + get jumpV() { return jumpV; }, + set jumpV(v) { jumpV = v; } + }, + CONFIG, + swapCharacter + ); }; onMount(() => { diff --git a/src/GameController.js b/src/GameController.js new file mode 100644 index 0000000..aeaaaa0 --- /dev/null +++ b/src/GameController.js @@ -0,0 +1,64 @@ +// GameController.js +// @ts-nocheck +export function handleInput(event, state, config, swapFn) { + if (!state.isPlaying || state.isDying) return; + + const actions = { + ArrowLeft: () => state.lane > -2 && state.lane--, + a: () => state.lane > -2 && state.lane--, + ArrowRight: () => state.lane < 2 && state.lane++, + d: () => state.lane < 2 && state.lane++, + " ": () => !state.isJumping && triggerJump(state, config, swapFn), + w: () => !state.isJumping && triggerJump(state, config, swapFn), + ArrowUp: () => !state.isJumping && triggerJump(state, config, swapFn) + }; + + actions[event.key]?.(); +} + +function triggerJump(state, config, swapFn) { + // This now triggers the 'set' in App.svelte + state.isJumping = true; + state.jumpV = config.jump; + swapFn("Jump.glb"); +} + +export function processInteraction(target, state, multiplier, duration) { + if (target.type === state.targetType) { + const gain = 100 * multiplier; + state.score += gain; + state.scorePopups.push({ x: target.x, y: target.y, opacity: 255, life: 1, val: `+${gain}` }); + } else if (target.type === "STAR") { + state.onActivateBoost(2, duration); // Ask the app to set the boost + state.scorePopups.push({ x: target.x, y: target.y, opacity: 255, life: 1, val: "X2 BOOST!" }); + } else { + if (state.lives > 0) state.lives--; + } +} + +export function updatePhysics(state, config, delta, swapFn) { + // 1. Smooth Lane Shifting + state.currX += (state.lane * config.lane - state.currX) * 0.18; + + // 2. Jump/Gravity Logic + if (state.isJumping) { + state.jumpV -= config.grav; + state.playerY += state.jumpV; + + if (state.playerY <= 0) { + state.playerY = 0; + state.isJumping = false; + if (!state.isDying) swapFn("Running.glb"); + } + } +} + +// GameController.js +export function updateGameFlow(state, delta) { + if (state.gamePhase === "INSTRUCTIONS") { + state.instructionTimer -= delta; + if (state.instructionTimer <= 0) { + state.gamePhase = "PLAYING"; + } + } +} \ No newline at end of file diff --git a/src/GameManager.js b/src/GameManager.js new file mode 100644 index 0000000..b2e15e2 --- /dev/null +++ b/src/GameManager.js @@ -0,0 +1,22 @@ +// @ts-nocheck +export class GameManager { + static SPAWN_INTERVAL = 40; + static currentDistance = 0; + + static shouldSpawn(moveStep) { + this.currentDistance += moveStep; + if (this.currentDistance >= this.SPAWN_INTERVAL) { + this.currentDistance = 0; + return true; + } + return false; + } + + // Example of a static utility for difficulty scaling + static calculateSpeed(currentSpeed, acceleration, delta, maxSpeed) { + if (currentSpeed < maxSpeed) { + return currentSpeed + (acceleration * delta); + } + return currentSpeed; + } +} \ No newline at end of file diff --git a/src/LandingPage.svelte b/src/LandingPage.svelte index dd3ebf1..c9609ba 100644 --- a/src/LandingPage.svelte +++ b/src/LandingPage.svelte @@ -31,7 +31,7 @@ charScene.add(dirLight); const loader = new GLTFLoader(); - loader.load("/hiphop.glb", (gltf) => { + loader.load("3dmodels/hiphop.glb", (gltf) => { const model = gltf.scene; model.scale.setScalar(3.5); model.position.y = 0; @@ -73,7 +73,7 @@
- +