Code viewer for World: Infinite World Ground + Su...
// Cloned by Michael Walsh on 29 Oct 2023 from World "Infinite World Ground + Sun" by Enhanced
// Please leave this clone trail here.

//==============================================================================
//  Welcome to the Infinite world with only grounds !
//==============================================================================

// This program was made by Nathan Bonnard.
// In this world, you can generate an infinite world !
// But this isn't really infinite, it would be impossible,
// the world do a simple loop when the player reach the border. Like in real life !
// This is a little version of the real infinite world that you can find aswell on the enhanced page

'use strict';

const squaresize = 7 ; // The world is like a infinite grid of square. squaresize is the size of a square
const MOVESPEED = 7; // Speed of the player

const viewDistance = 2; // determinate the maximum distance to see ground (viewDistance * squaresize * groundMultiplier)
const groundMultiplier = 551; // The world is divided by zone (grounds) that size groundMultiplier*squaresize
const worldSize = 13; // dimension of the world, it is not really infinite but big and can loop. There is worldSize * worldSize Grounds

const SKYDISTANCE = 3000; // Distance of stars and sun
const NBSTARS = 100; //number of stars

const MAXPOS				= 4000 ;								
const startRadiusConst	 	= MAXPOS * 0.5 ;		// distance from centre to start the camera at
const maxRadiusConst 		= MAXPOS * 5 ;		// maximum distance from camera we will render things

const SKYCOLOR 	= 0x6495ED;
const LIGHTCOLOR = 0xffffff;

ABHandler.MAXCAMERAPOS = MAXPOS * 10 ;// allow camera go far away
ABWorld.drawCameraControls = false;

AB.clockTick	   = 20;	

// Speed of run: Step every n milliseconds. Default 100.

AB.maxSteps		= 1000000;	

// Length of run: Maximum length of run in steps. Default 1000.

AB.screenshotStep  = 50;

// Take screenshot on this step. (All resources should have finished loading.) Default 50.


//==============================================================================
//  Defines the THREE.PointerLockControls class, source at https://threejs.org/
//==============================================================================

class PointerLock  { // extends THREE.PointerLockControls
    pitchObject = new THREE.Object3D();
    yawObject = new THREE.Object3D();
    attachedObject = new THREE.Object3D();
    PI_2 = Math.PI / 2;

    // assumes the camera itself is not rotated
    direction = new THREE.Vector3( 0, 0, - 1 );
    rotation = new THREE.Euler( 0, 0, 0, 'YXZ' );

	enabled = false;

	constructor(camera) {
        camera.rotation.set( 0, 0, 0 );

        this.pitchObject.add( camera );

        this.yawObject.position.y = 10;
        this.yawObject.add( this.pitchObject );

        this.yawObject.add(this.attachedObject);
        this.attachedObject.position.set(0, -this.yawObject.position.y, -150);

        document.addEventListener( 'mousemove', this.onMouseMove, false );
	}

    onMouseMove = (event) => {
        if ( this.enabled === false ) return;

        let movementX = event.movementX || 0;
        let movementY = event.movementY || 0;

        this.yawObject.rotation.y -= movementX * 0.002;
        this.pitchObject.rotation.x -= movementY * 0.002;

        this.pitchObject.rotation.x = Math.max( - this.PI_2, Math.min( this.PI_2, this.pitchObject.rotation.x ) );
    };

	getAttachedObject() {
		return this.attachedObject;
	}

	dispose() {
		document.removeEventListener( 'mousemove', this.onMouseMove, false );
	}

	getObject() {
		return this.yawObject;
	}

	getDirection(v) {
        rotation.set( pitchObject.rotation.x, yawObject.rotation.y, 0 );
        v.copy( this.direction ).applyEuler( this.rotation );
        return v;
	}
}

//==============================================================================
//  Sun Class
//==============================================================================

// Here is the Sun Object.
class Sun
{
    target;
    size;
    speed;
    skyDistance;
    angle = 0;
    object;
    d = 1000;
    stars = [];
    starMaterial;
    starGyroscope = new THREE.Mesh();
    object = new THREE.DirectionalLight( 0xffffff, 0.1 );

    // target (Object3D) : the player, or camera if you are in first person view. The sun need to be relative to a point
    // size (number): the size of the sun & stars
    // speed (number): the speed of the rotation of the sun
    // skyDistance (number): the distance of the sky from the target.
    constructor(target, size, speed, skyDistance) {
        this.target = target;
        this.size = size;
        this.speed = speed;
        this.skyDistance = skyDistance;

        this.object.position.set(0,
                                 Math.cos(this.angle) * this.skyDistance,
                                 Math.sin(this.angle) * this.skyDistance
                                 );
        this.object.castShadow = true;
        this.object.shadow.mapSize.width = 1024;
        this.object.shadow.mapSize.height = 1024;
        this.object.shadow.camera.near = 10;
        this.object.shadow.camera.far = 4000;
        this.object.shadow.camera.left = -this.d;
        this.object.shadow.camera.right = this.d;
        this.object.shadow.camera.top = this.d;
        this.object.shadow.camera.bottom = -this.d;
        this.object.shadow.bias = -0.0001;

        ABWorld.scene.add(this.object);
        this.starMaterial = new THREE.MeshBasicMaterial({color : "white", fog: false}) ;
        this.starMaterial.transparent = true;
        this.starMaterial.opacity = 0;

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

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


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

    //Use this in nextStep to make the sun move
    animate()
    {
        this.angle += 0.001 * this.speed;

        //normalize this.angle between -PI and PI
        while (this.angle <= -Math.PI) this.angle += Math.PI*2;
        while (this.angle > Math.PI) this.angle -= Math.PI*2;

        this.object.position.set(this.target.position.x, Math.cos(this.angle)*this.skyDistance, Math.sin(this.angle)*this.skyDistance + this.target.position.z);

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

        let c = new THREE.Color(this.getSkyColor());
        ABWorld.scene.background = c;

        this.setStarsOpacity();
        this.starGyroscope.rotation.set(-this.target.rotation.x, -this.target.rotation.y, -this.target.rotation.z);
    }

    //change star opacity and fog depending of the position of the sun
    setStarsOpacity()
    {
        if (this.angle > Math.PI/2  &&  this.angle < Math.PI*3/4)
        {
            this.starMaterial.opacity = this.map(this.angle, Math.PI/2, Math.PI*3/4, 0, 0.8);
        }
        else if (this.angle > -Math.PI*3/4  &&  this.angle < -Math.PI/2)
        {
            this.starMaterial.opacity = this.map(this.angle, -Math.PI*3/4, -Math.PI/2, 0.8, 0);
        }
        console.log(Math.round(this.angle,1), Math.round((this.angle / Math.PI) * 8, 1), this.starMaterial.opacity);
    }

    /**
     * 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()
    {
        if (this.angle > -Math.PI*3/8  &&  this.angle < Math.PI*3/8)
        {
            // console.log("day");
            return 0x7ec0ee;
        }
        else if (this.angle > Math.PI*3/8  &&  this.angle < Math.PI/2)
        {
            // console.log("Sunset 1");
            return this.lerpColor(0x7ec0ee, 0xfd5e53, this.map(this.angle, Math.PI*3/8,Math.PI/2,0,1));
        }
        else if (this.angle > Math.PI/2  &&  this.angle < Math.PI*5/8)
        {
            // console.log("Sunset 2");
            return this.lerpColor(0xfd5e53, 0x0c3166, this.map(this.angle, Math.PI/2,Math.PI*5/8,0,1));
        }
        else if (this.angle > Math.PI*5/8  ||  this.angle < -Math.PI*3/4)
        {
            // console.log("night");
            return 0x0c3166;
        }
        else if (this.angle > -Math.PI*3/4  &&  this.angle < -Math.PI/2)
        {
            // console.log("Sunrise 1");
            return this.lerpColor(0x0c3166, 0xfd5e53, this.map(this.angle, -Math.PI*3/4,-Math.PI/2,0,1));
        }
        else if (this.angle > -Math.PI/2  &&  this.angle < -Math.PI*3/8)
        {
            // console.log("Sunrise 2");
            return this.lerpColor(0xfd5e53, 0x7ec0ee, this.map(this.angle, -Math.PI/2, -Math.PI*3/8, 0, 1));
        }
    }

    //return intensity of the sun
    getSunIntensity()
    {
        if (this.angle > -Math.PI*3/8  &&  this.angle < Math.PI*3/8)
        {
            return 2;
        }
        else if (this.angle > Math.PI*3/8  &&  this.angle < Math.PI/2)
        {
            return this.map(this.angle, Math.PI*3/8,Math.PI/2,2,1);
        }
        else if (this.angle > Math.PI/2  &&  this.angle < Math.PI*3/4)
        {
            return this.map(this.angle, Math.PI/2,Math.PI*3/4,1,0);
        }
        else if (this.angle > Math.PI*3/4  ||  this.angle < -Math.PI*3/4)
        {
            return 0;
        }
        else if (this.angle > -Math.PI*3/4  &&  this.angle < -Math.PI/2)
        {
            return this.map(this.angle, -Math.PI*3/4,-Math.PI/2,0,1);
        }
        else if (this.angle > -Math.PI/2  &&  this.angle < -Math.PI*3/8)
        {
            return this.map(this.angle, -Math.PI/2, -Math.PI*3/8, 1, 2);
        }
    }
}

//==============================================================================
//  World Definition
//==============================================================================
class World {

    //==============================================================================
    //  All variables
    //==============================================================================

    groundTexture; // Texture of the ground

    grounds; // Array2d : Stock all ground of the world
    pai;
    paj; // past position in grounds of the player
    raycaster; // to determine direction of a click

    controls;

    moveForward = false;
    moveBackward = false;
    moveLeft = false;
    moveRight = false;
    canJump = false;

    prevTime = performance.now();
    velocity = new THREE.Vector3();

    sun;//sun and all functions with it (also stars)

    endCondition = false;

    //==============================================================================
    //==============================================================================

    //==============================================================================
    //==============================================================================

    //Just return a ground Elements that receive shadow
    createGround() {
        let gr = new THREE.Mesh (
            new THREE.PlaneGeometry ( squaresize*groundMultiplier, squaresize*groundMultiplier ),
            this.groundTexture );
        gr.receiveShadow = true;
        gr.rotation.x = (Math.PI / 2) * 3;
        return gr;
    }

    //initialize the ground of the world, to determine all type nd create fake this.grounds
    //around the world to fake a loop
    initGround() {
        for(let i = 0; i < worldSize; i++)
        {
            for(let j = 0; j < worldSize; j++)
            {
                this.grounds[i][j] = this.createGround();
                this.grounds[i][j].position.set((i- Math.trunc(worldSize/2)) * squaresize * groundMultiplier,0, (j- Math.trunc(worldSize/2)) * squaresize*groundMultiplier);
                ABWorld.scene.add(this.grounds[i][j]);
            }
        }
    
        for(let i = - viewDistance; i < worldSize + viewDistance; i++)
        {
            for(let j = - viewDistance; j < worldSize + viewDistance; j++)
            {
                let b = (worldSize + viewDistance);
            
                if(i < 0 || j < 0 || i >= worldSize || j >= worldSize)
                {
                    let tmp;
                    if( i < 0)
                    {
                        if(j < 0)
                        {
                            tmp = this.grounds[worldSize + i][worldSize + j].clone();
                        }
                        else if(j >= worldSize)
                        {
                            tmp = this.grounds[worldSize + i][j - worldSize].clone();
                        }
                        else
                        {
                            tmp = this.grounds[worldSize + i][j].clone();
                        }
                    
                    }
                    else if(i >= worldSize)
                    {
                        if(j < 0)
                        {
                            tmp = this.grounds[i - worldSize][worldSize + j].clone();
                        }
                        else if(j >= worldSize)
                        {
                            tmp = this.grounds[i - worldSize][j - worldSize].clone();
                        }
                        else
                        {
                            tmp = this.grounds[i - worldSize][j].clone();
                        }
                    }
                    else if(i >= worldSize)
                    {
                        if(j < 0)
                        {
                            tmp = this.grounds[i - worldSize][worldSize + j].clone();
                        }
                        else if(j >= worldSize)
                        {
                            tmp = this.grounds[i - worldSize][j - worldSize].clone();
                        }
                        else
                        {
                            tmp = this.grounds[i - worldSize][j].clone();
                        }
                    }
                    else if( j < 0)
                    {
                        tmp = this.grounds[i][worldSize + j].clone();
                    }
                    else if(j >= worldSize)
                    {
                        tmp = this.grounds[i][j - worldSize].clone();
                    }
                    tmp.position.set((i - Math.trunc(worldSize/2)) * squaresize * groundMultiplier,0, (j - Math.trunc(worldSize/2)) * squaresize*groundMultiplier);
                    ABWorld.scene.add(tmp);
                }
            }
        }
    }

    //==============================================================================
    //==============================================================================

    //==============================================================================
    // Functions link to an event
    //==============================================================================

    //Create a new infinite world. First destroy the previous one and then recreate one.
    createNewInfiniteWorld() {
        while (ABWorld.scene.children.length)
        {
            ABWorld.scene.remove(ABWorld.scene.children[0]);
        }
        ABWorld.scene.add(this.controls.getObject());
        this.init();
    }

    onDocumentTouchStart( event ) {
        console.log("onDocumentTouchStart");
        event.preventDefault();

        event.clientX = event.touches[0].clientX;
        event.clientY = event.touches[0].clientY;
        onDocumentMouseDown( event );
    }

    //Called when a key is up
    handleKeyUp (e) {
        if((e.keyCode == 38) || (e.keyCode == 40))
        {
            this.velocity = 0;
        }
    }

    //==============================================================================
    //==============================================================================

    //Load all you need
    loader()
    {
        let planeTex = new THREE.TextureLoader().load( "/uploads/meak/grass.jpg" );
        planeTex.wrapS = planeTex.wrapT = THREE.RepeatWrapping;
        planeTex.repeat.set( 8, 8 );
    
        this.groundTexture = new THREE.MeshPhongMaterial({map: planeTex });
        //this.groundTexture.wrapAround = true;
        this.groundTexture.dithering = true;
        this.groundTexture.castShadow = true;
        this.groundTexture.receiveShadow = true;
    }

    //Initalization before running the world
    init()		
    {
        this.grounds = new Array(worldSize);
        for (let i = 0; i < worldSize; i++)
        {
            this.grounds[i] = new Array(worldSize);
            for (let j = 0; j < worldSize; j++)
            {
                this.grounds[i][j] = null;
            }
        }

        let pos = new THREE.Vector3();
        pos = this.camera.getWorldPosition(pos);

        this.pai = parseInt(worldSize/2 + pos.x/(squaresize*groundMultiplier));
        this.paj = parseInt(worldSize/2 + pos.z/(squaresize*groundMultiplier));
        this.initGround();
    
        // LIGHTS
        let hemiLight = new THREE.HemisphereLight( SKYCOLOR, SKYCOLOR, 0.4 );
        hemiLight.position.set( 0, 50, 0 );
        ABWorld.scene.add( hemiLight );
    }

    checkPositionPlayer()
    {
        let ai, aj;

        let pos = new THREE.Vector3();
        pos = this.camera.getWorldPosition(pos);

        //Update the position of the payer in the world so that it can generate ground around the player, depending on the viewDistance
        if(worldSize/2 + pos.x/(squaresize*groundMultiplier) < 0)
            ai = -Math.ceil(-(worldSize/2 + pos.x/(squaresize*groundMultiplier)));
        else
            ai = Math.trunc(worldSize/2 + pos.x/(squaresize*groundMultiplier));

        if(worldSize/2 + pos.z/(squaresize*groundMultiplier) < 0)
            aj = -Math.ceil(-(worldSize/2 + pos.z/(squaresize*groundMultiplier)));
        else
            aj = Math.trunc(worldSize/2 + pos.z/(squaresize*groundMultiplier));
    
        if(ai < 0)
            this.controls.getObject().position.x = (worldSize/2) * groundMultiplier * squaresize - 0.01;

        if(ai >= worldSize)
            this.controls.getObject().position.x = - worldSize/2 * groundMultiplier * squaresize;

        if(aj >= worldSize)
            this.controls.getObject().position.z = - worldSize/2 * groundMultiplier * squaresize;

        if(aj < 0)
           this.controls.getObject().position.z = (worldSize/2) * groundMultiplier * squaresize - 0.01;

        if(ai != this.pai || aj != this.paj)
        {
            this.pai = ai;
            this.paj = aj;
        }
    }
    //==============================================================================
    //==============================================================================

    //==============================================================================
    // Function newRun, init and endRun
    //==============================================================================

    onKeyDown = (event) => {
        switch ( event.keyCode ) {
        case 38: // up
        case 87: // w
            this.moveForward = true;
            break;

        case 37: // left
        case 65: // a
            this.moveLeft = true; break;

        case 40: // down
        case 83: // s
            this.moveBackward = true;
            break;

        case 39: // right
        case 68: // d
            this.moveRight = true;
            break;

        case 32: // space
            if ( this.canJump === true || true ) this.velocity.y += 350;
            this.canJump = false;
            break;
        }
    };

    onKeyUp = ( event ) => {
        switch( event.keyCode ) {
        case 38: // up
        case 87: // w
            this.moveForward = false;
            break;

        case 37: // left
        case 65: // a
            this.moveLeft = false;
            break;

        case 40: // down
        case 83: // s
            this.moveBackward = false;
            break;

        case 39: // right
        case 68: // d
            this.moveRight = false;
            break;
        }
    };


    newRun()
    {
        this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, SKYDISTANCE );
        ABWorld.camera = this.camera;
    
        ABWorld.init3d ( 0, 0, SKYCOLOR  );
        // can adjust renderer:
        ABWorld.renderer.shadowMap.enabled = true;
        ABWorld.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    
        //load ground texture
        this.loader();
    
        //First person controller
        this.controls = new PointerLock( this.camera );
    
        //This will handle key presses
        document.addEventListener('keydown', this.onKeyDown, false );
        document.addEventListener('keyup', this.onKeyUp, false );
    
        this.raycaster = new THREE.Raycaster( new THREE.Vector3(), new THREE.Vector3( 0, - 1, 0 ), 0, 10 );

        //The following handles pointer locking when clicking the window
        if ( 'pointerLockElement' in document ) {
            let element = document.body;

            let pointerlockchange = ( event ) => {

                if ( document.pointerLockElement === element || document.mozPointerLockElement === element || document.webkitPointerLockElement === element )
                {
                    if(!this.controls.enabled && this.controls.getAttachedObject().children.length !== 0)
                    {
                        this.controls.getAttachedObject().remove(this.controls.getAttachedObject().children[1]);
                        this.controls.getAttachedObject().remove(this.controls.getAttachedObject().children[0]);
                    }
                    if(!this.controls.enabled)
                    {
                        this.controls.enabled = true;
                        $("#user_span10").html("");
                    }
                } else {
                    this.controls.enabled = false;
                    $("#user_span10").html("<p><b>Click screen to enable mouse this.controls</b></p>");
                }
            };
            let pointerlockerror = ( event ) => {
                console.error("pointerlockerror");
            };

            // Hook pointer lock state change events
            document.addEventListener( 'pointerlockchange', pointerlockchange, false );
            document.addEventListener( 'pointerlockerror', pointerlockerror, false );

            document.addEventListener( 'click', function ( event ) {
                // Ask the browser to lock the pointer
                element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock;
                element.requestPointerLock();
            }, false );
        } else {
            $("#user_span1").html('<p>Your browser doesn\'t seem to support Pointer Lock API</p>');
        }

        if ( AB.onDesktop() ) {
            $("#user_span1").html("<p>Use WASD or Arrows to move, mouse to look around and space to jump.</p>");
            $("#user_span10").html("<p><b>Click screen to enable mouse this.controls</b></p>");
        }
        else
        {
            $("#user_span1").html( "<p><b>This World currently only works on desktop.</b></p>" );
        }
        this.createNewInfiniteWorld();

        //add this.sun
        this.sun = new Sun(this.controls.getObject(), 100, 3, SKYDISTANCE);
    };


    nextStep() {
        //======================================================================
        //This will handle moving the player and the camera
        //======================================================================

        this.sun.animate();
        this.raycaster.ray.origin.copy( this.controls.getObject().position );
        this.raycaster.ray.origin.y -= 10;

        let time = performance.now();
        let delta = ( time - this.prevTime ) / 1000;

        this.velocity.x -= this.velocity.x * 10.0 * delta;
        this.velocity.z -= this.velocity.z * 10.0 * delta;

        this.velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
    
        const direction = new THREE.Vector3();

        direction.z = Number( this.moveForward ) - Number( this.moveBackward );
        direction.x = Number( this.moveLeft ) - Number( this.moveRight );
        direction.normalize(); // this ensures consistent movements in all directions

        /*if(this.moveLeft && !this.moveRight) {
            this.controls.getObject().rotation.y += 0.02;
        } else if(!this.moveLeft && this.moveRight) {
            this.controls.getObject().rotation.y -= 0.02;
        }*/

        this.controls.getObject().rotation.y += direction.x * 0.03;

        //let q =  new THREE.Vector3();
        //this.camera.getWorldDirection (q);
        //console.log(q);

        if ( this.moveForward || this.moveBackward ) this.velocity.z -= direction.z * 400.0 * MOVESPEED * delta;
        //if ( this.moveLeft || this.moveRight ) this.velocity.x -= direction.x * 400.0 * MOVESPEED * delta;

        //this.controls.getObject().translateX( this.velocity.x * delta );
        this.controls.getObject().translateY( this.velocity.y * delta );
        this.controls.getObject().translateZ( this.velocity.z * delta );

        if ( this.controls.getObject().position.y < 10 ) {

            this.velocity.y = 0;
            this.controls.getObject().position.y = 10;

            this.canJump = true;

        }
        this.prevTime = time;

        //Function to do the loop if the player is at the edge of the world
        this.checkPositionPlayer();
    };
    //==============================================================================
    //==============================================================================

}

//AB.world = new World();
//AB.world.newRun();