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)
        }
    }
}