Code viewer for World: New World

// Cloned by Extra test on 2 May 2024 from World "3D AI World" by Nithin Sai K J 
// Please leave this clone trail here.
 
// import * as THREE from '/api/threemodule/libs/three.module.js';
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.117.1/build/three.module.js';
import { OrbitControls } from '/uploads/threejs/OrbitControls.js';
import { GLTFLoader } from '/uploads/threejs/GLTFLoader.js';
import { FBXLoader } from 'https://cdn.jsdelivr.net/npm/three@0.117.1/examples/jsm/loaders/FBXLoader.js';

// ENABLE AT OWN RISK
const USE_PROFANITY = false;

const scene = new THREE.Scene();
const loader = new GLTFLoader();

let r = 0;

const mixer = [];

/*
3D models:

1. FBX models by Mixamo: https://www.mixamo.com/#/?page=1&type=Character
2. GLTF models by Sketchfab: https://sketchfab.com/feed
*/

let totalModels = 7;
let loadedModels = 0;

function loadGLTF(x, y, z, url, mixer = null, scX = 1, scY = 1, scZ = 1, rx = 0, ry = 0, rz = 0) {
    loader.load(
        url,
        function (gltf) {
            loadedModels += 1;
            const model = gltf.scene;
            model.position.set(x, y, z);
            model.scale.set(scX, scY, scZ);
            model.rotation.x = rx;
            model.rotation.y = ry;
            model.rotation.z = rz;
            scene.add(model);

            if (gltf.animations && gltf.animations.length > 0 && mixer) {
                const animationMixer = new THREE.AnimationMixer(model);
                const animationAction = animationMixer.clipAction(gltf.animations[0]);
                animationAction.play();
                mixer.push(animationMixer);
            }
        },
        undefined,
        function (error) {
            console.log('An error happened');
            console.log(error);
        }
    );
}

loadGLTF(-3, 5.5, -5, '/uploads/nithinsai/scifi_light_02.glb', null, 0.03, 0.03, 0.03, 1.8, -0.03, 0.03);
loadGLTF(3, 5.5, -5, '/uploads/nithinsai/scifi_light_02.glb', null, 0.03, 0.03, 0.03, 1.8, 0.03, 0.03);
loadGLTF(50, 50, -240, '/uploads/nithinsai/sun_and_solar_flares.glb', null, 50, 50, 50);

/*
Credits:

scifi_light_02.glb : "SciFi Light 02" (https://skfb.ly/6nBOJ) by inuhitman is licensed under Creative Commons Attribution-NonCommercial (http://creativecommons.org/licenses/by-nc/4.0/).
sun_and_solar_flares: "Sun and solar flares" (https://skfb.ly/XZLG) by Chaitanya Krishnan is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
*/

const backPointLight = new THREE.PointLight(0xfcffb5, 2, 300);
backPointLight.position.set(10, 5, -240);
backPointLight.castShadow = true;
scene.add(backPointLight);

backPointLight.shadow.mapSize.width = 1024;
backPointLight.shadow.mapSize.height = 1024;
backPointLight.shadow.camera.near = 0.9;
backPointLight.shadow.camera.far = 300;


const sittingSoldier = "/uploads/nithinsai/MaleSittingPose.fbx";
const standingSoldier = "/uploads/nithinsai/MaleStandingPose.fbx";
const sadSoldier = "/uploads/nithinsai/SadWalk.fbx";
const dismissingGesture = "/uploads/nithinsai/DismissingGesture-Vanguard.fbx";
const thoughtfulHeadShake = "/uploads/nithinsai/ThoughtfulHeadShake-Vanguard.fbx";
const acknowledging = "/uploads/nithinsai/Acknowledging-Vanguard.fbx";
const sitting = "/uploads/nithinsai/Sitting-Vanguard.fbx";
const LayingIdle = "/uploads/nithinsai/LayingIdle-Vanguard.fbx";
const MaleSittingPose2 = "/uploads/nithinsai/MaleSittingPose2-Vanguard.fbx";
const FemaleDynamicPose2 = "/uploads/nithinsai/FemaleDynamicPose2-Vanguard.fbx";

/*
Credits:
All FBX models from Mixamo: https://www.mixamo.com/#/?page=1&type=Character
*/

const fbxLoader = new FBXLoader();
const animationActions = [];

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 9;
camera.position.y = 1.5;
camera.lookAt(scene.position);
const listener = new THREE.AudioListener();
camera.add(listener);

function loadFBX(x, y, z, url, scale = 1, rx = 0, ry = 0, rz = 0) {
    fbxLoader.load(
        url,
        (object) => {
            loadedModels += 1;
            object.position.set(x, y, z);
            object.scale.set(scale, scale, scale);
            object.rotation.x = rx;
            object.rotation.y = ry;
            object.rotation.z = rz;
            object.traverse(c => {
                if (c.isMesh) {
                    c.castShadow = true;
                }
            });
            object.add(sound);
            const mixer = new THREE.AnimationMixer(object);
            const animationAction = mixer.clipAction(object.animations[0]);
            animationAction.play();
            animationActions.push(animationAction);
            scene.add(object);
            return object;
        },
        (xhr) => {
            console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
        },
        (error) => {
            console.log(error);
        }
    );
}

const sittingSoldier1FXB = loadFBX(-10, -1.5, 0, sittingSoldier, 0.035, 0, 1.5, 0);
const FemaleDynamicPose2FXB = loadFBX(9.5, 4, -5, FemaleDynamicPose2, 0.035, 0, -1.5, 0);
const sitting1FXB = loadFBX(9.5, -1.5, 1, sitting, 0.035, 0, -1.5, 0);
const selfSoldierFXB = loadFBX(0, -1.5, 9, sittingSoldier, 0.035, 0, -3, 0);

camera.add(listener);

// create the PositionalAudio object (passing in the listener)
const sound = new THREE.PositionalAudio(listener);

// load a sound and set it as the PositionalAudio object's buffer
const audioLoader = new THREE.AudioLoader();


// MH edit
var renderer;
if ( AB.isScreenshotRun() )
	  renderer = new THREE.WebGLRenderer ( { preserveDrawingBuffer : true } );
else                       
	  renderer = new THREE.WebGLRenderer ( { preserveDrawingBuffer : false } );
	
	
// const renderer = new THREE.WebGLRenderer();

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enableZoom = false;
controls.target.set(0, 0, 0);
controls.update();

const textureLoader = new THREE.TextureLoader();

const floorG = new THREE.BoxGeometry(4, 1, 4);
const floorTexture = textureLoader.load('/uploads/nithinsai/1701264706.png');
const florMaterialWithTexture = new THREE.MeshStandardMaterial({
    map: floorTexture,
    roughness: 0.8, // Adjust as needed
    metalness: 0.2, // Adjust as needed
});

const floorGroup = new THREE.Group();

for (let x = -12; x <= 12; x += 4) {
    for (let z = -10; z <= 9; z += 4) {
        const floor = new THREE.Mesh(floorG, florMaterialWithTexture);
        floor.position.x = x;
        floor.position.y = -2;
        floor.position.z = z;
        floor.castShadow = true; // Enable shadow casting
        floor.receiveShadow = true; // Enable shadow receiving on the floor
        floorGroup.add(floor);
    }
}
scene.add(floorGroup);

// draw a cube for sitting

const cubeG = new THREE.BoxGeometry(1, 2, 1);
const cubeM = new THREE.MeshStandardMaterial({ color: 0x558866 });
const cube = new THREE.Mesh(cubeG, cubeM);
cube.position.set(9.5, -0.75, 1);
cube.rotation.y = 1.5;
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);


const roofG = new THREE.BoxGeometry(4, 1, 4);
const roofMaterialWithTexture = new THREE.MeshStandardMaterial({ map: floorTexture });

const createRoof = () => {
    const roofGroup = new THREE.Group();

    for (let x = -12; x <= 12; x += 4) {
        for (let z = -10; z <= 9; z += 4) {
            const roof = new THREE.Mesh(roofG, roofMaterialWithTexture);
            roof.position.set(x, 10, z);
            roof.rotation.y = -Math.PI;
            roof.receiveShadow = true;
            roofGroup.add(roof);
        }
    }

    scene.add(roofGroup);
};

createRoof();

const leftWallG = new THREE.BoxGeometry(4, 6, 1);
const leftWallTexture = textureLoader.load('/uploads/nithinsai/1701264706.png');
const leftWallMaterialWithTexture = new THREE.MeshStandardMaterial({ map: leftWallTexture });

const leftWallGroup = new THREE.Group();

const addLeftWalls = (x, y, zStart, zEnd) => {
    for (let z = zStart; z <= zEnd; z += 4) {
        const leftWall = new THREE.Mesh(leftWallG, leftWallMaterialWithTexture);
        leftWall.position.set(x, y, z);
        leftWall.rotation.y = Math.PI / 2;
        leftWall.receiveShadow = true;
        leftWallGroup.add(leftWall);
    }
};

addLeftWalls(-11, 1, -9, 10);
addLeftWalls(-12, 7, -9, 10);

scene.add(leftWallGroup);


const rightWallG = new THREE.BoxGeometry(4, 6, 1);
const rightWallTexture = textureLoader.load('/uploads/nithinsai/1701264706.png');
const rightWallMaterialWithTexture = new THREE.MeshStandardMaterial({ map: rightWallTexture });

const rightWallGroup = new THREE.Group();

const addRightWalls = (x, y, zStart, zEnd) => {
    for (let z = zStart; z <= zEnd; z += 4) {
        const rightWall = new THREE.Mesh(rightWallG, rightWallMaterialWithTexture);
        rightWall.position.set(x, y, z);
        rightWall.rotation.y = -Math.PI / 2;
        rightWall.receiveShadow = true;
        rightWallGroup.add(rightWall);
    }
};

addRightWalls(11, 1, -9, 8);
addRightWalls(12, 7, -9, 8);

scene.add(rightWallGroup);

const backWallG = new THREE.BoxGeometry(4, 6, 1);
const backWallMaterialWithTexture = new THREE.MeshStandardMaterial({ map: rightWallTexture });

const createBackWall = (y) => {
    const backWallGroup = new THREE.Group();

    for (let x = -10; x <= 10; x += 4) {
        const backWall = new THREE.Mesh(backWallG, backWallMaterialWithTexture);
        backWall.position.set(x, y, -10);
        backWall.receiveShadow = true;
        backWall.castShadow = true;
        backWallGroup.add(backWall);
    }

    scene.add(backWallGroup);
};

createBackWall(0.5);
createBackWall(7.5);

const backLeftPointLight = new THREE.PointLight(0xffffff, 1.3, 55, 5);
backLeftPointLight.position.set(-3, 6, -5);
backLeftPointLight.castShadow = true;
scene.add(backLeftPointLight);

backLeftPointLight.shadow.mapSize.width = 1024;
backLeftPointLight.shadow.mapSize.height = 1024;
backLeftPointLight.shadow.camera.near = 0.9;
backLeftPointLight.shadow.camera.far = 50;


const backRightPointLight = new THREE.PointLight(0xffffff, 1.3, 55, 5);
backRightPointLight.position.set(3, 6, -5);
backRightPointLight.castShadow = true;
scene.add(backRightPointLight);

backRightPointLight.shadow.mapSize.width = 1024;
backRightPointLight.shadow.mapSize.height = 1024;
backRightPointLight.shadow.camera.near = 0.9;
backRightPointLight.shadow.camera.far = 50;

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const mouse = new THREE.Vector2();
const windowHalf = new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2);

document.addEventListener('mousemove', onDocumentMouseMove, false);

function onDocumentMouseMove(event) {
    mouse.x = (event.clientX - windowHalf.x) / windowHalf.x;
    mouse.y = (event.clientY - windowHalf.y) / windowHalf.y;
}

let API_KEY = "";
const characterVoiceMap = {
    'Onyx': 'onyx',
    'Orion': 'shimmer',
    'Cyrus': 'echo',
};

const history = [
    {
        'character': "Onyx",
        'message': "You know, Cyrus, we've danced through hell together, haven't we? But now, here we are, prisoners of our own making. My loyalty, an unbreakable tether to duty, has led us to this cosmic prison hurtling towards the very flames we once embraced as our salvation. Funny how fate works its twisted magic."
    }
];

const div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = '50%';
div.style.left = '50%';
div.style.transform = 'translate(-50%, -50%)';
div.style.width = '500px';
div.style.backgroundColor = 'white';
div.style.color = 'black';
div.style.padding = '10px';
div.style.borderRadius = '10px';
div.style.display = 'none';
div.style.zIndex = '1000';
div.style.textAlign = 'center';
div.style.fontSize = '20px';
div.style.fontFamily = 'Arial';
div.style.fontWeight = 'bold';
div.id = 'loading-screen';

document.body.appendChild(div);

const input = document.createElement('input');
input.style.position = 'absolute';
input.style.top = '50%';
input.style.left = '50%';
input.style.transform = 'translate(-50%, -50%)';
input.style.width = '90%';
input.style.backgroundColor = 'white';
input.style.color = 'black';
input.style.padding = '10px';
input.style.borderRadius = '10px';
input.style.display = 'block';
input.style.zIndex = '1000';
input.style.fontSize = '20px';

const characterIputDiv = document.createElement('div');
characterIputDiv.style.position = 'absolute';
characterIputDiv.style.top = '70%';
characterIputDiv.style.left = '50%';
characterIputDiv.style.transform = 'translate(-50%, -50%)';
characterIputDiv.style.width = '300px';
characterIputDiv.style.backgroundColor = '#00000066';
characterIputDiv.style.color = 'white';
characterIputDiv.style.padding = '10px';
characterIputDiv.style.borderRadius = '10px';
characterIputDiv.style.display = 'none';
characterIputDiv.style.zIndex = '1000';
characterIputDiv.style.fontSize = '14px';
characterIputDiv.style.fontFamily = 'Arial';
characterIputDiv.style.fontWeight = 'bold';
characterIputDiv.innerText = 'Respond as Onyx';
characterIputDiv.id = 'character-input';

document.body.appendChild(characterIputDiv);

const characterInputElement = document.createElement('input');
characterInputElement.style.width = '100%';
characterInputElement.style.backgroundColor = '#00000000';
characterInputElement.style.color = 'white';
characterInputElement.style.border = 'none';
characterInputElement.style.outline = 'none';
characterInputElement.style.borderRadius = 'none';
characterInputElement.style.display = 'block';
characterInputElement.style.zIndex = '1000';
characterInputElement.style.fontSize = '14px';
characterInputElement.id = 'character-input-element';

characterIputDiv.appendChild(characterInputElement);



const loadingScreen = document.getElementById('loading-screen');

const updateCameraRotation = () => {
    camera.rotation.x = mouse.y * -0.3;
    camera.rotation.y = mouse.x * -0.3;
};

const updateMixers = () => {
    for (const mixer of animationActions.map((action) => action.getMixer())) {
        mixer.update(0.01);
    }
};

const handleApiKeyInput = () => {
    if (API_KEY === "") {
        div.innerHTML = "Please enter your API key";
        div.style.display = 'block';
        div.style.height = '150px';
        div.appendChild(input);
        input.focus();
        input.addEventListener('keyup', (e) => {
            if (e.key === 'Enter') {
                API_KEY = input.value;
                characterInput.style.display = 'block';
            }
        });
    }
};


const playPositionalAudio = (audio, position, onEnd) => {

// mh edit
console.log ( "playPositionalAudio");

    try {
        audioLoader.load(audio, function (buffer) {
            console.log("buffer");
            console.log(buffer);
            sound.setBuffer(buffer);
            sound.setRefDistance(20);
            sound.play();
        });

        // create an object for the sound to play from

        const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
        const material = new THREE.MeshBasicMaterial({ color: 0x00000000 });
        const cube = new THREE.Mesh(geometry, material);
        cube.position.set(position.x, position.y, position.z);
        scene.add(cube);

        // finally add the sound to the mesh
        cube.add(sound);

        // remove mesh after playing the sound
        sound.onEnded = () => {
            scene.remove(cube);
            onEnd();
            return true;
        };

    } catch (error) {
        console.error(error);
        return false;
    }

}


const renderScene = () => {
    loadingScreen.style.display = 'none';
    renderer.render(scene, camera);
};

const updateLoadingScreen = () => {
    loadingScreen.innerHTML = `Loading ${Math.floor((loadedModels / totalModels) * 100)}%`;
    loadingScreen.style.display = 'block';
};

const animate = function () {
    requestAnimationFrame(animate);

    controls.update();

    updateCameraRotation();
    updateMixers();

    if (loadedModels === totalModels) {
        if (API_KEY === "") {
            handleApiKeyInput();
        } else {
            renderScene();
        }
    } else {
        updateLoadingScreen();
    }

};

animate();

const playAudio = (audio, onEnd) => {
    
// mh edit
console.log ( "playAudio");
    
    try {
        const audioURL = URL.createObjectURL(audio);
        const audioElement = new Audio(audioURL);
        audioElement.play();
        audioElement.onended = () => {
            onEnd();
        }
    }
    catch (error) {
        console.error(error);
    }
};
 
const getAudio = async (text, voice, API_KEY) => {
    try {
        const url = 'https://api.openai.com/v1/audio/speech';

        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${API_KEY}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                model: 'tts-1',
                input: text,
                voice: voice,
            }),
        });

        if (!response.ok) {
            console.error(response);
            throw new Error('Network response was not ok');
        }

        const data = await response.blob();
        return data;
    } catch (error) {
        console.error(error);
        return error;
    }
};

const callChatAPI = async (userInput, history, API_KEY) => {
    var myHeaders = new Headers();
    myHeaders.append("Content-Type", "application/json");
    myHeaders.append("Access-Control-Allow-Origin", "*");

    var raw = JSON.stringify({
        "key": API_KEY,
        "userInput": userInput,
        history: history.map((message) => {
            return message.character + ": " + message.message + "\n";
        })[0],
        "profanity": USE_PROFANITY
    })

    var requestOptions = {
        method: 'POST',
        headers: myHeaders,
        body: raw,
        redirect: 'follow'
    };

    try {
        const response = await fetch("https://ai-backend-seven.vercel.app/api/openai", requestOptions);
        if (!response.ok) {
            console.error(response);
            return false;
        }
        const data = await response.json();
        console.log(data);
        return data;
    } catch (error) {
        console.error(error);
        return false;
    }
}


const characterInputDiv = document.createElement('div');
characterInputDiv.id = 'character-input-div';
characterInputDiv.style.position = 'absolute';
characterInputDiv.style.top = '70%';
characterInputDiv.style.left = '50%';
characterInputDiv.style.transform = 'translate(-50%, -50%)';
characterInputDiv.style.zIndex = 9999;
characterInputDiv.style.backgroundColor = '#00000066';
characterInputDiv.style.color = '#ffffff';
characterInputDiv.style.border = 'none';
characterInputDiv.style.outline = 'none';
characterInputDiv.style.padding = '8px';
characterInputDiv.style.borderRadius = '8px';
characterInputDiv.style.fontFamily = 'monospace';
characterInputDiv.style.fontSize = '14px';
characterInputDiv.style.fontWeight = 'bold';
characterInputDiv.style.display = 'flex';
characterInputDiv.style.alignItems = 'start';
characterInputDiv.style.justifyContent = 'start';
characterInputDiv.style.flexDirection = 'column';
characterInputDiv.style.gap = '8px';
characterInputDiv.style.width = '400px';
document.body.appendChild(characterInputDiv);

const messagesElement = document.createElement('div');
messagesElement.style.display = 'flex';
messagesElement.style.alignItems = 'start';
messagesElement.style.justifyContent = 'start';
messagesElement.style.flexDirection = 'column';
messagesElement.style.gap = '8px';
messagesElement.style.width = 'fit-content';
messagesElement.style.maxHeight = '300px';
messagesElement.style.overflowY = 'auto';
messagesElement.style.width = '100%';
messagesElement.style.wordWrap = 'break-word';
messagesElement.style.backgroundColor = '#00000066';
messagesElement.style.borderRadius = '8px';
characterInputDiv.appendChild(messagesElement);


const characterInput = document.createElement('input');
characterInput.type = 'text';
characterInput.id = 'character-character-input';

characterInput.style.zIndex = 9999;
characterInput.style.backgroundColor = '#00000088';
characterInput.style.color = '#ffffff';
characterInput.style.border = 'none';
characterInput.style.outline = 'none';
characterInput.style.padding = '4px 8px';
characterInput.style.width = '96%';
characterInput.style.borderRadius = '8px';
characterInput.style.display = 'none';
characterInput.style.fontFamily = 'monospace';
characterInput.placeholder = 'Respond as Onyx...';
characterInput.focus();
characterInputDiv.appendChild(characterInput);

// add a listener to the input

function messageElement(message) {
    const messageElement = document.createElement('div');
    messageElement.style.color = '#ffffff';
    messageElement.style.border = 'none';
    messageElement.style.outline = 'none';
    messageElement.style.width = '96%';
    messageElement.style.backgroundColor = '#00000044';
    messageElement.style.borderRadius = '8px';
    messageElement.style.fontFamily = 'monospace';
    messageElement.style.fontSize = '14px';
    messageElement.style.fontWeight = 'bold';
    messageElement.style.padding = '8px';
    messageElement.innerText = message;
    messagesElement.appendChild(messageElement);
}

var showInput = true;

function inputs() {
    if (showInput) {
        characterInputDiv.style.display = 'flex';
    } else {
        characterInputDiv.style.display = 'none';
    }
}

// if (API_KEY !== "") {
//     showInput = false;
//     const audio = await getAudio(history[0].message, "onyx", API_KEY);
//     playAudio(audio, async () => {
//         showInput = false;
//     })
// }

characterInput.addEventListener('keydown', async function (e) {
    if (e.key === 'Enter') {

        if (showInput === false) {
            return;
        }

        var characterInputValue = characterInput.value;
        characterInput.value = '';
        console.log(characterInputValue);

        history.push({
            'character': "Onyx",
            'message': characterInputValue
        });

        messageElement("Onyx: " + characterInputValue);

        const audio = await getAudio(characterInputValue, "onyx", API_KEY);
        playAudio(audio, async () => {
            const response = await callChatAPI(characterInputValue, history, API_KEY);
            if (response) {
                showInput = false;
                history.push({
                    'character': response['character'],
                    'message': response['output']
                });
                messageElement(response['character'] + ": " + response['output']);

                const audio = await getAudio(response['output'], characterVoiceMap[response['character']], API_KEY);
                playAudio(audio, async () => {
                    showInput = true;
                    inputs();
                });

            }
        });
    }
});

inputs();