cleaned up a bit

This commit is contained in:
Mak
2026-05-10 17:39:40 +09:00
parent 845c4de6ee
commit 2f04d4f905
10 changed files with 180 additions and 1181 deletions

View File

@@ -11,31 +11,16 @@ 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 { handleInput, processInteraction, updatePhysics, updateGameFlow } from './GameController.js';
import { createWorldChunk, createClouds, moveWorld,animateClouds } 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;
await tick();
init();
// 3. Setup the Bridge Object
const gameState = {
get isPlaying() { return isPlaying; },
get score() { return score; },
@@ -50,13 +35,10 @@ async function handleStart() {
set gamePhase(v) { gamePhase = v; },
get lastStarScore() { return lastStarScore; },
set lastStarScore(v) { lastStarScore = v; },
// FIX: Use getters for the arrays so they don't get stale!
get targets() { return targets; },
get scorePopups() { return scorePopups; },
onHit: (t) => {
// MVC: Delegate the 'Decision' to the Controller
processInteraction(t, gameState, scoreMultiplier, BOOST_DURATION);
},
onActivateBoost: (mult, dur) => {
@@ -64,8 +46,6 @@ async function handleStart() {
multiplierTimer = dur;
}
};
// 4. Initialize p5 now that p5Container exists
p5Instance = new p5(createSketch(gameState, textures), p5Container);
lastTime = performance.now();
@@ -81,72 +61,87 @@ async function handleStart() {
loop();
}
//App flow & timing
let showLanding = true; // Toggles between LandingPage and the Game Wrapper
let lastTime = performance.now(); // High-resolution timestamp for delta time calculation
// Game Constants & State Variables for physical rules of the world
const CONFIG = {
lane: 2.5,
jump: 0.35,
grav: 0.015,
playerScale: 1.7,
grav: 0.015, // Gravity constant applied per frame
playerScale: 1.7, // Visual scale multiplier for the 3D character
START_SPEED: 45, // Initial slow speed
MAX_SPEED: 95, // The "chaos" threshold
MAX_SPEED: 95, // The maximum speed threshold
ACCELERATION: 1, // Speed added per second
CYCLE_INTERVAL: 7000
};
const BOOST_DURATION = 20; // Length of the 'Star' multiplier in seconds
let currentSpeed = CONFIG.START_SPEED;
// RENDERING REFERENCES
//View references used to bridge Svelte with Three.js and P5.js
let container, canvas, p5Container; // HTML Element bindings
let scene, camera, renderer; // Three.js Core components
let p5Instance; // p5.js Overlay instance
let animationFrame; // ID for the requestAnimationFrame loop
let uTime = { value: 0 }; // Global time tracker for shader animations and world logic
// Game State Variables
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
let attentiveness = 100;
let lives = 5;
let lives = 5; let isDying = false, hitFlash = false;
let currentSpeed = CONFIG.START_SPEED; // The active world velocity
//Player physics, tracks the 3D position and physics state of the character.
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 playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
let spawnDistanceTracker = 0;
// WORLD OBJECTS & GENERATION
let worldObjects = [];
let spawnDistanceTracker = 0; // Tracks distance traveled since last spawn
const SPAWN_INTERVAL = 40; // Physical distance between obstacles
let cloudGroup;
let cloudGroup; // Container for background parallax clouds
let CHUNKS = []; // Ground segments for the infinite loop
const CHUNK_COUNT = 3; // Number of segments in the pool
const CHUNK_SIZE = 140;
// ENVIRONMENT & LIGHTING
let sun, moon, ambientLight, sunLight, headLight;
// 2D Game Logic
let gamePhase = "START";
let instructionTimer = 3;
let targetType = "STRAWBERRY";
let targets = [];
let targets = []; // Active 2D floating target objects
let scorePopups = []; // To track the floating +100 labels
let scoreMultiplier = 1; //to double the score when star is active
let multiplierTimer = 0; // Countdown for active Star power-up
let lastStarScore = 0; // Checkpoint to trigger Star spawn every 10k points
let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
let CHUNKS = [];
const CHUNK_COUNT = 3;
const CHUNK_SIZE = 140;
let uTime = { value: 0 };
// ASSET CACHE & LOADING
// Memory management for models and textures to prevent redundant loads.
const loader = new GLTFLoader();
const glbCache = new Map();
let textures = {};
let scoreMultiplier = 1;
let multiplierTimer = 0; // Remaining seconds of boost
let lastStarScore = 0; // Add this line to prevent the crash
const BOOST_DURATION = 20; // 20 seconds
async function getCachedGLTF(file) {
if (!glbCache.has(file)) glbCache.set(file, await loader.loadAsync(file));
return glbCache.get(file);
}
// Asynchronously swaps the 3D player model and manages its animations
async function swapCharacter(file, isDeathAnimation = false) {
// Incrementing a token ensures that if multiple swaps are called rapidly,
// only the most recent request (the latest token) actually updates the scene.
const myToken = ++swapToken;
const source = await getCachedGLTF(`3dmodels/${file}`);
if (myToken !== swapToken) return;
if (myToken !== swapToken) return; // Exit if a newer swap request has already started (stale request prevention)
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; }
if (isDeathAnimation) { action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; } // Stop on the last frame instead of resetting
action.play();
}
if (currentModel) playerAnchor.remove(currentModel);
@@ -154,6 +149,7 @@ async function swapCharacter(file, isDeathAnimation = false) {
playerAnchor.add(currentModel);
}
// Initializes the Three.js engine, environment, and procedural world elements
function init() {
if (!canvas || !container) return; // Safety check
scene = new THREE.Scene();
@@ -189,29 +185,15 @@ function init() {
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);
// 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
// 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 }));
@@ -220,30 +202,40 @@ function init() {
sun.position.set(60, 100, -250);
moon.position.set(60, -100, -250);
scene.add(sun, moon);
// Responsive Design (Viewport Observer)
// Automatically handles canvas resizing without reloading the engine
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() {
async function spawn() { //spawning obstacles with bigger one having 0.2 chance, smaller ones 0.8 chance
const isRare = Math.random() < 0.2;
const modelFile = isRare ? "bird_in_a_claw_machine.glb" : "Simple computer.glb";
const source = await getCachedGLTF(modelFile);
// Call the module function
const obstacleData = createObstacle(isRare, source, CONFIG.lane);
scene.add(obstacleData.mesh);
worldObjects = [...worldObjects, obstacleData];
}
// Main Animation Loop: Executed every frame to update game state and rendering
function update() {
const now = performance.now();
const now = performance.now(); // Time Synchronization
const delta = (now - lastTime) / 1000;
lastTime = now;
uTime.value += delta;
uTime.value += delta; // Update global shader uniforms and active 3D animations
if (currentMixer) currentMixer.update(delta);
if (!isPlaying) return;
// --- ADD THIS LINE HERE ---
// This updates the instruction timer and switches the phase
updateGameFlow({
get gamePhase() { return gamePhase; },
@@ -252,21 +244,18 @@ function update() {
set instructionTimer(v) { instructionTimer = v; }
}, 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;
if (gamePhase === "INSTRUCTIONS") return; // Halt world movement and physics during the instruction countdown
const moveStep = currentSpeed * delta;
const moveStep = currentSpeed * delta; // Determine distance traveled this frame based on current velocity
// 1. Environment Controller
// Environment: Updates Day/Night cycle, lighting, and celestial movement
updateEnvironment(uTime.value, scene, { ambientLight, sunLight, headLight }, { sun, moon });
// 2. World Controller
// Loops ground segments and animates background parallax clouds
moveWorld(CHUNKS, moveStep, CHUNK_SIZE, CHUNK_COUNT);
animateClouds(cloudGroup, moveStep);
// 3. Player Physics Controller
updatePhysics(
updatePhysics( // Processes gravity, jumping, and lane-shifting kinematics
{
get lane() { return lane; },
get currX() { return currX; }, set currX(v) { currX = v; },
@@ -282,22 +271,24 @@ function update() {
playerAnchor.position.x = currX;
playerAnchor.position.y = playerY;
// 4. Obstacle Controller
worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; });
// Obstacle Controller
worldObjects.forEach(obj => { obj.mesh.position.z += moveStep; }); // Move obstacles toward the player and check for bounding-box intersections
worldObjects = handleCollisions(worldObjects, lane, playerY, triggerGameOver);
worldObjects = worldObjects.filter(obj => {
worldObjects = worldObjects.filter(obj => { // Garbage Collection: Remove obstacles that have passed behind the camera to free memory
const active = obj.mesh.position.z < 25;
if (!active) scene.remove(obj.mesh);
return active;
});
// 5. Scoring & Spawning Logic
// Manage active multiplier power-ups (Star/Boost)
if (multiplierTimer > 0) {
multiplierTimer -= delta;
if (multiplierTimer <= 0) { multiplierTimer = 0; scoreMultiplier = 1; }
}
// Calculate score based on speed and active multiplier
score += Math.floor((currentSpeed / 40) * scoreMultiplier);
// Gradually increase velocity to scale game difficulty
if (currentSpeed < CONFIG.MAX_SPEED) currentSpeed += CONFIG.ACCELERATION * delta;
// 1. Ask the Static Manager if we should spawn
@@ -308,11 +299,6 @@ function update() {
worldObjects = [...worldObjects, obstacle];
});
}
// spawnDistanceTracker += moveStep;
// if (spawnDistanceTracker >= SPAWN_INTERVAL) {
// spawn();
// spawnDistanceTracker = 0;
// }
}
@@ -327,8 +313,8 @@ function triggerGameOver() {
async function startGame() {
if (!scene) return;
currentSpeed = CONFIG.START_SPEED;
// Choose a random target type for this mission
const types = ["STRAWBERRY", "WATERMELON", "BLUEBERRY"];
const types = ["STRAWBERRY", "WATERMELON", "BLUEBERRY"]; // Choose a random target type for this mission
targetType = types[Math.floor(Math.random() * types.length)];
worldObjects.forEach(obj => scene.remove(obj.mesh));
targets.length = 0;
@@ -343,7 +329,7 @@ async function startGame() {
}
const handleKeyDown = (e) => {
// MVC: Delegate keyboard input to the Controller
// Delegate keyboard input to the Controller
handleInput(e,
{
isPlaying,
@@ -364,7 +350,7 @@ const handleKeyDown = (e) => {
onMount(() => {
loadLeaderboard();
window.addEventListener("keydown", handleKeyDown);
return () => {
return () => { // This return block executes when the component is destroyed (e.g., navigating away)
cancelAnimationFrame(animationFrame);
window.removeEventListener("keydown", handleKeyDown);
if (p5Instance) p5Instance.remove();
@@ -479,7 +465,7 @@ onMount(() => {
text-align: center;
font-size: 0.8rem;
letter-spacing: 2px;
color: hsl(308, 100%, 87%); /* Change color to cyan to match your theme */
color: hsl(308, 100%, 87%);
text-transform: uppercase;
}

View File

@@ -1,4 +1,3 @@
// GameController.js
// @ts-nocheck
export function handleInput(event, state, config, swapFn) {
if (!state.isPlaying || state.isDying) return;
@@ -53,7 +52,6 @@ export function updatePhysics(state, config, delta, swapFn) {
}
}
// GameController.js
export function updateGameFlow(state, delta) {
if (state.gamePhase === "INSTRUCTIONS") {
state.instructionTimer -= delta;

View File

@@ -8,29 +8,29 @@
let container;
let charCanvas;
let animFrame;
let animFrame; // Animation and rendering state
let charMixer;
onMount(() => {
const clock = new THREE.Clock();
// Alpha: true allows the CSS background-image to show through the canvas
const charRenderer = new THREE.WebGLRenderer({
canvas: charCanvas,
antialias: true,
alpha: true
});
const charScene = new THREE.Scene();
const charScene = new THREE.Scene(); //Scene and Camera Configuration
const charCam = new THREE.PerspectiveCamera(40, charCanvas.clientWidth / charCanvas.clientHeight, 0.1, 200);
charCam.position.set(0, 3, 13);
charCam.lookAt(0, 2.5, 0);
// Lighting Setup
charScene.add(new THREE.AmbientLight(0xffffff, 7));
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 5, 5);
charScene.add(dirLight);
const loader = new GLTFLoader();
const loader = new GLTFLoader(); //Asset Loading (3D Model & Animation)
loader.load("3dmodels/hiphop.glb", (gltf) => {
const model = gltf.scene;
model.scale.setScalar(3.5);
@@ -38,13 +38,13 @@
model.position.x = -1;
charScene.add(model);
if (gltf.animations.length) {
if (gltf.animations.length) { // Initialize AnimationMixer if clips are present in the GLB
charMixer = new THREE.AnimationMixer(model);
charMixer.clipAction(gltf.animations[0]).play();
}
});
const handleResize = () => {
const handleResize = () => { //Responsive Handling
charRenderer.setSize(charCanvas.clientWidth, charCanvas.clientHeight);
charCam.aspect = charCanvas.clientWidth / charCanvas.clientHeight;
charCam.updateProjectionMatrix();
@@ -53,7 +53,7 @@
window.addEventListener('resize', handleResize);
handleResize();
function loop() {
function loop() { //Component Animation Loop
animFrame = requestAnimationFrame(loop);
const delta = clock.getDelta();
charRenderer.render(charScene, charCam);
@@ -61,7 +61,7 @@
}
loop();
return () => {
return () => { //Cleanup & Resource Disposal
cancelAnimationFrame(animFrame);
window.removeEventListener('resize', handleResize);
};
@@ -75,6 +75,12 @@
<img src="images/overload_trans.png" alt="Overload Logo" class="game-logo" />
<p class="game-description">
Collect targets to maintain focus while navigating through obstacles. Hearts are only for missed or wrong targets,
it won't save you from crashing into obstacles. The higher the score, you might get reward boosters for setting records!
Rapid task-switching trains mental flexibility, attention, and working memory.
</p>
<button class="start-btn" on:click={onStart}>
START RUN
</button>
@@ -87,6 +93,58 @@
</div>
<style>
.game-description {
max-width: 600px; /* Widened slightly for better monospaced flow */
color: #fff82e;
/* Retro/Computer game font stack */
font-family: 'Courier New', Courier, monospace;
font-weight: 900;
font-size: 1.1rem;
line-height: 1.4;
text-align: center;
margin-bottom: 50px;
padding: 0; /* Removed padding since background is gone */
background: none; /* Background removed */
backdrop-filter: none;
border: none;
/* Strong black outline to keep it readable against the background */
text-shadow:
-2px -2px 0 #0c430c,
2px -2px 0 #0b5b26,
-2px 2px 0 #0b5b26,
2px 2px 0 #0b5b26,
0px 4px 10px rgba(0,0,0,1);
text-transform: uppercase; /* Makes it feel more like a retro game UI */
animation: fadeIn 1s ease-out;
}
.left-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
/* Adjust padding to balance the logo, text, and button */
padding: 0 0 10vh 5vw;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Ensure center alignment for the description in the left section */
.left-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
padding: 0 0 15vh 10vw; /* Reduced padding slightly to fit the text */
}
#landing-ui {
position: fixed;
inset: 0;
@@ -108,15 +166,6 @@
z-index: 10;
}
.left-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center; /* Changed to center to align logo and button */
padding: 0 0 25vh 10vw;
}
/* LOGO STYLING & ANIMATION */
.game-logo {
width: 700px; /* Adjust size as needed */

View File

@@ -1,4 +1,3 @@
// ObstacleFactory.js
// @ts-nocheck
import { createObstacle } from './obstacles.js';
@@ -7,11 +6,10 @@ export class ObstacleFactory {
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)
// Check if we have it, if not, load it
if (!glbCache.has(fullPath)) {
glbCache.set(fullPath, await loader.loadAsync(fullPath));
}
const source = glbCache.get(fullPath);
return createObstacle(isRare, source, laneWidth);
}

View File

@@ -61,17 +61,11 @@ export function createClouds(group) {
});
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);

View File

@@ -1,296 +0,0 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -1,748 +0,0 @@
<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;
// }
// }

View File

@@ -1,9 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
// @ts-nocheck
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.getElementById('app'),
})
});
export default app
export default app;

View File

@@ -3,15 +3,33 @@ export const createSketch = (state, texturesRef) => {
return (p) => {
// --- PRIVATE UTILITIES ---
const drawHeart = (x, y, size, active) => {
const s = size / 5;
// Helper to draw the 8-bit grid shape
const drawShape = (posX, posY, pixelSize) => {
p.rect(posX + pixelSize, posY, pixelSize, pixelSize);
p.rect(posX + 3 * pixelSize, posY, pixelSize, pixelSize);
p.rect(posX, posY + pixelSize, 5 * pixelSize, pixelSize);
p.rect(posX, posY + 2 * pixelSize, 5 * pixelSize, pixelSize);
p.rect(posX + pixelSize, posY + 3 * pixelSize, 3 * pixelSize, pixelSize);
p.rect(posX + 2 * pixelSize, posY + 4 * pixelSize, pixelSize, pixelSize);
};
p.push();
p.noStroke();
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);
// 1. Draw the Outline (Black, slightly offset/larger)
p.fill(0);
// We draw it 2 pixels wider in all directions for that thick Minecraft border
drawShape(x - 2, y - 2, (size + 4) / 5);
// 2. Draw the Inner Heart
p.fill(active ? [255, 50, 50] : [60, 60, 60, 180]);
drawShape(x, y, s);
// 3. Add a "Highlight" pixel (Minecraft hearts have a little white glint)
if (active) {
p.fill(255, 150);
p.rect(x + s, y + s, s, s);
}
p.pop();
};
@@ -21,7 +39,6 @@ export const createSketch = (state, texturesRef) => {
p.loadImage(`images/${path}`, img => resolve(img), () => resolve(null));
});
// Load into the reference object passed from App.svelte
texturesRef.STRAWBERRY = await loadImg('strawberry.png');
texturesRef.WATERMELON = await loadImg('watermelon.png');
texturesRef.BLUEBERRY = await loadImg('blubb.png');
@@ -70,7 +87,7 @@ export const createSketch = (state, texturesRef) => {
}
// 4. Render Hearts
for (let i = 0; i < 5; i++) drawHeart(20 + (i * 35), 20, 25, i < state.lives);
Array.from({ length: 5 }).map((_, i) => drawHeart(25 + (i * 60), 25, 45, i < state.lives));
// 5. Random Target Spawning
if (p.random(1) < 0.004) {
@@ -83,12 +100,12 @@ export const createSketch = (state, texturesRef) => {
// 6. Current Target Box
p.push();
p.fill(0, 150); p.rect(10, 60, 120, 140, 15);
p.fill(0, 150); p.rect(10, 80, 140, 140, 15);
p.fill(255); p.textSize(14); p.textAlign(p.CENTER);
p.text("CURRENT TARGET", 70, 85);
p.text("CURRENT TARGET",80, 105);
const boxImg = texturesRef[state.targetType];
if (boxImg) p.image(boxImg, 40, 95, 60, 60);
p.fill(0, 255, 200); p.text(state.targetType, 70, 185);
if (boxImg) p.image(boxImg, 40, 120, 70, 70);
p.fill(0, 255, 200); p.text(state.targetType, 80, 210);
p.pop();
// 7. Render/Move Targets

View File

@@ -4,4 +4,5 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
base: './',
})