403Webshell
Server IP : 209.209.40.120  /  Your IP : 216.73.217.112
Web Server : Microsoft-IIS/10.0
System : Windows NT NEWWWW 10.0 build 17763 (Windows Server 2019) i586
User : NEWWWW$ ( 0)
PHP Version : 8.3.30
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : OFF  |  Perl : OFF  |  Python : OFF  |  Sudo : OFF  |  Pkexec : OFF
Directory :  C:/HostingSpaces/admin/chatme24.com/wwwroot/_server/city_js/dev/data/scripts/rubik/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : C:/HostingSpaces/admin/chatme24.com/wwwroot/_server/city_js/dev/data/scripts/rubik/main.js
// Three.js - https://github.com/mrdoob/babylon.js/
// RoundedBoxGeometry - https://github.com/pailhead/babylon-rounded-box

var RubikAnimationEngine = function() {
    this.ids = [];
    this.animations = {};
    this.update = this.update.bind(this);
    this.raf = 0;
    this.time = 0;
};

RubikAnimationEngine.prototype = {
    constructor: RubikAnimationEngine,
    update: function() {
        const now = performance.now();
        const delta = now - this.time;
        this.time = now;

        let i = this.ids.length;

        //this.raf = i ? requestAnimationFrame(this.update) : 0;

        while (i--)
            this.animations[this.ids[i]] && this.animations[this.ids[i]].update(delta);
    },

    add: function(animation) {

        animation.id = chat3d.world.location.uniqueID++;

        this.ids.push(animation.id);
        this.animations[animation.id] = animation;

        if (this.raf !== 0) return;

        this.time = performance.now();
        this.raf = requestAnimationFrame(this.update);

    },

    remove: function(animation) {

        const index = this.ids.indexOf(animation.id);

        if (index < 0) return;

        this.ids.splice(index, 1);
        delete this.animations[animation.id];
        animation = null;

    }


};

var RubikAnimation = function(engine) {
    if (engine) this.start(engine);
};

RubikAnimation.prototype = {
    constructor: RubikAnimation,
    start: function() {
        var engine = chat3d.world.location.animEngine;
        engine.add(this);
    },

    stop: function() {
        var engine = chat3d.world.location.animEngine;
        engine.remove(this);
    },

    update: function(delta) {

    }
};

RubikWorld = function(game, engine) {
    RubikAnimation.call(engine);
    this.game = game;
    this.scene = new BABYLON.Scene();

    /*this.container = this.game.dom.game;
    this.scene = new BABYLON.Scene();

    this.renderer = new BABYLON.WebGLRenderer({ antialias: true, alpha: true });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.container.appendChild(this.renderer.domElement);

    this.camera = new BABYLON.PerspectiveCamera(2, 1, 0.1, 10000);

    this.stage = { width: 2, height: 3 };
    this.fov = 10;*/

    this.createLights();

};

RubikWorld.prototype = Object.create(RubikAnimation.prototype);
RubikWorld.prototype.createLights = function() {

    this.lights = {
        holder: new BABYLON.Object3D,
        ambient: new BABYLON.AmbientLight(0xffffff, 0.69),
        front: new BABYLON.DirectionalLight(0xffffff, 0.36),
        back: new BABYLON.DirectionalLight(0xffffff, 0.19),
    };

    this.lights.front.position.set(1.5, 5, 3);
    this.lights.back.position.set(-1.5, -5, -3);

    this.lights.holder.add(this.lights.ambient);
    this.lights.holder.add(this.lights.front);
    this.lights.holder.add(this.lights.back);

    chat3d.world.scene.add(this.lights.holder);

};

RubikWorld.prototype.RemoveLights = function() {
    chat3d.world.scene.remove(this.lights.holder);
};



var RubikRoundedBoxGeometry = function(size, radius, radiusSegments) {

    BABYLON.BufferGeometry.call(this);

    this.type = 'RoundedBoxGeometry';

    radiusSegments = !isNaN(radiusSegments) ? Math.max(1, Math.floor(radiusSegments)) : 1;

    var width, height, depth;

    width = height = depth = size;
    radius = size * radius;

    radius = Math.min(radius, Math.min(width, Math.min(height, Math.min(depth))) / 2);

    var edgeHalfWidth = width / 2 - radius;
    var edgeHalfHeight = height / 2 - radius;
    var edgeHalfDepth = depth / 2 - radius;

    this.parameters = {
        width: width,
        height: height,
        depth: depth,
        radius: radius,
        radiusSegments: radiusSegments
    };

    var rs1 = radiusSegments + 1;
    var totalVertexCount = (rs1 * radiusSegments + 1) << 3;

    var positions = new BABYLON.BufferAttribute(new Float32Array(totalVertexCount * 3), 3);
    var normals = new BABYLON.BufferAttribute(new Float32Array(totalVertexCount * 3), 3);

    var
        cornerVerts = [],
        cornerNormals = [],
        normal = new BABYLON.Vector3(),
        vertex = new BABYLON.Vector3(),
        vertexPool = [],
        normalPool = [],
        indices = [];

    var
        lastVertex = rs1 * radiusSegments,
        cornerVertNumber = rs1 * radiusSegments + 1;

    doVertices();
    doFaces();
    doCorners();
    doHeightEdges();
    doWidthEdges();
    doDepthEdges();

    function doVertices() {

        var cornerLayout = [
            new BABYLON.Vector3(1, 1, 1),
            new BABYLON.Vector3(1, 1, -1),
            new BABYLON.Vector3(-1, 1, -1),
            new BABYLON.Vector3(-1, 1, 1),
            new BABYLON.Vector3(1, -1, 1),
            new BABYLON.Vector3(1, -1, -1),
            new BABYLON.Vector3(-1, -1, -1),
            new BABYLON.Vector3(-1, -1, 1)
        ];

        for (var j = 0; j < 8; j++) {

            cornerVerts.push([]);
            cornerNormals.push([]);

        }

        var PIhalf = Math.PI / 2;
        var cornerOffset = new BABYLON.Vector3(edgeHalfWidth, edgeHalfHeight, edgeHalfDepth);

        for (var y = 0; y <= radiusSegments; y++) {

            var v = y / radiusSegments;
            var va = v * PIhalf;
            var cosVa = Math.cos(va);
            var sinVa = Math.sin(va);

            if (y == radiusSegments) {

                vertex.set(0, 1, 0);
                var vert = vertex.clone().multiplyScalar(radius).add(cornerOffset);
                cornerVerts[0].push(vert);
                vertexPool.push(vert);
                var norm = vertex.clone();
                cornerNormals[0].push(norm);
                normalPool.push(norm);
                continue;

            }

            for (var x = 0; x <= radiusSegments; x++) {

                var u = x / radiusSegments;
                var ha = u * PIhalf;
                vertex.x = cosVa * Math.cos(ha);
                vertex.y = sinVa;
                vertex.z = cosVa * Math.sin(ha);

                var vert = vertex.clone().multiplyScalar(radius).add(cornerOffset);
                cornerVerts[0].push(vert);
                vertexPool.push(vert);

                var norm = vertex.clone().normalize();
                cornerNormals[0].push(norm);
                normalPool.push(norm);

            }

        }

        for (var i = 1; i < 8; i++) {

            for (var j = 0; j < cornerVerts[0].length; j++) {

                var vert = cornerVerts[0][j].clone().multiply(cornerLayout[i]);
                cornerVerts[i].push(vert);
                vertexPool.push(vert);

                var norm = cornerNormals[0][j].clone().multiply(cornerLayout[i]);
                cornerNormals[i].push(norm);
                normalPool.push(norm);

            }

        }

    }

    function doCorners() {

        var flips = [
            true,
            false,
            true,
            false,
            false,
            true,
            false,
            true
        ];

        var lastRowOffset = rs1 * (radiusSegments - 1);

        for (var i = 0; i < 8; i++) {

            var cornerOffset = cornerVertNumber * i;

            for (var v = 0; v < radiusSegments - 1; v++) {

                var r1 = v * rs1;
                var r2 = (v + 1) * rs1;

                for (var u = 0; u < radiusSegments; u++) {

                    var u1 = u + 1;
                    var a = cornerOffset + r1 + u;
                    var b = cornerOffset + r1 + u1;
                    var c = cornerOffset + r2 + u;
                    var d = cornerOffset + r2 + u1;

                    if (!flips[i]) {

                        indices.push(a);
                        indices.push(b);
                        indices.push(c);

                        indices.push(b);
                        indices.push(d);
                        indices.push(c);

                    } else {

                        indices.push(a);
                        indices.push(c);
                        indices.push(b);

                        indices.push(b);
                        indices.push(c);
                        indices.push(d);

                    }

                }

            }

            for (var u = 0; u < radiusSegments; u++) {

                var a = cornerOffset + lastRowOffset + u;
                var b = cornerOffset + lastRowOffset + u + 1;
                var c = cornerOffset + lastVertex;

                if (!flips[i]) {

                    indices.push(a);
                    indices.push(b);
                    indices.push(c);

                } else {

                    indices.push(a);
                    indices.push(c);
                    indices.push(b);

                }

            }

        }

    }

    function doFaces() {

        var a = lastVertex;
        var b = lastVertex + cornerVertNumber;
        var c = lastVertex + cornerVertNumber * 2;
        var d = lastVertex + cornerVertNumber * 3;

        indices.push(a);
        indices.push(b);
        indices.push(c);
        indices.push(a);
        indices.push(c);
        indices.push(d);

        a = lastVertex + cornerVertNumber * 4;
        b = lastVertex + cornerVertNumber * 5;
        c = lastVertex + cornerVertNumber * 6;
        d = lastVertex + cornerVertNumber * 7;

        indices.push(a);
        indices.push(c);
        indices.push(b);
        indices.push(a);
        indices.push(d);
        indices.push(c);

        a = 0;
        b = cornerVertNumber;
        c = cornerVertNumber * 4;
        d = cornerVertNumber * 5;

        indices.push(a);
        indices.push(c);
        indices.push(b);
        indices.push(b);
        indices.push(c);
        indices.push(d);

        a = cornerVertNumber * 2;
        b = cornerVertNumber * 3;
        c = cornerVertNumber * 6;
        d = cornerVertNumber * 7;

        indices.push(a);
        indices.push(c);
        indices.push(b);
        indices.push(b);
        indices.push(c);
        indices.push(d);

        a = radiusSegments;
        b = radiusSegments + cornerVertNumber * 3;
        c = radiusSegments + cornerVertNumber * 4;
        d = radiusSegments + cornerVertNumber * 7;

        indices.push(a);
        indices.push(b);
        indices.push(c);
        indices.push(b);
        indices.push(d);
        indices.push(c);

        a = radiusSegments + cornerVertNumber;
        b = radiusSegments + cornerVertNumber * 2;
        c = radiusSegments + cornerVertNumber * 5;
        d = radiusSegments + cornerVertNumber * 6;

        indices.push(a);
        indices.push(c);
        indices.push(b);
        indices.push(b);
        indices.push(c);
        indices.push(d);

    }

    function doHeightEdges() {

        for (var i = 0; i < 4; i++) {

            var cOffset = i * cornerVertNumber;
            var cRowOffset = 4 * cornerVertNumber + cOffset;
            var needsFlip = i & 1 === 1;

            for (var u = 0; u < radiusSegments; u++) {

                var u1 = u + 1;
                var a = cOffset + u;
                var b = cOffset + u1;
                var c = cRowOffset + u;
                var d = cRowOffset + u1;

                if (!needsFlip) {

                    indices.push(a);
                    indices.push(b);
                    indices.push(c);
                    indices.push(b);
                    indices.push(d);
                    indices.push(c);

                } else {

                    indices.push(a);
                    indices.push(c);
                    indices.push(b);
                    indices.push(b);
                    indices.push(c);
                    indices.push(d);

                }

            }

        }

    }

    function doDepthEdges() {

        var cStarts = [0, 2, 4, 6];
        var cEnds = [1, 3, 5, 7];

        for (var i = 0; i < 4; i++) {

            var cStart = cornerVertNumber * cStarts[i];
            var cEnd = cornerVertNumber * cEnds[i];

            var needsFlip = 1 >= i;

            for (var u = 0; u < radiusSegments; u++) {

                var urs1 = u * rs1;
                var u1rs1 = (u + 1) * rs1;

                var a = cStart + urs1;
                var b = cStart + u1rs1;
                var c = cEnd + urs1;
                var d = cEnd + u1rs1;

                if (needsFlip) {

                    indices.push(a);
                    indices.push(c);
                    indices.push(b);
                    indices.push(b);
                    indices.push(c);
                    indices.push(d);

                } else {

                    indices.push(a);
                    indices.push(b);
                    indices.push(c);
                    indices.push(b);
                    indices.push(d);
                    indices.push(c);

                }

            }

        }

    }

    function doWidthEdges() {

        var end = radiusSegments - 1;

        var cStarts = [0, 1, 4, 5];
        var cEnds = [3, 2, 7, 6];
        var needsFlip = [0, 1, 1, 0];

        for (var i = 0; i < 4; i++) {

            var cStart = cStarts[i] * cornerVertNumber;
            var cEnd = cEnds[i] * cornerVertNumber;

            for (var u = 0; u <= end; u++) {

                var a = cStart + radiusSegments + u * rs1;
                var b = cStart + (u != end ? radiusSegments + (u + 1) * rs1 : cornerVertNumber - 1);

                var c = cEnd + radiusSegments + u * rs1;
                var d = cEnd + (u != end ? radiusSegments + (u + 1) * rs1 : cornerVertNumber - 1);

                if (!needsFlip[i]) {

                    indices.push(a);
                    indices.push(b);
                    indices.push(c);
                    indices.push(b);
                    indices.push(d);
                    indices.push(c);

                } else {

                    indices.push(a);
                    indices.push(c);
                    indices.push(b);
                    indices.push(b);
                    indices.push(c);
                    indices.push(d);

                }

            }

        }

    }

    var index = 0;

    for (var i = 0; i < vertexPool.length; i++) {

        positions.setXYZ(
            index,
            vertexPool[i].x,
            vertexPool[i].y,
            vertexPool[i].z
        );

        normals.setXYZ(
            index,
            normalPool[i].x,
            normalPool[i].y,
            normalPool[i].z
        );

        index++;

    }

    this.setIndex(new BABYLON.BufferAttribute(new Uint16Array(indices), 1));
    this.addAttribute('position', positions);
    this.addAttribute('normal', normals);

}

RubikRoundedBoxGeometry.prototype = Object.create(BABYLON.BufferGeometry.prototype);
RubikRoundedBoxGeometry.constructor = RubikRoundedBoxGeometry;


function RubikRoundedPlaneGeometry(size, radius, depth) {

    //console.log(size, radius, depth);

    var x, y, width, height;

    x = y = -size / 2;
    width = height = size;
    radius = size * radius;

    //console.log(x, y, width, radius);

    const shape = new BABYLON.Shape();



    shape.moveTo(x, y + radius);
    shape.lineTo(x, y + height - radius);
    shape.quadraticCurveTo(x, y + height, x + radius, y + height);
    shape.lineTo(x + width - radius, y + height);
    shape.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
    shape.lineTo(x + width, y + radius);
    shape.quadraticCurveTo(x + width, y, x + width - radius, y);
    shape.lineTo(x + radius, y);
    shape.quadraticCurveTo(x, y, x, y + radius);




    const geometry = new BABYLON.ExtrudeBufferGeometry(
        shape, { depth: depth, bevelEnabled: false, curveSegments: 3 }


    );

    return geometry;

}

RubikCube = function(game) {
    this.game = game;
    this.size = 3;

    this.geometry = {
        pieceCornerRadius: 0.12,
        edgeCornerRoundness: 0.15,
        edgeScale: 0.82,
        edgeDepth: 0.01,
    };

    this.holder = new BABYLON.Object3D();
    this.object = new BABYLON.Object3D();
    this.animator = new BABYLON.Object3D();

    this.holder.add(this.animator);
    this.animator.add(this.object);

    this.game.world.scene.add(this.holder);
};

RubikCube.prototype = {
    constructor: RubikCube,
    init: function() {
        this.cubes = [];
        this.object.children = [];
        this.object.add(this.game.controls.group);

        if (this.size === 2) this.scale = 1.25;
        else if (this.size === 3) this.scale = 1;
        else if (this.size > 3) this.scale = 3 / this.size;

        this.object.scale.set(this.scale, this.scale, this.scale);

        const controlsScale = this.size === 2 ? 0.825 : 1;
        this.game.controls.edges.scale.set(controlsScale, controlsScale, controlsScale);

        this.generatePositions();
        this.generateModel();

        this.pieces.forEach(piece => {
            //console.log(piece);
            this.cubes.push(piece.userData.cube);
            this.object.add(piece);

        });

        this.holder.traverse(node => {

            if (node.frustumCulled) node.frustumCulled = false;

        });

        this.updateColors(this.game.themes.getColors());

        this.sizeGenerated = this.size;
        //console.log('this.size', this.size);
    },


    resize: function(force = false) {

        if (this.size !== this.sizeGenerated || force) {

            this.size = this.game.preferences.ranges.size.value;


            this.reset();
            this.init();

            this.game.saved = false;
            this.game.timer.reset();
            this.game.storage.clearGame();

        }
        //console.log('this.size', this.size);

    },

    reset: function() {

        this.game.controls.edges.rotation.set(0, 0, 0);

        this.holder.rotation.set(0, 0, 0);
        this.object.rotation.set(0, 0, 0);
        this.animator.rotation.set(0, 0, 0);

    },

    generatePositions: function() {

        const m = this.size - 1;
        const first = this.size % 2 !== 0 ?
            0 - Math.floor(this.size / 2) :
            0.5 - this.size / 2;

        let x, y, z;

        this.positions = [];

        for (x = 0; x < this.size; x++) {
            for (y = 0; y < this.size; y++) {
                for (z = 0; z < this.size; z++) {

                    let position = new BABYLON.Vector3(first + x, first + y, first + z);
                    let edges = [];

                    if (x == 0) edges.push(0);
                    if (x == m) edges.push(1);
                    if (y == 0) edges.push(2);
                    if (y == m) edges.push(3);
                    if (z == 0) edges.push(4);
                    if (z == m) edges.push(5);

                    position.edges = edges;
                    this.positions.push(position);

                }
            }
        }

    },


    generateModel: function() {

        this.pieces = [];
        this.edges = [];

        const pieceSize = 1 / 3;

        const mainMaterial = new BABYLON.MeshLambertMaterial();

        const pieceMesh = new BABYLON.Mesh(
            new RubikRoundedBoxGeometry(pieceSize, this.geometry.pieceCornerRadius, 3 / 10),
            mainMaterial.clone()
        );

        const edgeGeometry = RubikRoundedPlaneGeometry(
            pieceSize,
            this.geometry.edgeCornerRoundness,
            this.geometry.edgeDepth
        );

        edgeGeometry.scale(1, 1, 0.0002);

        this.positions.forEach((position, index) => {

            const piece = new BABYLON.Object3D();
            const pieceCube = pieceMesh.clone();
            const pieceEdges = [];

            piece.position.copy(position.clone().divideScalar(3));
            piece.add(pieceCube);
            piece.name = index;
            piece.edgesName = '';

            position.edges.forEach(position => {

                const edge = new BABYLON.Mesh(edgeGeometry, mainMaterial.clone());
                const name = ['L', 'R', 'D', 'U', 'B', 'F'][position];
                const distance = pieceSize / 2;

                edge.position.set(
                    distance * [-1, 1, 0, 0, 0, 0][position],
                    distance * [0, 0, -1, 1, 0, 0][position],
                    distance * [0, 0, 0, 0, -1, 1][position]
                );

                edge.rotation.set(
                    Math.PI / 2 * [0, 0, 1, -1, 0, 0][position],
                    Math.PI / 2 * [-1, 1, 0, 0, 2, 0][position],
                    0
                );

                edge.scale.set(
                    this.geometry.edgeScale,
                    this.geometry.edgeScale,
                    this.geometry.edgeScale
                );

                //console.log(edge.scale);

                edge.name = name;

                piece.add(edge);
                pieceEdges.push(name);
                this.edges.push(edge);

            });

            piece.userData.edges = pieceEdges;
            piece.userData.cube = pieceCube;

            piece.userData.start = {
                position: piece.position.clone(),
                rotation: piece.rotation.clone(),
            };

            this.pieces.push(piece);

        });

    },

    updateColors: function(colors) {

        if (typeof this.pieces !== 'object' && typeof this.edges !== 'object') return;

        this.pieces.forEach(piece => piece.userData.cube.material.color.setHex(colors.P));
        this.edges.forEach(edge => edge.material.color.setHex(colors[edge.name]));

    },

    loadFromData: function(data) {

        this.size = data.size;

        //this.reset();
        //this.init();

        this.pieces.forEach(piece => {

            const index = data.names.indexOf(piece.name);

            const position = data.positions[index];
            const rotation = data.rotations[index];

            piece.position.set(position.x, position.y, position.z);
            piece.rotation.set(rotation.x, rotation.y, rotation.z);

        });

    }


};


var Easing = {

    Power: {

        In: power => {

            power = Math.round(power || 1);

            return t => Math.pow(t, power);

        },

        Out: power => {

            power = Math.round(power || 1);

            return t => 1 - Math.abs(Math.pow(t - 1, power));

        },

        InOut: power => {

            power = Math.round(power || 1);

            return t => (t < 0.5) ?
                Math.pow(t * 2, power) / 2 :
                (1 - Math.abs(Math.pow((t * 2 - 1) - 1, power))) / 2 + 0.5;

        },

    },

    Sine: {

        In: () => t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2),

        Out: () => t => Math.sin(Math.PI / 2 * t),

        InOut: () => t => (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2,

    },

    Back: {

        Out: s => {

            s = s || 1.70158;

            return t => { return (t -= 1) * t * ((s + 1) * t + s) + 1; };

        },

        In: s => {

            s = s || 1.70158;

            return t => { return t * t * ((s + 1) * t - s); };

        }

    },

    Elastic: {

        Out: (amplitude, period) => {

            let PI2 = Math.PI * 2;

            let p1 = (amplitude >= 1) ? amplitude : 1;
            let p2 = (period || 0.3) / (amplitude < 1 ? amplitude : 1);
            let p3 = p2 / PI2 * (Math.asin(1 / p1) || 0);

            p2 = PI2 / p2;

            return t => { return p1 * Math.pow(2, -10 * t) * Math.sin((t - p3) * p2) + 1 }

        },

    },

};

var RubikTween = function(options) {
    RubikAnimation.call();
    this.duration = options.duration || 500;
    this.easing = options.easing || (t => t);
    this.onUpdate = options.onUpdate || (() => {});
    this.onComplete = options.onComplete || (() => {});

    this.delay = options.delay || false;
    this.yoyo = options.yoyo ? false : null;

    this.progress = 0;
    this.value = 0;
    this.delta = 0;

    this.getFromTo(options);

    if (this.delay) setTimeout(() => this.start(), this.delay);
    else this.start();

    this.onUpdate(this);
};

RubikTween.prototype = Object.create(RubikAnimation.prototype);
RubikTween.prototype.constructor = RubikTween;

RubikTween.prototype.update = function(delta) {

    const old = this.value * 1;
    const direction = (this.yoyo === true) ? -1 : 1;

    this.progress += (delta / this.duration) * direction;

    this.value = this.easing(this.progress);
    this.delta = this.value - old;

    if (this.values !== null) this.updateFromTo();

    if (this.yoyo !== null) this.updateYoyo();
    else if (this.progress <= 1) this.onUpdate(this);
    else {

        this.progress = 1;
        this.value = 1;
        this.onUpdate(this);
        this.onComplete(this);
        this.stop();

    }

};


RubikTween.prototype.updateYoyo = function() {

    if (this.progress > 1 || this.progress < 0) {

        this.value = this.progress = (this.progress > 1) ? 1 : 0;
        this.yoyo = !this.yoyo;

    }

    this.onUpdate(this);

};

RubikTween.prototype.updateFromTo = function() {

    this.values.forEach(key => {

        this.target[key] = this.from[key] + (this.to[key] - this.from[key]) * this.value;

    });

};

RubikTween.prototype.getFromTo = function(options) {

    if (!options.target || !options.to) {

        this.values = null;
        return;

    }

    this.target = options.target || null;
    this.from = options.from || {};
    this.to = options.to || null;
    this.values = [];

    if (Object.keys(this.from).length < 1)
        Object.keys(this.to).forEach(key => { this.from[key] = this.target[key]; });

    Object.keys(this.to).forEach(key => { this.values.push(key); });

};

var STILL = 0;
var PREPARING = 1;
var ROTATING = 2;
var ANIMATING = 3;

var RubikControls = function(game) {
    this.game = game;

    this.flipConfig = 0;

    this.flipEasings = [Easing.Power.Out(3), Easing.Sine.Out(), Easing.Back.Out(1.5)];
    this.flipSpeeds = [125, 200, 300];

    this.raycaster = new BABYLON.Raycaster();

    const helperMaterial = new BABYLON.MeshBasicMaterial({ depthWrite: false, transparent: true, opacity: 0.5, color: 0x0033ff, side: 2, visible: false });

    this.group = new BABYLON.Object3D();
    this.group.name = 'controls';
    this.game.cube.object.add(this.group);

    this.helper = new BABYLON.Mesh(
        new BABYLON.PlaneBufferGeometry(2.5, 2.5),
        helperMaterial.clone()
    );

    this.helper.rotation.set(0, Math.PI / 4, 0);
    this.game.world.scene.add(this.helper);

    this.edges = new BABYLON.Mesh(
        new BABYLON.BoxBufferGeometry(1, 1, 1),
        helperMaterial.clone(),
    );

    this.game.world.scene.add(this.edges);

    this.onSolved = () => {};
    this.onMove = () => {};

    this.momentum = [];

    this.scramble = null;
    this.state = STILL;
    this.enabled = false;

    this.initDraggable();
};

RubikControls.prototype = {

    initDraggable: function() {

        this.draggable = new RubikDraggable(this.game.dom.game);

        this.draggable.onDragStart = position => {

            if (this.scramble !== null) return;
            if (this.state === PREPARING || this.state === ROTATING) return;

            this.gettingDrag = this.state === ANIMATING;

            const edgeIntersect = this.getIntersect(position.current, this.edges, false);

            if (edgeIntersect !== false) {

                this.dragIntersect = this.getIntersect(position.current, this.game.cube.cubes, true);

            }

            //console.log(edgeIntersect, this.dragIntersect);

            if (edgeIntersect !== false && this.dragIntersect !== false) {

                this.dragNormal = edgeIntersect.face.normal.round();
                this.flipType = 'layer';

                //this.attach(this.helper, this.edges);

                //this.helper.rotation.set(0, 0, 0);
                this.helper.position.set(0, 0, 0);
                //this.helper.lookAt(this.dragNormal);
                this.helper.lookAt(chat3d.world.eyes.position);

                /*this.helper.translateZ(0.5);
                this.helper.updateMatrixWorld();

                this.detach(this.helper, this.edges);*/

            } else {

                this.dragNormal = new BABYLON.Vector3(0, 0, 1);
                this.flipType = 'cube';

                this.helper.position.set(0, 0, 0);
                //this.helper.rotation.set(0, Math.PI / 4, 0);
                this.helper.lookAt(chat3d.world.eyes.position);
                //this.helper.updateMatrixWorld();

            }

            let planeIntersect = this.getIntersect(position.current, this.helper, false);
            //console.log(planeIntersect, position.current, this.helper);

            if (planeIntersect === false) return;

            this.dragCurrent = this.helper.worldToLocal(planeIntersect.point);
            this.dragTotal = new BABYLON.Vector3();
            this.state = (this.state === STILL) ? PREPARING : this.state;
            return true;

        };

        this.draggable.onDragMove = position => {
            //console.log(this.scramble);
            //console.log(this.state, this.gettingDrag);

            this.helper.position.set(0, 0, 0);
            this.helper.lookAt(chat3d.world.eyes.position);

            if (this.scramble !== null) return;
            if (this.state === STILL || (this.state === ANIMATING && this.gettingDrag === false)) return;



            const planeIntersect = this.getIntersect(position.current, this.helper, false);
            //console.log(planeIntersect, position.current, this.helper);
            if (planeIntersect === false) return;

            const point = this.helper.worldToLocal(planeIntersect.point.clone());

            this.dragDelta = point.clone().sub(this.dragCurrent).setZ(0);
            this.dragTotal.add(this.dragDelta);
            this.dragCurrent = point;
            this.addMomentumPoint(this.dragDelta);
            //console.log(this.state, this.dragTotal.length(), PREPARING, this.flipType);

            if (this.state === PREPARING && this.dragTotal.length() > 0.05) {

                this.dragDirection = this.getMainAxis(this.dragTotal);

                if (this.flipType === 'layer') {
                    console.log('layer');

                    const direction = new BABYLON.Vector3();
                    direction[this.dragDirection] = 1;

                    const worldDirection = this.helper.localToWorld(direction).sub(this.helper.position);
                    const objectDirection = this.edges.worldToLocal(worldDirection).round();

                    this.flipAxis = objectDirection.cross(this.dragNormal).negate();

                    this.selectLayer(this.getLayer(false));

                } else {

                    const axis = (this.dragDirection != 'x') ?
                        ((this.dragDirection == 'y' && position.current.x > this.game.world.width / 2) ? 'z' : 'x') :
                        'y';

                    this.flipAxis = new BABYLON.Vector3();
                    this.flipAxis[axis] = 1 * ((axis == 'x') ? -1 : 1);

                }

                this.flipAngle = 0;
                this.state = ROTATING;

            } else if (this.state === ROTATING) {

                const rotation = this.dragDelta[this.dragDirection];

                if (this.flipType === 'layer') {

                    this.group.rotateOnAxis(this.flipAxis, rotation);
                    this.flipAngle += rotation;

                } else {
                    //console.log(this.edges);
                    this.edges.rotateOnAxis(this.flipAxis, rotation);
                    //console.log(this.edges.rotation);
                    this.game.cube.object.rotation.copy(this.edges.rotation);
                    this.flipAngle += rotation;

                }

            }
            return true;

        };

        this.draggable.onDragEnd = position => {

            if (this.scramble !== null) return;
            if (this.state !== ROTATING) {

                this.gettingDrag = false;
                this.state = STILL;
                return;

            }

            this.state = ANIMATING;

            const momentum = this.getMomentum()[this.dragDirection];
            const flip = (Math.abs(momentum) > 0.05 && Math.abs(this.flipAngle) < Math.PI / 2);

            const angle = flip ?
                this.roundAngle(this.flipAngle + Math.sign(this.flipAngle) * (Math.PI / 4)) :
                this.roundAngle(this.flipAngle);

            const delta = angle - this.flipAngle;

            if (this.flipType === 'layer') {

                this.rotateLayer(delta, false, layer => {

                    this.game.storage.saveGame();

                    this.state = this.gettingDrag ? PREPARING : STILL;
                    this.gettingDrag = false;

                    this.checkIsSolved();

                });

            } else {

                this.rotateCube(delta, () => {

                    this.state = this.gettingDrag ? PREPARING : STILL;
                    this.gettingDrag = false;

                });

            }

        };

    },

    enable: function() {
        //this.draggable.enable();
        this.enabled = true;
    },


    disable: function() {

        //this.draggable.disable();
        this.enabled = false;

    },

    onStart: function() {
        if (this.enable) {
            if (this.scramble !== null) return;
            if (this.state === PREPARING || this.state === ROTATING) return;

            this.gettingDrag = this.state === ANIMATING;

            const edgeIntersect = this.getIntersect(position.current, this.edges, false);

            if (edgeIntersect !== false) {

                this.dragIntersect = this.getIntersect(position.current, this.game.cube.cubes, true);

            }

            if (edgeIntersect !== false && this.dragIntersect !== false) {

                this.dragNormal = edgeIntersect.face.normal.round();
                this.flipType = 'layer';

                this.attach(this.helper, this.edges);

                this.helper.rotation.set(0, 0, 0);
                this.helper.position.set(0, 0, 0);
                this.helper.lookAt(this.dragNormal);
                this.helper.translateZ(0.5);
                this.helper.updateMatrixWorld();

                this.detach(this.helper, this.edges);

            } else {

                this.dragNormal = new BABYLON.Vector3(0, 0, 1);
                this.flipType = 'cube';

                this.helper.position.set(0, 0, 0);
                this.helper.rotation.set(0, Math.PI / 4, 0);
                this.helper.updateMatrixWorld();

            }

            let planeIntersect = this.getIntersect(position.current, this.helper, false);
            if (planeIntersect === false) return;

            this.dragCurrent = this.helper.worldToLocal(planeIntersect.point);
            this.dragTotal = new BABYLON.Vector3();
            this.state = (this.state === STILL) ? PREPARING : this.state;
        }
    },

    onMove: function() {
        if (this.scramble !== null) return;
        if (this.state === RybikSTILL || (this.state === RybikANIMATING && this.gettingDrag === false)) return;

        const planeIntersect = this.getIntersect(position.current, this.helper, false);
        if (planeIntersect === false) return;

        const point = this.helper.worldToLocal(planeIntersect.point.clone());

        this.dragDelta = point.clone().sub(this.dragCurrent).setZ(0);
        this.dragTotal.add(this.dragDelta);
        this.dragCurrent = point;
        this.addMomentumPoint(this.dragDelta);

        if (this.state === RubikPREPARING && this.dragTotal.length() > 0.05) {

            this.dragDirection = this.getMainAxis(this.dragTotal);

            if (this.flipType === 'layer') {
                console.log(this.flipType);

                const direction = new BABYLON.Vector3();
                direction[this.dragDirection] = 1;

                const worldDirection = this.helper.localToWorld(direction).sub(this.helper.position);
                const objectDirection = this.edges.worldToLocal(worldDirection).round();

                this.flipAxis = objectDirection.cross(this.dragNormal).negate();

                this.selectLayer(this.getLayer(false));

            } else {

                const axis = (this.dragDirection != 'x') ?
                    ((this.dragDirection == 'y' && position.current.x > this.game.world.width / 2) ? 'z' : 'x') :
                    'y';

                this.flipAxis = new BABYLON.Vector3();
                this.flipAxis[axis] = 1 * ((axis == 'x') ? -1 : 1);

            }

            this.flipAngle = 0;
            this.state = RybikROTATING;

        } else if (this.state === RybikROTATING) {

            const rotation = this.dragDelta[this.dragDirection];

            if (this.flipType === 'layer') {

                this.group.rotateOnAxis(this.flipAxis, rotation);
                this.flipAngle += rotation;

            } else {

                this.edges.rotateOnWorldAxis(this.flipAxis, rotation);
                this.game.cube.object.rotation.copy(this.edges.rotation);
                this.flipAngle += rotation;

            }

        }
    },

    onEnd: function() {
        if (this.scramble !== null) return;
        if (this.state !== RybikROTATING) {

            this.gettingDrag = false;
            this.state = RybikSTILL;
            return;

        }

        this.state = RybikANIMATING;

        const momentum = this.getMomentum()[this.dragDirection];
        const flip = (Math.abs(momentum) > 0.05 && Math.abs(this.flipAngle) < Math.PI / 2);

        const angle = flip ?
            this.roundAngle(this.flipAngle + Math.sign(this.flipAngle) * (Math.PI / 4)) :
            this.roundAngle(this.flipAngle);

        const delta = angle - this.flipAngle;

        if (this.flipType === 'layer') {

            this.rotateLayer(delta, false, layer => {

                this.game.storage.saveGame();

                this.state = this.gettingDrag ? RybikPREPARING : RybikSTILL;
                this.gettingDrag = false;

                this.checkIsSolved();

            });

        } else {

            this.rotateCube(delta, () => {

                this.state = this.gettingDrag ? RybikPREPARING : RybikSTILL;
                this.gettingDrag = false;

            });

        }
    },

    rotateLayer: function(rotation, scramble, callback) {
        const config = scramble ? 0 : this.flipConfig;

        const easing = this.flipEasings[config];
        const duration = this.flipSpeeds[config];
        const bounce = (config == 2) ? this.bounceCube() : (() => {});

        this.rotationTween = new RubikTween({
            easing: easing,
            duration: duration,
            onUpdate: tween => {

                let deltaAngle = tween.delta * rotation;
                this.group.rotateOnAxis(this.flipAxis, deltaAngle);
                bounce(tween.value, deltaAngle, rotation);

            },
            onComplete: () => {

                if (!scramble) this.onMove();

                const layer = this.flipLayer.slice(0);

                this.game.cube.object.rotation.setFromVector3(this.snapRotation(this.game.cube.object.rotation.toVector3()));
                this.group.rotation.setFromVector3(this.snapRotation(this.group.rotation.toVector3()));
                this.deselectLayer(this.flipLayer);

                callback(layer);

            },
        });
    },

    bounceCube: function() {
        let fixDelta = true;

        return (progress, delta, rotation) => {

            if (progress >= 1) {

                if (fixDelta) {

                    delta = (progress - 1) * rotation;
                    fixDelta = false;

                }

                this.game.cube.object.rotateOnAxis(this.flipAxis, delta);

            }

        }
    },

    rotateCube: function(rotation, callback) {
        const config = this.flipConfig;
        const easing = [Easing.Power.Out(4), Easing.Sine.Out(), Easing.Back.Out(2)][config];
        const duration = [100, 150, 350][config];

        this.rotationTween = new RubikTween({
            easing: easing,
            duration: duration,
            onUpdate: tween => {

                this.edges.rotateOnAxis(this.flipAxis, tween.delta * rotation);
                this.game.cube.object.rotation.copy(this.edges.rotation);

            },
            onComplete: () => {

                this.edges.rotation.setFromVector3(this.snapRotation(this.edges.rotation.toVector3()));
                this.game.cube.object.rotation.copy(this.edges.rotation);
                callback();

            },
        });
    },

    selectLayer: function(layer) {
        this.group.rotation.set(0, 0, 0);
        this.movePieces(layer, this.game.cube.object, this.group);
        this.flipLayer = layer;
    },

    deselectLayer: function(layer) {

        this.movePieces(layer, this.group, this.game.cube.object);
        this.flipLayer = null;

    },

    movePieces: function(layer, from, to) {
        //console.log(layer);

        from.updateMatrixWorld();
        to.updateMatrixWorld();

        layer.forEach(index => {

            const piece = this.game.cube.pieces[index];

            piece.applyMatrix(from.matrixWorld);
            from.remove(piece);
            piece.applyMatrix(new BABYLON.Matrix4().getInverse(to.matrixWorld));
            to.add(piece);

        });

    },


    getLayer: function(position) {

        const scalar = { 2: 6, 3: 3, 4: 4, 5: 3 }[this.game.cube.size];
        const layer = [];

        let axis;

        if (position === false) {

            const piece = this.dragIntersect.object.parent;

            axis = this.getMainAxis(this.flipAxis);
            position = piece.position.clone().multiplyScalar(scalar).round();

        } else {

            axis = this.getMainAxis(position);

        }

        this.game.cube.pieces.forEach(piece => {

            const piecePosition = piece.position.clone().multiplyScalar(scalar).round();

            if (piecePosition[axis] == position[axis]) layer.push(piece.name);

        });

        return layer;

    },


    keyboardMove: function(type, move, callback) {

        if (this.state !== STILL) return;
        if (this.enabled !== true) return;

        if (type === 'LAYER') {

            const layer = this.getLayer(move.position);

            this.flipAxis = new BABYLON.Vector3();
            this.flipAxis[move.axis] = 1;
            this.state = ROTATING;

            this.selectLayer(layer);
            this.rotateLayer(move.angle, false, layer => {

                this.game.storage.saveGame();
                this.state = STILL;
                this.checkIsSolved();

            });

        } else if (type === 'CUBE') {

            this.flipAxis = new BABYLON.Vector3();
            this.flipAxis[move.axis] = 1;
            this.state = ROTATING;

            this.rotateCube(move.angle, () => {

                this.state = STILL;

            });

        }

    },


    scrambleCube: function() {

        if (this.scramble == null) {

            this.scramble = this.game.scrambler;
            this.scramble.callback = (typeof callback !== 'function') ? () => {} : callback;

        }

        const converted = this.scramble.converted;
        const move = converted[0];
        const layer = this.getLayer(move.position);

        this.flipAxis = new BABYLON.Vector3();
        this.flipAxis[move.axis] = 1;

        this.selectLayer(layer);
        this.rotateLayer(move.angle, true, () => {

            converted.shift();

            if (converted.length > 0) {

                this.scrambleCube();

            } else {

                this.scramble = null;
                this.game.storage.saveGame();

            }

        });

    },

    getIntersect: function(position, object, multiple) {

        this.raycaster.setFromCamera(
            this.draggable.convertPosition(position.clone()),
            chat3d.world.camera
        );

        //console.log(this.raycaster, this.draggable.convertPosition(position.clone()));

        const intersect = (multiple) ?
            this.raycaster.intersectObjects(object) :
            this.raycaster.intersectObject(object);

        return (intersect.length > 0) ? intersect[0] : false;

    },

    getMainAxis: function(vector) {

        return Object.keys(vector).reduce(
            (a, b) => Math.abs(vector[a]) > Math.abs(vector[b]) ? a : b
        );

    },


    detach: function(child, parent) {

        child.applyMatrix(parent.matrixWorld);
        parent.remove(child);
        this.game.world.scene.add(child);

    },

    attach: function(child, parent) {

        child.applyMatrix(new BABYLON.Matrix4().getInverse(parent.matrixWorld));
        this.game.world.scene.remove(child);
        parent.add(child);

    },


    addMomentumPoint: function(delta) {

        const time = Date.now();

        this.momentum = this.momentum.filter(moment => time - moment.time < 500);

        if (delta !== false) this.momentum.push({ delta, time });

    },

    getMomentum: function() {

        const points = this.momentum.length;
        const momentum = new BABYLON.Vector2();

        this.addMomentumPoint(false);

        this.momentum.forEach((point, index) => {

            momentum.add(point.delta.multiplyScalar(index / points));

        });

        return momentum;

    },

    roundAngle: function(angle) {

        const round = Math.PI / 2;
        return Math.sign(angle) * Math.round(Math.abs(angle) / round) * round;

    },

    snapRotation: function(angle) {

        return angle.set(
            this.roundAngle(angle.x),
            this.roundAngle(angle.y),
            this.roundAngle(angle.z)
        );

    },

    checkIsSolved: function() {

        const start = performance.now();

        let solved = true;
        const sides = { 'x-': [], 'x+': [], 'y-': [], 'y+': [], 'z-': [], 'z+': [] };

        this.game.cube.edges.forEach(edge => {

            const position = edge.parent
                .localToWorld(edge.position.clone())
                .sub(this.game.cube.object.position);

            const mainAxis = this.getMainAxis(position);
            const mainSign = position.multiplyScalar(2).round()[mainAxis] < 1 ? '-' : '+';

            sides[mainAxis + mainSign].push(edge.name);

        });

        Object.keys(sides).forEach(side => {

            if (!sides[side].every(value => value === sides[side][0])) solved = false;

        });

        if (solved) this.onSolved();

    }


};


var RubikScrambler = function(game) {
    this.game = game;

    this.dificulty = 0;

    this.scrambleLength = {
        2: [7, 9, 11],
        3: [20, 25, 30],
        4: [30, 40, 50],
        5: [40, 60, 80],
    };

    this.moves = [];
    this.conveted = [];
    this.pring = '';
};

RubikScrambler.prototype = {
    constructor: RubikScrambler,
    scramble: function(scramble) {

        let count = 0;
        this.moves = (typeof scramble !== 'undefined') ? scramble.split(' ') : [];

        if (this.moves.length < 1) {

            const scrambleLength = this.scrambleLength[this.game.cube.size][this.dificulty];

            const faces = this.game.cube.size < 4 ? 'UDLRFB' : 'UuDdLlRrFfBb';
            const modifiers = ["", "'", "2"];
            const total = (typeof scramble === 'undefined') ? scrambleLength : scramble;

            while (count < total) {

                const move =
                    faces[Math.floor(Math.random() * faces.length)] +
                    modifiers[Math.floor(Math.random() * 3)];

                if (count > 0 && move.charAt(0) == this.moves[count - 1].charAt(0)) continue;
                if (count > 1 && move.charAt(0) == this.moves[count - 2].charAt(0)) continue;

                this.moves.push(move);
                count++;

            }

        }

        this.callback = () => {};
        this.convert();
        this.print = this.moves.join(' ');

        return this;

    },

    convert: function(moves) {

        this.converted = [];

        this.moves.forEach(move => {

            const convertedMove = this.convertMove(move);
            const modifier = move.charAt(1);

            this.converted.push(convertedMove);
            if (modifier == "2") this.converted.push(convertedMove);

        });

    },

    convertMove: function(move) {

        const face = move.charAt(0);
        const modifier = move.charAt(1);

        const axis = { D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[face.toUpperCase()];
        let row = { D: -1, U: 1, L: -1, R: 1, F: 1, B: -1 }[face.toUpperCase()];

        if (this.game.cube.size > 3 && face !== face.toLowerCase()) row = row * 2;

        const position = new BABYLON.Vector3();
        position[{ D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[face.toUpperCase()]] = row;

        const angle = (Math.PI / 2) * -row * ((modifier == "'") ? -1 : 1);

        return { position, axis, angle, name: move };

    }
};

var RubikTransition = function(game) {
    this.game = game;

    this.tweens = {};
    this.durations = {};
    this.data = {
        cubeY: -0.2,
        cameraZoom: 0.85,
    };

    this.activeTransitions = 0;
};

RubikTransition.prototype = {
    constructor: RubikTransition,
    init: function() {
        this.game.controls.disable();

        this.game.cube.object.position.y = this.data.cubeY;
        this.game.cube.animator.position.y = 4;
        this.game.cube.animator.rotation.x = -Math.PI / 3;
        //this.game.world.camera.zoom = this.data.cameraZoom;
        //this.game.world.camera.updateProjectionMatrix();

        this.tweens.buttons = {};
        this.tweens.timer = [];
        this.tweens.title = [];
        this.tweens.best = [];
        this.tweens.complete = [];
        this.tweens.prefs = [];
        this.tweens.theme = [];
        this.tweens.stats = [];
    },

    button: function(show, hide) {

    },


    cube: function(show, theming = false) {

        this.activeTransitions++;

        try { this.tweens.cube.stop(); } catch (e) {}
        const currentY = this.game.cube.animator.position.y;
        const currentRotation = this.game.cube.animator.rotation.x;

        this.tweens.cube = new RubikTween({
            duration: show ? 3000 : 1250,
            easing: show ? Easing.Elastic.Out(0.8, 0.6) : Easing.Back.In(1),
            onUpdate: tween => {

                this.game.cube.animator.position.y = show ?
                    (theming ? 0.9 + (1 - tween.value) * 3.5 : (1 - tween.value) * 4) :
                    currentY + tween.value * 4;

                this.game.cube.animator.rotation.x = show ?
                    (1 - tween.value) * Math.PI / 3 :
                    currentRotation + tween.value * -Math.PI / 3;

            },
        });

        if (theming) {

            if (show) {

                this.game.world.camera.zoom = 0.75;
                this.game.world.camera.updateProjectionMatrix();

            } else {

                setTimeout(() => {

                    this.game.world.camera.zoom = this.data.cameraZoom;
                    this.game.world.camera.updateProjectionMatrix();

                }, 1500);

            }

        }

        this.durations.cube = show ? 1500 : 1500;

        setTimeout(() => this.activeTransitions--, this.durations.cube);

    },

    float: function() {

        try { this.tweens.float.stop(); } catch (e) {}
        this.tweens.float = new RubikTween({
            duration: 1500,
            easing: Easing.Sine.InOut(),
            yoyo: true,
            onUpdate: tween => {

                this.game.cube.holder.position.y = (-0.02 + tween.value * 0.04);
                this.game.cube.holder.rotation.x = 0.005 - tween.value * 0.01;
                this.game.cube.holder.rotation.z = -this.game.cube.holder.rotation.x;
                this.game.cube.holder.rotation.y = this.game.cube.holder.rotation.x;

                this.game.controls.edges.position.y =
                    this.game.cube.holder.position.y + this.game.cube.object.position.y;

            },
        });

    },

    zoom: function(play, time) {

        this.activeTransitions++;

        const zoom = (play) ? 1 : this.data.cameraZoom;
        const duration = (time > 0) ? Math.max(time, 1500) : 1500;
        const rotations = (time > 0) ? Math.round(duration / 1500) : 1;
        const easing = Easing.Power.InOut((time > 0) ? 2 : 3);

        this.tweens.zoom = new RubikTween({
            target: this.game.world.camera,
            duration: duration,
            easing: easing,
            to: { zoom: zoom },
            onUpdate: () => { chat3d.world.camera.updateProjectionMatrix(); },
        });

        this.tweens.rotate = new RubikTween({
            target: this.game.cube.animator.rotation,
            duration: duration,
            easing: easing,
            to: { y: -Math.PI * 2 * rotations },
            onComplete: () => { this.game.cube.animator.rotation.y = 0; },
        });

        this.durations.zoom = duration;

        setTimeout(() => this.activeTransitions--, this.durations.zoom);

    },

    elevate: function(complete) {

        this.activeTransitions++;

        const cubeY =

            this.tweens.elevate = new Tween({
                target: this.game.cube.object.position,
                duration: complete ? 1500 : 0,
                easing: Easing.Power.InOut(3),
                to: { y: complete ? -0.05 : this.data.cubeY }
            });

        this.durations.elevate = 1500;

        setTimeout(() => this.activeTransitions--, this.durations.elevate);

    },

    complete: function(show, best) {

        this.activeTransitions++;

        const text = best ? this.game.dom.texts.best : this.game.dom.texts.complete;

        if (text.querySelector('span i') === null)
            text.querySelectorAll('span').forEach(span => this.splitLetters(span));

        const letters = text.querySelectorAll('.icon, i');

        this.flipLetters(best ? 'best' : 'complete', letters, show);

        text.style.opacity = 1;

        const duration = this.durations[best ? 'best' : 'complete'];

        if (!show) setTimeout(() => this.game.dom.texts.timer.style.transform = '', duration);

        setTimeout(() => this.activeTransitions--, duration);

    },

    stats: function(show) {

        if (show) this.game.scores.calcStats();

        this.activeTransitions++;

        this.tweens.stats.forEach(tween => {
            tween.stop();
            tween = null;
        });

        let tweenId = -1;

        const stats = this.game.dom.stats.querySelectorAll('.stats');
        const easing = show ? Easing.Power.Out(2) : Easing.Power.In(3);

        stats.forEach((stat, index) => {

            const delay = index * (show ? 80 : 60);

            this.tweens.stats[tweenId++] = new Tween({
                delay: delay,
                duration: 400,
                easing: easing,
                onUpdate: tween => {

                    const translate = show ? (1 - tween.value) * 2 : tween.value;
                    const opacity = show ? tween.value : (1 - tween.value);

                    stat.style.transform = `translate3d(0, ${translate}em, 0)`;
                    stat.style.opacity = opacity;

                }
            });

        });

        this.durations.stats = 0;

        setTimeout(() => this.activeTransitions--, this.durations.stats);

    },

    preferences: function(show) {

        this.ranges(this.game.dom.prefs.querySelectorAll('.range'), 'prefs', show);

    },

    theming: function(show) {

        this.ranges(this.game.dom.theme.querySelectorAll('.range'), 'prefs', show);

    },


    ranges: function(ranges, type, show) {

        this.activeTransitions++;

        this.tweens[type].forEach(tween => {
            tween.stop();
            tween = null;
        });

        const easing = show ? Easing.Power.Out(2) : Easing.Power.In(3);

        let tweenId = -1;
        let listMax = 0;

        ranges.forEach((range, rangeIndex) => {

            const label = range.querySelector('.range__label');
            const track = range.querySelector('.range__track-line');
            const handle = range.querySelector('.range__handle');
            const list = range.querySelectorAll('.range__list div');

            const delay = rangeIndex * (show ? 120 : 100);

            label.style.opacity = show ? 0 : 1;
            track.style.opacity = show ? 0 : 1;
            handle.style.opacity = show ? 0 : 1;
            handle.style.pointerEvents = show ? 'all' : 'none';

            this.tweens[type][tweenId++] = new Tween({
                delay: show ? delay : delay,
                duration: 400,
                easing: easing,
                onUpdate: tween => {

                    const translate = show ? (1 - tween.value) : tween.value;
                    const opacity = show ? tween.value : (1 - tween.value);

                    label.style.transform = `translate3d(0, ${translate}em, 0)`;
                    label.style.opacity = opacity;

                }
            });

            this.tweens[type][tweenId++] = new Tween({
                delay: show ? delay + 100 : delay,
                duration: 400,
                easing: easing,
                onUpdate: tween => {

                    const translate = show ? (1 - tween.value) : tween.value;
                    const scale = show ? tween.value : (1 - tween.value);
                    const opacity = scale;

                    track.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, 1, 1)`;
                    track.style.opacity = opacity;

                }
            });

            this.tweens[type][tweenId++] = new Tween({
                delay: show ? delay + 100 : delay,
                duration: 400,
                easing: easing,
                onUpdate: tween => {

                    const translate = show ? (1 - tween.value) : tween.value;
                    const opacity = 1 - translate;
                    const scale = 0.5 + opacity * 0.5;

                    handle.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, ${scale}, ${scale})`;
                    handle.style.opacity = opacity;

                }
            });

            list.forEach((listItem, labelIndex) => {

                listItem.style.opacity = show ? 0 : 1;

                this.tweens[type][tweenId++] = new Tween({
                    delay: show ? delay + 200 + labelIndex * 50 : delay,
                    duration: 400,
                    easing: easing,
                    onUpdate: tween => {

                        const translate = show ? (1 - tween.value) : tween.value;
                        const opacity = show ? tween.value : (1 - tween.value);

                        listItem.style.transform = `translate3d(0, ${translate}em, 0)`;
                        listItem.style.opacity = opacity;

                    }
                });

            });

            listMax = list.length > listMax ? list.length - 1 : listMax;

            range.style.opacity = 1;

        });

        this.durations[type] = show ?
            ((ranges.length - 1) * 100) + 200 + listMax * 50 + 400 :
            ((ranges.length - 1) * 100) + 400;

        setTimeout(() => this.activeTransitions--, this.durations[type]);

    },

    title: function(show) {

        /*this.activeTransitions++;

        const title = this.game.dom.texts.title;

        if (title.querySelector('span i') === null)
            title.querySelectorAll('span').forEach(span => this.splitLetters(span));

        const letters = title.querySelectorAll('i');

        this.flipLetters('title', letters, show);

        title.style.opacity = 1;

        const note = this.game.dom.texts.note;

        this.tweens.title[letters.length] = new Tween({
            target: note.style,
            easing: Easing.Sine.InOut(),
            duration: show ? 800 : 400,
            yoyo: show ? true : null,
            from: { opacity: show ? 0 : (parseFloat(getComputedStyle(note).opacity)) },
            to: { opacity: show ? 1 : 0 },
        });

        setTimeout(() => this.activeTransitions--, this.durations.title);*/

    },

    timer: function(show) {

        this.activeTransitions++;

        /* const timer = this.game.dom.texts.timer;

         timer.style.opacity = 0;
         this.game.timer.convert();
         this.game.timer.setText();

         this.splitLetters(timer);
         const letters = timer.querySelectorAll('i');
         this.flipLetters('timer', letters, show);

         timer.style.opacity = 1;*/

        setTimeout(() => this.activeTransitions--, this.durations.timer);

    },

    splitLetters: function(element) {

        const text = element.innerHTML;

        element.innerHTML = '';

        text.split('').forEach(letter => {

            const i = document.createElement('i');

            i.innerHTML = letter;

            element.appendChild(i);

        });

    },

    flipLetters: function(type, letters, show) {

        try { this.tweens[type].forEach(tween => tween.stop()); } catch (e) {}
        letters.forEach((letter, index) => {

            letter.style.opacity = show ? 0 : 1;

            this.tweens[type][index] = new Tween({
                easing: Easing.Sine.Out(),
                duration: show ? 800 : 400,
                delay: index * 50,
                onUpdate: tween => {

                    const rotation = show ? (1 - tween.value) * -80 : tween.value * 80;

                    letter.style.transform = `rotate3d(0, 1, 0, ${rotation}deg)`;
                    letter.style.opacity = show ? tween.value : (1 - tween.value);

                },
            });

        });

        this.durations[type] = (letters.length - 1) * 50 + (show ? 800 : 400);

    }
};

var RubikTimer = function(game) {
    RubikAnimation.call();
    this.game = game;
    console.log(this);
    this.reset();
}



RubikTimer.prototype = Object.create(RubikAnimation.prototype);

RubikTimer.prototype.constructor = RubikTimer;

RubikTimer.prototype.super_start = RubikTimer.prototype.start;

RubikTimer.prototype.start = function(continueGame) {

    this.startTime = continueGame ? (Date.now() - this.deltaTime) : Date.now();
    this.deltaTime = 0;
    this.converted = this.convert();

    this.super_start();

};

RubikTimer.prototype.reset = function() {

    this.startTime = 0;
    this.currentTime = 0;
    this.deltaTime = 0;
    this.converted = '0:00';

};

RubikTimer.prototype.super_stop = RubikTimer.prototype.stop;

RubikTimer.prototype.stop = function() {

    this.currentTime = Date.now();
    this.deltaTime = this.currentTime - this.startTime;
    this.convert();

    this.super_stop();

    return { time: this.converted, millis: this.deltaTime };

};


RubikTimer.prototype.update = function() {

    const old = this.converted;

    this.currentTime = Date.now();
    this.deltaTime = this.currentTime - this.startTime;
    this.convert();

    if (this.converted != old) {

        localStorage.setItem('theCube_time', this.deltaTime);
        this.setText();

    }

};

RubikTimer.prototype.convert = function() {

    const seconds = parseInt((this.deltaTime / 1000) % 60);
    const minutes = parseInt((this.deltaTime / (1000 * 60)));

    this.converted = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;

};

RubikTimer.prototype.setText = function() {

    //this.game.dom.texts.timer.innerHTML = this.converted;

};


var RubikDraggable = function(element, options) {
    this.position = {
        current: new BABYLON.Vector2(),
        start: new BABYLON.Vector2(),
        delta: new BABYLON.Vector2(),
        old: new BABYLON.Vector2(),
        drag: new BABYLON.Vector2(),
    };

    this.options = Object.assign({
        calcDelta: false,
    }, options || {});

    this.element = element;
    this.touch = null;

    this.drag = {

        start: (event) => {

            if (event.type == 'mousedown' && event.which != 1) return;
            if (event.type == 'touchstart' && event.touches.length > 1) return;

            this.getPositionCurrent(event);

            if (this.options.calcDelta) {

                this.position.start = this.position.current.clone();
                this.position.delta.set(0, 0);
                this.position.drag.set(0, 0);

            }

            this.touch = (event.type == 'touchstart');

            return this.onDragStart(this.position);

            //window.addEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
            //window.addEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );

        },

        move: (event) => {

            if (this.options.calcDelta) {

                this.position.old = this.position.current.clone();

            }

            this.getPositionCurrent(event);

            if (this.options.calcDelta) {

                this.position.delta = this.position.current.clone().sub(this.position.old);
                this.position.drag = this.position.current.clone().sub(this.position.start);

            }

            return this.onDragMove(this.position);

        },

        end: (event) => {

            this.getPositionCurrent(event);

            this.onDragEnd(this.position);

            //window.removeEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
            //window.removeEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );

        },

    };

    this.onDragStart = () => {};
    this.onDragMove = () => {};
    this.onDragEnd = () => {};

    this.enable();

    return this;

};

RubikDraggable.prototype = {
    constructor: RubikDraggable,
    enable: function() {

        //this.element.addEventListener( 'touchstart', this.drag.start, false );
        //this.element.addEventListener( 'mousedown', this.drag.start, false );

        return this;

    },

    disable: function() {

        //this.element.removeEventListener( 'touchstart', this.drag.start, false );
        //this.element.removeEventListener( 'mousedown', this.drag.start, false );

        return this;

    },

    getPositionCurrent: function(event) {

        const dragEvent = event.touches ?
            (event.touches[0] || event.changedTouches[0]) :
            event;

        this.position.current.set(dragEvent.pageX, dragEvent.pageY);

    },


    convertPosition: function(position) {

        position.x = (position.x / window.innerWidth) * 2 - 1;
        position.y = -((position.y / window.innerHeight) * 2 - 1);

        return position;

    }


};

var RubikRange = function(name, options) {
    options = Object.assign({
        range: [0, 1],
        value: 0,
        step: 0,
        onUpdate: () => {},
        onComplete: () => {},
    }, options || {});

    //this.element = document.querySelector('.range[name="' + name + '"]');
    //this.track = this.element.querySelector('.range__track');
    //this.handle = this.element.querySelector('.range__handle');
    //this.list = [].slice.call(this.element.querySelectorAll('.range__list div'));

    this.value = options.value;
    this.min = options.range[0];
    this.max = options.range[1];
    this.step = options.step;

    this.onUpdate = options.onUpdate;
    this.onComplete = options.onComplete;

    this.setValue(this.value);

    this.initDraggable();
    console.log(name, options);

};

RubikRange.prototype = {
    constructor: RubikRange,
    setValue: function(value) {

        this.value = this.round(this.limitValue(value));
        this.setHandlePosition();

    },

    initDraggable: function() {

        let current;

        this.draggable = new RubikDraggable(this.handle, { calcDelta: true });

        this.draggable.onDragStart = position => {

            current = this.positionFromValue(this.value);
            this.handle.style.left = current + 'px';

        };

        this.draggable.onDragMove = position => {

            current = this.limitPosition(current + position.delta.x);
            this.value = this.round(this.valueFromPosition(current));
            this.setHandlePosition();

            this.onUpdate(this.value);

        };

        this.draggable.onDragEnd = position => {

            this.onComplete(this.value);

        };

    },

    round: function(value) {

        if (this.step < 1) return value;

        return Math.round((value - this.min) / this.step) * this.step + this.min;

    },

    limitValue: function(value) {

        const max = Math.max(this.max, this.min);
        const min = Math.min(this.max, this.min);

        return Math.min(Math.max(value, min), max);

    },

    limitPosition: function(position) {

        return Math.min(Math.max(position, 0), this.track.offsetWidth);

    },

    percentsFromValue: function(value) {

        return (value - this.min) / (this.max - this.min);

    },

    valueFromPosition: function(position) {

        return this.min + (this.max - this.min) * (position / this.track.offsetWidth);

    },

    positionFromValue: function(value) {

        return this.percentsFromValue(value) * this.track.offsetWidth;

    },

    setHandlePosition: function() {

        //this.handle.style.left = this.percentsFromValue(this.value) * 100 + '%';

    }



};


var RubikPreferences = function(game) {
    this.game = game;
};

RubikPreferences.prototype = {
    constructor: RubikPreferences,

    init: function() {

        this.ranges = {

            size: new RubikRange('size', {
                value: this.game.cube.size,
                range: [2, 5],
                step: 1,
                onUpdate: value => {

                    this.game.cube.size = value;

                    this.game.preferences.ranges.scramble.list.forEach((item, i) => {

                        item.innerHTML = this.game.scrambler.scrambleLength[this.game.cube.size][i];

                    });

                },
                onComplete: () => this.game.storage.savePreferences(),
            }),

            flip: new RubikRange('flip', {
                value: this.game.controls.flipConfig,
                range: [0, 2],
                step: 1,
                onUpdate: value => {

                    this.game.controls.flipConfig = value;

                },
                onComplete: () => this.game.storage.savePreferences(),
            }),

            scramble: new RubikRange('scramble', {
                value: this.game.scrambler.dificulty,
                range: [0, 2],
                step: 1,
                onUpdate: value => {

                    this.game.scrambler.dificulty = value;

                },
                onComplete: () => this.game.storage.savePreferences()
            }),

            fov: new RubikRange('fov', {
                value: this.game.world.fov,
                range: [2, 45],
                onUpdate: value => {

                    this.game.world.fov = value;
                    this.game.world.resize();

                },
                onComplete: () => this.game.storage.savePreferences()
            }),

            theme: new RubikRange('theme', {
                value: { cube: 0, erno: 1, dust: 2, camo: 3, rain: 4 }[this.game.themes.theme],
                range: [0, 4],
                step: 1,
                onUpdate: value => {

                    const theme = ['cube', 'erno', 'dust', 'camo', 'rain'][value];
                    this.game.themes.setTheme(theme);

                },
                onComplete: () => this.game.storage.savePreferences()
            }),

            hue: new RubikRange('hue', {
                value: 0,
                range: [0, 360],
                onUpdate: value => this.game.themeEditor.updateHSL(),
                onComplete: () => this.game.storage.savePreferences(),
            }),

            saturation: new RubikRange('saturation', {
                value: 100,
                range: [0, 100],
                onUpdate: value => this.game.themeEditor.updateHSL(),
                onComplete: () => this.game.storage.savePreferences(),
            }),

            lightness: new RubikRange('lightness', {
                value: 50,
                range: [0, 100],
                onUpdate: value => this.game.themeEditor.updateHSL(),
                onComplete: () => this.game.storage.savePreferences(),
            }),

        };

        /*this.ranges.scramble.list.forEach((item, i) => {

            item.innerHTML = this.game.scrambler.scrambleLength[this.game.cube.size][i];

        });*/

    }
};

var RubikConfetti = function(game) {
    this.game = game;
    this.started = 0;

    this.options = {
        speed: { min: 0.0011, max: 0.0022 },
        revolution: { min: 0.01, max: 0.05 },
        size: { min: 0.1, max: 0.15 },
        colors: [0x41aac8, 0x82ca38, 0xffef48, 0xef3923, 0xff8c0a],
    };

    this.geometry = new BABYLON.PlaneGeometry(1, 1);
    this.material = new BABYLON.MeshLambertMaterial({ side: BABYLON.DoubleSide });

    this.holders = [
        new RubikConfettiStage(this.game, this, 1, 20),
        new RubikConfettiStage(this.game, this, -1, 30),
    ];
};

RubikConfetti.prototype = {
    constructor: RubikConfetti,

    start: function() {
        if (this.started > 0) return;

        this.holders.forEach(holder => {

            this.game.world.scene.add(holder.holder);
            holder.start();
            this.started++;

        });
    },

    stop: function() {
        if (this.started == 0) return;

        this.holders.forEach(holder => {

            holder.stop(() => {

                this.game.world.scene.remove(holder.holder);
                this.started--;

            });

        });
    },

    updateColors: function(colors) {
        this.holders.forEach(holder => {

            holder.options.colors.forEach((color, index) => {

                holder.options.colors[index] = colors[['D', 'F', 'R', 'B', 'L'][index]];

            });

        });
    }
};

var RubikConfettiStage = function(game, parent, distance, count) {
    RubikAnimation.call(false);
    this.game = game;
    this.parent = parent;

    this.distanceFromCube = distance;

    this.count = count;
    this.particles = [];

    this.holder = new BABYLON.Object3D();
    this.holder.rotation.copy(chat3d.world.eyes.rotation);

    this.object = new BABYLON.Object3D();
    this.holder.add(this.object);

    this.resizeViewport = this.resizeViewport.bind(this);
    //this.game.world.onResize.push(this.resizeViewport);
    this.resizeViewport();

    this.geometry = this.parent.geometry;
    this.material = this.parent.material;

    this.options = this.parent.options;

    let i = this.count;
    while (i--) this.particles.push(new RubikParticle(this));
};

RubikConfettiStage.prototype = Object.create(RubikAnimation.prototype);

RubikConfettiStage.prototype.constructor = RubikConfettiStage;

RubikConfettiStage.prototype.super_start = RubikConfettiStage.prototype.start;

RubikConfettiStage.prototype.start = function() {
    this.time = performance.now();
    this.playing = true;

    let i = this.count;
    while (i--) this.particles[i].reset();

    this.super_start();
};

RubikConfettiStage.prototype.super_stop = RubikConfettiStage.prototype.stop;

RubikConfettiStage.prototype.stop = function(callback) {

    this.playing = false;
    this.completed = 0;
    this.callback = callback;
};




RubikConfettiStage.prototype.reset = function() {

    this.super_stop();

    this.callback();

};

RubikConfettiStage.prototype.update = function() {

    const now = performance.now();
    const delta = now - this.time;
    this.time = now;

    let i = this.count;

    while (i--)
        if (!this.particles[i].completed) this.particles[i].update(delta);

    if (!this.playing && this.completed == this.count) this.reset();

};

RubikConfettiStage.prototype.resizeViewport = function() {

    const fovRad = chat3d.world.camera.fov * BABYLON.Math.DEG2RAD;

    this.height = 2 * Math.tan(fovRad / 2) * (chat3d.world.eyes.position.length() - this.distanceFromCube);
    this.width = this.height * chat3d.world.camera.aspect;

    const scale = 1 / this.game.transition.data.cameraZoom;

    this.width *= scale;
    this.height *= scale;

    this.object.position.z = this.distanceFromCube;
    this.object.position.y = this.height / 2;

};



RubikParticle = function(confetti) {
    this.confetti = confetti;
    this.options = this.confetti.options;

    this.velocity = new BABYLON.Vector3();
    this.force = new BABYLON.Vector3();

    this.mesh = new BABYLON.Mesh(this.confetti.geometry, this.confetti.material.clone());
    this.confetti.object.add(this.mesh);

    this.size = BABYLON.Math.randFloat(this.options.size.min, this.options.size.max);
    this.mesh.scale.set(this.size, this.size, this.size);

    return this;
};

RubikParticle.prototype = {
    reset: function(randomHeight = true) {

        this.completed = false;

        this.color = new BABYLON.Color(this.options.colors[Math.floor(Math.random() * this.options.colors.length)]);
        this.mesh.material.color.set(this.color);

        this.speed = BABYLON.Math.randFloat(this.options.speed.min, this.options.speed.max) * -1;
        this.mesh.position.x = BABYLON.Math.randFloat(-this.confetti.width / 2, this.confetti.width / 2);
        this.mesh.position.y = (randomHeight) ?
            BABYLON.Math.randFloat(this.size, this.confetti.height + this.size) :
            this.size;

        this.revolutionSpeed = BABYLON.Math.randFloat(this.options.revolution.min, this.options.revolution.max);
        this.revolutionAxis = ['x', 'y', 'z'][Math.floor(Math.random() * 3)];
        this.mesh.rotation.set(Math.random() * Math.PI / 3, Math.random() * Math.PI / 3, Math.random() * Math.PI / 3);

    },

    stop: function() {
        this.completed = true;
        this.confetti.completed++;

    },

    update: function(delta) {

        this.mesh.position.y += this.speed * delta;
        this.mesh.rotation[this.revolutionAxis] += this.revolutionSpeed;

        if (this.mesh.position.y < -this.confetti.height - this.size)
            (this.confetti.playing) ? this.reset(false) : this.stop();

    }
};

var RubikScores = function(game) {
    this.game = game;

    this.data = {
        2: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
        },
        3: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
        },
        4: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
        },
        5: {
            scores: [],
            solves: 0,
            best: 0,
            worst: 0,
        }
    };
};

RubikScores.prototype = {
    constructor: RubikScores,

    addScore: function(time) {
        const data = this.data[this.game.cube.sizeGenerated];

        data.scores.push(time);
        data.solves++;

        if (data.scores.lenght > 100) data.scores.shift();

        let bestTime = false;

        if (time < data.best || data.best === 0) {

            data.best = time;
            bestTime = true;

        }

        if (time > data.worst) data.worst = time;

        this.game.storage.saveScores();

        return bestTime;
    },

    calcStats: function() {

        const s = this.game.cube.sizeGenerated;
        const data = this.data[s];

        this.setStat('cube-size', `${s}<i>x</i>${s}<i>x</i>${s}`);
        this.setStat('total-solves', data.solves);
        this.setStat('best-time', this.convertTime(data.best));
        this.setStat('worst-time', this.convertTime(data.worst));
        this.setStat('average-5', this.getAverage(5));
        this.setStat('average-12', this.getAverage(12));
        this.setStat('average-25', this.getAverage(25));

    },

    setStat: function(name, value) {

        if (value === 0) value = '-';

        //this.game.dom.stats.querySelector(`.stats[name="${name}"] b`).innerHTML = value;

    },

    getAverage: function(count) {

        const data = this.data[this.game.cube.sizeGenerated];

        if (data.scores.length < count) return 0;

        return this.convertTime(data.scores.slice(-count).reduce((a, b) => a + b, 0) / count);

    },

    convertTime: function(time) {

        if (time <= 0) return 0;

        const seconds = parseInt((time / 1000) % 60);
        const minutes = parseInt((time / (1000 * 60)));

        return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;

    }
};

var RubikStorage = function(game) {
    this.game = game;

    const userVersion = localStorage.getItem('theCube_version');

    if (!userVersion || userVersion !== window.gameVersion) {

        this.clearGame();
        this.clearPreferences();
        this.migrateScores();
        localStorage.setItem('theCube_version', window.gameVersion);

    }
};


RubikStorage.prototype = {
    constructor: RubikStorage,
    init: function() {

        this.loadPreferences();
        this.loadScores();

    },

    loadGame: function() {

        try {

            const gameInProgress = localStorage.getItem('theCube_playing') === 'true';

            if (!gameInProgress) throw new Error();

            const gameCubeData = JSON.parse(localStorage.getItem('theCube_savedState'));
            const gameTime = parseInt(localStorage.getItem('theCube_time'));

            if (!gameCubeData || gameTime === null) throw new Error();
            if (gameCubeData.size !== this.game.cube.sizeGenerated) throw new Error();

            this.game.cube.loadFromData(gameCubeData);

            this.game.timer.deltaTime = gameTime;

            this.game.saved = true;

        } catch (e) {

            this.game.saved = false;

        }

    },

    saveGame: function() {

        const gameInProgress = true;
        const gameCubeData = { names: [], positions: [], rotations: [] };
        const gameTime = this.game.timer.deltaTime;

        gameCubeData.size = this.game.cube.sizeGenerated;

        this.game.cube.pieces.forEach(piece => {

            gameCubeData.names.push(piece.name);
            gameCubeData.positions.push(piece.position);
            gameCubeData.rotations.push(piece.rotation.toVector3());

        });

        /*localStorage.setItem('theCube_playing', gameInProgress);
        localStorage.setItem('theCube_savedState', JSON.stringify(gameCubeData));
        localStorage.setItem('theCube_time', gameTime);*/

        chat3d.world.location.hub.broadcast('gameState', gameCubeData);

    },

    clearGame: function() {

        localStorage.removeItem('theCube_playing');
        localStorage.removeItem('theCube_savedState');
        localStorage.removeItem('theCube_time');

    },

    loadScores: function() {

        try {

            const scoresData = JSON.parse(localStorage.getItem('theCube_scores'));

            if (!scoresData) throw new Error();

            this.game.scores.data = scoresData;

        } catch (e) {}

    },

    saveScores: function() {

        const scoresData = this.game.scores.data;

        localStorage.setItem('theCube_scores', JSON.stringify(scoresData));

    },

    clearScores: function() {

        localStorage.removeItem('theCube_scores');

    },

    migrateScores: function() {

        try {

            const scoresData = JSON.parse(localStorage.getItem('theCube_scoresData'));
            const scoresBest = parseInt(localStorage.getItem('theCube_scoresBest'));
            const scoresWorst = parseInt(localStorage.getItem('theCube_scoresWorst'));
            const scoresSolves = parseInt(localStorage.getItem('theCube_scoresSolves'));

            if (!scoresData || !scoresBest || !scoresSolves || !scoresWorst) return false;

            this.game.scores.data[3].scores = scoresData;
            this.game.scores.data[3].best = scoresBest;
            this.game.scores.data[3].solves = scoresSolves;
            this.game.scores.data[3].worst = scoresWorst;

            localStorage.removeItem('theCube_scoresData');
            localStorage.removeItem('theCube_scoresBest');
            localStorage.removeItem('theCube_scoresWorst');
            localStorage.removeItem('theCube_scoresSolves');

        } catch (e) {}

    },

    loadPreferences: function() {

        try {

            const preferences = JSON.parse(localStorage.getItem('theCube_preferences'));

            if (!preferences) throw new Error();

            this.game.cube.size = parseInt(preferences.cubeSize);
            this.game.controls.flipConfig = parseInt(preferences.flipConfig);
            this.game.scrambler.dificulty = parseInt(preferences.dificulty);

            this.game.world.fov = parseFloat(preferences.fov);
            this.game.world.resize();

            this.game.themes.colors = preferences.colors;
            this.game.themes.setTheme(preferences.theme);

            return true;

        } catch (e) {

            this.game.cube.size = 3;
            this.game.controls.flipConfig = 0;
            this.game.scrambler.dificulty = 1;

            this.game.world.fov = 10;
            //this.game.world.resize();

            this.game.themes.setTheme('cube');

            this.savePreferences();

            return false;

        }

    },

    savePreferences: function() {

        const preferences = {
            cubeSize: this.game.cube.size,
            flipConfig: this.game.controls.flipConfig,
            dificulty: this.game.scrambler.dificulty,
            fov: this.game.world.fov,
            theme: this.game.themes.theme,
            colors: this.game.themes.colors,
        };

        localStorage.setItem('theCube_preferences', JSON.stringify(preferences));

    },

    clearPreferences: function() {

        localStorage.removeItem('theCube_preferences');

    }

};


var RubikThemes = function(game) {
    this.game = game;
    this.theme = null;

    this.defaults = {
        cube: {
            U: 0xfff7ff, // white
            D: 0xffef48, // yellow
            F: 0xef3923, // red
            R: 0x41aac8, // blue
            B: 0xff8c0a, // orange
            L: 0x82ca38, // green
            P: 0x08101a, // piece
            G: 0xd1d5db, // background
        },
        erno: {
            U: 0xffffff,
            D: 0xffd500,
            F: 0xc41e3a,
            R: 0x0051ba,
            B: 0xff5800,
            L: 0x009e60,
            P: 0x08101a,
            G: 0x8abdff,
        },
        dust: {
            U: 0xfff6eb,
            D: 0xe7c48d,
            F: 0x8f253e,
            R: 0x607e69,
            B: 0xbe6f62,
            L: 0x849f5d,
            P: 0x08101a,
            G: 0xE7C48D,
        },
        camo: {
            U: 0xfff6eb,
            D: 0xbfb672,
            F: 0x37241c,
            R: 0x718456,
            B: 0x805831,
            L: 0x37431d,
            P: 0x08101a,
            G: 0xBFB672,
        },
        rain: {
            U: 0xfafaff,
            D: 0xedb92d,
            F: 0xce2135,
            R: 0x449a89,
            B: 0xec582f,
            L: 0xa3a947,
            P: 0x08101a,
            G: 0x87b9ac,
        },
    };

    this.colors = JSON.parse(JSON.stringify(this.defaults));
};

RubikThemes.prototype = {
    constructor: RubikThemes,
    getColors: function() {
        return this.colors[this.theme];
    },

    setTheme: function(theme = false, force = false) {

        if (theme === this.theme && force === false) return;
        if (theme !== false) this.theme = theme;

        const colors = this.getColors();

        /*this.game.dom.prefs.querySelectorAll('.range__handle div').forEach(range => {

            range.style.background = '#' + colors.R.toString(16).padStart(6, '0');

        });*/

        this.game.cube.updateColors(colors);

        this.game.confetti.updateColors(colors);

        //this.game.dom.back.style.background = '#' + colors.G.toString(16).padStart(6, '0');

    }
};

var RubikThemeEditor = function(game) {
    this.game = game;

    this.editColor = 'R';

    this.getPieceColor = this.getPieceColor.bind(this);
};

RubikThemeEditor.prototype = {
    constructor: RubikThemeEditor,
    colorFromHSL: function(h, s, l) {

        h = Math.round(h);
        s = Math.round(s);
        l = Math.round(l);

        return new BABYLON.Color(`hsl(${h}, ${s}%, ${l}%)`);

    },

    setHSL: function(color = null, animate = false) {

        this.editColor = (color === null) ? 'R' : color;

        const hsl = new BABYLON.Color(this.game.themes.getColors()[this.editColor]);

        const { h, s, l } = hsl.getHSL(hsl);
        const { hue, saturation, lightness } = this.game.preferences.ranges;

        if (animate) {

            const ho = hue.value / 360;
            const so = saturation.value / 100;
            const lo = lightness.value / 100;

            const colorOld = this.colorFromHSL(hue.value, saturation.value, lightness.value);

            if (this.tweenHSL) this.tweenHSL.stop();

            this.tweenHSL = new Tween({
                duration: 200,
                easing: Easing.Sine.Out(),
                onUpdate: tween => {

                    hue.setValue((ho + (h - ho) * tween.value) * 360);
                    saturation.setValue((so + (s - so) * tween.value) * 100);
                    lightness.setValue((lo + (l - lo) * tween.value) * 100);

                    const colorTween = colorOld.clone().lerp(hsl, tween.value);

                    const colorTweenStyle = colorTween.getStyle();
                    const colorTweenHex = colorTween.getHSL(colorTween);

                    hue.handle.style.color = colorTweenStyle;
                    saturation.handle.style.color = colorTweenStyle;
                    lightness.handle.style.color = colorTweenStyle;

                    saturation.track.style.color =
                        this.colorFromHSL(colorTweenHex.h * 360, 100, 50).getStyle();
                    lightness.track.style.color =
                        this.colorFromHSL(colorTweenHex.h * 360, colorTweenHex.s * 100, 50).getStyle();

                    this.game.dom.theme.style.display = 'none';
                    this.game.dom.theme.offsetHeight;
                    this.game.dom.theme.style.display = '';

                },
                onComplete: () => {

                    this.updateHSL();
                    this.game.storage.savePreferences();

                },
            });

        } else {

            hue.setValue(h * 360);
            saturation.setValue(s * 100);
            lightness.setValue(l * 100);

            this.updateHSL();
            this.game.storage.savePreferences();

        }

    },

    updateHSL: function() {

        const { hue, saturation, lightness } = this.game.preferences.ranges;

        const h = hue.value;
        const s = saturation.value;
        const l = lightness.value;

        const color = this.colorFromHSL(h, s, l).getStyle();

        hue.handle.style.color = color;
        saturation.handle.style.color = color;
        lightness.handle.style.color = color;

        saturation.track.style.color = this.colorFromHSL(h, 100, 50).getStyle();
        lightness.track.style.color = this.colorFromHSL(h, s, 50).getStyle();

        this.game.dom.theme.style.display = 'none';
        this.game.dom.theme.offsetHeight;
        this.game.dom.theme.style.display = '';

        const theme = this.game.themes.theme;

        this.game.themes.colors[theme][this.editColor] = this.colorFromHSL(h, s, l).getHex();
        this.game.themes.setTheme();

    },

    colorPicker: function(enable) {

        /*if (enable) {

            this.game.dom.game.addEventListener('click', this.getPieceColor, false);

        } else {

            this.game.dom.game.removeEventListener('click', this.getPieceColor, false);

        }*/

    },

    getPieceColor: function(event) {

        const clickEvent = event.touches ?
            (event.touches[0] || event.changedTouches[0]) :
            event;

        const clickPosition = new BABYLON.Vector2(clickEvent.pageX, clickEvent.pageY);

        let edgeIntersect = this.game.controls.getIntersect(clickPosition, this.game.cube.edges, true);
        let pieceIntersect = this.game.controls.getIntersect(clickPosition, this.game.cube.cubes, true);

        if (edgeIntersect !== false) {

            const edge = edgeIntersect.object;

            const position = edge.parent
                .localToWorld(edge.position.clone())
                .sub(this.game.cube.object.position)
                .sub(this.game.cube.animator.position);

            const mainAxis = this.game.controls.getMainAxis(position);
            if (position.multiplyScalar(2).round()[mainAxis] < 1) edgeIntersect = false;

        }

        const name = edgeIntersect ? edgeIntersect.object.name : pieceIntersect ? 'P' : 'G';

        this.setHSL(name, true);

    },

    resetTheme: function() {

        this.game.themes.colors[this.game.themes.theme] =
            JSON.parse(JSON.stringify(this.game.themes.defaults[this.game.themes.theme]));

        this.game.themes.setTheme();

        this.setHSL(this.editColor, true);

    }

};

var RubikStates = {
    3: {
        checkerboard: {
            names: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
            positions: [
                { "x": 1 / 3, "y": -1 / 3, "z": 1 / 3 },
                { "x": -1 / 3, "y": 1 / 3, "z": 0 },
                { "x": 1 / 3, "y": -1 / 3, "z": -1 / 3 },
                { "x": -1 / 3, "y": 0, "z": -1 / 3 },
                { "x": 1 / 3, "y": 0, "z": 0 },
                { "x": -1 / 3, "y": 0, "z": 1 / 3 },
                { "x": 1 / 3, "y": 1 / 3, "z": 1 / 3 },
                { "x": -1 / 3, "y": -1 / 3, "z": 0 },
                { "x": 1 / 3, "y": 1 / 3, "z": -1 / 3 },
                { "x": 0, "y": 1 / 3, "z": -1 / 3 },
                { "x": 0, "y": -1 / 3, "z": 0 },
                { "x": 0, "y": 1 / 3, "z": 1 / 3 },
                { "x": 0, "y": 0, "z": 1 / 3 },
                { "x": 0, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": -1 / 3 },
                { "x": 0, "y": -1 / 3, "z": -1 / 3 },
                { "x": 0, "y": 1 / 3, "z": 0 },
                { "x": 0, "y": -1 / 3, "z": 1 / 3 },
                { "x": -1 / 3, "y": -1 / 3, "z": 1 / 3 },
                { "x": 1 / 3, "y": 1 / 3, "z": 0 },
                { "x": -1 / 3, "y": -1 / 3, "z": -1 / 3 },
                { "x": 1 / 3, "y": 0, "z": -1 / 3 },
                { "x": -1 / 3, "y": 0, "z": 0 },
                { "x": 1 / 3, "y": 0, "z": 1 / 3 },
                { "x": -1 / 3, "y": 1 / 3, "z": 1 / 3 },
                { "x": 1 / 3, "y": -1 / 3, "z": 0 },
                { "x": -1 / 3, "y": 1 / 3, "z": -1 / 3 }
            ],
            rotations: [
                { "x": -Math.PI, "y": 0, "z": Math.PI, },
                { "x": Math.PI, "y": 0, "z": 0 },
                { "x": -Math.PI, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": -Math.PI, "y": 0, "z": Math.PI },
                { "x": Math.PI, "y": 0, "z": 0 },
                { "x": -Math.PI, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": -Math.PI, "y": 0, "z": 0 },
                { "x": Math.PI, "y": 0, "z": Math.PI },
                { "x": Math.PI, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": Math.PI, "y": 0, "z": Math.PI },
                { "x": -Math.PI, "y": 0, "z": 0 },
                { "x": Math.PI, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": 0, "y": 0, "z": Math.PI },
                { "x": 0, "y": 0, "z": 0 },
                { "x": Math.PI, "y": 0, "z": Math.PI },
                { "x": -Math.PI, "y": 0, "z": 0 },
                { "x": Math.PI, "y": 0, "z": Math.PI }
            ],
            size: 3,
        },
    }
};

var RubikIconsConverter = function(options) {
    options = Object.assign({
        tagName: 'icon',
        className: 'icon',
        styles: false,
        icons: {},
        observe: false,
        convert: false,
    }, options || {});

    this.tagName = options.tagName;
    this.className = options.className;
    this.icons = options.icons;

    this.svgTag = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    this.svgTag.setAttribute('class', this.className);

    if (options.styles) this.addStyles();
    if (options.convert) this.convertAllIcons();

    if (options.observe) {

        const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
        this.observer = new MutationObserver(mutations => { this.convertAllIcons(); });
        this.observer.observe(document.documentElement, { childList: true, subtree: true });

    }

    return this;
};

RubikIconsConverter.prototype = {
    constructor: RubikIconsConverter,

    convertAllIcons: function() {

        //document.querySelectorAll(this.tagName).forEach(icon => { this.convertIcon(icon); });

    },

    convertIcon: function(icon) {

        const svgData = this.icons[icon.attributes[0].localName];

        if (typeof svgData === 'undefined') return;

        const svg = this.svgTag.cloneNode(true);
        const viewBox = svgData.viewbox.split(' ');

        svg.setAttributeNS(null, 'viewBox', svgData.viewbox);
        svg.style.width = viewBox[2] / viewBox[3] + 'em';
        svg.style.height = '1em';
        svg.innerHTML = svgData.content;

        icon.parentNode.replaceChild(svg, icon);

    },

    addStyles: function() {

        /*const style = document.createElement('style');
        style.innerHTML = `.${this.className} { display: inline-block; font-size: inherit; overflow: visible; vertical-align: -0.125em; preserveAspectRatio: none; }`;
        document.head.appendChild(style);*/

    }

};

var RubikIcons = new RubikIconsConverter({

    icons: {
        settings: {
            viewbox: '0 0 512 512',
            content: '<path fill="currentColor" d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z" />',
        },
        back: {
            viewbox: '0 0 512 512',
            content: '<path transform="translate(512, 0) scale(-1,1)" fill="currentColor" d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z" />',
        },
        trophy: {
            viewbox: '0 0 576 512',
            content: '<path fill="currentColor" d="M552 64H448V24c0-13.3-10.7-24-24-24H152c-13.3 0-24 10.7-24 24v40H24C10.7 64 0 74.7 0 88v56c0 66.5 77.9 131.7 171.9 142.4C203.3 338.5 240 360 240 360v72h-48c-35.3 0-64 20.7-64 56v12c0 6.6 5.4 12 12 12h296c6.6 0 12-5.4 12-12v-12c0-35.3-28.7-56-64-56h-48v-72s36.7-21.5 68.1-73.6C498.4 275.6 576 210.3 576 144V88c0-13.3-10.7-24-24-24zM64 144v-16h64.2c1 32.6 5.8 61.2 12.8 86.2-47.5-16.4-77-49.9-77-70.2zm448 0c0 20.2-29.4 53.8-77 70.2 7-25 11.8-53.6 12.8-86.2H512v16zm-127.3 4.7l-39.6 38.6 9.4 54.6c1.7 9.8-8.7 17.2-17.4 12.6l-49-25.8-49 25.8c-8.8 4.6-19.1-2.9-17.4-12.6l9.4-54.6-39.6-38.6c-7.1-6.9-3.2-19 6.7-20.5l54.8-8 24.5-49.6c4.4-8.9 17.1-8.9 21.5 0l24.5 49.6 54.8 8c9.6 1.5 13.5 13.6 6.4 20.5z" />',
        },
        cancel: {
            viewbox: '0 0 352 512',
            content: '<path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />',
        },
        theme: {
            viewbox: '0 0 512 512',
            content: '<path fill="currentColor" d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"/>',
        },
        reset: {
            viewbox: '0 0 512 512',
            content: '<path fill="currentColor" d="M370.72 133.28C339.458 104.008 298.888 87.962 255.848 88c-77.458.068-144.328 53.178-162.791 126.85-1.344 5.363-6.122 9.15-11.651 9.15H24.103c-7.498 0-13.194-6.807-11.807-14.176C33.933 94.924 134.813 8 256 8c66.448 0 126.791 26.136 171.315 68.685L463.03 40.97C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.749zM32 296h134.059c21.382 0 32.09 25.851 16.971 40.971l-41.75 41.75c31.262 29.273 71.835 45.319 114.876 45.28 77.418-.07 144.315-53.144 162.787-126.849 1.344-5.363 6.122-9.15 11.651-9.15h57.304c7.498 0 13.194 6.807 11.807 14.176C478.067 417.076 377.187 504 256 504c-66.448 0-126.791-26.136-171.315-68.685L48.97 471.03C33.851 486.149 8 475.441 8 454.059V320c0-13.255 10.745-24 24-24z" />',
        },
        trash: {
            viewbox: '0 0 448 512',
            content: '<path fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" />',
        },
    },

    convert: true,

});


var RubikSTATE = {
    Menu: 0,
    Playing: 1,
    Complete: 2,
    Stats: 3,
    Prefs: 4,
    Theme: 5,
};


var RubikBUTTONS = {
    Menu: ['stats', 'prefs'],
    Playing: ['back'],
    Complete: [],
    Stats: [],
    Prefs: ['back', 'theme'],
    Theme: ['back', 'reset'],
    None: [],
};

var RubikSHOW = true;
var RubikHIDE = false;

var RubikGame = function() {
    this.dom = {
        /*ui: document.querySelector('.ui'),
        game: document.querySelector('.ui__game'),
        back: document.querySelector('.ui__background'),
        prefs: document.querySelector('.ui__prefs'),
        theme: document.querySelector('.ui__theme'),
        stats: document.querySelector('.ui__stats'),
        texts: {
            title: document.querySelector('.text--title'),
            note: document.querySelector('.text--note'),
            timer: document.querySelector('.text--timer'),
            complete: document.querySelector('.text--complete'),
            best: document.querySelector('.text--best-time'),
            theme: document.querySelector('.text--theme'),
        },
        buttons: {
            prefs: document.querySelector('.btn--prefs'),
            back: document.querySelector('.btn--back'),
            stats: document.querySelector('.btn--stats'),
            reset: document.querySelector('.btn--reset'),
            theme: document.querySelector('.btn--theme'),
        },*/
    };

    this.world = new RubikWorld(this);
    this.cube = new RubikCube(this);
    this.controls = new RubikControls(this);
    this.scrambler = new RubikScrambler(this);
    this.transition = new RubikTransition(this);
    this.timer = new RubikTimer(this);
    this.preferences = new RubikPreferences(this);
    this.scores = new RubikScores(this);
    this.storage = new RubikStorage(this);
    this.confetti = new RubikConfetti(this);
    this.themes = new RubikThemes(this);
    this.themeEditor = new RubikThemeEditor(this);

    this.initActions();

    this.state = RubikSTATE.Menu;
    this.newGame = false;
    this.saved = false;

    this.storage.init();
    this.preferences.init();
    this.cube.init();
    this.transition.init();

    this.storage.loadGame();
    this.scores.calcStats();

    setTimeout(() => {

        this.transition.float();
        this.transition.cube(RubikSHOW);

        /*setTimeout(() => this.transition.title(RubikSHOW), 700);*/
        /*setTimeout(() => this.transition.buttons(RubikBUTTONS.Menu, RubikBUTTONS.None), 1000);*/

    }, 500);
};

RubikGame.prototype = {
    initActions: function() {
        let tappedTwice = false;

        /*this.dom.game.addEventListener('click', event => {

            if (this.transition.activeTransitions > 0) return;
            if (this.state === STATE.Playing) return;

            if (this.state === STATE.Menu) {

                if (!tappedTwice) {

                    tappedTwice = true;
                    setTimeout(() => tappedTwice = false, 300);
                    return false;

                }

                this.game(SHOW);

            } else if (this.state === STATE.Complete) {

                this.complete(HIDE);

            } else if (this.state === STATE.Stats) {

                this.stats(HIDE);

            }

        }, false);*/

        this.controls.onMove = () => {

            if (this.newGame) {

                this.timer.start(true);
                this.newGame = false;

            }

        };

        /*this.dom.buttons.back.onclick = event => {

            if (this.transition.activeTransitions > 0) return;

            if (this.state === STATE.Playing) {

                this.game(HIDE);

            } else if (this.state === STATE.Prefs) {

                this.prefs(HIDE);

            } else if (this.state === STATE.Theme) {

                this.theme(HIDE);

            }

        };*/

        /*this.dom.buttons.reset.onclick = event => {

            if (this.state === STATE.Theme) {

                this.themeEditor.resetTheme();

            }

        };

        this.dom.buttons.prefs.onclick = event => this.prefs(SHOW);

        this.dom.buttons.theme.onclick = event => this.theme(SHOW);

        this.dom.buttons.stats.onclick = event => this.stats(SHOW);*/

        this.controls.onSolved = () => this.complete(SHOW);
    },

    game: function(show) {
        if (show) {

            if (!this.saved) {

                this.scrambler.scramble();
                this.controls.scrambleCube();
                this.newGame = true;

            }

            const duration = this.saved ? 0 :
                this.scrambler.converted.length * (this.controls.flipSpeeds[0] + 10);

            this.state = RubikSTATE.Playing;
            //this.saved = true;

            //this.transition.buttons(RubikBUTTONS.None, RubikBUTTONS.Menu);

            this.transition.zoom(RubikSTATE.Playing, duration);
            //this.transition.title(RubikHIDE);

            setTimeout(() => {

                this.transition.timer(RubikSHOW);
                //this.transition.buttons(RubikBUTTONS.Playing, RubikBUTTONS.None);

            }, this.transition.durations.zoom - 1000);

            setTimeout(() => {

                this.controls.enable();
                if (!this.newGame) this.timer.start(true);

            }, this.transition.durations.zoom);

        } else {

            this.state = RubikSTATE.Menu;

            //this.transition.buttons(BUTTONS.Menu, BUTTONS.Playing);

            //this.transition.zoom(STATE.Menu, 0);

            //this.controls.disable();
            if (!this.newGame) this.timer.stop();
            //this.transition.timer(HIDE);

            //setTimeout(() => this.transition.title(SHOW), this.transition.durations.zoom - 1000);

            this.playing = false;
            this.controls.disable();

        }
    },

    prefs: function(show) {

        if (show) {

            if (this.transition.activeTransitions > 0) return;

            this.state = RubikSTATE.Prefs;

            this.transition.buttons(BUTTONS.Prefs, BUTTONS.Menu);

            this.transition.title(HIDE);
            this.transition.cube(HIDE);

            setTimeout(() => this.transition.preferences(SHOW), 1000);

        } else {

            this.cube.resize();

            this.state = RubikSTATE.Menu;

            this.transition.buttons(BUTTONS.Menu, BUTTONS.Prefs);

            this.transition.preferences(HIDE);

            setTimeout(() => this.transition.cube(SHOW), 500);
            setTimeout(() => this.transition.title(SHOW), 1200);

        }

    },

    theme: function(show) {

        this.themeEditor.colorPicker(show);

        if (show) {

            if (this.transition.activeTransitions > 0) return;

            this.cube.loadFromData(RubikStates['3']['checkerboard']);

            this.themeEditor.setHSL(null, false);

            this.state = STATE.Theme;

            this.transition.buttons(BUTTONS.Theme, BUTTONS.Prefs);

            this.transition.preferences(HIDE);

            setTimeout(() => this.transition.cube(SHOW, true), 500);
            setTimeout(() => this.transition.theming(SHOW), 1000);

        } else {

            this.state = STATE.Prefs;

            this.transition.buttons(BUTTONS.Prefs, BUTTONS.Theme);

            this.transition.cube(HIDE, true);
            this.transition.theming(HIDE);

            setTimeout(() => this.transition.preferences(SHOW), 1000);
            setTimeout(() => {

                const gameCubeData = JSON.parse(localStorage.getItem('theCube_savedState'));

                if (!gameCubeData) {

                    this.cube.resize(true);
                    return;

                }

                this.cube.loadFromData(gameCubeData);

            }, 1500);

        }
    },


    stats: function(show) {

        if (show) {

            if (this.transition.activeTransitions > 0) return;

            this.state = STATE.Stats;

            this.transition.buttons(BUTTONS.Stats, BUTTONS.Menu);

            this.transition.title(HIDE);
            this.transition.cube(HIDE);

            setTimeout(() => this.transition.stats(SHOW), 1000);

        } else {

            this.state = STATE.Menu;

            this.transition.buttons(BUTTONS.Menu, BUTTONS.None);

            this.transition.stats(HIDE);

            setTimeout(() => this.transition.cube(SHOW), 500);
            setTimeout(() => this.transition.title(SHOW), 1200);

        }

    },

    complete: function(show) {

        if (show) {

            this.transition.buttons(RubikBUTTONS.Complete, RubikBUTTONS.Playing);

            this.state = RubikSTATE.Complete;
            this.saved = false;

            this.controls.disable();
            this.timer.stop();
            this.storage.clearGame();

            this.bestTime = this.scores.addScore(this.timer.deltaTime);

            this.transition.zoom(RubikSTATE.Menu, 0);
            this.transition.elevate(RubikSHOW);

            setTimeout(() => {

                this.transition.complete(RubikSHOW, this.bestTime);
                this.confetti.start();

            }, 1000);

        } else {

            this.state = RubikSTATE.Stats;
            this.saved = false;

            this.transition.timer(RubikHIDE);
            this.transition.complete(RubikHIDE, this.bestTime);
            this.transition.cube(RubikHIDE);
            this.timer.reset();

            setTimeout(() => {

                this.cube.reset();
                this.confetti.stop();

                this.transition.stats(RubikSHOW);
                this.transition.elevate(0);

            }, 1000);

            return false;

        }

    }

};

locations[51].onInit = function() {
    this.uniqueID = 0;
    this.animEngine = new RubikAnimationEngine();
    this.game = new RubikGame();
    chat3d.world.scene.add(this.game.world.scene);
    this.game.world.scene.scale.set(250, 250, 250);

    //console.log(this.game.world.scene);
    this.game.world.scene.position.y = 40;
    this.resetDelay = setTimeout(function() {
        this.game.game(true);
    }.bind(this), 2000);
    //this.game.game(true);

    this.resetbox = new BABYLON.Mesh(new BABYLON.CubeGeometry(20, 4, 20), new BABYLON.MeshBasicMaterial({
        visible: false
    }));
    this.resetbox.position.set(0, 90, -610);
    this.resetbox.rotation.x = Math.PI / 4;
    chat3d.world.scene.add(this.resetbox);

    this.hub = new p2phub();

    this.isResetable = function(x, y) {

        var raycast = chat3d.view.raycaster;
        raycast.ray.origin.setFromMatrixPosition(chat3d.world.camera.matrixWorld);

        var vector = new BABYLON.Vector3((x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1, 0.9);
        vector.unproject(chat3d.world.camera);
        vector.sub(raycast.ray.origin).normalize();
        raycast.far = 1000;
        raycast.ray.direction = vector;
        intersects = raycast.intersectObject(this.resetbox);
        if (intersects.length) {
            return true;
        }


        return false;

    };

    this.resetBut = function(x, y) {
        if (this.isResetable(x, y, true)) {
            //chat3d.msg.send('confirm', 'game_reset');
            this.game.game(true);
            /*chat3d.world.scene.remove(this.game.world.scene);
            this.game = new RubikGame();
            chat3d.world.scene.add(this.game.world.scene);
            this.game.world.scene.scale.set(250, 250, 250);


            this.game.world.scene.position.y = 40;
            this.game.game(true);

            console.log(this.game.world.scene);*/

            console.log('send reset');
            return true;
        }
        return false;

    };

    this.setGameState = function(data, age, counter) {
        //console.log(data);
        clearTimeout(this.resetDelay);
        this.game.cube.loadFromData(data);

        //setTimeout(function() { this.game.cube.loadFromData(data) }.bind(this), 5000);
    };



    this.set_player = function(id) {

        if (id != this.player) {

            if (this.player == chat3d.selfid && id != chat3d.selfid) {
                console.log('%cim droped from player1 by id ' + id, ' background: #DAFF7F');
            }

            this.player = id;

            if (!id) {
                this.player_ttl = 0;

            } else {
                this.player_ttl = 15;


            }
        }

    };




};

locations[51].onMessage = function(data) {
    if (data) {

        for (var i in data) {
            var msg = data[i];
            if (this.hub.pCounters[msg.type] && this.hub.pCounters[msg.type] > msg.counter) {
                console.log('%c old data!!', ' background: #FF2222');

            } else {
                switch (msg.type) {


                    case 'gameState':
                        this.setGameState(msg.data, msg.age, msg.counter);

                        break;

                    case 'set_player':
                        this.set_player(msg.data);
                        break;



                    case 'resetmsg':
                        if (msg.age < 10 && this.hub.checkCounter(msg)) {
                            chat3d.msg.send('alert', 'reseted');
                            console.log('%creset alert', 'background: #DAFF7F');
                        }
                        break;

                    default:
                        console.log('old or unknown', msg);
                        break;


                }
                this.hub.onMessage(msg);
            }
        }
    }

};



locations[51].onClean = function() {
    chat3d.world.scene.remove(this.game.world.scene);
    chat3d.world.scene.remove(this.resetbox);
    $('canvas').removeClass('pointer_on_game_obj');
    $('canvas').removeClass('pointer_on_game_obj_move');
};

locations[51].onRender = function(delta) {
    this.animEngine.update();
    //console.log(this.game.world.scene.children[0].position);
    this.uniforms.pos.value.copy(this.game.world.scene.children[0].position).multiplyScalar(1000);
    this.uniforms.pos.value.y -= 150;
};



locations[51].onView = function(x, y) {
    //console.log(this.game.controls.helper);
    var raycast = chat3d.view.raycaster;
    raycast.ray.origin.setFromMatrixPosition(chat3d.world.camera.matrixWorld);

    var vector = new BABYLON.Vector3((x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1, 0.9);
    vector.unproject(chat3d.world.camera);
    vector.sub(raycast.ray.origin).normalize();
    raycast.far = 1000;
    raycast.ray.direction = vector;
    intersects = raycast.intersectObject(this.game.controls.helper);
    if (intersects.length) {
        $('canvas').removeClass('pointer_on_game_obj');
        $('canvas').addClass('pointer_on_game_obj_move');
    } else {
        $('canvas').removeClass('pointer_on_game_obj_move');
        if (this.isResetable(x, y)) {
            $('#world canvas').attr('title', chat3d.translate.reset);
            $('canvas').removeClass('pointer_on_game_obj_move');
            $('canvas').addClass('pointer_on_game_obj');
        } else {
            $('canvas').removeClass('pointer_on_game_obj').removeClass('pointer_on_game_obj_move');
        }
    }



};

locations[51].onMouseMove = function(e) {
    this.game.controls.draggable.drag.move(e);
    return true;
};

locations[51].onMouseDown = function(e) {
    if (!this.player || this.player == chat3d.selfid) {
        if (this.game.controls.draggable.drag.start(e)) {
            this.set_player(chat3d.selfid);
            this.hub.broadcast('set_player', chat3d.selfid);
            return false;
        }
        this.resetBut(e.clientX, e.clientY);
    }
    return true;
};

locations[51].onMouseUp = function(e) {
    if (!this.player || this.player == chat3d.selfid) {
        this.game.controls.draggable.drag.end(e);
    }
    return true;
};


locations[51].onTouchUpEnd = function(e) {
    if (!this.player || this.player == chat3d.selfid) {
        this.game.controls.draggable.drag.end(e);
    }
    return true;

};



locations[51].onTouchDown = function(e) {
    if (!this.player || this.player == chat3d.selfid) {
        if (this.game.controls.draggable.drag.start(e)) {
            this.set_player(chat3d.selfid);
            this.hub.broadcast('set_player', chat3d.selfid);
            return false;
        }
        this.resetBut(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
    }
    return true;
};

locations[51].onTouchMove = function(e) {
    if (!this.player || this.player == chat3d.selfid) {
        if (this.game.controls.draggable.drag.move(e)) {
            return false;
        }
    }
    return true;
};


locations[51].onLoop = function(delta) {
    if (this.player) {
        this.player_ttl -= delta;
        if (this.player_ttl < 0) {
            if (this.player == chat3d.selfid) {
                this.hub.broadcast('set_player', null);
            }
            this.player = null;
        }
    }

};

Youez - 2016 - github.com/yon3zu
LinuXploit