Code viewer for World: Bowling
    //import { OrbitControls } from '/uploads/cadogav2/OrbitControls.js';
    
    //===============================================================================================================
    //          VARIABLES
    //===============================================================================================================
    
    // 3D MODELS PATHS
    const PIN_OBJ_PATH = '/uploads/cadogav2/pin_s.obj';
    const FLOOR_OBJ_PATH = '/uploads/cadogav2/floor_s.obj';
    const GUTTER_OBJ_PATH = '/uploads/cadogav2/gutter_s.obj';
    const BACK_GUTTER_OBJ_PATH = '/uploads/cadogav2/back_gutter_s.obj';
    const BANNER_OBJ_PATH = '/uploads/cadogav2/banner_s.obj';
    
    // TEXTURES PATHS
    const PIN_TEXTURE_PATH = '/uploads/cadogav2/pin_tex.png';
    const ALLEY_TEXTURE_PATH = '/uploads/cadogav2/alley_tex.jpg';
    
    // LOAD TEXTURES
    let textureLoader = new THREE.TextureLoader();
    const pinTexture = textureLoader.load(PIN_TEXTURE_PATH);
    pinTexture.minFilter = THREE.NearestFilter;
    pinTexture.magFilter = THREE.NearestFilter;
    
    const alleyTexture = textureLoader.load(ALLEY_TEXTURE_PATH);
    alleyTexture.minFilter = THREE.NearestFilter;
    alleyTexture.magFilter = THREE.NearestFilter;

    
    // MATERIALS
    let pinMat = new THREE.MeshToonMaterial({map: pinTexture});
    let floorMat = new THREE.MeshToonMaterial({map: alleyTexture, side: THREE.DoubleSide});
    let gutterMat = new THREE.MeshToonMaterial({color: 0x444444});
    let backGutterMat = new THREE.MeshToonMaterial({color: 0x116482});
    let ballMat = new THREE.MeshToonMaterial({color: 0x061429}); //010e21
    let bannerMat = new THREE.MeshToonMaterial({map: alleyTexture});
    
    // MEMBERS
    const SKYCOLOR = 0x31408f;
    
    let pinMesh, floorMesh, gutterMesh, bannerMesh;
    let pins = [];
    // all objects used in physics simulation
    let dynamicObjects = [];
    let physicsWorld;
    let tmpTransform = undefined;
    let mouseCoords = new THREE.Vector2();
    let raycaster = new THREE.Raycaster();
    let rayPos = new THREE.Vector3();
    let ammoLoaded = false;
    let gravity = -50;
    let ballMass = 500;
    let pinMass = 20;
    let clock;
    
    AB.clockTick = 25;
    AB.maxSteps = Infinity;
    
    // GAME LOGIC VARIABLES
    let turn = false;    // false = player 1, true = player 2
    let shotsTaken = 0;
    let ballMoving = false;
    let p1Score = 0, p2Score = 0;
    let strikeAnimPlaying = false;
    let gameOver = false;
    
    //===============================================================================================================
    //          LOAD DEPENDENCY SCRIPTS
    //===============================================================================================================

    // Load CSS
    AB.loadCSS("/uploads/cadogav2/bowling_styles2.css");

    $.getScript ("/uploads/cadogav2/ammo.wasm.js", function(){
        console.log('script loaded')
        Ammo().then(startAmmo);
    });
    
    //===============================================================================================================
    //          PHYSICS METHODS
    //===============================================================================================================

    
    function startAmmo(){
        console.log('start ammo')
        
        initPhysicsWorld();
        addEventListeners();

        createPhysicalPlane({w:10, h:0.5, d:80},{x:0, y:-0.25, z:38}, 0);
        
        createPhyicalPins();
        
        //let shape = new AmmoLib.btBoxShape( new AmmoLib.btVector3( sx, sy, sz ) );
		//shape.setMargin( 0.05 );
    }
    
    function initPhysicsWorld(){
        let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
        let dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
        let overlappingPairCache = new Ammo.btDbvtBroadphase();
        let solver = new Ammo.btSequentialImpulseConstraintSolver();

        physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
        physicsWorld.setGravity(new Ammo.btVector3(0, -50, 0));
        
        tmpTransform = new Ammo.btTransform();
        ammoLoaded = true;
    }
    
    function createPhysicalPlane(dimension, position, mass){
        let planeGeometry = new THREE.BoxGeometry(dimension.w, dimension.h, dimension.d);
        //let planeMaterial = new THREE.MeshPhongMaterial({ color: 0x111111 });
        let plane = new THREE.Mesh(planeGeometry);
        plane.position.set(position.x, position.y, position.z);
        
        // Default Motion State - starting position and rotation
        let transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin(new Ammo.btVector3( position.x, position.y, position.z));
        transform.setRotation(new Ammo.btQuaternion(0, 0, 0, 1));
        let motionState = new Ammo.btDefaultMotionState(transform);
        
        // Collision Geometry
        let colGeometry = new Ammo.btBoxShape(new Ammo.btVector3(dimension.w * 0.5, dimension.h * 0.5, dimension.d * 0.5));
        colGeometry.setMargin(0.05);
        
        // Set Inertia
        let localInertia = new Ammo.btVector3(0, 0, 0);
        colGeometry.calculateLocalInertia(mass, localInertia);
        
        // Create RigidBody
        let rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, colGeometry, localInertia);
        let body = new Ammo.btRigidBody(rbInfo);

        //plane.visible = false;
        physicsWorld.addRigidBody(body);
        
        // make plane hold a reference to it's rigidbody
        plane.userData.physicsBody = body;
        dynamicObjects.push(plane);
        //ABWorld.scene.add(plane);
    }
    
    function createPhyicalBall(radius, position, mass){
        let ball = new THREE.Mesh(new THREE.SphereBufferGeometry(radius, 32, 16), ballMat);
        ball.position.set(position.x, position.y, position.z);
        ball.castShadow = true;
        
        // add to the scene
        ABWorld.scene.add(ball);
        
        // Default Motion State - starting position and rotation
        let transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z));
        transform.setRotation(new Ammo.btQuaternion(0, 0, 0, 1));
        let motionState = new Ammo.btDefaultMotionState(transform);
        
        // Collision Geometry
        let colGeometry = new Ammo.btSphereShape(radius);
        colGeometry.setMargin(0.05);
        
        // Set Inertia
        let localInertia = new Ammo.btVector3(0, 0, 0);
        colGeometry.calculateLocalInertia(mass, localInertia);
        
        // Create RigidBody
        let rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, colGeometry, localInertia);
        let body = new Ammo.btRigidBody(rbInfo);
        body.setFriction(0.5);

        physicsWorld.addRigidBody(body);
        
        // place the ball with inital velocity
        rayPos.copy(raycaster.ray.direction);
        // the velocity
        rayPos.multiplyScalar(100);
        // fire ball
        body.setLinearVelocity(new Ammo.btVector3(rayPos.x, rayPos.y, rayPos.z));
        
        ball.userData.physicsBody = body;
        dynamicObjects.push(ball);
    }
    
    function createPhyicalPins(){
        if (!pins.length){
            console.log("Couldn't create pins rigidbody");
            return;
        }
        
        pins.forEach(pin => {
            console.log("pin")
            //console.log(pin)
            createConvexHullNew(pin, pinMass);
        })
    }
    
    function createConvexHullPhysicsShape(vertices) {
        let shape = new Ammo.btConvexHullShape();
        
        let tmpVec = new Ammo.btVector3(0, 0, 0)
    
        for (var i = 0, il = vertices.length; i < il; i += 3) {
            tmpVec.setValue(vertices[i], vertices[i + 1], vertices[i + 2]);
            var lastOne = i >= il - 3;
            shape.addPoint(tmpVec, lastOne);
        }
    
        return shape;
    }
    
    function createConvexHullNew(pin, mass){
        let geometry = pin.children[0].geometry;
        // console.log("geo")
        // console.log(geometry)
        let vertices = geometry.attributes.position.array;
        
        //createConvexHullOld(pin)
        let shape = createConvexHullPhysicsShape(vertices);
        
        let transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin(new Ammo.btVector3(pin.position.x, pin.position.y, pin.position.z));
        let motionState = new Ammo.btDefaultMotionState(transform);
        
        let localInertia = new Ammo.btVector3(0, 0, 0);
        shape.calculateLocalInertia(mass, localInertia);
        
        let rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
        let body = new Ammo.btRigidBody(rbInfo);
        
        body.setRestitution(1);
        body.setActivationState(4)
        
        physicsWorld.addRigidBody(body);
        pin.userData.physicsBody = body;
        
        dynamicObjects.push(pin);
    }
    
    function resetPins(){
        let spacing = 2;
        let spread = spacing / 2;
        let offset = spacing + spread;
        
        let pinIndex = 0;
        for (let row = 0; row <= 4; row++){
            for (let i = 0; i < 4 - row; i++){
                let pin = pins[pinIndex].userData.physicsBody;
                
                pin.getWorldTransform().setIdentity();
                pin.getWorldTransform().setRotation(new Ammo.btQuaternion(0, 0, 0, 1));
                pin.setLinearVelocity(new Ammo.btVector3(0, 0, 0));
                pin.setAngularVelocity(new Ammo.btVector3(0, 0, 0));
                let origin = pin.getWorldTransform().getOrigin();
                
                let x = (i * spacing) + (row*spread) - offset;
                let z = row * spacing;
                
                origin.setX(x);
                origin.setY(0);
                origin.setZ(z);
        
                pin.activate();
                pinIndex++;
            }
        }
    }
    
    //===============================================================================================================
    //          EVENTS
    //===============================================================================================================
    
    function addEventListeners(){
        document.getElementById('ab-runcanvas').addEventListener('mousedown', onMouseDown);
    }
    
    function onMouseDown(event){
        if (!ammoLoaded || gameOver) return;
        
        console.log('clicked',ballMoving,!ballMoving)
        
        // should also block player whos turn it's not
        if (!ballMoving){
            ballMoving = true;
            shotsTaken++;
            
            console.log(`player ${turn + 1} taken shot ${shotsTaken}`)
            
            throwBall();

            // ballMoving will be set to false after 6 sec in 
            setTimeout(function(){
                ballMoving = false;
                calculateScore();
              
                if (shotsTaken >= 2){
                    // reset
                    shotsTaken = 0;
                    // switch turns for players
                    turn = !turn;
                    previousKnockedOver = 0;
                    
                    $("#p1_details").toggleClass("active");
                    $("#p2_details").toggleClass("active");
                    
                    // reset pins
                    resetPins();
                    
                    // check who won, if any
                    checkForWinner();
                }
              
            }, 6000);
        }
        
    }
    
    function checkForWinner(){
        if (!gameOver && (p1Score >= 50 || p2Score >= 50)){
            let w = p1Score >= 50 ? 1 : 2;
            gameOver = true;
            $('.anim_strike').text(`WINNER Player ${w}!`);
            $('#player_info').text(`Congratulations!`);
            $("#strike_msg").toggleClass("hide");
        }
    }
    
    function throwBall(){
        // normalise mouse coords between -1 to 1 with origin in center of screem
        mouseCoords.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1);
        
        raycaster.setFromCamera(mouseCoords, ABWorld.camera);
        
        // where the ray intersects the camera screen from the mouse position
        rayPos.copy(raycaster.ray.direction);
        rayPos.add(raycaster.ray.origin)
        console.log(rayPos)
        // ball properties
        let pos = {x: rayPos.x, y: rayPos.y, z: rayPos.z};
        
        // create ball
        // instead make single instance of ball and reset it position
        console.log('create ball')
        createPhyicalBall(1, pos, ballMass);
    }
    
    ABHandler.MouseDown = onMouseDown;
    let previousKnockedOver = 0;
    
    function calculateScore(){
        let knockedOver = 0;
        
        pins.forEach(pin => {
            let posY = pin.position.y;
            console.log("posY",posY)
            
            if (posY <= -0.2 || posY >= 2) {
                knockedOver++;
            }
        });
        
        
        let score = 0;
        
        // strike condition
        if (shotsTaken === 1 && knockedOver === 10){
            score = 14;
            shotsTaken++;
            addScore(score);
            checkForWinner();
            // show strike massage
            if (!strikeAnimPlaying && !gameOver){
                $('.anim_strike').text(`STRIKE!`);
                $('#player_info').text(`for Player ${turn + 1}`);
                $("#strike_msg").toggleClass("hide");
                strikeAnimPlaying = true;
                
                setTimeout(function(){
                  $("#strike_msg").toggleClass("hide");
                  strikeAnimPlaying = false;
                }, 1200);
            }
            return;
        } else if (shotsTaken === 1) {
            // first shot
            previousKnockedOver = knockedOver;
            score = knockedOver;
        } else  {
            // second shot
            score = knockedOver - previousKnockedOver;
        }
        
        addScore(score);
        
    }
    
    function addScore(score){
        if (!turn){
            // player 1
            p1Score += score;
        } else {
            // player 2
            p2Score += score;
        }
        
        $('#p1_score').text(p1Score+"");
        $('#p2_score').text(p2Score+"");
    }
    
    //===============================================================================================================
    //          LOAD RESOURCE METHODS
    //===============================================================================================================
    
    function asynchFinished()
    {
        // add more models and assets here!
    	if (pinMesh && floorMesh && gutterMesh && bannerMesh) {  
    	    return true;
    	} else {
    	    return false;
    	}
    }
    
    function assignMaterial(obj, material, castShadow=true, receiveShadow=true){
        obj.traverse(function(child) {
            if (child instanceof THREE.Mesh) {
                child.material = material;
                child.castShadow = castShadow;
                child.receiveShadow = receiveShadow;
            }
        });
    }
 
    function loadResources(){
        // Load Pin Model
        let objLoader = new THREE.OBJLoader(new THREE.LoadingManager());
	
    	objLoader.load(PIN_OBJ_PATH, function (obj)
    	{
            assignMaterial(obj, pinMat, true, false);
            pinMesh = obj;
            
            let boundingBox = new THREE.Box3().setFromObject(pinMesh)
            console.log( "measure" );
            console.log( boundingBox.getSize() );
            console.log( "obj" );
            console.log( pinMesh );
            
            //let offset = 12;
            // spacing between rows
            let spacing = 2;
            // spread bewteen each pin for each row
            let spread = spacing / 2;
            let offset = spacing + spread;
            for (let row = 0; row <= 4; row++){
                for (let i = 0; i < 4 - row; i++){
                    let clone = pinMesh.clone();
                    let x = (i * spacing) + (row*spread) - offset;
                    let z = row * spacing;
                    clone.position.set(x, 0, z);
                    
                    pins.push(clone);
                    ABWorld.scene.add(clone);
                }
            }
    		
    		if (asynchFinished()) initScene();		 
    	});
    	
    	// Load Floor Model
    	objLoader.load(FLOOR_OBJ_PATH, function (obj)
    	{
    	    assignMaterial(obj, floorMat, true, true);
    	    floorMesh = obj;
    	    ABWorld.scene.add(floorMesh);
    	    
    	    let clone = floorMesh.clone();
    	    clone.position.x = -40;
    	    clone.position.z = 83;
    	    clone.rotation.y = Math.PI / 2;
    	    ABWorld.scene.add(clone);
    	    
    	    if (asynchFinished()) initScene();
    	});
    	
    	// Load Gutter Model
    	objLoader.load(GUTTER_OBJ_PATH, function (obj)
    	{
    	    assignMaterial(obj, gutterMat, true, true);
    	    obj.position.x = 6;
    	    obj.position.z = 68;
    	    gutterMesh = obj;
    	    ABWorld.scene.add(gutterMesh);
    	    
    	    let clone = gutterMesh.clone();
    	    clone.position.x = -6;
    	    ABWorld.scene.add(clone);
    	    if (asynchFinished()) initScene();
    	});
    	
    	// Load Back Gutter Model
    	objLoader.load(BACK_GUTTER_OBJ_PATH, function (obj)
    	{
    	    assignMaterial(obj, backGutterMat, false, true);
    	    backGutterMesh = obj;
    	    ABWorld.scene.add(backGutterMesh);

    	    if (asynchFinished()) initScene();
    	});
    	
    	// Load Banner Model
    	objLoader.load(BANNER_OBJ_PATH, function (obj)
    	{
    	    assignMaterial(obj, bannerMat, true, true);
    	    bannerMesh = obj;
    	    ABWorld.scene.add(bannerMesh);
    	    if (asynchFinished()) initScene();
    	});
    	
    	
    }
    
    //===============================================================================================================
    //          HELPER METHODS
    //===============================================================================================================
    
    function showAxesHelper(){
        const axesHelper = new THREE.AxesHelper( 15 );
        
	    const color = new THREE.Color();
		const array = axesHelper.geometry.attributes.color.array;

        // x
		color.set( new THREE.Color('red') );
		color.toArray( array, 0 );
		color.toArray( array, 3 );

        // y
		color.set( new THREE.Color('green') );
		color.toArray( array, 6 );
		color.toArray( array, 9 );

        // z
		color.set( new THREE.Color('blue') );
		color.toArray( array, 12 );
		color.toArray( array, 15 );
		axesHelper.geometry.attributes.color.needsUpdate = true;
        ABWorld.scene.add( axesHelper );
    }
    
    //===============================================================================================================
    //          INTIALISE METHODS
    //===============================================================================================================
    
    function initScene() {
        console.log('init')
        let ambient = new THREE.AmbientLight(0xddd1ff);
        ambient.intensity = 0.5;
        ABWorld.scene.add( ambient );
        
        // 0xffeed4
        // 0xfaddb1
        let light = new THREE.DirectionalLight( 0xfaddb1, 0.5);
	    light.position.set(4,20,8);
	    light.castShadow = true;
	    light.shadowCameraLeft = -100;
        light.shadowCameraRight = 100;
        light.shadowCameraTop = 100;
        light.shadowCameraBottom = -100;
	    light.shadow.mapSize.width  = 2048;//3072;//2048;
	    light.shadow.mapSize.height = 2048;//3072;//2048;
	    light.shadow.camera.near = 1;
	    light.shadow.camera.far = 30;
	    light.shadow.bias = -0.005;
	    light.shadow.radius = 10;
	    ABWorld.scene.add(light);
	    
	    //const clh = new THREE.CameraHelper(light.shadow.camera);
	    //ABWorld.scene.add(clh);
	    
	    //const dlh = new THREE.DirectionalLightHelper(light, 2);
	    //ABWorld.scene.add(dlh);
	    
	    //showAxesHelper();
        console.log('finished init')
    }
    
    //===============================================================================================================
    //          RUN GAME METHODS
    //===============================================================================================================
	
	AB.world.newRun = function()
	{
	    document.write(`
	    <div id="sidebar">
    	    <div class="active details" id="p1_details">
    	        <h1>Player 1</h1>
    	        <div id="score_display">
    	            <p class="bold">Score</p>
    	            <p id="p1_score">0</p>
    	        </div>
    	    </div>
    	    <div class="details" id="p2_details">
    	        <h1>Player 2</h1>
    	        <div id="score_display">
    	            <p class="bold">Score</p>
    	            <p id="p2_score">0</p>
    	        </div>
    	    </div>
    	    
	    </div>`);
	    document.write(`
	    <div id="strike_msg" class="hide">
	        <p class="anim_strike">STRIKE!</p>
	        <p id="player_info">for Player</p>
	    </div>`);
	    console.log('new run')
	    ABWorld.renderer = new THREE.WebGLRenderer ( { antialias: true } );
	    ABWorld.renderer.shadowMap.enabled = true;
	    ABWorld.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
	    
		ABWorld.init3d(1250, 3000, SKYCOLOR); 
		
		ABWorld.camera.position.x = 0;
        ABWorld.camera.position.y = 5;
        ABWorld.camera.position.z = 85;


	    loadResources();
	    
	    clock = new THREE.Clock();
	};


	AB.world.nextStep = function()		 
	{
		// Code for Three.js re-drawing of objects.  
// 		console.log('next')
		if (ammoLoaded){
		    let delta = clock.getDelta();
		    updatePhysics(delta);
		}
	};
	
	function updatePhysics(deltaTime){
	   // console.log('deltaTime')
	   // console.log(deltaTime)
	    physicsWorld.stepSimulation(deltaTime, 10);
	    
	    for (let i = 0; i < dynamicObjects.length; i++){
	        let threejsObj = dynamicObjects[i];
	        let physicsObj = threejsObj.userData.physicsBody;
	        
	        let motionState = physicsObj.getMotionState();
	        if (motionState){
	            motionState.getWorldTransform(tmpTransform);
	            let newPos = tmpTransform.getOrigin();
	            let newRot = tmpTransform.getRotation();
	            
	            threejsObj.position.set(newPos.x(), newPos.y(), newPos.z());
	            threejsObj.quaternion.set(newRot.x(), newRot.y(), newRot.z(), newRot.w());
	        }
	    }
	}

	AB.world.endRun = function()
	{
	};