added grass and changed the moving logic

This commit is contained in:
Mak
2026-04-24 15:38:04 +09:00
parent e33f9a804f
commit d4ef4fe0e7

View File

@@ -9,70 +9,83 @@ const CONFIG = {
lane: 2.5, lane: 2.5,
jump: 0.35, jump: 0.35,
grav: 0.015, grav: 0.015,
speed: 0.3, speed: 55, // Keeping your faster speed
gameOverDelay: 1400, playerScale: 1.7 // Increased size significantly
playerScale: 1.25
}; };
let score = 0; let score = 0, isPlaying = false, gameOver = false, startScreen = true;
let isPlaying = false; let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
let gameOver = false; let container, canvas, scene, camera, renderer;
let startScreen = true; let worldObjects = [], animationFrame;
let showGameOverModal = false; let isDying = false, hitFlash = false;
let lane = 0; let playerAnchor, currentModel = null, currentMixer = null, swapToken = 0;
let currX = 0;
let isJumping = false;
let jumpV = 0;
let playerY = 0;
let container, canvas, scene, camera, renderer, floor; // Treadmill System
let worldObjects = []; const CHUNKS = [];
let resizeObserver; const CHUNK_COUNT = 3;
let animationFrame; const CHUNK_SIZE = 140;
let isDying = false;
let hitFlash = false;
let shake = 0;
let playerAnchor;
let currentModel = null;
let currentMixer = null;
let currentAction = null;
let currentState = "run";
let swapToken = 0;
let uTime = { value: 0 };
const loader = new GLTFLoader(); const loader = new GLTFLoader();
const clock = new THREE.Clock(); const clock = new THREE.Clock();
const glbCache = new Map(); const glbCache = new Map();
function normalizeModel(root) { const grassVertex = `
root.scale.setScalar(CONFIG.playerScale); varying vec2 vUv;
root.rotation.y = Math.PI; 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);
}
`;
root.traverse((obj) => { const grassFragment = `
if (obj.isMesh || obj.isSkinnedMesh) { varying vec2 vUv;
obj.castShadow = true; void main() {
obj.receiveShadow = true; gl_FragColor = vec4(mix(vec3(0.12, 0.28, 0.18), vec3(0.5, 0.72, 0.4), vUv.y), 1.0);
obj.frustumCulled = false; }
} `;
if (obj.isBone) {
obj.frustumCulled = false; function 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; // Adjusted for slightly larger floor
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
}); });
root.updateMatrixWorld(true); const mesh = new THREE.InstancedMesh(geo, mat, count);
const dummy = new THREE.Object3D();
const box = new THREE.Box3().setFromObject(root); for (let i = 0; i < count; i++) {
const center = new THREE.Vector3(); let x = (Math.random() - 0.5) * 120;
box.getCenter(center); if (x > -10 && x < 10) x += (x > 0) ? 10 : -10;
let z = (Math.random() - 0.5) * CHUNK_SIZE;
root.position.x -= center.x; dummy.position.set(x, 0, z);
root.position.z -= center.z; dummy.rotation.y = Math.random() * Math.PI;
root.position.y -= box.min.y; dummy.scale.setScalar(0.7 + Math.random() * 1.6);
dummy.updateMatrix();
root.updateMatrixWorld(true); mesh.setMatrixAt(i, dummy.matrix);
return root; }
group.add(mesh);
return group;
} }
async function getCachedGLTF(file) { async function getCachedGLTF(file) {
@@ -83,345 +96,180 @@ async function getCachedGLTF(file) {
return glbCache.get(file); return glbCache.get(file);
} }
function disposeCurrentCharacter() { async function swapCharacter(file, isDeathAnimation = false) {
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 myToken = ++swapToken;
const source = await getCachedGLTF(file); const source = await getCachedGLTF(file);
if (myToken !== swapToken) return; if (myToken !== swapToken) return;
const model = normalizeModel(cloneSkeleton(source.scene)); const model = cloneSkeleton(source.scene);
const mixer = new THREE.AnimationMixer(model); model.scale.setScalar(CONFIG.playerScale);
model.rotation.y = Math.PI;
let action = null; const mixer = new THREE.AnimationMixer(model);
if (source.animations && source.animations.length > 0) { if (source.animations?.length) {
action = mixer.clipAction(source.animations[0]); const action = mixer.clipAction(source.animations[0]);
if (loop) {
action.setLoop(THREE.LoopRepeat, Infinity); if (isDeathAnimation) {
} else {
action.setLoop(THREE.LoopOnce, 1); action.setLoop(THREE.LoopOnce, 1);
action.clampWhenFinished = true; action.clampWhenFinished = true; // STOPS AT THE LAST FRAME
action.play();
} else {
action.play();
} }
action.reset();
action.play();
} }
disposeCurrentCharacter(); if (currentModel) playerAnchor.remove(currentModel);
currentModel = model; currentMixer = mixer;
currentModel = model;
currentMixer = mixer;
currentAction = action;
currentState = stateName;
playerAnchor.add(currentModel); 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() { function init() {
scene = new THREE.Scene(); scene = new THREE.Scene();
const skyColor = 0xa4c3b2;
scene.background = new THREE.Color(skyColor);
scene.fog = new THREE.Fog(skyColor, 35, 150);
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000); camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(0, 5, 10); camera.position.set(0, 4.5, 13); // Lowered slightly to match bigger player
camera.lookAt(0, 0, -5); camera.lookAt(0, 1, -5);
renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
scene.add(new THREE.AmbientLight(0xffffff, 0.8)); scene.add(new THREE.AmbientLight(0xffffff, 1.2));
const sun = new THREE.DirectionalLight(0xffffff, 1); for (let i = 0; i < CHUNK_COUNT; i++) {
sun.position.set(5, 10, 7); const chunk = createWorldChunk(-i * CHUNK_SIZE);
scene.add(sun); CHUNKS.push(chunk);
scene.add(chunk);
const floorGeo = new THREE.PlaneGeometry(100, 2000); }
floor = new THREE.Mesh(
floorGeo,
new THREE.MeshStandardMaterial({ color: 0x222222 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
playerAnchor = new THREE.Group(); playerAnchor = new THREE.Group();
scene.add(playerAnchor); scene.add(playerAnchor);
resizeObserver = new ResizeObserver(() => { new ResizeObserver(() => {
const { width, height } = container.getBoundingClientRect(); 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();
}); }).observe(container);
resizeObserver.observe(container);
} }
function spawn() { function spawn() {
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.8, 1.8, 1.8),
new THREE.MeshStandardMaterial({ color: 0x00fff2 }) new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 })
); );
mesh.position.set(l * CONFIG.lane, 0.75, -100); mesh.position.set(l * CONFIG.lane, 0.9, -130);
scene.add(mesh); scene.add(mesh);
worldObjects.push({ mesh, lane: l }); worldObjects.push({ mesh, lane: l });
} }
function update() { function update() {
const delta = clock.getDelta(); const delta = clock.getDelta();
uTime.value = clock.getElapsedTime();
if (currentMixer) { if (currentMixer) currentMixer.update(delta);
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; if (!isPlaying) return;
const moveStep = CONFIG.speed * delta;
score++; score++;
currX += (lane * CONFIG.lane - currX) * 0.15;
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) { if (isJumping) {
jumpV -= CONFIG.grav; jumpV -= CONFIG.grav;
playerY += jumpV; playerY += jumpV;
if (playerY <= 0) { if (playerY <= 0) {
playerY = 0; playerY = 0;
isJumping = false; isJumping = false;
if (!isDying) showRun(); if (!isDying) swapCharacter("Running.glb");
} }
} }
playerAnchor.position.y = playerY;
worldObjects = worldObjects worldObjects = worldObjects.map(obj => {
.map((obj) => { obj.mesh.position.z += moveStep;
obj.mesh.position.z += CONFIG.speed; // Collision box adjusted for larger character
if (Math.abs(obj.mesh.position.z) < 1.3 && obj.lane === lane && playerY < 1.5) triggerGameOver();
if ( return obj;
Math.abs(obj.mesh.position.z) < 0.8 && }).filter(obj => {
obj.lane === lane && const keep = obj.mesh.position.z < 25;
playerY < 1 if (!keep) scene.remove(obj.mesh);
) { return keep;
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(); if (score % 30 === 0) spawn();
} }
function triggerGameOver() {
isPlaying = false; gameOver = true; isDying = true; hitFlash = true;
swapCharacter("Falling Back Death.glb", true); // Pass 'true' for death logic
setTimeout(() => hitFlash = false, 150);
}
async function startGame() { async function startGame() {
worldObjects.forEach((obj) => scene.remove(obj.mesh)); worldObjects.forEach(obj => scene.remove(obj.mesh));
worldObjects = []; worldObjects = [];
score = 0; isPlaying = true; gameOver = false; startScreen = false;
lane = 0; currX = 0; isJumping = false; jumpV = 0; playerY = 0; isDying = false;
score = 0; CHUNKS.forEach((chunk, i) => {
isPlaying = true; chunk.position.z = -i * CHUNK_SIZE;
gameOver = false; });
startScreen = false;
showGameOverModal = false;
lane = 0; await swapCharacter("Running.glb");
currX = 0;
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();
} }
function handleKeyDown(e) { function handleKeyDown(e) {
if (!isPlaying || isDying) return; if (!isPlaying || isDying) 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; swapCharacter("Jumping.glb");
jumpV = CONFIG.jump;
showJump();
} }
} }
onMount(() => { onMount(() => {
init(); init();
const loop = () => { animationFrame = requestAnimationFrame(loop); update(); renderer.render(scene, camera); };
const loop = () => {
animationFrame = requestAnimationFrame(loop);
update();
renderer.render(scene, camera);
};
loop(); loop();
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => { cancelAnimationFrame(animationFrame); window.removeEventListener("keydown", handleKeyDown); };
return () => {
cancelAnimationFrame(animationFrame);
window.removeEventListener("keydown", handleKeyDown);
resizeObserver?.disconnect();
if (currentMixer) currentMixer.stopAllAction();
renderer?.dispose();
};
}); });
</script> </script>
<style> <style>
:global(body, html) { :global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #a4c3b2; }
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%; .ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: 'Segoe UI', sans-serif; }
overflow: hidden; .score { font-size: 2.5rem; margin-top: 30px; font-weight: 800; text-shadow: 0 4px 10px rgba(0,0,0,0.2); }
background: #000; .modal { pointer-events: auto; background: rgba(255, 255, 255, 0.98); padding: 50px; border-radius: 30px; margin-top: 10vh; color: #1e2b21; display: inline-block; box-shadow: 0 25px 60px rgba(0,0,0,0.15); }
} button { padding: 18px 50px; background: #1e2b21; color: white; border: none; border-radius: 15px; cursor: pointer; font-weight: bold; font-size: 1.2rem; transition: all 0.2s; }
button:hover { transform: translateY(-3px); background: #2b3d2f; }
#wrapper { .flash { position: absolute; inset: 0; background: white; z-index: 10; pointer-events: none; }
position: absolute;
inset: 0;
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>
{#if hitFlash} <div class="flash"></div> {/if}
{#if hitFlash}
<div class="flash"></div>
{/if}
<div class="ui"> <div class="ui">
<div class="score">{score}</div> <div class="score">{score}</div>
{#if startScreen || gameOver}
{#if startScreen || showGameOverModal}
<div class="modal"> <div class="modal">
<h1>{showGameOverModal ? "GAME OVER" : "RUNNER"}</h1> <h1 style="margin: 0 0 10px 0; font-size: 2.5rem;">{gameOver ? "NEURO BREAK" : "NEURO RUNNER"}</h1>
<p>{showGameOverModal ? "Score: " + score : "Arrows to Move • Space to Jump"}</p> <p style="margin-bottom: 35px; font-size: 1.1rem; opacity: 0.7;">{gameOver ? "Final Score: " + score : "Ready to focus?"}</p>
<button on:click={startGame}> <button on:click={startGame}>{gameOver ? "RETRY" : "START RUN"}</button>
{showGameOverModal ? "RETRY" : "START"}
</button>
</div> </div>
{/if} {/if}
</div> </div>