cleaned up a bit
This commit is contained in:
166
src/App.svelte
166
src/App.svelte
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
296
src/app.css
296
src/app.css
@@ -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);
|
||||
}
|
||||
}
|
||||
748
src/cool.svelte
748
src/cool.svelte
@@ -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;
|
||||
|
||||
// }
|
||||
// }
|
||||
10
src/main.js
10
src/main.js
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -4,4 +4,5 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: './',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user