Code viewer for World: New World (clone by MENGTE...

// Cloned by MENGTE ZHU on 21 Mar 2024 from World "New World" by Michael Walsh 
// Please leave this clone trail here.
 
'use strict';
// imported from https://threejs.org/examples/misc_controls_pointerlock.html

import * as THREE from "/uploads/mjwalsh/three.module.js";
import { GLTFLoader } from '/uploads/mjwalsh/GLTFLoader.js';
import * as SkeletonUtils from '/uploads/mjwalsh/SkeletonUtils.js';

const SKYDISTANCE = 900; // Distance of stars and sun
const NBSTARS = 100; //number of stars
const PI_2 = Math.PI / 2;
const LEVEL = 4;

const STANDING = 'Idle';
const JUMPING = 'TPose';
const RUNNING = 'Run';
const WALKING = 'Walk';
const FADE_DURATION = 0.2;

let gWorldId = 0;
let gGLTF = null;
let gSoldier = null
let gSocketStarted = false;
let gPlayers = {};
let gPlayerObjects = {};
let gPlayerId;
let gLastMessage = 0;
let gInitialised = false;
let gCamera;
let gPlayerOne;
let gControls;
let gScene;
let gWorld;
let gGoForward = false;
let gLookLeft = false;
let gLookRight = false;
let gCanJump = false;
let gLookUp = false;
let gLookDown = false;
const gGltfLoader = new GLTFLoader();

class World
{
    renderer;
    objects = [];
    sun;
    stars;
    hour;
    gltf;

    clock = new THREE.Clock(true);
    velocity = new THREE.Vector3();
    vertex = new THREE.Vector3();

    constructor(ws) {
        gWorld = this;
        this.gltf = ws;

        AB.msg('<ul style="padding-left: 2ch; user-select: none">'
            + '<li>Move back and forward using up and down keys</li>'
            + '<li>Rotate view using left and right keys</li>'
            + '<li>Look up and down using your A and Z keys</li>'
            + '<li id="lockUnlock">Click the screen to lock in your mouse</li>');

        const color = new THREE.Color();

        gCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
        gCamera.position.y = LEVEL;

        gScene = new THREE.Scene();
        gScene.add(gCamera);

        gControls = new Controls();
        gPlayerOne = new Player();

        document.addEventListener('click', () => {
            gControls.lock();
        });

        // grass
        let floorGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100);
        floorGeometry.rotateX(- Math.PI / 2);

        let position = floorGeometry.attributes.position;

        for (let i = 0, l = position.count; i < l; i ++) {

            this.vertex.fromBufferAttribute(position, i);

            this.vertex.x += THREE.MathUtils.seededRandom() * 20 - 10;
            this.vertex.y += THREE.MathUtils.seededRandom() * 2;
            this.vertex.z += THREE.MathUtils.seededRandom() * 20 - 10;

            position.setXYZ(i, this.vertex.x, this.vertex.y, this.vertex.z);
        }

        floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices

        position = floorGeometry.attributes.position;
        const colorsFloor = [];

        for (let i = 0, l = position.count; i < l; i ++) {
            switch(Math.floor(THREE.MathUtils.seededRandom() * 3)) {
            case 0:
                colorsFloor.push(0.6, 0.98, 0.6);
                break;
            case 1:
                colorsFloor.push(0.13, 0.55, 0.13);
                break;
            case 2:
                colorsFloor.push(0.2, 0.8, 0.2);
                break;
            }
        }

        floorGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colorsFloor, 3));

        const floorMaterial = new THREE.MeshLambertMaterial({ vertexColors: true });
        const floor = new THREE.Mesh(floorGeometry, floorMaterial);
        floor.receiveShadow = true;
        gScene.add(floor);

        // calculate the dimensions of the city model
        const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 0));
        box.expandByObject(this.gltf.scene);
        const dim = new THREE.Vector3();
        box.getSize(dim);

        // add nine cities
        for(let i = 0; i < 3; i++) {
            for(let j = 0; j < 3; j++) {
                const nCity = SkeletonUtils.clone(this.gltf.scene);
                nCity.position.set(dim.x * i, 2, dim.z * j);
                gScene.add(nCity);
            }
        }

        // add the sun
        this.sun = new Sun(30, 3);
        this.stars = new Stars(30);

        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.renderer.shadowMap.enabled = true;

        document.body.appendChild(this.renderer.domElement);

        // capture resize events
        window.addEventListener('resize', this.onWindowResize);
    }

    onWindowResize = () => {
        gCamera.aspect = window.innerWidth / window.innerHeight;
        gCamera.updateProjectionMatrix();

        this.renderer.setSize(window.innerWidth, window.innerHeight);
    };

    animate = () => {
        this.hour = ((new Date().getTime() % 120000) / 120000) * 24;

        requestAnimationFrame(this.animate);

        const delta = this.clock.getDelta();

        this.velocity.x -= this.velocity.x * 5 * delta;
        this.velocity.z -= this.velocity.z * 5 * delta;
        this.velocity.y -= 490 * delta;

        let z = Number(gGoForward);
        let x = Number(gLookRight) - Number(gLookLeft);
        let y = Number(gLookDown) - Number(gLookUp);

        if (z != 0) this.velocity.z -= z * 200 * delta;

        // pretend to be a mouse
        gControls.changeAngles(20 * x, 20 * y);

        // moving around on the flat
        gControls.moveForward(- this.velocity.z * (delta / 2));

        // change height
        gPlayerOne.model.position.y += (this.velocity.y * (delta / 10));
        gCamera.position.y += (this.velocity.y * (delta / 10));

        if (gPlayerOne.model.position.y < LEVEL - 2) {
            this.velocity.y = 0;
            gPlayerOne.model.position.y = LEVEL - 2;

            gCanJump = true;
        }

        // set model speed
        let pose;
        const speed = Math.abs(this.velocity.z);
        if(gPlayerOne.model.position.y > 4) pose = JUMPING;
        else if(speed < 1) pose = STANDING;
        else if(speed > 20) pose = RUNNING;
        else pose = WALKING;

        AB.socketOut({
            action: 'player_pos',
            worldId: gWorldId,
            player: gPlayerId,
            position: {
                x: gPlayerOne.model.position.x,
                y: gPlayerOne.model.position.y,
                z: gPlayerOne.model.position.z,
            },
            pose: pose,
            quaternion: gPlayerOne.model.quaternion.toArray(),
        });

        for(const p in gPlayers) {
            if(!gPlayerObjects[p])
                gPlayerObjects[p] = new Player();

            gPlayerObjects[p].animate(gPlayers[p]);
        }

        this.sun.animate();
        this.stars.animate();
        this.renderer.render(gScene, gCamera);
    };
}

class Controls extends THREE.EventDispatcher
{
    changeEvent = { type: 'change' };
    lockEvent = { type: 'lock' };
    unlockEvent = { type: 'unlock' };

    startY;
    startX;

    constructor() {
        super();

        // Set to constrain the pitch of the camera
        // Range is 0 to Math.PI radians
        this.maxPolarAngle = Math.PI; // radians

        this.pointerSpeed = 1.0;

        document.addEventListener('keydown', this.onKeyDown);
        document.addEventListener('keyup', this.onKeyUp);
        document.addEventListener('touchstart', this.onTouchStart);
        document.addEventListener('touchmove', this.onTouchMove);
        this.connect();
    }

    connect = () => {
        document.addEventListener('pointerlockchange', this.onPointerlockChange);
        document.addEventListener('pointerlockerror', this.onPointerlockError);
        document.addEventListener('mousedown', this.onMouseDown);
        document.addEventListener('mouseup', this.onMouseUp);
    };

    disconnect = () => {
        document.removeEventListener('pointerlockchange', this.onPointerlockChange);
        document.removeEventListener('pointerlockerror', this.onPointerlockError);
        document.removeEventListener('mousedown', this.onMouseDown);
        document.removeEventListener('mouseup', this.onMouseUp);
    };

    dispose = () => {
        this.disconnect();
    };

    getDirection = (v) => {
        return v.set(0, 0, - 1).applyQuaternion(gPlayerOne.model.quaternion);
    };

    moveForward = (distance) => {
        // move forward parallel to the xz-plane
        // assumes camera.up is y-up
        let vector = new THREE.Vector3();
        vector.setFromMatrixColumn(gPlayerOne.model.matrix, 0);
        vector.crossVectors(gPlayerOne.model.up, vector);        
        gPlayerOne.animate2(vector, distance);
        
        const v = gPlayerOne.model.position.clone();
        vector.setLength(4);
        v.sub(vector);
        
        gCamera.position.x = v.x;
        gCamera.position.y = gPlayerOne.model.position.y + 2;
        gCamera.position.z = v.z;
    };

    lock = () => {
        document.body.requestPointerLock();
    };

    unlock = () => {
        document.exitPointerLock();
    };

    // event listeners

    onMouseDown = event => {
        gGoForward = true;
    }

    onMouseUp = event => {
        gGoForward = false;
    }

    onMouseMove = (event) => {
        this.changeAngles(event.movementX, event.movementY);
    }

    onTouchStart = (e) => {
        if(e.touches.length != 1) return;

        this.startY = e.touches[0].pageY;
        this.startX = e.touches[0].pageX;
    };

    onTouchMove = (e) => {
        if(e.touches.length != 1) return;

        const x = this.startX - e.touches[0].pageX;
        const y = this.startY - e.touches[0].pageY;

        this.startY = e.touches[0].pageY;
        this.startX = e.touches[0].pageX;

        this.changeAngles(x, y);
    };

    onKeyDown = (event) => {
        switch (event.code) {
        case 'KeyA':       gLookUp = true;    break;
        case 'KeyZ':       gLookDown = true;  break;
        case 'ArrowUp':    gGoForward = true; break;
        case 'ArrowLeft':  gLookLeft = true;  break;
        case 'ArrowRight': gLookRight = true; break;

        case 'Space':
            if (gCanJump === true) gWorld.velocity.y += 350;
            gCanJump = false;
            break;
        }
    };

    onKeyUp = (event) => {
        switch (event.code) {
        case 'KeyA':       gLookUp = false;    break;
        case 'KeyZ':       gLookDown = false;  break;
        case 'ArrowUp':    gGoForward = false; break;
        case 'ArrowLeft':  gLookLeft = false;  break;
        case 'ArrowRight': gLookRight = false; break;
        }
    };

    changeAngles = (x, y) => {
        // do the camera first
        const euler = new THREE.Euler(0, 0, 0, 'YXZ');
        euler.setFromQuaternion(gCamera.quaternion);
        euler.y -= x * 0.002 * this.pointerSpeed;
        euler.x -= y * 0.002 * this.pointerSpeed;
        euler.x = THREE.MathUtils.clamp(euler.x, -PI_2, PI_2);
        gCamera.quaternion.setFromEuler(euler);

        // don't recline the soldier
        euler.x = 0;
        gPlayerOne.model.quaternion.setFromEuler(euler);
    };

    onPointerlockChange = () => {
        if (document.pointerLockElement === document.body) {
            document.addEventListener('mousemove', this.onMouseMove);
        } else {
            document.removeEventListener('mousemove', this.onMouseMove);
        }
    };

    onPointerlockError = () => {
        console.error('Unable to use Pointer Lock API');
    };
}


class Sun
{
    size;
    speed;
    angle = 0;
    d = 1000;
    sunlight = new THREE.DirectionalLight(0xffffff, 0.1);
    sunball;

    // size (number): the size of the sun & stars
    // speed (number): the speed of the rotation of the sun
    constructor(size, speed) {
        this.size = size;
        this.speed = speed;

        this.sunlight.position.set(0, -SKYDISTANCE, 0);

        this.sunlight.castShadow = true;

        this.sunball = new THREE.Mesh(new THREE.SphereGeometry(this.size, 32, 32),
                                     new THREE.MeshBasicMaterial({color : "yellow", fog: false}));

        gScene.add(this.sunlight);
        gScene.add(this.sunball);
    }

    map(n, start1, stop1, start2, stop2)
    {
        return ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
    }

    //Use this in nextStep to make the sun move
    animate = () =>
    {
        // a day lasts two minutes
        // this way time is the same in different computers
        this.angle = gWorld.hour / 12;

        const sunPos = new THREE.Vector3(
            0,
            Math.sin(this.angle * Math.PI) * SKYDISTANCE,
            Math.cos(this.angle * Math.PI) * SKYDISTANCE);

        this.sunball.position.copy(sunPos);

        // place the light source a little higher
        sunPos.y += 8500;
        this.sunlight.position.copy(sunPos);

        this.sunlight.intensity = this.getSunIntensity();

        gScene.background = new THREE.Color(this.getSkyColor());
    };

    /**
     * A linear interpolator for hexadecimal colors
     * @param {Int} a
     * @param {Int} b
     * @param {Number} amount
     * @example
     * // returns 0x7F7F7F
     * lerpColor(0x000000, 0xffffff, 0.5)
     * @returns {Int}
     */
    lerpColor(a, b, amount) {

        let ah = a,
            ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
            bh = b,
            br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
            rr = ar + amount * (br - ar),
            rg = ag + amount * (bg - ag),
            rb = ab + amount * (bb - ab);

        return ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0);
    }

    //return the color of the sky depending of the position of the sun (to get the sunrise)
    getSkyColor()
    {
        // range from 0 to 16
        let x = this.angle * 8;
        switch(Math.floor(x)) {
        case 0:
            return this.lerpColor(0xfd5e53, 0x7ec0ee, this.map(x, 0, 1, 0, 1));
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
        default:
        case 6:
            return 0x7ec0ee;
        case 7:
            return this.lerpColor(0x7ec0ee, 0xfd5e53, this.map(x, 7, 8, 0, 1));
        case 8:
            return this.lerpColor(0xfd5e53, 0x0c3166, this.map(x, 8, 9, 0, 1));
        case 9:
        case 10:
        case 11:
        case 12:
        case 13:
        case 14:
            return 0x0c3166;
        case 15:
        case 16:
            return this.lerpColor(0x0c3166, 0xfd5e53, this.map(x, 15, 16, 0, 1));
        }
    }

    //return intensity of the sun
    getSunIntensity()
    {
        // range from 0 to 16
        let x = this.angle * 8;

        if (x > 1 && x <= 7)
        {
            return 2;
        }
        else if (x > 7 && x <= 8)
        {
            return this.map(x, 7, 8, 2, 1);
        }
        else if (x > 8 && x <= 9)
        {
            return this.map(x, 8, 9, 1, 0);
        }
        else if (x > 9 || x <= 15)
        {
            return 0;
        }
        else if (x > 15)
        {
            return this.map(x, 15, 16, 0, 1);
        }
        else if (x <= 1)
        {
            return this.map(x, 0, 1, 1, 2);
        }
    }
}


class Stars
{
    stars = [];
    starMaterial;
    moonMaterial;
    starGyroscope = new THREE.Mesh();
    moonlight = new THREE.DirectionalLight(0xffffff, 0.1);;
    size;

    constructor(size) {
        this.starMaterial = new THREE.MeshBasicMaterial({color : "white", transparent: true, fog: false}) ;
        this.starMaterial.transparent = true;
        this.starMaterial.opacity = 0.2;
        this.size = size;

        this.moonlight.castShadow = true;
        this.moonlight.shadow.mapSize.width = 1024;
        this.moonlight.shadow.mapSize.height = 1024;
        this.moonlight.shadow.camera.near = 10;
        this.moonlight.shadow.camera.far = 4000;
        this.moonlight.shadow.camera.left = -this.d;
        this.moonlight.shadow.camera.right = this.d;
        this.moonlight.shadow.camera.top = this.d;
        this.moonlight.shadow.camera.bottom = -this.d;
        this.moonlight.shadow.bias = -0.0001;
        this.moonlight.shadow.radius = 0.05;
        this.moonlight.shadow.normalBias = 0.05;

        this.moonMaterial = new THREE.MeshBasicMaterial({color: "lightyellow", transparent: true, fog: false})
        let moon = new THREE.Mesh(new THREE.SphereGeometry(50, 32, 32), this.moonMaterial);
        moon.position.set(-658, 376, -484);
        this.starGyroscope.add(moon);

        this.moonlight.position.copy(moon.position);

        gScene.add(this.moonlight);

        gScene.add(this.starGyroscope);

        //Create all this.stars
        for (let i = 0; i < NBSTARS; i++)
        {
            let radius = Stars.randomFloatAtoB (this.size/25, this.size/8);
            let star = new THREE.Mesh(new THREE.SphereGeometry(radius, 8, 8), this.starMaterial);
            let s = Stars.randomFloatAtoB (0, Math.PI * 2);
            let t = Stars.randomFloatAtoB (0, Math.PI / 2);
            star.position.set(SKYDISTANCE*Math.cos(s)*Math.sin(t), SKYDISTANCE*Math.cos(t), SKYDISTANCE*Math.sin(s)*Math.sin(t));
            this.starGyroscope.add(star);
        }
    }

    static randomFloatAtoB(t, n) {
        return t + THREE.MathUtils.seededRandom() * (n - t);
    }

    map(n, start1, stop1, start2, stop2)
    {
        return ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
    }

    // show or hide the moon and stars depending on the hour
    animate = () =>
    {
        if (gWorld.hour > 0.5 && gWorld.hour < 11.5)
        {
            // day time
            this.moonMaterial.opacity = this.starMaterial.opacity = 0;
            //this.moonlight.visible = false;
        }

        else if (gWorld.hour >= 11.5 && gWorld.hour <= 12.5)
        {
            this.moonMaterial.opacity = this.starMaterial.opacity = gWorld.hour - 11.5;
            //this.moonlight.visible = false;
        }
        else if (gWorld.hour > 12.5 && gWorld.hour < 23.5)
        {
            // night time
            this.moonMaterial.opacity = this.starMaterial.opacity = 1;
            //this.moonlight.visible = true;
        }
        else
        {
            if(gWorld.hour <= 0.5) gWorld.hour += 24;

            // night time
            this.moonMaterial.opacity = this.starMaterial.opacity = 24.5 - gWorld.hour;
            //this.moonlight.visible = false;
        }
    }
}

class Player
{
    model = null;
    mixer = null;
    animations = {};
    currentPose = STANDING;
    static clock = new THREE.Clock();

    constructor() {
        this.model = SkeletonUtils.clone(gSoldier.scene);

        console.log("creating player");
        this.mixer = new THREE.AnimationMixer(this.model);

        for(const animation of gSoldier.animations) {
            this.animations[animation.name] = this.mixer.clipAction(animation);
        }

        this.animations[this.currentPose].play();
        console.log("loaded a soldier");
        gScene.add(this.model);
        this.mixer.update(Player.clock.getDelta());
        this.model.position.set(20, 2, 0);
    }

    animate(d) {
        this.model.position.copy(d.position);
        this.model.quaternion.fromArray(d.quaternion);
        if(d.pose != this.currentPose) {
            this.animations[this.currentPose].fadeOut(FADE_DURATION);
            this.animations[d.pose].reset().fadeIn(FADE_DURATION).play();
            this.currentPose = d.pose;
        }
        this.mixer.update(Player.clock.getDelta());
    }

    animate2(vector, distance) {
        this.model.position.addScaledVector(vector, distance);
  
        // set model speed
        let pose;
        if(this.model.position.y > 4) pose = JUMPING;
        else if(Math.abs(distance) < 0.01) pose = STANDING;
        else if(Math.abs(distance) > 1) pose = RUNNING;
        else pose = WALKING;
    
        if(pose != this.currentPose) {
            this.animations[this.currentPose].fadeOut(FADE_DURATION);
            this.animations[pose].reset().fadeIn(FADE_DURATION).play();
            this.currentPose = pose;
        }
        this.mixer.update(Player.clock.getDelta());
    }
}

function startUp() {
    if(gInitialised || !gGLTF || !AB.socket || !gSoldier) return;

    gInitialised = true;

    if(gWorldId == 0) {
        //create a new world since we have not received one
        gWorldId = Math.random();
        console.log("creating a new world", gWorldId);
    } else {
        console.log("joining an existing new world", gWorldId);
    }

    // this will create and identical world based on the same seed
    THREE.MathUtils.seededRandom(gWorldId);
    gPlayerId = Math.random();

    let w = new World(gGLTF);
    w.animate();
}

// https://stackoverflow.com/questions/7307983/while-variable-is-not-defined-wait

// This nifty piece of code waits for the socket to be available
// I couldn't find a event handler for this
AB.socketStart();
AB.runReady = true;
Object.defineProperty(AB, "socket", {
    configurable: true,
    set(v) {
        Object.defineProperty(AB, "socket", { value: v });
        console.log("Socket has started");
        setTimeout(startUp, 2000);
    }
});

gGltfLoader.load('/uploads/mjwalsh/ccity_building_set_1.glb', (gltf) => {
    gGLTF = gltf;
    gGLTF.scene.traverse((object) => {
        if (object.isMesh) object.castShadow = true;
    });
    gGLTF.scene.scale.set(0.01, 0.01, 0.01);
    setTimeout(startUp, 2000);
}, null, null);

gGltfLoader.load('/uploads/mjwalsh/Soldier.glb', (gltf) => {
    gSoldier = gltf;
    gSoldier.scene.traverse((object) => {
        if (object.isMesh) object.castShadow = true;
    });
    gSoldier.scene.scale.set(1.75, 1.75, 1.75);
    setTimeout(startUp, 2000);
}, null, null);


AB.socketIn = function(data)
{
    if (!AB.runReady) return;

    if(gWorldId == 0) gWorldId = data.worldId;

    switch(data.action) {
    case 'player_pos':
        if(gWorldId == data.worldId)
            gPlayers[data.player] = data;
        break;
    }
};