added character, jumping, running, cute falling animationgit add . love it so much
This commit is contained in:
BIN
Falling Back Death.glb
Normal file
BIN
Falling Back Death.glb
Normal file
Binary file not shown.
BIN
Running.glb
Normal file
BIN
Running.glb
Normal file
Binary file not shown.
484
src/App.svelte
484
src/App.svelte
@@ -1,197 +1,427 @@
|
|||||||
<script>
|
<script>
|
||||||
//@ts-nocheck - to stop annoying red lines of typescript check
|
// @ts-nocheck
|
||||||
|
import { onMount } from "svelte";
|
||||||
import { onMount, onDestroy } from "svelte";
|
|
||||||
//onMount - do this the second the component appears on screen
|
|
||||||
//onDestroy - clean up after losing
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||||
|
|
||||||
//config for physics
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
lane: 2.5, //distance between the lanes
|
lane: 2.5,
|
||||||
jump: 0.35, //jump hight
|
jump: 0.35,
|
||||||
grav: 0.015, //how fast to land after jump
|
grav: 0.015,
|
||||||
speed: 0.3 //how fast world is moving towards us
|
speed: 0.3,
|
||||||
//typically in infinite runner games the environment is moving, not the player as it may seem
|
gameOverDelay: 1400,
|
||||||
|
playerScale: 1.25
|
||||||
};
|
};
|
||||||
|
|
||||||
//variables to track the current state of the game
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
let gameOver = false;
|
let gameOver = false;
|
||||||
let startScreen = true;
|
let startScreen = true;
|
||||||
|
let showGameOverModal = false;
|
||||||
|
|
||||||
//player position variables
|
let lane = 0;
|
||||||
let lane = 0; //target lane (-2 to 2 for 5 lanes)
|
let currX = 0;
|
||||||
let currX = 0; //actual X position used for smooth transition
|
|
||||||
let isJumping = false;
|
let isJumping = false;
|
||||||
let jumpV = 0; //Jump velocity
|
let jumpV = 0;
|
||||||
let playerY = 0; //height off the ground
|
let playerY = 0;
|
||||||
|
|
||||||
//making the scene
|
let container, canvas, scene, camera, renderer, floor;
|
||||||
let container, canvas, scene, camera, renderer, player, floor;
|
|
||||||
let worldObjects = [];
|
let worldObjects = [];
|
||||||
let resizeObserver;
|
let resizeObserver;
|
||||||
|
let animationFrame;
|
||||||
|
|
||||||
function init(){
|
let isDying = false;
|
||||||
// Creating the universe
|
let hitFlash = false;
|
||||||
|
let shake = 0;
|
||||||
|
|
||||||
|
let playerAnchor;
|
||||||
|
let currentModel = null;
|
||||||
|
let currentMixer = null;
|
||||||
|
let currentAction = null;
|
||||||
|
let currentState = "run";
|
||||||
|
let swapToken = 0;
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
const glbCache = new Map();
|
||||||
|
|
||||||
|
function normalizeModel(root) {
|
||||||
|
root.scale.setScalar(CONFIG.playerScale);
|
||||||
|
root.rotation.y = Math.PI;
|
||||||
|
|
||||||
|
root.traverse((obj) => {
|
||||||
|
if (obj.isMesh || obj.isSkinnedMesh) {
|
||||||
|
obj.castShadow = true;
|
||||||
|
obj.receiveShadow = true;
|
||||||
|
obj.frustumCulled = false;
|
||||||
|
}
|
||||||
|
if (obj.isBone) {
|
||||||
|
obj.frustumCulled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const box = new THREE.Box3().setFromObject(root);
|
||||||
|
const center = new THREE.Vector3();
|
||||||
|
box.getCenter(center);
|
||||||
|
|
||||||
|
root.position.x -= center.x;
|
||||||
|
root.position.z -= center.z;
|
||||||
|
root.position.y -= box.min.y;
|
||||||
|
|
||||||
|
root.updateMatrixWorld(true);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCachedGLTF(file) {
|
||||||
|
if (!glbCache.has(file)) {
|
||||||
|
const gltf = await loader.loadAsync(file);
|
||||||
|
glbCache.set(file, gltf);
|
||||||
|
}
|
||||||
|
return glbCache.get(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeCurrentCharacter() {
|
||||||
|
if (currentMixer) {
|
||||||
|
currentMixer.stopAllAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentModel && playerAnchor) {
|
||||||
|
playerAnchor.remove(currentModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentModel = null;
|
||||||
|
currentMixer = null;
|
||||||
|
currentAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function swapCharacter(file, stateName, { loop = true } = {}) {
|
||||||
|
const myToken = ++swapToken;
|
||||||
|
|
||||||
|
const source = await getCachedGLTF(file);
|
||||||
|
if (myToken !== swapToken) return;
|
||||||
|
|
||||||
|
const model = normalizeModel(cloneSkeleton(source.scene));
|
||||||
|
const mixer = new THREE.AnimationMixer(model);
|
||||||
|
|
||||||
|
let action = null;
|
||||||
|
if (source.animations && source.animations.length > 0) {
|
||||||
|
action = mixer.clipAction(source.animations[0]);
|
||||||
|
if (loop) {
|
||||||
|
action.setLoop(THREE.LoopRepeat, Infinity);
|
||||||
|
} else {
|
||||||
|
action.setLoop(THREE.LoopOnce, 1);
|
||||||
|
action.clampWhenFinished = true;
|
||||||
|
}
|
||||||
|
action.reset();
|
||||||
|
action.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeCurrentCharacter();
|
||||||
|
|
||||||
|
currentModel = model;
|
||||||
|
currentMixer = mixer;
|
||||||
|
currentAction = action;
|
||||||
|
currentState = stateName;
|
||||||
|
|
||||||
|
playerAnchor.add(currentModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRun() {
|
||||||
|
return swapCharacter("Running.glb", "run", { loop: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showJump() {
|
||||||
|
return swapCharacter("Jumping.glb", "jump", { loop: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDeath() {
|
||||||
|
return swapCharacter("Falling Back Death.glb", "death", { loop: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerGameOver() {
|
||||||
|
if (isDying || gameOver) return;
|
||||||
|
|
||||||
|
isPlaying = false;
|
||||||
|
gameOver = true;
|
||||||
|
isDying = true;
|
||||||
|
hitFlash = true;
|
||||||
|
shake = 0.35;
|
||||||
|
|
||||||
|
showDeath();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hitFlash = false;
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showGameOverModal = true;
|
||||||
|
}, CONFIG.gameOverDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
|
|
||||||
//Setup the field of view, aspect ration, near plane, far plane
|
|
||||||
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
|
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
|
||||||
camera.position.set(0, 5, 10); //put the camera from the back of the player
|
camera.position.set(0, 5, 10);
|
||||||
camera.lookAt(0, 0, -5);
|
camera.lookAt(0, 0, -5);
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({canvas, antialias: true}); //antialias makes edges smooth
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
||||||
//adding lights to the scene
|
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
||||||
scene.add(new THREE.AmbientLight(0xffffff, 0.8)); //soft general light
|
|
||||||
const sun = new THREE.DirectionalLight(0xffffff, 1);
|
const sun = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
sun.position.set(5, 10, 7); //light from the side
|
sun.position.set(5, 10, 7);
|
||||||
scene.add(sun);
|
scene.add(sun);
|
||||||
|
|
||||||
//build the ground
|
|
||||||
const floorGeo = new THREE.PlaneGeometry(100, 2000);
|
const floorGeo = new THREE.PlaneGeometry(100, 2000);
|
||||||
floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x222222 }));
|
floor = new THREE.Mesh(
|
||||||
floor.rotation.x = -Math.PI /2; //so it is horizontal plane
|
floorGeo,
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x222222 })
|
||||||
|
);
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
scene.add(floor);
|
scene.add(floor);
|
||||||
|
|
||||||
//to prevent game ratio from looking stretched when you resize the window
|
playerAnchor = new THREE.Group();
|
||||||
resizeObserver = new ResizeObserver(()=>{
|
scene.add(playerAnchor);
|
||||||
const {width, height} = container.getBoundingClientRect();
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
camera.aspect = width / height;
|
camera.aspect = width / height;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function spawn() {
|
||||||
if(!isPlaying || gameOver) return; //if paused or lost, do nothing
|
|
||||||
score++; //points for surviving longer
|
|
||||||
|
|
||||||
//for smooth movement
|
|
||||||
//move 15% of the way every frame to switch to new lane
|
|
||||||
currX += (lane * CONFIG.lane - currX) * 0.15;
|
|
||||||
if (player) player.position.x = currX;
|
|
||||||
|
|
||||||
//Jumping math
|
|
||||||
if (isJumping) {
|
|
||||||
jumpV -= CONFIG.grav; //gravity pulls velocity down
|
|
||||||
playerY += jumpV; //Velocity moves the player
|
|
||||||
if (playerY <= 0){ //hit the ground
|
|
||||||
playerY = 0;
|
|
||||||
isJumping = false;
|
|
||||||
}
|
|
||||||
if(player) player.position.y = playerY;
|
|
||||||
}
|
|
||||||
|
|
||||||
//obstacle movement & collision
|
|
||||||
worldObjects = worldObjects
|
|
||||||
.map(obj => {
|
|
||||||
//move
|
|
||||||
obj.mesh.position.z += CONFIG.speed;
|
|
||||||
|
|
||||||
//detect collision
|
|
||||||
if ( //to see if we are close to the object
|
|
||||||
Math.abs(obj.mesh.position.z) < 0.8 &&
|
|
||||||
obj.lane === lane &&
|
|
||||||
playerY < 1
|
|
||||||
) {
|
|
||||||
isPlaying = false;
|
|
||||||
gameOver = true;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
})
|
|
||||||
.filter(obj => {
|
|
||||||
//delete obstacles behind us
|
|
||||||
const keep = obj.mesh.position.z <= 15;
|
|
||||||
if(!keep) scene.remove(obj.mesh);
|
|
||||||
return keep;
|
|
||||||
});
|
|
||||||
// spawn new obstacles every 30 frames
|
|
||||||
if (score % 30 === 0) spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawn(){
|
|
||||||
// choose a random lane btw -2 and 2
|
|
||||||
const l = Math.floor(Math.random() * 5) - 2;
|
const l = Math.floor(Math.random() * 5) - 2;
|
||||||
const mesh = new THREE.Mesh(
|
const mesh = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(1.5, 1.5, 1.5),
|
new THREE.BoxGeometry(1.5, 1.5, 1.5),
|
||||||
new THREE.MeshStandardMaterial({ color: 0x00fff2 })
|
new THREE.MeshStandardMaterial({ color: 0x00fff2 })
|
||||||
);
|
);
|
||||||
mesh.position.set(l * CONFIG.lane, 0.75, -100); //start far
|
mesh.position.set(l * CONFIG.lane, 0.75, -100);
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
worldObjects.push({ mesh, lane: l});
|
worldObjects.push({ mesh, lane: l });
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGame(){
|
function update() {
|
||||||
//clear old obstacles
|
const delta = clock.getDelta();
|
||||||
worldObjects.map(obj => scene.remove(obj.mesh));
|
|
||||||
|
if (currentMixer) {
|
||||||
|
currentMixer.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shake > 0.001) {
|
||||||
|
shake *= 0.9;
|
||||||
|
camera.position.x = (Math.random() - 0.5) * shake;
|
||||||
|
} else {
|
||||||
|
shake = 0;
|
||||||
|
camera.position.x += (0 - camera.position.x) * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerAnchor) {
|
||||||
|
playerAnchor.position.set(currX, playerY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlaying) return;
|
||||||
|
|
||||||
|
score++;
|
||||||
|
currX += (lane * CONFIG.lane - currX) * 0.15;
|
||||||
|
|
||||||
|
if (isJumping) {
|
||||||
|
jumpV -= CONFIG.grav;
|
||||||
|
playerY += jumpV;
|
||||||
|
|
||||||
|
if (playerY <= 0) {
|
||||||
|
playerY = 0;
|
||||||
|
isJumping = false;
|
||||||
|
if (!isDying) showRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worldObjects = worldObjects
|
||||||
|
.map((obj) => {
|
||||||
|
obj.mesh.position.z += CONFIG.speed;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(obj.mesh.position.z) < 0.8 &&
|
||||||
|
obj.lane === lane &&
|
||||||
|
playerY < 1
|
||||||
|
) {
|
||||||
|
triggerGameOver();
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
})
|
||||||
|
.filter((obj) => {
|
||||||
|
const keep = obj.mesh.position.z <= 15;
|
||||||
|
if (!keep) scene.remove(obj.mesh);
|
||||||
|
return keep;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (score % 30 === 0) spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startGame() {
|
||||||
|
worldObjects.forEach((obj) => scene.remove(obj.mesh));
|
||||||
worldObjects = [];
|
worldObjects = [];
|
||||||
|
|
||||||
//creating the player CUBE for now
|
score = 0;
|
||||||
if (player) scene.remove(player);
|
isPlaying = true;
|
||||||
player = new THREE.Group();
|
gameOver = false;
|
||||||
player.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0xffffff })));
|
startScreen = false;
|
||||||
scene.add(player);
|
showGameOverModal = false;
|
||||||
|
|
||||||
//Resey all variables
|
lane = 0;
|
||||||
score = 0; lane = 0; playerY = 0; currX = 0; isJumping = false;
|
currX = 0;
|
||||||
startScreen = false; gameOver = false; isPlaying = true;
|
isJumping = false;
|
||||||
|
jumpV = 0;
|
||||||
|
playerY = 0;
|
||||||
|
|
||||||
|
isDying = false;
|
||||||
|
hitFlash = false;
|
||||||
|
shake = 0;
|
||||||
|
|
||||||
|
if (playerAnchor) {
|
||||||
|
playerAnchor.position.set(0, 0, 0);
|
||||||
|
playerAnchor.rotation.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
await showRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
function handleKeyDown(e) {
|
||||||
init(); //build the world
|
if (!isPlaying || isDying) return;
|
||||||
|
|
||||||
//the loop of rendering, updating, repeating
|
|
||||||
const loop = () => {
|
|
||||||
requestAnimationFrame(loop);
|
|
||||||
update();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
};
|
|
||||||
loop();
|
|
||||||
|
|
||||||
//listening to keys
|
|
||||||
window.addEventListener("keydown", (e) => {
|
|
||||||
if(!isPlaying) return;
|
|
||||||
if (e.key === "ArrowLeft" && lane > -2) lane--;
|
if (e.key === "ArrowLeft" && lane > -2) lane--;
|
||||||
if (e.key === "ArrowRight" && lane < 2) lane ++;
|
if (e.key === "ArrowRight" && lane < 2) lane++;
|
||||||
|
|
||||||
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
|
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
|
||||||
isJumping = true;
|
isJumping = true;
|
||||||
jumpV = CONFIG.jump;
|
jumpV = CONFIG.jump;
|
||||||
|
showJump();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
init();
|
||||||
|
|
||||||
|
const loop = () => {
|
||||||
|
animationFrame = requestAnimationFrame(loop);
|
||||||
|
update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
loop();
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
|
||||||
|
if (currentMixer) currentMixer.stopAllAction();
|
||||||
|
renderer?.dispose();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => resizeObserver?.disconnect()); //disconnect resizeobserver so it doesn;t leak memory adn cpu
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Make the game full screen */
|
:global(body, html) {
|
||||||
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #000; }
|
margin: 0;
|
||||||
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
padding: 0;
|
||||||
canvas { width: 100% !important; height: 100% !important; display: block; }
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
/* The UI Overlay */
|
#wrapper {
|
||||||
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: sans-serif; }
|
position: absolute;
|
||||||
.modal { pointer-events: auto; background: rgba(0,0,0,0.9); padding: 40px; border-radius: 20px; margin-top: 20vh;}
|
inset: 0;
|
||||||
button { padding: 15px 40px; background: #00fff2; border: none; cursor: pointer; font-weight: bold; }
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-top: 20vh;
|
||||||
|
border: 1px solid #00fff2;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 15px 40px;
|
||||||
|
background: #00fff2;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
color: black;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: white;
|
||||||
|
opacity: 0;
|
||||||
|
animation: flash-hit 180ms ease-out forwards;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-hit {
|
||||||
|
0% { opacity: 0.95; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="wrapper" bind:this={container}>
|
<div id="wrapper" bind:this={container}>
|
||||||
<canvas bind:this={canvas}></canvas>
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
|
||||||
<div class="ui">
|
{#if hitFlash}
|
||||||
<div style="font-size: 2rem; margin-top: 20px;">{score}</div>
|
<div class="flash"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if startScreen || gameOver}
|
<div class="ui">
|
||||||
|
<div class="score">{score}</div>
|
||||||
|
|
||||||
|
{#if startScreen || showGameOverModal}
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h1>{gameOver ? 'GAME OVER' : 'CUBE RUNNER'}</h1>
|
<h1>{showGameOverModal ? "GAME OVER" : "RUNNER"}</h1>
|
||||||
<p>{gameOver ? 'Final Score: ' + score : 'Arrows to Move • Space to Jump'}</p>
|
<p>{showGameOverModal ? "Score: " + score : "Arrows to Move • Space to Jump"}</p>
|
||||||
<button on:click={startGame}>{gameOver ? 'RETRY' : 'START'}</button>
|
<button on:click={startGame}>
|
||||||
|
{showGameOverModal ? "RETRY" : "START"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
285
src/cool.svelte
Normal file
285
src/cool.svelte
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<script>
|
||||||
|
//@ts-nocheck
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
lane: 2.5,
|
||||||
|
jump: 0.35,
|
||||||
|
grav: 0.015,
|
||||||
|
speed: 0.3
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
|
||||||
|
let hitFlash = false;
|
||||||
|
|
||||||
|
// Physics/Logic State
|
||||||
|
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
|
||||||
|
let isDying = false;
|
||||||
|
let shake = 0;
|
||||||
|
let fade = 1;
|
||||||
|
|
||||||
|
let container, canvas, scene, camera, renderer;
|
||||||
|
let worldObjects = [];
|
||||||
|
let resizeObserver;
|
||||||
|
|
||||||
|
// Character Refs
|
||||||
|
let playerGroup, mixer, currentAction, animations = [];
|
||||||
|
let clock = new THREE.Clock();
|
||||||
|
|
||||||
|
function playAnimation(name, loop = true) {
|
||||||
|
if (!mixer || !animations.length) return;
|
||||||
|
const clip = animations.find(a => a.name === name);
|
||||||
|
if (!clip) return;
|
||||||
|
|
||||||
|
const newAction = mixer.clipAction(clip);
|
||||||
|
if (currentAction === newAction) return;
|
||||||
|
|
||||||
|
if (currentAction) currentAction.fadeOut(0.2);
|
||||||
|
|
||||||
|
newAction.reset()
|
||||||
|
.setEffectiveTimeScale(1)
|
||||||
|
.setEffectiveWeight(1)
|
||||||
|
.fadeIn(0.2)
|
||||||
|
.play();
|
||||||
|
|
||||||
|
newAction.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce);
|
||||||
|
newAction.clampWhenFinished = !loop;
|
||||||
|
currentAction = newAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
|
||||||
|
camera.position.set(0, 5, 10);
|
||||||
|
camera.lookAt(0, 0, -5);
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
||||||
|
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
||||||
|
const sun = new THREE.DirectionalLight(0x00FFD1, 1.2);
|
||||||
|
sun.position.set(5, 10, 7);
|
||||||
|
scene.add(sun);
|
||||||
|
|
||||||
|
const floorGeo = new THREE.PlaneGeometry(100, 2000);
|
||||||
|
const floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2, metalness: 0.5 }));
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
|
scene.add(floor);
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
// Using the RobotExpressive model from your logic reference
|
||||||
|
loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', (gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
model.scale.set(0.4, 0.4, 0.4);
|
||||||
|
model.rotation.y = Math.PI;
|
||||||
|
|
||||||
|
playerGroup = new THREE.Group();
|
||||||
|
playerGroup.add(model);
|
||||||
|
scene.add(playerGroup);
|
||||||
|
|
||||||
|
animations = gltf.animations;
|
||||||
|
mixer = new THREE.AnimationMixer(model);
|
||||||
|
playAnimation('Running');
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const delta = clock.getDelta();
|
||||||
|
if (mixer) mixer.update(delta);
|
||||||
|
|
||||||
|
// 1. Camera Shake Logic
|
||||||
|
if (shake > 0) {
|
||||||
|
shake -= 0.05;
|
||||||
|
camera.position.x = (Math.random() - 0.5) * shake;
|
||||||
|
camera.position.y = 5 + (Math.random() - 0.5) * shake;
|
||||||
|
} else {
|
||||||
|
camera.position.x = 0;
|
||||||
|
camera.position.y = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Death Fade Logic
|
||||||
|
if (isDying && fade > 0) {
|
||||||
|
fade -= 0.005;
|
||||||
|
playerGroup?.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.material.transparent = true;
|
||||||
|
child.material.opacity = fade;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop physics if not playing or dying
|
||||||
|
if (!isPlaying || isDying || gameOver) return;
|
||||||
|
|
||||||
|
score++;
|
||||||
|
|
||||||
|
// Smooth movement
|
||||||
|
currX += (lane * CONFIG.lane - currX) * 0.15;
|
||||||
|
if (playerGroup) playerGroup.position.x = currX;
|
||||||
|
|
||||||
|
// Jump Physics
|
||||||
|
if (isJumping) {
|
||||||
|
jumpV -= CONFIG.grav;
|
||||||
|
playerY += jumpV;
|
||||||
|
if (playerY <= 0) {
|
||||||
|
playerY = 0;
|
||||||
|
isJumping = false;
|
||||||
|
}
|
||||||
|
if (playerGroup) playerGroup.position.y = playerY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obstacles & Collision
|
||||||
|
worldObjects = worldObjects.map(obj => {
|
||||||
|
obj.mesh.position.z += CONFIG.speed;
|
||||||
|
|
||||||
|
if (Math.abs(obj.mesh.position.z) < 0.8 && obj.lane === lane && playerY < 1) {
|
||||||
|
// --- TRIGGER DEATH SEQUENCE ---
|
||||||
|
isDying = true;
|
||||||
|
shake = 0.5;
|
||||||
|
hitFlash = true;
|
||||||
|
playAnimation('Death', false);
|
||||||
|
|
||||||
|
setTimeout(() => { hitFlash = false; }, 150);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isPlaying = false;
|
||||||
|
gameOver = true;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}).filter(obj => {
|
||||||
|
const keep = obj.mesh.position.z <= 15;
|
||||||
|
if (!keep) scene.remove(obj.mesh);
|
||||||
|
return keep;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (score % 30 === 0) spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawn() {
|
||||||
|
const l = Math.floor(Math.random() * 5) - 2;
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(1.5, 1.5, 1.5),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xFF0055,
|
||||||
|
emissive: 0xFF0055,
|
||||||
|
emissiveIntensity: 0.5
|
||||||
|
})
|
||||||
|
);
|
||||||
|
mesh.position.set(l * CONFIG.lane, 0.75, -100);
|
||||||
|
scene.add(mesh);
|
||||||
|
worldObjects.push({ mesh, lane: l });
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
worldObjects.forEach(obj => scene.remove(obj.mesh));
|
||||||
|
worldObjects = [];
|
||||||
|
|
||||||
|
score = 0; lane = 0; playerY = 0; currX = 0;
|
||||||
|
isJumping = false; isDying = false; gameOver = false;
|
||||||
|
fade = 1; shake = 0; startScreen = false; isPlaying = true;
|
||||||
|
|
||||||
|
if (playerGroup) {
|
||||||
|
playerGroup.position.set(0, 0, 0);
|
||||||
|
playerGroup.traverse(child => {
|
||||||
|
if (child.isMesh) child.material.opacity = 1;
|
||||||
|
});
|
||||||
|
playAnimation('Running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
init();
|
||||||
|
const loop = () => {
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
loop();
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (!isPlaying || isDying) return;
|
||||||
|
if (e.key === "ArrowLeft" && lane > -2) lane--;
|
||||||
|
if (e.key === "ArrowRight" && lane < 2) lane++;
|
||||||
|
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
|
||||||
|
isJumping = true;
|
||||||
|
jumpV = CONFIG.jump;
|
||||||
|
playAnimation('Jump', false);
|
||||||
|
// Switch back to running after jump time (approx 800ms)
|
||||||
|
setTimeout(() => { if(!isDying) playAnimation('Running'); }, 800);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => resizeObserver?.disconnect());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #050505; }
|
||||||
|
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||||
|
canvas { width: 100% !important; height: 100% !important; display: block; }
|
||||||
|
|
||||||
|
.flash { position: absolute; inset: 0; background: white; z-index: 100; pointer-events: none; }
|
||||||
|
|
||||||
|
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: sans-serif; z-index: 50; }
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0,0,0,0.95);
|
||||||
|
padding: 60px;
|
||||||
|
border: 1px solid #FF0055;
|
||||||
|
box-shadow: 0 0 50px rgba(255, 0, 85, 0.2);
|
||||||
|
margin-top: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-hud { font-size: 5rem; font-weight: 900; color: #00FFD1; margin-top: 20px; font-style: italic; }
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 20px 60px;
|
||||||
|
background: #FF0055;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 900;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="wrapper" bind:this={container}>
|
||||||
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
|
||||||
|
{#if hitFlash}
|
||||||
|
<div class="flash"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="ui">
|
||||||
|
{#if isPlaying && !gameOver}
|
||||||
|
<div class="score-hud">{score}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if startScreen || gameOver}
|
||||||
|
<div class="modal">
|
||||||
|
<h1 style="font-size: 4rem; font-style: italic; font-weight: 900;">
|
||||||
|
{gameOver ? 'SYSTEM FAILURE' : 'CYBER RUN'}
|
||||||
|
</h1>
|
||||||
|
<p style="letter-spacing: 5px; opacity: 0.6; margin-bottom: 40px;">
|
||||||
|
{gameOver ? 'FINAL SCORE: ' + score : 'NEURAL LINK READY'}
|
||||||
|
</p>
|
||||||
|
<button on:click={startGame}>{gameOver ? 'RE-INITIATE' : 'START SIMULATION'}</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user