added obstacle factory, worldscene, game controller, game manager and structured images&glbs into folders
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 717 KiB After Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 819 KiB After Width: | Height: | Size: 819 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
266
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<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);
|
||||
@@ -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(() => {
|
||||
|
||||
64
src/GameController.js
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/GameManager.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<div class="left-section">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<img src="/overload_trans.png" alt="Overload Logo" class="game-logo" />
|
||||
<img src="images/overload_trans.png" alt="Overload Logo" class="game-logo" />
|
||||
|
||||
<button class="start-btn" on:click={onStart}>
|
||||
START RUN
|
||||
@@ -93,7 +93,7 @@
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
background-image: url('/bggame.png');
|
||||
background-image: url('images/bggame.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
|
||||
18
src/ObstacleFactory.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// ObstacleFactory.js
|
||||
// @ts-nocheck
|
||||
import { createObstacle } from './obstacles.js';
|
||||
|
||||
export class ObstacleFactory {
|
||||
static async spawnRandom(glbCache, loader, laneWidth) {
|
||||
const isRare = Math.random() < 0.2;
|
||||
const modelFile = isRare ? "bird_in_a_claw_machine.glb" : "Simple computer.glb";
|
||||
const fullPath = `3dmodels/${modelFile}`;
|
||||
// Check if we have it, if not, load it (Standard Factory behavior)
|
||||
if (!glbCache.has(fullPath)) {
|
||||
glbCache.set(fullPath, await loader.loadAsync(fullPath));
|
||||
}
|
||||
|
||||
const source = glbCache.get(fullPath);
|
||||
return createObstacle(isRare, source, laneWidth);
|
||||
}
|
||||
}
|
||||
104
src/WorldScene.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// @ts-nocheck
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
|
||||
const CHUNK_SIZE = 140;
|
||||
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
||||
export function createWorldChunk(zOffset, uTime) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function createClouds(group) {
|
||||
const cloudMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
});
|
||||
|
||||
const thickness = 2;
|
||||
// Increase count to 40 for a denser sky
|
||||
for (let i = 0; i < 40; 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;
|
||||
|
||||
// FIX: Spread clouds from +50 (behind camera) to -400 (deep horizon)
|
||||
// This ensures that as soon as the game starts, there are clouds
|
||||
// already "waiting" far in the distance.
|
||||
const z = (Math.random() * -450) + 50;
|
||||
|
||||
cloud.position.set((Math.random() - 0.5) * 280, y, z);
|
||||
group.add(cloud);
|
||||
}
|
||||
}
|
||||
|
||||
export function moveWorld(chunks, moveStep, chunkSize, count) {
|
||||
chunks.forEach(chunk => {
|
||||
chunk.position.z += moveStep;
|
||||
// Logic for looping the world
|
||||
if (chunk.position.z > chunkSize) {
|
||||
chunk.position.z -= chunkSize * count;
|
||||
}
|
||||
});
|
||||
}
|
||||
export function animateClouds(cloudGroup, moveStep) {
|
||||
if (!cloudGroup) return;
|
||||
|
||||
cloudGroup.children.forEach(cloud => {
|
||||
cloud.position.z += moveStep * 0.4;
|
||||
|
||||
// Reset cloud position further back to -400
|
||||
// This way they stay hidden behind the fog until they naturally drift forward
|
||||
if (cloud.position.z > 100) {
|
||||
cloud.position.z = -400;
|
||||
cloud.position.x = (Math.random() - 0.5) * 280;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -648,4 +648,101 @@ onMount(() => {
|
||||
button:hover { transform: translateY(-2px); filter: brightness(1.1); }
|
||||
|
||||
.flash { position: absolute; inset: 0; background: white; z-index: 20; pointer-events: none; }
|
||||
</style>
|
||||
</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;
|
||||
|
||||
// }
|
||||
// }
|
||||
@@ -18,7 +18,7 @@ export const createSketch = (state, texturesRef) => {
|
||||
p.setup = async () => {
|
||||
p.createCanvas(p.windowWidth, p.windowHeight);
|
||||
const loadImg = (path) => new Promise(resolve => {
|
||||
p.loadImage(path, img => resolve(img), () => resolve(null));
|
||||
p.loadImage(`images/${path}`, img => resolve(img), () => resolve(null));
|
||||
});
|
||||
|
||||
// Load into the reference object passed from App.svelte
|
||||
@@ -115,6 +115,7 @@ export const createSketch = (state, texturesRef) => {
|
||||
const renderPopups = (p, popups) => {
|
||||
for (let i = popups.length - 1; i >= 0; i--) {
|
||||
let pop = popups[i];
|
||||
if (!pop || !pop.val) continue; // SKIP IF DATA IS MISSING
|
||||
p.push();
|
||||
p.fill(255, 230, 0, pop.opacity);
|
||||
p.textSize(32 + (1 - pop.life) * 20);
|
||||
@@ -134,15 +135,15 @@ export const createSketch = (state, texturesRef) => {
|
||||
};
|
||||
|
||||
p.mousePressed = () => {
|
||||
if (state.gamePhase !== "PLAYING") return;
|
||||
for (let i = state.targets.length - 1; i >= 0; i--) {
|
||||
let t = state.targets[i];
|
||||
if (p.dist(p.mouseX, p.mouseY, t.x, t.y) < 60) {
|
||||
state.onHit(t);
|
||||
state.targets.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The View (p5) detects the raw click...
|
||||
const clickedTarget = state.targets.find(t => p.dist(p.mouseX, p.mouseY, t.x, t.y) < 60);
|
||||
|
||||
if (clickedTarget) {
|
||||
// ...but the Controller (state.onHit) decides what the rules are.
|
||||
state.onHit(clickedTarget);
|
||||
// Remove from View list
|
||||
state.targets.splice(state.targets.indexOf(clickedTarget), 1);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
BIN
strawberry.png
|
Before Width: | Height: | Size: 819 KiB |