how to do this : we
5:47
could just make a small solid edge
5:49
around the portal to stand on as long as
5:51
we're inside the portal the only thing
5:53
we can collide with is the outer edges
5:54
of the portal and this allows us to
5:57
stand halfway inside a wall without ever
5:59
colliding with that wall it's a cheap
6:01
trick but as far as I can see it just
6:03
works so good in here : class World {
constructor() {
this.game = new Game()
this.scene = this.game.scene
this.physics = this.initPhysics()
this.physics.gravity.set(0, -20, 0)
this.defaultMaterial = new CANNON.Material('default')
this.raycaster = new THREE.Raycaster()
this.mouse = new THREE.Vector2()
this.leftPortal = []
this.rightPortal = []
this.portalPhysics = false
this.collisionDetected = false
this.virtualCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
this.scene.add(this.virtualCamera)
this.collisionDetected = false
this.portalHandler = new Portal(this.scene, this.game.renderer.instance)
this.active = ''
this.previousContacts = []
this.worldBounds = {
min: new CANNON.Vec3(-100, -100, -100),
max: new CANNON.Vec3(100, 100, 100)
}
const defaultContactMaterial = new CANNON.ContactMaterial(
this.defaultMaterial,
this.defaultMaterial,
{
friction: 0,
restitution: 0
}
)
this.physics.defaultContactMaterial = defaultContactMaterial
window.addEventListener('click', this.shoot.bind(this))


    this.isHoldingCube = false
    this.holdDistance = 10
    this.pickupRange = 20

    this.holdingConstraint = null
    this.holdBody = null

    this.isJumping = true;

    this.onKeyDown = this.onKeyDown.bind(this)
    window.addEventListener('keydown', this.onKeyDown)

    this.setWorld()
    this.createCompanionCube()
    this.loadTextures()

    let upVector = new CANNON.Vec3(0, 1, 0);
    let contactNormal = new CANNON.Vec3(0, 0, 0);

    this.physics.addEventListener("postStep", (e) => {
        this.isJumping = true; // Assume jumping until proven otherwise
        if (this.physics.contacts.length > 0) {
            for (let contact of this.physics.contacts) {
                if (contact.bi.class == 'companionCube' || contact.bj.class == 'companionCube') {
                    if (contact.bi.class == 'companionCube') {
                        contactNormal = new CANNON.Vec3().copy(contact.ni).scale(-1);
                    } else {
                        contactNormal = new CANNON.Vec3().copy(contact.ni);
                    }
                    const collisionResponse = contact.bi.collisionResponse && contact.bj.collisionResponse;
                    this.isJumping = !(collisionResponse && contactNormal.dot(upVector) > 0.5);
                }
            }
        }
    });
}

initPhysics() {
    let world = new CANNON.World()
    world.quatNormalizeSkip = 0
    world.quatNormalizeFast = false

    var solver = new CANNON.GSSolver()

    world.defaultContactMaterial.contactEquationStiffness = 1e9
    world.defaultContactMaterial.contactEquationRelaxation = 4

    solver.iterations = 20
    solver.tolerance = 0.001
    let split = true
    if (split)
        world.solver = new CANNON.SplitSolver(solver)
    else
        world.solver = solver

    world.gravity.set(0, -9.8, 0)
    world.broadphase = new CANNON.NaiveBroadphase()

    return world
}

worldToLocal(object, vector) {
    const worldInverse = new THREE.Matrix4().copy(object.matrixWorld).invert()
    return vector.clone().applyMatrix4(worldInverse)
}

localToWorld(object, vector) {
    return vector.clone().applyMatrix4(object.matrixWorld)
}

shoot(event) {
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

    this.raycaster.setFromCamera({ x: 0, y: 0 }, this.game.camera.instance)

    const intersects = this.raycaster.intersectObjects([
        this.plane, this.roof, this.scene.getObjectByName('backWall'),
        this.scene.getObjectByName('frontWall'), this.scene.getObjectByName('leftWall'),
        this.scene.getObjectByName('rightWall')
    ])

    if (intersects.length > 0) {
        const intersection = intersects[0]
        const object = intersection.object

        const geometry = new THREE.CircleGeometry(5, 32)
        const material = new THREE.MeshBasicMaterial({ color: 0x000000 })
        const circle = new THREE.Mesh(geometry, material)

        const portalSize = new THREE.Vector2(10, 15)
        const halfPortalSize = portalSize.clone().multiplyScalar(0.5)
        const wallSize = new THREE.Vector2(object.geometry.parameters.width, object.geometry.parameters.height)
        const halfWallSize = wallSize.clone().multiplyScalar(0.5)

        let localPosition = this.worldToLocal(object, intersection.point)

        localPosition.x = THREE.MathUtils.clamp(localPosition.x, -halfWallSize.x + halfPortalSize.x, halfWallSize.x - halfPortalSize.x)
        localPosition.y = THREE.MathUtils.clamp(localPosition.y, -halfWallSize.y + halfPortalSize.y, halfWallSize.y - halfPortalSize.y)

        const validPoint = this.localToWorld(object, localPosition)

        circle.position.copy(validPoint)
        circle.rotation.copy(object.rotation)
        circle.scale.y = 1.5

        circle.position.addScaledVector(circle.getWorldDirection(new THREE.Vector3()), 0.01)
        this.boxBody = new CANNON.Body({
            collisionFilterGroup: GROUP_PORTAL,
            collisionFilterMask: GROUP_WALL | GROUP_DEFAULT | GROUP_CUBE | GROUP_PLAYER,
        })
        this.boxBody.mass = 0
        this.boxBody.material = this.defaultMaterial
        this.boxBody.collisionResponse = false
        this.boxBody.addShape(new CANNON.Box(new CANNON.Vec3(3.34, 5, 1.5)))
        this.boxBody.quaternion.copy(circle.quaternion)
        this.boxBody.position.copy(circle.position)
        this.boxBody.class = 'portal'
        this.boxBody.object = intersects[0].object

        let intersectionDetected = false
        if (event.button === 0) {
            this.rightPortal.forEach((rightPortal) => {
                if (this.boxBody.object === rightPortal.physicObject.object && this.detectIntersection(this.boxBody, rightPortal.physicObject)) {
                    intersectionDetected = true
                }
            })
        } else if (event.button === 2) {
            this.leftPortal.forEach((leftPortal) => {
                if (this.boxBody.object === leftPortal.physicObject.object && this.detectIntersection(this.boxBody, leftPortal.physicObject)) {
                    intersectionDetected = true
                }
            })
        }

        if (intersectionDetected) {
            return
        }

        this.physics.addBody(this.boxBody)
        circle.physicObject = this.boxBody

        const geometry1 = new THREE.PlaneGeometry(19, 19)
        let materialPortal

        if (event.button === 0) {
            this.materialBlue = new THREE.ShaderMaterial({
                uniforms: {
                    iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                    iTime: { value: 0 }
                },
                vertexShader: document.getElementById('vertexShader').textContent,
                fragmentShader: document.getElementById('fragmentShaderOrange').textContent
            })
            materialPortal = this.materialBlue
            this.boxBody.ref = 'left'
            this.leftPortal.push(circle)
        } else if (event.button === 2) {
            this.materialOrange = new THREE.ShaderMaterial({
                uniforms: {
                    iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                    iTime: { value: 0 }
                },
                vertexShader: document.getElementById('vertexShader').textContent,
                fragmentShader: document.getElementById('fragmentShaderBlue').textContent
            })
            materialPortal = this.materialOrange
            this.boxBody.ref = 'right'
            this.rightPortal.push(circle)
        }

        const planePortal = new THREE.Mesh(geometry1, materialPortal)
        planePortal.position.copy(circle.position)
        planePortal.rotation.copy(circle.rotation)
        planePortal.position.addScaledVector(circle.getWorldDirection(new THREE.Vector3()), 0.01)

        this.scene.add(planePortal)
        circle.torus = planePortal
    }
}

detectIntersection(bodyA, bodyB, buffer = 4) {
    const shapeA = bodyA.shapes[0]
    const shapeB = bodyB.shapes[0]

    const posA = bodyA.position
    const posB = bodyB.position

    const halfSizeA = shapeA.halfExtents
    const halfSizeB = shapeB.halfExtents

    return (
        Math.abs(posA.x - posB.x) < (halfSizeA.x + halfSizeB.x + buffer) &&
        Math.abs(posA.y - posB.y) < (halfSizeA.y + halfSizeB.y + buffer) &&
        Math.abs(posA.z - posB.z) < (halfSizeA.z + halfSizeB.z + buffer)
    )
}

calculateAngle(v1, v2) {
    const dot = v1.dot(v2)
    const angle = Math.acos(dot / (v1.length() * v2.length()))
    return angle
}

isBodyOutOfBounds(body) {
    const position = body.position
    return (
        position.x < this.worldBounds.min.x ||
        position.x > this.worldBounds.max.x ||
        position.y < this.worldBounds.min.y ||
        position.y > this.worldBounds.max.y ||
        position.z < this.worldBounds.min.z ||
        position.z > this.worldBounds.max.z
    )
}

setWorld() {
    const floorShape = new CANNON.Plane()
    const floorBody = new CANNON.Body({
        collisionFilterGroup: GROUP_WALL,
        collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
    })
    floorBody.mass = 0
    floorBody.addShape(floorShape)
    floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5)
    this.physics.addBody(floorBody)

    const floorTexture = new THREE.TextureLoader().load('/textures/concrete_modular_floor001c.png')
    floorTexture.repeat.set(4, 4)
    floorTexture.wrapS = THREE.RepeatWrapping
    floorTexture.wrapT = THREE.RepeatWrapping

    const floorMaterial = new THREE.MeshStandardMaterial({
        map: floorTexture,
        roughness: 0.2,
        metalness: 0.1
    })

    this.plane = new THREE.Mesh(
        new THREE.PlaneGeometry(200, 200),
        floorMaterial
    )
    this.plane.rotation.x = -Math.PI / 2
    this.plane.receiveShadow = true
    this.plane.position.set(0, 0, 0)
    this.scene.add(this.plane)
    this.plane.physicObject = floorBody

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

    const createWall = (width, height, depth, color, position, rotation) => {
        const wallTexture = new THREE.TextureLoader().load('/textures/concrete_modular_wall001a.png')
        wallTexture.repeat.set(4, 3)
        wallTexture.wrapS = THREE.RepeatWrapping
        wallTexture.wrapT = THREE.RepeatWrapping

        const wallMaterial = new THREE.MeshStandardMaterial({
            map: wallTexture,
            side: THREE.DoubleSide
        })

        const wallMesh = new THREE.Mesh(
            new THREE.PlaneGeometry(width, height),
            wallMaterial
        )
        wallMesh.position.set(position.x, position.y, position.z)
        wallMesh.rotation.set(rotation.x, rotation.y, rotation.z)
        this.scene.add(wallMesh)

        const wallBody = new CANNON.Body({
            collisionFilterGroup: GROUP_WALL,
            collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
        })
        wallBody.mass = 0
        wallBody.addShape(wallShape)
        wallBody.position.copy(wallMesh.position)
        wallBody.quaternion.copy(wallMesh.quaternion)
        this.physics.addBody(wallBody)

        wallMesh.physicObject = wallBody
        return wallMesh
    }

    const wallHeight = 100
    const wallYPosition = wallHeight / 2

    let backWallMesh = createWall(200, wallHeight, 1, 0xf0ffff, { x: 0, y: wallYPosition, z: 100 }, { x: 0, y: Math.PI, z: 0 })
    backWallMesh.name = 'backWall'

    let frontWallMesh = createWall(200, wallHeight, 1, 0xff0fff, { x: 0, y: wallYPosition, z: -100 }, { x: 0, y: 0, z: 0 })
    frontWallMesh.name = 'frontWall'

    let leftWallMesh = createWall(200, wallHeight, 1, 0xfff0ff, { x: -100, y: wallYPosition, z: 0 }, { x: 0, y: Math.PI / 2, z: 0 })
    leftWallMesh.name = 'leftWall'

    let rightWallMesh = createWall(200, wallHeight, 1, 0xffff0f, { x: 100, y: wallYPosition, z: 0 }, { x: 0, y: -Math.PI / 2, z: 0 })
    rightWallMesh.name = 'rightWall'

    const roofTexture = new THREE.TextureLoader().load('/textures/concrete_modular_ceiling001a.png')
    roofTexture.repeat.set(4, 4)
    roofTexture.wrapS = THREE.RepeatWrapping
    roofTexture.wrapT = THREE.RepeatWrapping

    const roofMaterial = new THREE.MeshStandardMaterial({
        map: roofTexture,
    })

    this.roof = new THREE.Mesh(
        new THREE.PlaneGeometry(200, 200),
        roofMaterial
    )
    this.roof.rotation.x = Math.PI / 2
    this.roof.position.y = wallHeight
    this.scene.add(this.roof)

    const roofShape = new CANNON.Plane()
    const roofBody = new CANNON.Body({
        collisionFilterGroup: GROUP_WALL,
        collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
    })
    roofBody.mass = 0
    roofBody.addShape(roofShape)
    roofBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI * 0.5)
    roofBody.position.y = wallHeight
    this.physics.addBody(roofBody)

    this.roof.physicObject = roofBody
}

loadTextures() {
    this.game.resources.on('ready', () => {
        // Load resources if needed
    })
}

renderPortal() {
    this.game.renderer.instance.clear()
    this.game.camera.instance.updateMatrixWorld(true)
    this.portalHandler.render(this.game.camera.instance, 0, this.virtualCamera, [this.rightPortal[0], this.leftPortal[0]], this.game.camera.instance.matrixWorld.clone(), this.game.camera.instance.projectionMatrix.clone())
}

isWithinFunnel(coneOrigin, coneDirection, objectPosition, coneAngle, coneHeight = 10) {
    const upDirection = new THREE.Vector3(0, 1, 0)
    const facingUpward = coneDirection.dot(upDirection) > 0.7

    if (!facingUpward) {
        return false
    }

    const toObject = new THREE.Vector3().subVectors(objectPosition, coneOrigin)

    const projectedDistance = toObject.dot(coneDirection)

    if (projectedDistance > coneHeight || projectedDistance < 0) {
        return false
    }

    const directionToObject = toObject.normalize()
    const angle = coneDirection.angleTo(directionToObject)

    return angle < coneAngle
}

funnelObjectTowardsPortal(objectPosition, coneOrigin) {
    const toConeOrigin = new THREE.Vector3().copy(coneOrigin).sub(objectPosition)
    toConeOrigin.y = 0

    const direction = toConeOrigin.normalize()

    const funnelSpeed = 0.2

    const newPosition = new THREE.Vector3().copy(objectPosition)
    newPosition.x += direction.x * funnelSpeed
    newPosition.z += direction.z * funnelSpeed

    this.game.controls.cameraBody.position.set(newPosition.x, this.game.controls.cameraBody.position.y, newPosition.z)
}

update() {
    this.physics.step(1 / 75, this.game.time.delta, 3)

    this.contact = false

    if(this.companionCubeBody){
        if (!this.isJumping) {
            this.companionCubeBody.linearDamping = 0.99
        } else {
            this.companionCubeBody.linearDamping = 0
        }
    }

    if (this.rightPortal.length > 0) {
        document.querySelector('.blue').classList.add('active')
    } else {
        document.querySelector('.blue').classList.remove('active')
    }

    if (this.leftPortal.length > 0) {
        document.querySelector('.orange').classList.add('active')
    } else {
        document.querySelector('.orange').classList.remove('active')
    }

    if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {
        const funnelConeAngle = Math.PI / 6
        const playerPosition = this.game.camera.instance.position

        this.leftPortal.forEach((portal) => {
            const coneOrigin = portal.position
            const coneDirection = new THREE.Vector3()
            portal.getWorldDirection(coneDirection)

            if (this.isWithinFunnel(coneOrigin, coneDirection, playerPosition, funnelConeAngle)) {
                this.funnelObjectTowardsPortal(playerPosition, coneOrigin)
            }
        })

        this.rightPortal.forEach((portal) => {
            const coneOrigin = portal.position
            const coneDirection = new THREE.Vector3()
            portal.getWorldDirection(coneDirection)

            if (this.isWithinFunnel(coneOrigin, coneDirection, playerPosition, funnelConeAngle)) {
                this.funnelObjectTowardsPortal(playerPosition, coneOrigin)
            }
        })
    }

    this.physics.contacts.forEach((contact) => {
        let bodyA = contact.bi
        let bodyB = contact.bj
        if (((bodyA.class == 'camera' && bodyB.class == 'portal') || (bodyB.class == 'camera' && bodyA.class == 'portal')) && (this.rightPortal.length > 0 && this.leftPortal.length > 0)) {
            this.contact = true
        }
    })

    if (this.materialBlue) {
        this.materialBlue.uniforms.iTime.value += 0.05
    }
    if (this.materialOrange) {
        this.materialOrange.uniforms.iTime.value += 0.05
    }

    if (this.isBodyOutOfBounds(this.game.controls.cameraBody)) {
        this.game.controls.cameraBody.sleep()
        this.game.controls.cameraBody.position.set(20, 10, 0)
        this.game.controls.cameraBody.wakeUp()
    }

    if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {
        this.rightPortal[0].pair = this.leftPortal[0]
        this.leftPortal[0].pair = this.rightPortal[0]
        this.game.camera.instance.layers.set(0)
        this.renderPortal()
        this.game.renderer.instance.clearDepth()
        this.game.camera.instance.layers.set(1)
        this.game.renderer.instance.render(this.scene, this.game.camera.instance)
    } else {
        this.tmpScene = new THREE.Scene()
        this.tmpScene.children = this.scene.children.filter((child) => child.class !== 'player')
        this.game.camera.instance.layers.set(0)
        this.game.renderer.instance.render(this.tmpScene, this.game.camera.instance)
        this.game.renderer.instance.clearDepth()
        this.game.camera.instance.layers.set(1)
        this.game.renderer.instance.render(this.tmpScene, this.game.camera.instance)
    }

    this.managePortals()

    if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {

        let sourcePortal = null
        let destinationPortal = null

        // Determine which portal the cube is colliding with
        for (let i = 0; i < this.physics.contacts.length; i++) {
            const contact = this.physics.contacts[i]
            const bodyA = contact.bi
            const bodyB = contact.bj

            if (
                (bodyA.class === 'companionCube' && bodyB.class === 'portal') ||
                (bodyA.class === 'portal' && bodyB.class === 'companionCube')
            ) {
                // Identify the portal body and its reference
                const portalBody = bodyA.class === 'portal' ? bodyA : bodyB
                const portalRef = portalBody.ref // 'left' or 'right'

                if (portalRef === 'left') {
                    sourcePortal = this.leftPortal[0]
                    destinationPortal = this.rightPortal[0]
                } else if (portalRef === 'right') {
                    sourcePortal = this.rightPortal[0]
                    destinationPortal = this.leftPortal[0]
                }

                // Break out of the loop once the colliding portal is found
                break
            }
        }

        // If the cube is not colliding with any portal, default to one pair
        if (!sourcePortal || !destinationPortal) {
            sourcePortal = this.leftPortal[0]
            destinationPortal = this.rightPortal[0]
        }

        // Synchronize the clone's position and rotation using source and destination portals
        const relativePosition = sourcePortal.worldToLocal(this.companionCube.position.clone())
        relativePosition.applyQuaternion(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))
        const newCubePosition = destinationPortal.localToWorld(relativePosition)
        this.cubeClone.position.copy(newCubePosition)

        // Synchronize rotation
        const relativeRotation = sourcePortal.quaternion.clone().invert().multiply(this.companionCube.quaternion)
        relativeRotation.premultiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))
        this.cubeClone.quaternion.copy(destinationPortal.quaternion.clone().multiply(relativeRotation))
        
        if(!this.holdBody){
            this.checkCubePortalTeleport()
        }

        let cubePortalContact = false
        let playerPortalContact = false

        for (let i = 0; i < this.physics.contacts.length; i++) {
            const contact = this.physics.contacts[i]
            const bodyA = contact.bi
            const bodyB = contact.bj

            if ((bodyA.class === 'companionCube' && bodyB.class === 'portal') ||
                (bodyA.class === 'portal' && bodyB.class === 'companionCube')) {
                this.companionCubeBody.collisionFilterMask &= ~GROUP_WALL
                cubePortalContact = true
            }

            if ((bodyA.class === 'camera' && bodyB.class === 'portal') ||
                (bodyA.class === 'portal' && bodyB.class === 'camera')) {
                this.game.controls.cameraBody.collisionFilterMask &= ~GROUP_WALL
                playerPortalContact = true
            }

            if ((bodyA.class === 'camera' && bodyB.ref === 'right') ||
                (bodyA.ref === 'right' && bodyB.class === 'camera')) {
                this.checkLeftPortalTeleport()
                break
            }
        }

        if (!cubePortalContact) {
            this.companionCubeBody.collisionFilterMask = GROUP_DEFAULT | GROUP_WALL | GROUP_PORTAL | GROUP_PLAYER
        }

        if (!playerPortalContact) {
            this.game.controls.cameraBody.collisionFilterMask = GROUP_DEFAULT | GROUP_WALL | GROUP_PORTAL | GROUP_CUBE
        }

        for (let i = 0; i < this.physics.contacts.length; i++) {
            const contact = this.physics.contacts[i]
            const bodyA = contact.bi
            const bodyB = contact.bj

            if ((bodyA.class === 'camera' && bodyB.ref === 'left') || (bodyA.ref === 'left' && bodyB.class === 'camera')) {
                this.checkRightPortalTeleport()
                break
            }
        }
    } else {
        this.resetPortalColors()
    }

    if (!this.collisionDetected) {
        this.game.controls.cameraBody.wakeUp()
    }

    if (this.holdBody) {
        const holdPosition = this.getHoldPosition()
        this.holdBody.position.copy(holdPosition)
        this.holdBody.quaternion.copy(this.game.camera.instance.quaternion)
    }

    if (this.companionCube) {
        this.companionCube.position.copy(this.companionCubeBody.position)
        this.companionCube.quaternion.copy(this.companionCubeBody.quaternion)
    }
}

managePortals() {
    this.leftPortal.forEach((portalData) => {
        this.scene.add(portalData)
    })

    if (this.leftPortal.length > 1) {
        const circleToRemove = this.leftPortal.shift()
        this.scene.remove(circleToRemove.box)
        this.scene.remove(circleToRemove.torus)
        this.physics.remove(circleToRemove.physicObject)
        this.scene.remove(circleToRemove)
    }

    this.rightPortal.forEach((portalData) => {
        this.scene.add(portalData)
    })

    if (this.rightPortal.length > 1) {
        const circleToRemove = this.rightPortal.shift()
        this.scene.remove(circleToRemove.box)
        this.scene.remove(circleToRemove.torus)
        this.physics.remove(circleToRemove.physicObject)
        this.scene.remove(circleToRemove)
    }
}

teleport(sourcePortal, camera, body) {
    const halfTurn = new THREE.Quaternion()
    halfTurn.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI)

    const forwardDirection = new THREE.Vector3()
    camera.getWorldDirection(forwardDirection)

    const relativePos = sourcePortal.worldToLocal(camera.position.clone())
    relativePos.applyQuaternion(halfTurn)
    const newPos = sourcePortal.pair.localToWorld(relativePos)
    if (newPos.y < 9.98) {
        newPos.y = 10
    }
    body.position.copy(newPos)

    const relativeRot = sourcePortal.quaternion.clone().invert().multiply(camera.quaternion)
    relativeRot.premultiply(halfTurn)
    camera.quaternion.copy(sourcePortal.pair.quaternion.clone().multiply(relativeRot))

    const newForwardDirection = new THREE.Vector3()
    camera.getWorldDirection(newForwardDirection)
    const targetRotation = new THREE.Quaternion().setFromRotationMatrix(
        new THREE.Matrix4().lookAt(
            new THREE.Vector3(0, 0, 0),
            newForwardDirection,
            new THREE.Vector3(0, 1, 0)
        )
    )

    camera.quaternion.copy(targetRotation)

    const inTransform = sourcePortal
    const outTransform = sourcePortal.pair

    if (body) {
        // Convert the body's velocity to THREE.Vector3
        const worldVelocity = new THREE.Vector3(body.velocity.x, body.velocity.y, body.velocity.z)

        // Transform the velocity into the portal's local space
        const relativeVelocity = worldVelocity.clone()
        relativeVelocity.applyQuaternion(sourcePortal.quaternion.clone().invert())

        // Apply the half-turn rotation
        relativeVelocity.applyQuaternion(halfTurn)

        // Transform the velocity into the destination portal's world space
        relativeVelocity.applyQuaternion(sourcePortal.pair.quaternion)

        // Set the body's velocity
        body.velocity.set(relativeVelocity.x, relativeVelocity.y, relativeVelocity.z)
    }
}

checkLeftPortalTeleport() {
    const portalForward = new THREE.Vector3()
    this.rightPortal[0].getWorldDirection(portalForward)

    const travelerPosition = this.game.camera.instance.position.clone()
    const portalPosition = this.rightPortal[0].position.clone()
    const portalToTraveler = travelerPosition.sub(portalPosition)

    const dotProduct = portalForward.dot(portalToTraveler)

    if (dotProduct < 0) {
        this.teleport(this.rightPortal[0], this.game.camera.instance, this.game.controls.cameraBody)
    }
}

checkRightPortalTeleport() {
    const portalForward = new THREE.Vector3()
    this.leftPortal[0].getWorldDirection(portalForward)

    const travelerPosition = this.game.camera.instance.position.clone()
    const portalPosition = this.leftPortal[0].position.clone()
    const portalToTraveler = travelerPosition.sub(portalPosition)

    const dotProduct = portalForward.dot(portalToTraveler)

    if (dotProduct < 0) {
        this.teleport(this.leftPortal[0], this.game.camera.instance, this.game.controls.cameraBody)
    }
}

resetPortalColors() {
    if (this.leftPortal.length > 0) {
        this.leftPortal[0].material.color = new THREE.Color(0xff9a00)
        this.leftPortal[0].material.needsUpdate = true
    }

    if (this.rightPortal.length > 0) {
        this.rightPortal[0].material.color = new THREE.Color(0x00a2ff)
        this.rightPortal[0].material.needsUpdate = true
    }
}

createCompanionCube() {
    const loader = new GLTFLoader()

    loader.load('/cube.glb', (gltf) => {
        this.companionCube = gltf.scene

        const box = new THREE.Box3().setFromObject(this.companionCube)
        const center = box.getCenter(new THREE.Vector3())
        this.companionCube.position.sub(center)

        this.companionCube.position.set(0, 5, -5)
        this.companionCube.scale.set(0.13, 0.13, 0.13)
        this.companionCube.castShadow = true
        this.companionCube.receiveShadow = true

        this.companionCube.traverse((child) => {
            if (child.isMesh && child.material) {
                if (child.material.map) {
                    child.material.map.colorSpace = THREE.SRGBColorSpace
                    child.material.metalness = 0.7
                }
                if (child.material.emissiveMap) {
                    child.material.emissiveMap.colorSpace = THREE.SRGBColorSpace
                }
                child.material.needsUpdate = true
            }
        })

        this.scene.add(this.companionCube)

        this.cubeClone = this.companionCube.clone()

        this.cubeClone.position.z = 200

        this.scene.add(this.cubeClone)

        const cubeShape = new CANNON.Box(new CANNON.Vec3(2.5, 2.5, 2.5))
        this.companionCubeBody = new CANNON.Body({
            mass: 1,
            material: this.defaultMaterial,
            collisionFilterGroup: GROUP_CUBE,
            collisionFilterMask: GROUP_WALL | GROUP_PLAYER | GROUP_DEFAULT | GROUP_PORTAL,
        })
        this.companionCubeBody.addShape(cubeShape)
        this.companionCubeBody.position.copy(this.companionCube.position)
        this.companionCubeBody.linearDamping = 0
        this.companionCubeBody.angularDamping = 0.5
        this.companionCubeBody.class = 'companionCube'

        this.physics.addBody(this.companionCubeBody)

        this.companionCube.body = this.companionCubeBody
    }, undefined, (error) => {
        console.error('An error occurred while loading the cube model:', error)
    })
}

getHoldPosition() {
    const camera = this.game.camera.instance
    const cameraDirection = new THREE.Vector3()
    camera.getWorldDirection(cameraDirection)
    cameraDirection.normalize()

    return new THREE.Vector3().copy(camera.position)
        .add(cameraDirection.multiplyScalar(this.holdDistance))
}

onKeyDown(event) {
    if (event.key === 'e') {
        if (!this.isHoldingCube) {
            this.tryPickupCube()
        } else {
            this.dropCube()
        }
    }
}

tryPickupCube() {
    const playerPosition = this.game.camera.instance.position
    const cubePosition = this.companionCube.position.clone()
    const distance = playerPosition.distanceTo(cubePosition)

    if (distance <= this.pickupRange) {
        this.isHoldingCube = true

        const holdPosition = this.getHoldPosition()
        this.holdBody = new CANNON.Body({
            mass: 0,
            type: CANNON.Body.KINEMATIC,
            collisionFilterGroup: 0,
            collisionFilterMask: 0
        })
        this.holdBody.position.copy(holdPosition)
        this.physics.addBody(this.holdBody)

        this.holdingConstraint = new CANNON.PointToPointConstraint(
            this.companionCubeBody,
            new CANNON.Vec3(0, 0, 0),
            this.holdBody,
            new CANNON.Vec3(0, 0, 0),
            1e6
        )
        this.physics.addConstraint(this.holdingConstraint)

        this.companionCubeBody.angularDamping = 1

        // Add this line to remove collision with the player
        this.companionCubeBody.collisionFilterMask &= ~GROUP_PLAYER
    }
}

dropCube() {
    this.isHoldingCube = false

    if (this.holdingConstraint) {
        this.physics.removeConstraint(this.holdingConstraint)
        this.holdingConstraint = null
    }

    if (this.holdBody) {
        this.physics.removeBody(this.holdBody)
        this.holdBody = null
    }

    // Add the player group back to the collision mask
    this.companionCubeBody.collisionFilterMask |= GROUP_PLAYER
    this.companionCubeBody.angularDamping = 0.5
}

checkCubePortalTeleport() {
    const cubePosition = new THREE.Vector3(
        this.companionCubeBody.position.x,
        this.companionCubeBody.position.y,
        this.companionCubeBody.position.z
    )

    const rightPortalPosition = this.rightPortal[0].position.clone()
    const rightPortalForward = new THREE.Vector3()
    this.rightPortal[0].getWorldDirection(rightPortalForward)
    const rightPortalToCube = cubePosition.clone().sub(rightPortalPosition)
    const rightDotProduct = rightPortalForward.dot(rightPortalToCube)

    if (rightDotProduct < 0) {
        this.teleport(this.rightPortal[0], this.companionCube, this.companionCubeBody)
    }

    const leftPortalPosition = this.leftPortal[0].position.clone()
    const leftPortalForward = new THREE.Vector3()
    this.leftPortal[0].getWorldDirection(leftPortalForward)
    const leftPortalToCube = cubePosition.clone().sub(leftPortalPosition)
    const leftDotProduct = leftPortalForward.dot(leftPortalToCube)

    if (leftDotProduct < 0) {
        this.teleport(this.leftPortal[0], this.companionCube, this.companionCubeBody)
    }
}
}