<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="author" content="Sean Bradley" />
        <title>Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs</title>
        <style>
            body {
                overflow: hidden;
                margin: 0px;
            }

            #instructions {
                color: white;
                position: absolute;
                left: 100px;
                top: 10px;
                font-family: monospace;
            }
        </style>        
        <script type="importmap">
            {
                "imports": {
                    "three": "/build/three.module.js",
                    "dat.gui": "/dat.gui",
                    "cannon-es": "/cannon-es"
                }
            }
        </script>
    </head>
    <body>
        <div id="instructions">W,A,S,D or Arrow Keys to Drive. Space to brake.</div>
        <script type="module">
            import * as THREE from 'three'
            import Stats from '/jsm/libs/stats.module.js'
            import { GUI } from 'dat.gui'
            import * as CANNON from 'cannon-es'
            import CannonDebugRenderer from '/utils/cannonDebugRenderer.js'

            const scene = new THREE.Scene()

            const light = new THREE.DirectionalLight()
            light.position.set(25, 50, 25)
            light.castShadow = true
            light.shadow.mapSize.width = 8192
            light.shadow.mapSize.height = 8192
            light.shadow.camera.near = 0.5
            light.shadow.camera.far = 100
            light.shadow.camera.top = 100
            light.shadow.camera.bottom = -100
            light.shadow.camera.left = -100
            light.shadow.camera.right = 100
            scene.add(light)

            const helper = new THREE.CameraHelper(light.shadow.camera)
            scene.add(helper)

            const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)

            const chaseCam = new THREE.Object3D()
            chaseCam.position.set(0, 0, 0)

            const chaseCamPivot = new THREE.Object3D()
            chaseCamPivot.position.set(0, 2, 4)
            chaseCam.add(chaseCamPivot)
            scene.add(chaseCam)

            const renderer = new THREE.WebGLRenderer()
            renderer.setSize(window.innerWidth, window.innerHeight)
            renderer.shadowMap.enabled = true
            renderer.shadowMap.type = THREE.PCFSoftShadowMap
            document.body.appendChild(renderer.domElement)

            const phongMaterial = new THREE.MeshPhongMaterial()

            const world = new CANNON.World()
            world.gravity.set(0, -9.82, 0)

            const groundMaterial = new CANNON.Material('groundMaterial')
            groundMaterial.friction = 0.25
            groundMaterial.restitution = 0.25

            const wheelMaterial = new CANNON.Material('wheelMaterial')
            wheelMaterial.friction = 0.25
            wheelMaterial.restitution = 0.25
            //ground

            const groundGeometry = new THREE.PlaneGeometry(100, 100)

            const groundMesh = new THREE.Mesh(groundGeometry, phongMaterial)
            groundMesh.rotateX(-Math.PI / 2)
            groundMesh.receiveShadow = true
            scene.add(groundMesh)

            const groundShape = new CANNON.Box(new CANNON.Vec3(50, 1, 50))

            const groundBody = new CANNON.Body({ mass: 0, material: groundMaterial })
            groundBody.addShape(groundShape)
            groundBody.position.set(0, -1, 0)
            world.addBody(groundBody)

            //jumps
            for (var i = 0; i < 100; i++) {
                const jump = new THREE.Mesh(new THREE.CylinderGeometry(0, 1, 0.5, 5), phongMaterial)
                jump.position.x = Math.random() * 100 - 50
                jump.position.y = 0.5
                jump.position.z = Math.random() * 100 - 50
                scene.add(jump)
                const cylinderShape = new CANNON.Cylinder(0.01, 1, 0.5, 5)
                const cylinderBody = new CANNON.Body({ mass: 0 })
                cylinderBody.addShape(cylinderShape, new CANNON.Vec3())
                cylinderBody.position.x = jump.position.x
                cylinderBody.position.y = jump.position.y
                cylinderBody.position.z = jump.position.z
                world.addBody(cylinderBody)
            }

            const carBodyGeometry = new THREE.BoxGeometry(1, 1, 2)
            const carBodyMesh = new THREE.Mesh(carBodyGeometry, phongMaterial)
            carBodyMesh.position.y = 3
            carBodyMesh.castShadow = true
            scene.add(carBodyMesh)
            carBodyMesh.add(chaseCam)

            const carBodyShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 1))
            const carBody = new CANNON.Body({ mass: 1 })
            carBody.addShape(carBodyShape)
            carBody.position.x = carBodyMesh.position.x
            carBody.position.y = carBodyMesh.position.y
            carBody.position.z = carBodyMesh.position.z
            world.addBody(carBody)
            //front left wheel

            const wheelLFGeometry = new THREE.CylinderGeometry(0.33, 0.33, 0.2)
            wheelLFGeometry.rotateZ(Math.PI / 2)

            const wheelLFMesh = new THREE.Mesh(wheelLFGeometry, phongMaterial)
            wheelLFMesh.position.x = -1
            wheelLFMesh.position.y = 3
            wheelLFMesh.position.z = -1
            wheelLFMesh.castShadow = true
            scene.add(wheelLFMesh)

            const wheelLFShape = new CANNON.Sphere(0.33)
            const wheelLFBody = new CANNON.Body({ mass: 1, material: wheelMaterial })
            wheelLFBody.addShape(wheelLFShape)
            wheelLFBody.position.x = wheelLFMesh.position.x
            wheelLFBody.position.y = wheelLFMesh.position.y
            wheelLFBody.position.z = wheelLFMesh.position.z
            world.addBody(wheelLFBody)
            //front right wheel

            const wheelRFGeometry = new THREE.CylinderGeometry(0.33, 0.33, 0.2)
            wheelRFGeometry.rotateZ(Math.PI / 2)

            const wheelRFMesh = new THREE.Mesh(wheelRFGeometry, phongMaterial)
            wheelRFMesh.position.y = 3
            wheelRFMesh.position.x = 1
            wheelRFMesh.position.z = -1
            wheelRFMesh.castShadow = true
            scene.add(wheelRFMesh)

            const wheelRFShape = new CANNON.Sphere(0.33)
            const wheelRFBody = new CANNON.Body({ mass: 1, material: wheelMaterial })
            wheelRFBody.addShape(wheelRFShape)
            wheelRFBody.position.x = wheelRFMesh.position.x
            wheelRFBody.position.y = wheelRFMesh.position.y
            wheelRFBody.position.z = wheelRFMesh.position.z
            world.addBody(wheelRFBody)
            //back left wheel
            const wheelLBGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.33)
            wheelLBGeometry.rotateZ(Math.PI / 2)
            const wheelLBMesh = new THREE.Mesh(wheelLBGeometry, phongMaterial)
            wheelLBMesh.position.y = 3
            wheelLBMesh.position.x = -1
            wheelLBMesh.position.z = 1
            wheelLBMesh.castShadow = true
            scene.add(wheelLBMesh)
            const wheelLBShape = new CANNON.Sphere(0.4)
            const wheelLBBody = new CANNON.Body({ mass: 1, material: wheelMaterial })
            wheelLBBody.addShape(wheelLBShape)
            wheelLBBody.position.x = wheelLBMesh.position.x
            wheelLBBody.position.y = wheelLBMesh.position.y
            wheelLBBody.position.z = wheelLBMesh.position.z
            world.addBody(wheelLBBody)
            //back right wheel
            const wheelRBGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.33)
            wheelRBGeometry.rotateZ(Math.PI / 2)
            const wheelRBMesh = new THREE.Mesh(wheelRBGeometry, phongMaterial)
            wheelRBMesh.position.y = 3
            wheelRBMesh.position.x = 1
            wheelRBMesh.position.z = 1
            wheelRBMesh.castShadow = true
            scene.add(wheelRBMesh)
            const wheelRBShape = new CANNON.Sphere(0.4)
            const wheelRBBody = new CANNON.Body({ mass: 1, material: wheelMaterial })
            wheelRBBody.addShape(wheelRBShape)
            wheelRBBody.position.x = wheelRBMesh.position.x
            wheelRBBody.position.y = wheelRBMesh.position.y
            wheelRBBody.position.z = wheelRBMesh.position.z
            world.addBody(wheelRBBody)
            const leftFrontAxis = new CANNON.Vec3(1, 0, 0)
            const rightFrontAxis = new CANNON.Vec3(1, 0, 0)
            const leftBackAxis = new CANNON.Vec3(1, 0, 0)
            const rightBackAxis = new CANNON.Vec3(1, 0, 0)
            const constraintLF = new CANNON.HingeConstraint(carBody, wheelLFBody, {
                pivotA: new CANNON.Vec3(-1, -0.5, -1),
                axisA: leftFrontAxis,
                maxForce: 0.99,
            })
            world.addConstraint(constraintLF)
            const constraintRF = new CANNON.HingeConstraint(carBody, wheelRFBody, {
                pivotA: new CANNON.Vec3(1, -0.5, -1),
                axisA: rightFrontAxis,
                maxForce: 0.99,
            })
            world.addConstraint(constraintRF)
            const constraintLB = new CANNON.HingeConstraint(carBody, wheelLBBody, {
                pivotA: new CANNON.Vec3(-1, -0.5, 1),
                axisA: leftBackAxis,
                maxForce: 0.99,
            })
            world.addConstraint(constraintLB)
            const constraintRB = new CANNON.HingeConstraint(carBody, wheelRBBody, {
                pivotA: new CANNON.Vec3(1, -0.5, 1),
                axisA: rightBackAxis,
                maxForce: 0.99,
            })
            world.addConstraint(constraintRB)
            //rear wheel drive
            constraintLB.enableMotor()
            constraintRB.enableMotor()
            const keyMap = {}
            const onDocumentKey = (e) => {
                keyMap[e.code] = e.type === 'keydown'
                return false
            }
            let forwardVelocity = 0
            let rightVelocity = 0
            document.addEventListener('keydown', onDocumentKey)
            document.addEventListener('keyup', onDocumentKey)
            window.addEventListener('resize', onWindowResize, false)
            function onWindowResize() {
                camera.aspect = window.innerWidth / window.innerHeight
                camera.updateProjectionMatrix()
                renderer.setSize(window.innerWidth, window.innerHeight)
                render()
            }
            const stats = new Stats()
            document.body.appendChild(stats.dom)
            const gui = new GUI()
            const physicsFolder = gui.addFolder('Physics')
            physicsFolder.add(world.gravity, 'x', -10.0, 10.0, 0.1)
            physicsFolder.add(world.gravity, 'y', -10.0, 10.0, 0.1)
            physicsFolder.add(world.gravity, 'z', -10.0, 10.0, 0.1)
            physicsFolder.open()
            const clock = new THREE.Clock()
            let delta
            const cannonDebugRenderer = new CannonDebugRenderer(scene, world)
            const v = new THREE.Vector3()
            let thrusting = false

            function animate() {
                requestAnimationFrame(animate)
                helper.update()
                delta = Math.min(clock.getDelta(), 0.1)
                world.step(delta)
                cannonDebugRenderer.update()
                // Copy coordinates from Cannon.js to Three.js
                carBodyMesh.position.set(carBody.position.x, carBody.position.y, carBody.position.z)
                carBodyMesh.quaternion.set(
                    carBody.quaternion.x,
                    carBody.quaternion.y,
                    carBody.quaternion.z,
                    carBody.quaternion.w
                )
                wheelLFMesh.position.set(
                    wheelLFBody.position.x,
                    wheelLFBody.position.y,
                    wheelLFBody.position.z
                )
                wheelLFMesh.quaternion.set(
                    wheelLFBody.quaternion.x,
                    wheelLFBody.quaternion.y,
                    wheelLFBody.quaternion.z,
                    wheelLFBody.quaternion.w
                )
                wheelRFMesh.position.set(
                    wheelRFBody.position.x,
                    wheelRFBody.position.y,
                    wheelRFBody.position.z
                )
                wheelRFMesh.quaternion.set(
                    wheelRFBody.quaternion.x,
                    wheelRFBody.quaternion.y,
                    wheelRFBody.quaternion.z,
                    wheelRFBody.quaternion.w
                )
                wheelLBMesh.position.set(
                    wheelLBBody.position.x,
                    wheelLBBody.position.y,
                    wheelLBBody.position.z
                )
                wheelLBMesh.quaternion.set(
                    wheelLBBody.quaternion.x,
                    wheelLBBody.quaternion.y,
                    wheelLBBody.quaternion.z,
                    wheelLBBody.quaternion.w
                )
                wheelRBMesh.position.set(
                    wheelRBBody.position.x,
                    wheelRBBody.position.y,
                    wheelRBBody.position.z
                )
                wheelRBMesh.quaternion.set(
                    wheelRBBody.quaternion.x,
                    wheelRBBody.quaternion.y,
                    wheelRBBody.quaternion.z,
                    wheelRBBody.quaternion.w
                )
                thrusting = false
                if (keyMap['KeyW'] || keyMap['ArrowUp']) {
                    if (forwardVelocity < 100.0) forwardVelocity += 1
                    thrusting = true
                }
                if (keyMap['KeyS'] || keyMap['ArrowDown']) {
                    if (forwardVelocity > -100.0) forwardVelocity -= 1
                    thrusting = true
                }
                if (keyMap['KeyA'] || keyMap['ArrowLeft']) {
                    if (rightVelocity > -1.0) rightVelocity -= 0.1
                }
                if (keyMap['KeyD'] || keyMap['ArrowRight']) {
                    if (rightVelocity < 1.0) rightVelocity += 0.1
                }
                if (keyMap['Space']) {
                    if (forwardVelocity > 0) {
                        forwardVelocity -= 1
                    }
                    if (forwardVelocity < 0) {
                        forwardVelocity += 1
                    }
                }
                if (!thrusting) {
                    //not going forward or backwards so gradually slow down
                    if (forwardVelocity > 0) {
                        forwardVelocity -= 0.25
                    }
                    if (forwardVelocity < 0) {
                        forwardVelocity += 0.25
                    }
                }
                constraintLB.setMotorSpeed(forwardVelocity)
                constraintRB.setMotorSpeed(forwardVelocity)
                constraintLF.axisA.z = rightVelocity
                constraintRF.axisA.z = rightVelocity
                camera.lookAt(carBodyMesh.position)
                chaseCamPivot.getWorldPosition(v)
                if (v.y < 1) {
                    v.y = 1
                }
                camera.position.lerpVectors(camera.position, v, 0.05)
                render()
                stats.update()
            }
            function render() {
                renderer.render(scene, camera)
            }
            animate()
            window.focus()
        </script>
    </body>
</html>