diff --git a/package-lock.json b/package-lock.json index 1d49092..9f7e3c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-gamepad": "^1.0.3", + "react-joystick-component": "^6.2.1", "three": "^0.160.1", "three-mesh-bvh": "^0.7.0", "zustand": "^4.5.0" @@ -2607,6 +2608,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-joystick-component": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/react-joystick-component/-/react-joystick-component-6.2.1.tgz", + "integrity": "sha512-0G5Y5aX4hNuXB3xJCwz6Q+nYQOtC6kprNGKmZxmfoPvhepNYUiid0DbLEGZxmr/UKip3S/LUbcQUobtRCuB8IQ==", + "peerDependencies": { + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } + }, "node_modules/react-merge-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz", diff --git a/package.json b/package.json index cca01c6..ff89a00 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-gamepad": "^1.0.3", + "react-joystick-component": "^6.2.1", "three": "^0.160.1", "three-mesh-bvh": "^0.7.0", "zustand": "^4.5.0" diff --git a/src/App.jsx b/src/App.jsx index 8ad7e45..401ec87 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -63,7 +63,6 @@ function App() { mode="concurrent" onCreated={({ gl, camera }) => { gl.toneMapping = THREE.AgXToneMapping - // gl.setClearColor(new THREE.Color('#020209')) }}> diff --git a/src/HUD.jsx b/src/HUD.jsx index 51b618a..6737d59 100644 --- a/src/HUD.jsx +++ b/src/HUD.jsx @@ -1,10 +1,11 @@ import React, { useEffect, useRef, useState } from "react"; import { useStore } from "./components/store"; +import { Joystick } from "react-joystick-component"; export const HUD = () => { const wheel = useRef(); const [image, setImage] = useState(""); - const { item, gameStarted } = useStore(); + const { item, gameStarted, actions } = useStore(); useEffect(() => { const handleMouseMove = (e) => { @@ -26,6 +27,14 @@ export const HUD = () => { }; }, []); + const handleMove = (e) => { + actions.setJoystickX(e.x); + }; + + const handleStop = () => { + actions.setJoystickX(0); + }; + useEffect(() => { switch (item) { case "banana": @@ -55,6 +64,38 @@ export const HUD = () => { +
+ +
+
{ + actions.setDriftButton(true); + }} + onMouseUp={(e) => { + actions.setDriftButton(false); + }} + > + drift +
+
{ + actions.setItemButton(true); + }} + onMouseUp={(e) => { + actions.setItemButton(false); + }} + > + item +
)} diff --git a/src/Landing.jsx b/src/Landing.jsx index 3c004cb..f4b6ae8 100644 --- a/src/Landing.jsx +++ b/src/Landing.jsx @@ -9,7 +9,7 @@ export const Landing = () => { const startButton = useRef(); const homeRef = useRef(); const [setupStatus, setSetupStatus] = useState(0); - const [controlStyle, setControlStyle] = useState(""); + const [controlStyle, setControlStyle] = useState("touch"); useEffect(() => { const tl = gsap.timeline(); diff --git a/src/components/Experience.jsx b/src/components/Experience.jsx index 913c4a8..5f750fd 100644 --- a/src/components/Experience.jsx +++ b/src/components/Experience.jsx @@ -9,6 +9,7 @@ import { Ground } from "./Ground"; import { PlayerController } from "./PlayerController"; import { PlayerControllerGamepad } from "./PlayerControllerGamepad"; import { PlayerControllerKeyboard } from "./PlayerControllerKeyboard"; +import { PlayerControllerTouch } from "./PlayerControllerTouch"; import { Paris } from "./models/tracks/Tour_paris_promenade"; import { EffectComposer, @@ -109,6 +110,8 @@ export const Experience = () => { ? PlayerControllerKeyboard : controls === "gamepad" ? PlayerControllerGamepad + : controls === "touch" + ? PlayerControllerTouch : PlayerController; return ( diff --git a/src/components/PlayerControllerTouch.jsx b/src/components/PlayerControllerTouch.jsx new file mode 100644 index 0000000..fd1faec --- /dev/null +++ b/src/components/PlayerControllerTouch.jsx @@ -0,0 +1,586 @@ +import { Controls } from "../App"; +import { BallCollider, RigidBody, useRapier, vec3 } from "@react-three/rapier"; +import { + useKeyboardControls, + PerspectiveCamera, + PositionalAudio, +} from "@react-three/drei"; +import { useFrame, useThree, extend } from "@react-three/fiber"; +import { useRef, useState, useEffect, useCallback } from "react"; +import * as THREE from "three"; + +import { Mario } from "./models/characters/Mario_kart"; +import { DriftParticlesLeft } from "./Particles/drifts/DriftParticlesLeft"; +import { DriftParticlesRight } from "./Particles/drifts/DriftParticlesRight"; + +import { PointParticle } from "./Particles/drifts/PointParticle"; + +import { FlameParticles } from "./Particles/flames/FlameParticles"; +import { useStore } from "./store"; +import { Cylinder } from "@react-three/drei"; +import FakeGlowMaterial from "./ShaderMaterials/FakeGlow/FakeGlowMaterial"; +import { HitParticles } from "./Particles/hits/HitParticles"; +import { CoinParticles } from "./Particles/coins/CoinParticles"; +import { ItemParticles } from "./Particles/items/ItemParticles"; +import { geometry } from "maath"; +import { useGamepad } from "./useGamepad"; +extend(geometry); + +export const PlayerControllerTouch = ({ + player, + userPlayer, + setNetworkBananas, + setNetworkShells, + networkBananas, + networkShells, +}) => { + + const [isOnGround, setIsOnGround] = useState(false); + const body = useRef(); + const kart = useRef(); + const cam = useRef(); + const initialSpeed = 0; + const maxSpeed = 30; + const boostSpeed = 50; + const acceleration = 0.1; + const decceleration = 0.2; + const damping = -0.1; + const MaxSteeringSpeed = 0.01; + const [currentSteeringSpeed, setCurrentSteeringSpeed] = useState(0); + const [currentSpeed, setCurrentSpeed] = useState(initialSpeed); + const camMaxOffset = 1; + let steeringAngle = 0; + const isOnFloor = useRef(false); + const jumpForce = useRef(0); + const jumpIsHeld = useRef(false); + const driftDirection = useRef(0); + const driftLeft = useRef(false); + const driftRight = useRef(false); + const driftForce = useRef(0); + const mario = useRef(); + const accumulatedDriftPower = useRef(0); + const blueTurboThreshold = 10; + const orangeTurboThreshold = 30; + const purpleTurboThreshold = 60; + const [turboColor, setTurboColor] = useState(0xffffff); + const boostDuration = useRef(0); + const [isBoosting, setIsBoosting] = useState(false); + let targetXPosition = 0; + let targetZPosition = 8; + const [steeringAngleWheels, setSteeringAngleWheels] = useState(0); + const engineSound = useRef(); + const driftSound = useRef(); + const driftTwoSound = useRef(); + const driftOrangeSound = useRef(); + const driftPurpleSound = useRef(); + const driftBlueSound = useRef(); + const jumpSound = useRef(); + const landingSound = useRef(); + const turboSound = useRef(); + const [scale, setScale] = useState(0); + const raycaster = new THREE.Raycaster(); + const downDirection = new THREE.Vector3(0, -1, 0); + const [shouldLaunch, setShouldLaunch] = useState(false); + const effectiveBoost = useRef(0); + const text = useRef(); + + const { actions, shouldSlowDown, item, bananas, coins, id, controls, joystickX, driftButton, itemButton } = useStore(); + const slowDownDuration = useRef(1500); + + useFrame(({ pointer, clock }, delta) => { + if (player.id !== id) return; + const time = clock.getElapsedTime(); + if (!body.current && !mario.current) return; + engineSound.current.setVolume(currentSpeed / 300 + 0.2); + engineSound.current.setPlaybackRate(currentSpeed / 10 + 0.1); + jumpSound.current.setPlaybackRate(1.5); + jumpSound.current.setVolume(0.5); + driftSound.current.setVolume(0.2); + + driftBlueSound.current.setVolume(0.5); + driftOrangeSound.current.setVolume(0.6); + driftPurpleSound.current.setVolume(0.7); + // HANDLING AND STEERING + const kartRotation = + kart.current.rotation.y - driftDirection.current * driftForce.current; + const forwardDirection = new THREE.Vector3( + -Math.sin(kartRotation), + 0, + -Math.cos(kartRotation) + ); + + // mouse steering + + if (!driftLeft.current && !driftRight.current) { + steeringAngle = currentSteeringSpeed * -joystickX; + targetXPosition = -camMaxOffset * -joystickX; + } else if (driftLeft.current && !driftRight.current) { + steeringAngle = currentSteeringSpeed * -(joystickX - 1); + targetXPosition = -camMaxOffset * -joystickX; + } else if (driftRight.current && !driftLeft.current) { + steeringAngle = currentSteeringSpeed * -(joystickX + 1); + targetXPosition = -camMaxOffset * -joystickX; + } + // ACCELERATING + const shouldSlow = actions.getShouldSlowDown(); + + if ( currentSpeed < maxSpeed) { + // Accelerate the kart within the maximum speed limit + setCurrentSpeed( + Math.min(currentSpeed + acceleration * delta * 144, maxSpeed) + ); + } else if ( + + currentSpeed > maxSpeed && + effectiveBoost.current > 0 + ) { + setCurrentSpeed( + Math.max(currentSpeed - decceleration * delta * 144, maxSpeed) + ); + } + + + if (currentSteeringSpeed < MaxSteeringSpeed) { + setCurrentSteeringSpeed( + Math.min( + currentSteeringSpeed + 0.0001 * delta * 144, + MaxSteeringSpeed + ) + ); + } + if (shouldSlow) { + setCurrentSpeed( + Math.max(currentSpeed - decceleration * 2 * delta * 144, 0) + ); + setCurrentSteeringSpeed(0); + slowDownDuration.current -= 1500 * delta; + setShouldLaunch(true); + if (slowDownDuration.current <= 1) { + actions.setShouldSlowDown(false); + slowDownDuration.current = 1500; + setShouldLaunch(false); + } + } + + // Update the kart's rotation based on the steering angle + kart.current.rotation.y += steeringAngle * delta * 144; + + // Apply damping to simulate slowdown when no keys are pressed + body.current.applyImpulse( + { + x: -body.current.linvel().x * (1 - damping) * delta * 144, + y: 0, + z: -body.current.linvel().z * (1 - damping) * delta * 144, + }, + true + ); + const bodyPosition = body.current.translation(); + kart.current.position.set( + bodyPosition.x, + bodyPosition.y - 0.5, + bodyPosition.z + ); + + // JUMPING + if (driftButton && isOnGround && !jumpIsHeld.current) { + jumpForce.current += 10; + isOnFloor.current = false; + jumpIsHeld.current = true; + jumpSound.current.play(); + setIsOnGround(false); + + if (jumpSound.current.isPlaying) { + jumpSound.current.stop(); + jumpSound.current.play(); + } + } + + if (isOnFloor.current && jumpForce.current > 0) { + landingSound.current.play(); + } + if (!isOnGround && jumpForce.current > 0) { + jumpForce.current -= 1 * delta * 144; + } + if (!driftButton) { + jumpIsHeld.current = false; + driftDirection.current = 0; + driftForce.current = 0; + driftLeft.current = false; + driftRight.current = false; + } + // DRIFTING + if ( + jumpIsHeld.current && + currentSteeringSpeed > 0 && + joystickX < -0.1 && + !driftRight.current + ) { + driftLeft.current = true; + } + if ( + jumpIsHeld.current && + currentSteeringSpeed > 0 && + joystickX > 0.1 && + !driftLeft.current + ) { + driftRight.current = true; + } + + if (!jumpIsHeld.current && !driftLeft.current && !driftRight.current) { + mario.current.rotation.y = THREE.MathUtils.lerp( + mario.current.rotation.y, + 0, + 0.0001 * delta * 144 + ); + setTurboColor(0xffffff); + accumulatedDriftPower.current = 0; + driftSound.current.stop(); + driftTwoSound.current.stop(); + driftOrangeSound.current.stop(); + driftPurpleSound.current.stop(); + } + + if (driftLeft.current) { + driftDirection.current = 1; + driftForce.current = 0.4; + mario.current.rotation.y = THREE.MathUtils.lerp( + mario.current.rotation.y, + steeringAngle * 25 + 0.4, + 0.05 * delta * 144 + ); + accumulatedDriftPower.current += 0.1 * (steeringAngle + 1) * delta * 144; + } + if (driftRight.current) { + driftDirection.current = -1; + driftForce.current = 0.4; + mario.current.rotation.y = THREE.MathUtils.lerp( + mario.current.rotation.y, + -(-steeringAngle * 25 + 0.4), + 0.05 * delta * 144 + ); + accumulatedDriftPower.current += 0.1 * (-steeringAngle + 1) * delta * 144; + } + if (!driftLeft.current && !driftRight.current) { + mario.current.rotation.y = THREE.MathUtils.lerp( + mario.current.rotation.y, + steeringAngle * 30, + 0.05 * delta * 144 + ); + setScale(0); + } + if (accumulatedDriftPower.current > blueTurboThreshold) { + setTurboColor(0x00ffff); + boostDuration.current = 50; + driftBlueSound.current.play(); + } + if (accumulatedDriftPower.current > orangeTurboThreshold) { + setTurboColor(0xffcf00); + boostDuration.current = 100; + driftBlueSound.current.stop(); + driftOrangeSound.current.play(); + } + if (accumulatedDriftPower.current > purpleTurboThreshold) { + setTurboColor(0xff00ff); + boostDuration.current = 250; + driftOrangeSound.current.stop(); + driftPurpleSound.current.play(); + } + + if (driftLeft.current || driftRight.current) { + const oscillation = Math.sin(time * 1000) * 0.1; + const vibration = oscillation + 0.9; + if (turboColor === 0xffffff) { + setScale(vibration * 0.8); + } else { + setScale(vibration); + } + if (isOnFloor.current && !driftSound.current.isPlaying) { + driftSound.current.play(); + driftTwoSound.current.play(); + landingSound.current.play(); + } + } + // RELEASING DRIFT + + if (boostDuration.current > 1 && !jumpIsHeld.current) { + setIsBoosting(true); + effectiveBoost.current = boostDuration.current; + boostDuration.current = 0; + } else if (effectiveBoost.current <= 1) { + targetZPosition = 8; + setIsBoosting(false); + } + + if (isBoosting && effectiveBoost.current > 1) { + setCurrentSpeed(boostSpeed); + effectiveBoost.current -= 1 * delta * 144; + targetZPosition = 10; + if (!turboSound.current.isPlaying) turboSound.current.play(); + driftTwoSound.current.play(); + driftBlueSound.current.stop(); + driftOrangeSound.current.stop(); + driftPurpleSound.current.stop(); + } else if (effectiveBoost.current <= 1) { + setIsBoosting(false); + targetZPosition = 8; + turboSound.current.stop(); + } + + // CAMERA WORK + + cam.current.updateMatrixWorld(); + + cam.current.position.x = THREE.MathUtils.lerp( + cam.current.position.x, + targetXPosition, + 0.01 * delta * 144 + ); + + cam.current.position.z = THREE.MathUtils.lerp( + cam.current.position.z, + targetZPosition, + 0.01 * delta * 144 + ); + + body.current.applyImpulse( + { + x: forwardDirection.x * currentSpeed * delta * 144, + y: 0 + jumpForce.current * delta * 144, + z: forwardDirection.z * currentSpeed * delta * 144, + }, + true + ); + + // Update the kart's rotation based on the steering angle + setSteeringAngleWheels(steeringAngle * 25); + + // SOUND WORK + + + // ITEMS + + if (itemButton && item === "banana") { + const distanceBehind = 2; + const scaledBackwardDirection = + forwardDirection.multiplyScalar(distanceBehind); + + const kartPosition = new THREE.Vector3( + ...vec3(body.current.translation()) + ); + + const bananaPosition = kartPosition.sub(scaledBackwardDirection); + const newBanana = { + id: Math.random() + "-" + +new Date(), + position: bananaPosition, + player: true, + }; + setNetworkBananas([...networkBananas, newBanana]); + + actions.useItem(); + } + + if (itemButton && item === "shell") { + const distanceBehind = -2; + const scaledBackwardDirection = + forwardDirection.multiplyScalar(distanceBehind); + + const kartPosition = new THREE.Vector3( + body.current.translation().x, + body.current.translation().y, + body.current.translation().z + ); + + const shellPosition = kartPosition.sub(scaledBackwardDirection); + const newShell = { + id: Math.random() + "-" + +new Date(), + position: shellPosition, + player: true, + rotation: kartRotation, + }; + setNetworkShells([...networkShells, newShell]); + actions.useItem(); + } + + if (itemButton && item === "mushroom") { + setIsBoosting(true); + effectiveBoost.current = 300; + actions.useItem(); + } + + player.setState("position", body.current.translation()); + player.setState("rotation", kartRotation + mario.current.rotation.y); + player.setState("isBoosting", isBoosting); + player.setState("shouldLaunch", shouldLaunch); + player.setState("turboColor", turboColor); + player.setState("scale", scale); + player.setState("bananas", bananas); + }); + + return player.id === id ? ( + + + { + isOnFloor.current = true; + setIsOnGround(true); + }} + onCollisionExit={({ other }) => { + isOnFloor.current = false; + setIsOnGround(false); + }} + /> + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + {/* */} + + + + + + + + + + + + + + ) : null; +}; diff --git a/src/components/store.jsx b/src/components/store.jsx index 99aeb8e..3c9549e 100644 --- a/src/components/store.jsx +++ b/src/components/store.jsx @@ -14,7 +14,7 @@ export const items = [ export const useStore = create((set, get) => ({ gameStarted: false, - controls: "", + controls: "touch", particles1: [], particles2: [], bodyPosition: [0, 0, 0], @@ -29,6 +29,9 @@ export const useStore = create((set, get) => ({ coins : 0, players : [], id : "", + joystickX: 0, + driftButton: false, + itemButton: false, addPastPosition: (position) => { set((state) => ({ pastPositions: [position, ...state.pastPositions.slice(0, 499)], @@ -147,8 +150,16 @@ export const useStore = create((set, get) => ({ }, setControls: (controls) => { set({ controls }); - } - + }, + setJoystickX: (joystickX) => { + set({ joystickX }); + }, + setDriftButton: (driftButton) => { + set({ driftButton }); + }, + setItemButton: (itemButton) => { + set({ itemButton }); + }, }, })); diff --git a/src/index.css b/src/index.css index f6cc3a3..feed5ed 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap"); #root { width: 100vw; @@ -87,22 +87,47 @@ body::-webkit-scrollbar { border-box; border: 2px solid transparent; border-radius: 50em; - .background{ - background-image: url('./images/scanline.jpg'); - background-position: center; - background-size: cover; - width: 100%; - height: 100%; - border-radius: 50em; - display: flex; - justify-content: center; - align-items: center; - + .background { + background-image: url("./images/scanline.jpg"); + background-position: center; + background-size: cover; + width: 100%; + height: 100%; + border-radius: 50em; + display: flex; + justify-content: center; + align-items: center; + } } } - } } +.controls { + position: absolute; + bottom: 150px; +} + +.joystick { + left: 150px; +} + +.drift { + right: 150px; + font-family: "Hanken Grotesk"; + border-radius: 100px; + background: rgba(255, 255, 255, 0.5); + height: 66.6667px; + width: 66.6667px; + border: none; + flex-shrink: 0; + touch-action: none; + color: white; + display: grid; + place-content: center; + cursor: pointer; +} + + @keyframes bounce { 0%, 100% { @@ -113,16 +138,15 @@ body::-webkit-scrollbar { } } -.annotation{ - display:flex; +.annotation { + display: flex; justify-content: center; align-items: center; - background:none; + background: none; backdrop-filter: blur(10px); pointer-events: none; - -} +} .home { position: absolute; @@ -248,11 +272,10 @@ body::-webkit-scrollbar { } } -.disabled{ +.disabled { pointer-events: none; cursor: not-allowed; opacity: 0.4; - } @keyframes blinking { 0% { @@ -265,3 +288,20 @@ body::-webkit-scrollbar { opacity: 1; } } + +.controls.itemButton{ + + right: 250px; + font-family: "Hanken Grotesk"; + border-radius: 100px; + background: rgba(255, 255, 255, 0.5); + height: 66.6667px; + width: 66.6667px; + border: none; + flex-shrink: 0; + touch-action: none; + color: white; + display: grid; + place-content: center; + cursor: pointer; +}