//check README.md for more information

//VS Code intellisense
/// <reference path="TSDef/p5.global-mode.d.ts" />

/*
The client and server version strings MUST be the same!
If the server gets updated it can be restarted, but if there are active clients (users' open browsers) they could be outdated and create issues.
*/
var VERSION = "1.0";

//for testing purposes I can skip the login phase
//and join with a random avatar
var QUICK_LOGIN = false;

//true: preview the room as invisible user
//false: go directly to the login without previewing the room
//ignored if QUICK_LOGIN is true
var LURK_MODE = true;

//expose the room locations on the url and make them them shareable
//you can access the world from any point. False ignores
var ROOM_LINK = true;

//can by changed by user
var SOUND = true;
var AFK = false;

//native canvas resolution
var NATIVE_WIDTH = 128;
var NATIVE_HEIGHT = 100;

/*
The original resolution (pre canvas stretch) is 128x100 multiplied by 2 because
otherwise there wouldn't be enough room for pixel text.
Basically the backgrounds' pixels are twice the pixels of the text.
ASSET_SCALE is a multiplier for all backgrounds, areas, things (sprites), and coordinates
that are natively drawn at 128x100
*/
var ASSET_SCALE = 2;

var WIDTH = NATIVE_WIDTH * ASSET_SCALE;
var HEIGHT = NATIVE_HEIGHT * ASSET_SCALE;

//dynamically adjusted based on the window
var canvasScale;

//all avatars are the same size
var AVATAR_W = 10;
var AVATAR_H = 18;
//number of avatars in the sheets
var AVATARS = 37;
//the big file if used
var ALL_AVATARS_SHEET = "allAvatars.png";
//the number of frames for walk cycle and emote animation
//the first frame of emote is also the idle frame
var WALK_F = 4;
var EMOTE_F = 2;

//the socket connection
var socket;
//sent by the server
var ROOMS;
var SETTINGS;
//preloaded sound image asset from data.js
var IMAGES, SOUNDS;

//avatar linear speed, pixels per milliseconds
var SPEED = 50;

var ASSETS_FOLDER = "assets/";

//text vars
//MONOSPACED FONT
//thank you https://datagoblin.itch.io/monogram
var FONT_FILE = "assets/monogram_extended.ttf";
var FONT_SIZE = 16; //to avoid blur
var font;
var TEXT_H = 8;
var TEXT_PADDING = 3;
var TEXT_LEADING = TEXT_H + 4;

var LOGO_FILE = "logo.png";
var MENU_BG_FILE = "menu_white.png";

//how long does the text bubble stay
var BUBBLE_TIME = 8;
var BUBBLE_MARGIN = 3;

//when lurking the logo can disappear
//in millisecs, -1 forever in lurk mode
var LOGO_STAY = -1;

//default page background 
var PAGE_COLOR = "#000000";

//sprite reference color for palette swap
//hair, skin, shirt, pants
var REF_COLORS = ["#413830", "#c0692a", "#ff004d", "#29adff"];
//the palettes that will respectively replace the colors above
//black and brown more common
var HAIR_COLORS = ["#413830", "#413830", "#413830", "#742f29", "#742f29", "#742f29", "#ffa300", "#a8e72e", "#a28879", "#be1250", "#ffec27", "#00b543", "#ff6c24"];
var SKIN_COLORS = ["#e27c32", "#8f3f17", "#ffccaa"];
var TOP_COLORS = ["#a8e72e", "#111d35", "#c2c3c7", "#f3ef7d", "#ca466d", "#111d35", "#ffec27", "#1e839d",
    "#ff004d", "#ff9d81", "#ff6c24", "#ffec27", "#be1250", "#b7250b"];
var BOTTOM_COLORS = ["#00b543", "#a28879", "#422136", "#ca466d", "#ffec27", "#1e839d", "#ff6c24", "#be1250", "#413830", "#c2c3c7"];

var HAIR_COLORS_RGB = [];
var SKIN_COLORS_RGB = [];
var TOP_COLORS_RGB = [];
var BOTTOM_COLORS_RGB = [];

//arrays to speed up the pix by pix recoloring
var REF_COLORS_RGB = [];

//at character selection save the generated palettes so you can go back
var generatedPalettes = [];
var paletteIndex = 0;

//GUI
var LABEL_NEUTRAL_COLOR = "#FFFFFF";
var UI_BG = "#000000";

//global vars! I love global vars ///////////////////

//preloaded images
var walkSheets = [];
var emoteSheets = [];
//the big spritesheet
var allSheets;

//current room bg and areas
var bg;
var areas;
var room; //the id

//command quequed for when the destination is reached
var nextCommand;

//my client only, rollover info
var areaLabel;
var labelColor;
var rolledSprite;

//GUI
//shows up at the beginning, centered, overlapped to room in lurk mode
var logo;
var logoCounter;

//when pointing on walkable area shows this
var walkIcon;

var menuBg, arrowButton;
//p5 play group, basically an arraylist of sprites
var menuGroup;
//p5 play animation
var avatarPreview;

//long text variables, message that shows on my client only (written text, narration, god messages...)
var longText = "";
var longTextLines;
var longTextAlign;
var longTextLink = "";
var LONG_TEXT_BOX_W = 220;
var LONG_TEXT_PADDING = 4;

//To show when banned or disconnected, disables the client on black screen
var errorMessage = "";

//speech bubbles
var bubbles = [];

//when the nickName is "" the player is invisible inactive: lurk mode
//for admins it contains the password so it shouldn't be shared
var nickName = "";

//these are indexes of arrays not images or colors
var currentAvatar;

//these are the colors that get passed around, indexes to the respective color arrays
//hair, skin, top, bottom
var currentColors = [0, 0, 0, 0];

//this object keeps track of all the current players in the room, coordinates, bodies and color
var players;
//a reference to my player
var me;
//the canvas object
var canvas;
//draw loop state: user(name), avatar selection or game
var screen;

//set the time at the beginning of the computing era, the SEVENTIES!
var lastMessage = 0;

//sparkles p5 play animations
var appearEffect, disappearEffect;

//sounds
var blips;
var appearSound, disappearSound;

//if the server restarts the clients reconnects seamlessly
//don't do first log kind of things
var firstLog = true;

//time since the server started
var START_TIME = -1;

//async check
var dataLoaded = false;
var gameStarted = false;



/*
Things are quite asynchronous here. This is the startup sequence:

* preload() preload general assets - avatars, icons etc
* setup() assets loaded - slice, create canvas and create a temporary socket that only listens to DATA
* wait for settings and room data from the server 
* data is received, ROOM object exists BUT its damn images aren't loaded yet
* check the loading images in draw() until they all look like something
* when room images are loaded go to setupGame() and to lurking mode or fast track with a random username due to QUICK_LOGIN
    In lurking mode (username "") you can see everybody, you are invisible and can't do anything
    BUT you are technically in the room
* when click on the "join" button go to the username and avatar selection screens, back and forth for name validation
* when name is approved the player joins ("playerJoined" event) the room again with the real name and avatar
* upon room join all clients send their intro information and the scene is populated
* changing room is handled in the same way with "playerJoined"
* if server restarts the assets and room data is kept on the clients and that's also a connect > playerJoined sequence
*/



//setup is called when all the assets have been loaded
function preload() {

    document.body.style.backgroundColor = PAGE_COLOR;

    //avatar spritesheets are programmatically tinted so they need to be pimages before being loaded as spritesheets

    //METHOD 1:
    //avatar spritesheets are numbered and sequential, one per animation like straight outta Piskel
    //it's a lot of requests for a bunch of tiny images
    /*
    for (var i = 0; i < AVATARS; i++) {
        walkSheets[i] = loadImage(ASSETS_FOLDER + "character" + i + ".png");
    }

    for (var i = 0; i < AVATARS; i++) {
        emoteSheets[i] = loadImage(ASSETS_FOLDER + "character" + i + "-emote.png");
    }
    */

    //METHOD 2:
    //all spritesheets are packed in one long file, preloaded and split on setup
    //packed with this tool  https://www.codeandweb.com/free-sprite-sheet-packer
    //layout horizontal, 0 padding (double check that)


    allSheets = loadImage(ASSETS_FOLDER + ALL_AVATARS_SHEET);

    REF_COLORS_RGB = [];
    //to make the palette swap faster I save colors as arrays 
    for (var i = 0; i < REF_COLORS.length; i++) {
        var rc = REF_COLORS[i];
        var r = red(rc);
        var g = green(rc);
        var b = blue(rc);
        REF_COLORS_RGB[i] = [r, g, b];
    }




    //to make the palette swap faster I save colors as arrays 
    for (var i = 0; i < HAIR_COLORS.length; i++) {
        HAIR_COLORS_RGB[i] = [];
        //each color
        for (var j = 0; j < HAIR_COLORS[i].length; j++) {

            var rc = HAIR_COLORS[i];
            HAIR_COLORS_RGB[i] = [red(rc), green(rc), blue(rc)];

        }

    }

    for (var i = 0; i < SKIN_COLORS.length; i++) {
        SKIN_COLORS_RGB[i] = [];
        //each color
        for (var j = 0; j < SKIN_COLORS[i].length; j++) {
            var rc = SKIN_COLORS[i];
            SKIN_COLORS_RGB[i] = [red(rc), green(rc), blue(rc)];
        }
    }

    for (var i = 0; i < TOP_COLORS.length; i++) {
        TOP_COLORS_RGB[i] = [];
        //each color
        for (var j = 0; j < TOP_COLORS[i].length; j++) {
            var rc = TOP_COLORS[i];
            TOP_COLORS_RGB[i] = [red(rc), green(rc), blue(rc)];
        }
    }

    for (var i = 0; i < BOTTOM_COLORS.length; i++) {
        BOTTOM_COLORS_RGB[i] = [];
        //each color
        for (var j = 0; j < BOTTOM_COLORS[i].length; j++) {
            var rc = BOTTOM_COLORS[i];
            BOTTOM_COLORS_RGB[i] = [red(rc), green(rc), blue(rc)];
        }
    }

    menuBg = loadImage(ASSETS_FOLDER + MENU_BG_FILE);
    arrowButton = loadImage(ASSETS_FOLDER + "arrowButton.png");

    var logoSheet = loadSpriteSheet(ASSETS_FOLDER + LOGO_FILE, 66, 82, 4);
    logo = loadAnimation(logoSheet);
    logo.frameDelay = 10;

    var walkIconSheet = loadSpriteSheet(ASSETS_FOLDER + "walkIcon.png", 6, 8, 4);
    walkIcon = loadAnimation(walkIconSheet);
    walkIcon.frameDelay = 8;

    var appearEffectSheet = loadSpriteSheet(ASSETS_FOLDER + "appearEffect.png", 10, 18, 10);
    appearEffect = loadAnimation(appearEffectSheet);
    appearEffect.frameDelay = 4;
    appearEffect.looping = false;

    var disappearEffectSheet = loadSpriteSheet(ASSETS_FOLDER + "disappearEffect.png", 10, 18, 10);
    disappearEffect = loadAnimation(disappearEffectSheet);
    //disappearEffect.frameDelay = 4;
    disappearEffect.looping = false;

    font = loadFont(FONT_FILE);

    //load sound
    soundFormats("mp3", "ogg");

    blips = [];
    for (var i = 0; i <= 5; i++) {
        var blip = loadSound(ASSETS_FOLDER + "blip" + i);
        blip.playMode("sustain");
        blip.setVolume(0.3);
        blips.push(blip);
    }

    appearSound = loadSound(ASSETS_FOLDER + "appear");
    appearSound.playMode("sustain");
    appearSound.setVolume(0.3);

    disappearSound = loadSound(ASSETS_FOLDER + "disappear");
    disappearSound.playMode("sustain");
    disappearSound.setVolume(0.3);


}


//this is called when the assets are loaded
function setup() {

    //create a canvas
    canvas = createCanvas(WIDTH, HEIGHT);
    //accept only the clicks on the canvas (not the ones on the UI)
    canvas.mousePressed(canvasPressed);
    canvas.mouseReleased(canvasReleased);
    canvas.mouseOut(outOfCanvas);
    //by default the canvas is attached to the bottom, i want it in the container
    canvas.parent("canvas-container");

    //adapt it to the browser window 
    scaleCanvas();

    //since my avatars are pixelated and scaled I kill the antialiasing on canvas
    noSmooth();

    //the page link below
    showInfo();

    //if using a single spritesheet slice it up
    if (walkSheets.length == 0 && allSheets != null) {

        var sliceX = 0;


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

            var walkSheet = createImage(AVATAR_W * WALK_F, AVATAR_H);
            walkSheet.copy(allSheets, sliceX, 0, AVATAR_W * WALK_F, AVATAR_H, 0, 0, AVATAR_W * WALK_F, AVATAR_H);
            walkSheets[i] = walkSheet;

            sliceX += AVATAR_W * WALK_F;

            var emoteSheet = createImage(AVATAR_W * EMOTE_F, AVATAR_H);
            emoteSheet.copy(allSheets, sliceX, 0, AVATAR_W * EMOTE_F, AVATAR_H, 0, 0, AVATAR_W * EMOTE_F, AVATAR_H);
            emoteSheets[i] = emoteSheet;

            sliceX += AVATAR_W * EMOTE_F;

        }
    }

    //I create a socket but I wait to assign all the functions before opening a connection
    socket = io({
        autoConnect: false
    });

    //server sends out the response to the name submission, only if lurk mode is disabled
    //it"s in a separate function because it is shared between the first provisional connection 
    //and the "real" one later
    socket.on("nameValidation", nameValidationCallBack);

    //first server message with version and game data
    socket.on("serverWelcome",
        function (serverVersion, DATA, _START_TIME) {
            if (socket.id) {
                console.log("Welcome! Server version: " + serverVersion + " - client version " + VERSION + " started " + _START_TIME);
                START_TIME = _START_TIME;

                //this is before canvas so I have to html brutally
                if (serverVersion != VERSION) {
                    errorMessage = "VERSION MISMATCH: PLEASE HARD REFRESH";
                    document.body.innerHTML = errorMessage;
                    socket.disconnect();
                }
                // MM2021HACKING
                window.DATA = DATA;
                ROOMS = DATA.ROOMS;
                SETTINGS = DATA.SETTINGS;

                //load room assets, should be in preload but
                for (var roomId in ROOMS) {
                    if (ROOMS.hasOwnProperty(roomId)) {
                        var room = ROOMS[roomId];

                        if (room.bg != null)
                            room.bgGraphics = loadImage(ASSETS_FOLDER + room.bg);
                        else
                            console.log("WARNING: room " + roomId + " has no background graphics");

                        if (room.area != null)
                            room.areaGraphics = loadImage(ASSETS_FOLDER + room.area);
                        else
                            console.log("WARNING: room " + roomId + " has no area graphics");

                        if (room.music != null) {
                            room.musicLoop = loadSound(ASSETS_FOLDER + room.music);
                            room.musicLoop.playMode('restart');
                        }


                        //preload sprites if any
                        if (ROOMS[roomId].things != null)
                            for (var id in ROOMS[roomId].things) {
                                var spr = ROOMS[roomId].things[id];
                                spr.spriteGraphics = loadImage(ASSETS_FOLDER + spr.file);
                            }
                    }
                }

                //load the misc images from data
                var imageData = DATA.IMAGES;
                IMAGES = {};
                for (var i = 0; i < imageData.length; i++) {
                    IMAGES[imageData[i][0]] = loadImage(ASSETS_FOLDER + imageData[i][1]);
                }

                //load the misc images from data
                var soundData = DATA.SOUNDS;
                SOUNDS = {};
                for (var i = 0; i < soundData.length; i++) {
                    SOUNDS[soundData[i][0]] = loadSound(ASSETS_FOLDER + soundData[i][1]);
                }


                print(">>> DATA RECEIVED " + (DATA.ROOMS != null));
            }
        }
    );

    //I can now open it
    socket.open();
}


function draw() {

    //this is like a second janky preload: I'm waiting for data from the server and for the images linked within it to load
    if (!dataLoaded && ROOMS != null) {
        //loaded except when it's NOT
        dataLoaded = true;

        for (var roomId in ROOMS) {
            var room = ROOMS[roomId];

            if (room.bgGraphics != null)
                if (room.bgGraphics.width == 1)
                    dataLoaded = false;

            if (room.areaGraphics != null)
                if (room.areaGraphics.width == 1)
                    dataLoaded = false;

            if (room.musicLoop != null)
                if (!room.musicLoop.isLoaded())
                    dataLoaded = false;

        }


        for (var imageId in IMAGES) {
            if (IMAGES[imageId].width == 1 && IMAGES[imageId].height == 1) {
                dataLoaded = false;
            }
        }

        for (var soundId in SOUNDS) {
            if (!SOUNDS[soundId].isLoaded()) {
                dataLoaded = false;
            }
        }

        if (dataLoaded)
            print("Room data and assets loaded");
    }

    if (dataLoaded && !gameStarted) {
        setupGame();
    }

    //this is the actual game loop
    if (gameStarted) {
        update();
    }
}

//called once upon data and image load
function setupGame() {

    gameStarted = true;

    //starting from default or from location on the url?
    if (ROOM_LINK) {
        //url parameters can pass the room so a room can be linked
        const urlParams = new URLSearchParams(window.location.search);
        const urlRoom = urlParams.get("room")

        if (urlRoom != null) {
            if (ROOMS[urlRoom] != null)
                SETTINGS.defaultRoom = urlRoom;
        }
    }


    if (QUICK_LOGIN) {
        //assign random name and avatar and get to the game
        nickName = "user" + floor(random(0, 1000));

        currentColors = randomizeAvatarColors();


        currentAvatar = floor(random(0, walkSheets.length));
        newGame();
    }
    else if (!LURK_MODE) {

        //paint background
        image(menuBg, 0, 0, WIDTH, HEIGHT);
        hideJoin();
        showUser();

        var field = document.getElementById("lobby-field");
        field.focus();

    }
    else {
        //nickname blank means invisible - lurk mode
        nickName = "";

        currentColors = randomizeAvatarColors();
        generatedPalettes = [];
        generatedPalettes.push(currentColors);
        paletteIndex = 0;
        currentAvatar = floor(random(0, walkSheets.length));
        newGame();
    }
}





function newGame() {

    screen = "game";
    nextCommand = null;
    areaLabel = "";
    rolledSprite = null;
    logoCounter = 0;

    hideUser();
    hideAvatar();

    if (menuGroup != null)
        menuGroup.removeSprites();

    if (nickName == "") {
        showJoin();
    }
    else {
        showChat();
    }

    //this is not super elegant but I create another socket for the actual game
    //because I've got the data from the server and I don't want to reinitiate everything 
    //if the server restarts
    if (socket != null) {
        //console.log("Lurker joins " + socket.id);
        socket.disconnect();
        socket = null;
    }

    //I create a socket but I wait to assign all the functions before opening a connection
    socket = io({
        autoConnect: false
    });

    //paint background
    background(UI_BG);

    //initialize players as object
    players = {};


    //all functions are in a try/catch to prevent a hacked client from sending garbage that crashes other clients 

    //if the client detects a server connection it may be because the server restarted 
    //in that case the clients reconnect automatically and are assigned new ids so I have to clear
    //the previous player list to avoid ghosts
    //as long as the clients are open they should not lose their avatar and position even if the server is down
    socket.on("connect", function () {

        try {
            players = {};

            //ayay: connection lost while setting up character, just force a refresh
            if (screen == "avatar" || screen == "user") {
                screen = "error";
                errorMessage = "SERVER RESTARTED: PLEASE REFRESH";
                socket.disconnect();
            }


            bubbles = [];

            //first time
            if (me == null) {
                //join offscreen if missing parameters?
                var sx = -100;
                var sy = -100;

                if (ROOMS[SETTINGS.defaultRoom].spawn == null) {
                    console.log("WARNING: " + SETTINGS.defaultRoom + " has no spawn area");
                }
                else {
                    spawnZone = ROOMS[SETTINGS.defaultRoom].spawn;
                    //randomize position if it"s the first time
                    var sx = round(random(spawnZone[0] * ASSET_SCALE, spawnZone[2] * ASSET_SCALE));
                    var sy = round(random(spawnZone[1] * ASSET_SCALE, spawnZone[3] * ASSET_SCALE));
                }


                //send the server my name and avatar
                socket.emit("join", { nickName: nickName, colors: currentColors, avatar: currentAvatar, room: SETTINGS.defaultRoom, x: sx, y: sy });
            }
            else {

                socket.emit("join", { nickName: nickName, colors: currentColors, avatar: currentAvatar, room: me.room, x: me.x, y: me.y });
            }
        } catch (e) {
            console.log("Error on connect");
            console.error(e);
        }

    });//end connect


    //when somebody joins the game create a new player
    socket.on("playerJoined",
        function (p) {
            try {


                //console.log("new player in the room " + p.room + " " + p.id + " " + p.x + " " + p.y + " color " + p.color);

                //stop moving
                p.destinationX = p.x;
                p.destinationY = p.y;

                //if it's me///////////
                if (socket.id == p.id) {
                    rolledSprite = null;

                    //the location appears in the url
                    if (ROOM_LINK && ROOMS[p.room] != null)
                        window.history.replaceState(null, null, "?room=" + p.room);

                    players = {};
                    bubbles = [];

                    deleteAllSprites();

                    players[p.id] = me = new Player(p);

                    /*
                    me.sprite.mouseActive = false;
                    me.sprite.onMouseOver = function () { };
                    me.sprite.onMouseOut = function () { };
                    */

                    //click on me = emote
                    me.sprite.onMousePressed = function () { socket.emit("emote", { room: me.room, em: true }); };
                    me.sprite.onMouseReleased = function () { socket.emit("emote", { room: me.room, em: false }); };

                    room = p.room;

                    //if a page background is specified change it
                    if (ROOMS[p.room].pageBg != null)
                        document.body.style.backgroundColor = ROOMS[p.room].pageBg;
                    else
                        document.body.style.backgroundColor = PAGE_COLOR;

                    //load level background

                    if (ROOMS[p.room].bgGraphics != null) {
                        //can be static or spreadsheet
                        var bgg = ROOMS[p.room].bgGraphics;

                        //find frame number
                        var f = 1;
                        if (ROOMS[p.room].frames != null)
                            f = ROOMS[p.room].frames;

                        var ss = loadSpriteSheet(bgg, NATIVE_WIDTH, NATIVE_HEIGHT, f);
                        bg = loadAnimation(ss);

                        if (ROOMS[p.room].frameDelay != null) {
                            bg.frameDelay = ROOMS[p.room].frameDelay;
                        }
                    }

                    if (ROOMS[p.room].avatarScale == null)
                        ROOMS[p.room].avatarScale = 2;

                    areas = ROOMS[p.room].areaGraphics;
                    if (areas == null)
                        print("ERROR: no area assigned to  " + p.room);

                    //create sprites
                    if (ROOMS[p.room].things != null)
                        for (var tId in ROOMS[p.room].things) {

                            var thing = ROOMS[p.room].things[tId];

                            createThing(thing, tId);

                        }//


                    //start the music if any
                    //music is synched across clients
                    if (ROOMS[p.room].musicLoop != null && SOUND) {
                        var vol = 1;
                        if (ROOMS[p.room].musicVolume != null)
                            vol = ROOMS[p.room].musicVolume;

                        //all music "starts" at server's last restart
                        var now = Date.now();
                        //time difference in seconds
                        var timeDiff = (now - START_TIME) / 1000;
                        //figure out at what point of the loop
                        var l = ROOMS[p.room].musicLoop;
                        var startTime = timeDiff % l.duration();
                        l.loop(0, 1, vol, startTime);
                    }


                    //initialize the mod if any
                    if (window.initMod != null) {
                        window.initMod(p.id, p.room);
                    }

                }//it me
                else {
                    //
                    players[p.id] = new Player(p);

                    // console.log("I shall introduce myself to " + p.id);

                    //If I"m not the new player send an introduction to the new player
                    socket.emit("intro", p.id, {
                        id: socket.id,
                        nickName: me.nickName,

                        colors: me.colors,
                        avatar: me.avatar,
                        room: me.room,
                        x: me.x,
                        y: me.y,
                        destinationX: me.destinationX,
                        destinationY: me.destinationY
                    });
                }

                if (p.new && p.nickName != "" && firstLog) {
                    var spark = createSprite(p.x, p.y - AVATAR_H + 1);
                    spark.addAnimation("spark", appearEffect);
                    spark.scale = ASSET_SCALE;
                    spark.life = 60;
                    if (SOUND)
                        appearSound.play();

                    if (p.id == me.id) {
                        longText = SETTINGS.INTRO_TEXT;
                        longTextLines = 1;
                        longTextAlign = "center";//or center
                    }

                    firstLog = false;
                }

                console.log("There are now " + Object.keys(players).length + " players in this room");

                //calling a custom function roomnameEnter if it exists
                if (window[p.room + "Enter"] != null) {
                    window[p.room + "Enter"](p.id, p.room);
                }

            }
            catch (e) {
                console.log("Error on playerJoined");
                console.error(e);
            }
        }
    );

    //each existing player sends me an object with their parameters
    socket.on("onIntro",
        function (p) {
            try {
                //console.log("Hello newcomer I'm " + p.nickName + " " + p.id);
                //console.log("INTRO from " + p.room + " " + me.room);

                players[p.id] = new Player(p);

                if (p.room != null)
                    if (window[p.room + "Intro"] != null) {
                        window[p.room + "Intro"](p.id, p.room);
                    }
                console.log("There are now " + Object.keys(players).length + " players in this room");
            } catch (e) {
                console.log("Error on onIntro");
                console.error(e);
            }
        }
    );


    //when somebody clicks to move, update the destination (not the position)
    socket.on("playerMoved",
        function (p) {
            try {
                //console.log(p.id + " moves to: " + p.destinationX + " " + p.destinationY);

                //make sure the player exists
                if (players.hasOwnProperty(p.id)) {

                    players[p.id].destinationX = p.destinationX;
                    players[p.id].destinationY = p.destinationY;
                }
            } catch (e) {
                console.log("Error on playerMoved");
                console.error(e);
            }
        });


    //when somebody disconnects/leaves the room
    socket.on("playerLeft",
        function (p) {
            try {
                console.log("Player " + p.id + " left " + p.room);

                if (p.id == me.id) {
                    print("STOP MUSIC");
                    //stop music before you leave, if any
                    if (ROOMS[p.room].musicLoop != null) {
                        ROOMS[p.room].musicLoop.stop();
                    }
                }

                if (players[p.id] != null) {

                    //calling a custom function roomnameEnter if it exists
                    if (window[p.room + "Exit"] != null) {
                        window[p.room + "Exit"](p.id);
                    }



                    if (p.disconnect && players[p.id].nickName != "") {
                        var spark = createSprite(players[p.id].x, players[p.id].y - AVATAR_H + 1);
                        spark.addAnimation("spark", disappearEffect);
                        spark.scale = ASSET_SCALE;
                        spark.life = 60;
                        if (SOUND)
                            disappearSound.play();
                    }

                    if (players[p.id].sprite != null) {
                        if (players[p.id].sprite == rolledSprite) {
                            rolledSprite = null;
                        }

                        removeSprite(players[p.id].sprite);
                    }


                }

                delete players[p.id];
                console.log("There are now " + Object.keys(players).length + " players in this room");

            } catch (e) {
                console.log("Error on playerLeft");
                console.error(e);
            }
        }
    );



    //when somebody talks
    socket.on("playerTalked",
        function (p) {
            try {

                console.log("new message from " + p.id + ": " + p.message + " bubble color " + p.color);

                //make sure the player exists in the client
                if (players.hasOwnProperty(p.id)) {

                    if (!players[p.id].ignore) {
                        //minimum y of speech bubbles depends on room, typically higher half
                        var offY = ROOMS[me.room].bubblesY * ASSET_SCALE;
                        var newBubble = new Bubble(p.id, p.message, p.color, p.x, p.y, offY);

                        //calling a custom function 
                        if (window[me.room + "Talk"] != null) {
                            window[me.room + "Talk"](p.id, newBubble);
                        }

                        pushBubbles(newBubble);
                        bubbles.push(newBubble);

                        if (SOUND) {
                            blips[floor(random(0, blips.length))].play();
                        }

                    }
                }
            } catch (e) {
                console.log("Error on playerTalked");
                console.error(e);
            }
        }
    );

    //when an NPC or a thing talks
    socket.on("nonPlayerTalked",
        function (np) {
            try {

                //minimum y of speech bubbles depends on room, typically higher half
                var offY = ROOMS[np.room].bubblesY * ASSET_SCALE;
                var newBubble = new Bubble(np.id, np.message, 0, np.x, np.y, offY);
                newBubble.color = color(np.labelColor)

                pushBubbles(newBubble);
                bubbles.push(newBubble);

                if (SOUND) {
                    blips[floor(random(0, blips.length))].play();
                }
            } catch (e) {
                console.log("Error on nonPlayerTalked");
                console.error(e);
            }
        }
    );



    //displays a message upon connection refusal (server full etc)
    //this is an end state and requires a refresh or a join
    socket.on("errorMessage",
        function (msg) {
            if (socket.id) {
                screen = "error";
                errorMessage = msg;
                hideChat();
                hideUser();
                hideAvatar();
                hideJoin();
            }
        }
    );



    //when a server message arrives
    socket.on("godMessage",
        function (msg) {
            if (socket.id) {

                longText = msg;
                longTextLines = -1;
                longTextAlign = "center";
                longTextLink = "";
            }
        }
    );

    //when a server message arrives
    socket.on("playerEmoted",
        function (id, em) {
            try {
                if (players[id] != null) {
                    if (players[id].sprite != null) {

                        if (em) {
                            players[id].sprite.changeAnimation("emote");
                            players[id].sprite.animation.changeFrame(1);
                            players[id].sprite.animation.stop();
                        }
                        else {
                            players[id].sprite.changeAnimation("emote");
                            players[id].sprite.animation.changeFrame(0);
                            players[id].sprite.animation.stop();
                        }
                    }


                }

            } catch (e) {
                console.log("Error on playerTalked");
                console.error(e);
            }
        });


    socket.on("thingChanged", function (t) {

        //find the data thing
        var dataThing = ROOMS[t.room].things[t.thingId];

        if (dataThing != null && t.room == me.room) {

            //remove the visual representation
            removeThing(t.thingId, t.room);

            //change the value
            dataThing[t.property] = t.value;

            print("Change property " + t.property + " of " + t.thingId + " in room " + t.room + " to " + t.value);

            //recreate it
            createThing(dataThing, t.thingId);
        }
        else {
            //print("Warning: I can't find " + t.thingId + " in room " + t.room);
        }
    });


    //server sends out the response to the name submission, only if lurk mode is enabled
    //it"s in a separate function because it is shared between the first provisional connection 
    //and the "real" one later
    socket.on("nameValidation", nameValidationCallBack);


    //when a server message arrives
    socket.on("popup",
        function (msg) {
            if (socket.id) {
                alert(msg);
            }
        }
    );

    //player in the room is AFK
    socket.on("playerBlurred", function (id) {

        if (players[id] != null)
            players[id].sprite.transparent = true;
    });

    //player in the room is AFK
    socket.on("playerFocused", function (id) {
        if (players[id] != null)
            players[id].sprite.transparent = false;

    });

    //when the client realizes it's being disconnected
    socket.on("disconnect", function () {
        //console.log("OH NO");
    });

    //server forces refresh (on disconnect or to force load a new version of the client)
    socket.on("refresh", function () {
        socket.disconnect();
        location.reload(true);
    });

    //I can now open it

    socket.open();

}



//this p5 function is called continuously 60 times per second by default
function update() {

    if (screen == "user") {
        image(menuBg, 0, 0, WIDTH, HEIGHT);
    }
    //renders the avatar selection screen which can be fully within the canvas
    else if (screen == "avatar") {
        image(menuBg, 0, 0, WIDTH, HEIGHT);

        textFont(font, FONT_SIZE * 2);
        textAlign(CENTER, BASELINE);
        fill(0);
        text("Body", 24 * ASSET_SCALE, 44 * ASSET_SCALE);
        text("Color", 105 * ASSET_SCALE, 44 * ASSET_SCALE);

        text("Choose your avatar", 64 * ASSET_SCALE, 18 * ASSET_SCALE);

        menuGroup.draw();

    }
    else if (screen == "error") {
        //end state, displays a message in full screen
        textFont(font, FONT_SIZE);
        textAlign(CENTER, CENTER);
        fill(UI_BG);
        rect(0, 0, WIDTH, HEIGHT);
        fill(LABEL_NEUTRAL_COLOR);

        text(errorMessage, floor(WIDTH / 8), floor(HEIGHT / 8), WIDTH - floor(WIDTH / 4), HEIGHT - floor(HEIGHT / 4) + 1);
    }
    else if (screen == "game") {

        //calling a custom function
        if (window[room + "Update"] != null) {
            window[room + "Update"]();
        }

        //draw a background
        background(UI_BG);
        imageMode(CORNER);

        if (bg != null) {
            push();
            scale(ASSET_SCALE);
            translate(-NATIVE_WIDTH / 2, -NATIVE_HEIGHT / 2);
            animation(bg, floor(WIDTH / 2), floor(HEIGHT / 2));
            pop();
        }

        textFont(font, FONT_SIZE);

        //iterate through the players
        for (var playerId in players) {
            if (players.hasOwnProperty(playerId)) {

                var p = players[playerId];

                var prevX, prevY;

                //make sure the coordinates are non null since I may have created a player
                //but I may still be waiting for the first update
                if (p.x != null && p.y != null) {
                    //save in case of undo
                    prevX = p.x;
                    prevY = p.y;

                    //position and destination are different, move
                    if (p.x != p.destinationX || p.y != p.destinationY) {

                        //a series of vector operations to move toward a point at a linear speed

                        // create vectors for position and dest.
                        var destination = createVector(p.destinationX, p.destinationY);
                        var position = createVector(p.x, p.y);

                        // Calculate the distance between your destination and position
                        var distance = destination.dist(position);

                        // this is where you actually calculate the direction
                        // of your target towards your rect. subtraction dx-px, dy-py.
                        var delta = destination.sub(position);

                        // then you're going to normalize that value
                        // (normalize sets the length of the vector to 1)
                        delta.normalize();

                        // then you can multiply that vector by the desired speed
                        var increment = delta.mult(SPEED * deltaTime / 1000);

                        /*
                        IMPORTANT
                        deltaTime The system variable deltaTime contains the time difference between 
                        the beginning of the previous frame and the beginning of the current frame in milliseconds.
                        the speed is not based on the client framerate which can be variable but on the actual time that passes
                        between frames.
                        */

                        //increment the position
                        position.add(increment);

                        //calculate new distance
                        var newDistance = position.dist(createVector(p.destinationX, p.destinationY));

                        //if I got farther than I was originally, I overshot so set position to destination
                        if (newDistance > distance) {

                            p.x = p.destinationX;
                            p.y = p.destinationY;
                            p.stopWalkingAnimation();

                        }
                        else {
                            //this system is not pathfinding but it makes characters walk "around" corners instead of getting stuck

                            //test new position for obstacle repeat for both sides
                            var obs = isObstacle(position.x - AVATAR_W / 2, position.y, p.room, areas);
                            var obs2 = isObstacle(position.x + AVATAR_W / 2, position.y, p.room, areas);
                            var obs3 = isObstacle(position.x, position.y, p.room, areas);

                            if (!obs && !obs2 && !obs3) {
                                p.x = position.x;
                                p.y = position.y;
                                p.playWalkingAnimation();
                            }
                            //if obstacle test only x movement
                            else {

                                var obsX = isObstacle(position.x - AVATAR_W / 2, p.y, p.room, areas);
                                var obsX2 = isObstacle(position.x + AVATAR_W / 2, p.y, p.room, areas);
                                var obsX3 = isObstacle(position.x, p.y, p.room, areas);

                                //if not obstacle move only horizontally at full speed
                                if (!obsX && !obsX2 && !obsX3 && abs(delta.x) > 0.1) {

                                    p.x += SPEED * deltaTime / 1000 * (p.x > position.x) ? -1 : 1;
                                    p.playWalkingAnimation();

                                }
                                else {
                                    //if obs on y test the y
                                    var obsY = isObstacle(p.x - AVATAR_W / 2, position.y, p.room, areas);
                                    var obsY2 = isObstacle(p.x + AVATAR_W / 2, position.y, p.room, areas);
                                    var obsY3 = isObstacle(p.x, position.y, p.room, areas);

                                    if (!obsY && !obsY2 && !obsY3 && abs(delta.y) > 0.1) {

                                        p.y += SPEED * deltaTime / 1000 * (p.y > position.y) ? -1 : 1;
                                        p.playWalkingAnimation();

                                    }
                                    else {
                                        //if not complete block
                                        p.destinationX = p.x;
                                        p.destinationY = p.y;
                                        p.stopWalkingAnimation();
                                        //cancel command if me
                                        if (p == me) {
                                            nextCommand = null;

                                            //stop if moving
                                            socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: me.x, destinationY: me.y });
                                        }
                                    }

                                }
                            }

                            //change dir
                            if (prevX != p.x) {
                                p.dir = (prevX > p.x) ? -1 : 1;
                                p.sprite.mirrorX(p.dir);
                            }
                        }

                    }
                    else {

                        p.stopWalkingAnimation();
                    }

                    //The clients should never take a player to an illegal place (transparent area pixels or non walkable areas)
                    //but occasionally a dude will open the developer console and change the coordinates to make his avatar fly around
                    //Good for him! Let him have fun in his own little client! 
                    //All the others players will just stop displaying and hearing him
                    //because I really don't want to enforce this on the server side
                    var illegal = isObstacle(p.x, p.y, p.room, areas);
                    if (illegal) {
                        //print(">>>>>>>>>>>" + p.id + " is in an illegal position<<<<<<<<<<<<<<<");
                        p.ignore = true;
                        if (p.sprite != null)
                            p.sprite.ignore = true;
                    }
                    else {
                        p.ignore = false;
                        if (p.sprite != null)
                            p.sprite.ignore = false;
                    }

                    //////this part is only triggered by ME
                    if (p == me) {
                        //reached destination, execute action
                        if (me.x == me.destinationX && me.y == me.destinationY && nextCommand != null) {
                            executeCommand(nextCommand);
                            nextCommand = null;
                        }

                    }

                    p.updatePosition();

                }

            }
        }//player update cycle


        //set the existing sprites' depths in relation to their position
        for (var i = 0; i < allSprites.length; i++) {
            //allSprites[i].debug = true;

            var dOff = 0;

            if (allSprites[i].depthOffset != null)
                dOff = allSprites[i].depthOffset;

            allSprites[i].depth = allSprites[i].position.y + dOff;
        }

        //
        drawSprites();


        //GUI
        if (nickName != "" && rolledSprite == null && areaLabel == "")
            animation(walkIcon, floor(mouseX + 6), floor(mouseY - 6));

        //draw all the speech bubbles lines first only if the players have not moves since speaking
        for (var i = 0; i < bubbles.length; i++) {
            var b = bubbles[i];
            var speaker = players[bubbles[i].pid];
            if (speaker != null && !b.orphan) {
                if (round(speaker.x) == b.px && round(speaker.y) == b.py) {
                    var s = ROOMS[speaker.room].avatarScale;

                    strokeWeight(s);
                    stroke(UI_BG);
                    strokeCap(SQUARE);
                    line(floor(speaker.x), floor(speaker.y - AVATAR_H * s - BUBBLE_MARGIN), floor(speaker.x), floor(b.y));
                }
                else {
                    //once it moves break the line
                    b.orphan = true;
                }
            }
        }



        //draw speech bubbles
        for (var i = 0; i < bubbles.length; i++) {
            bubbles[i].update();
        }
        //delete the expired ones
        for (var i = 0; i < bubbles.length; i++) {
            if (bubbles[i].counter < 0) {
                bubbles.splice(i, 1);
                i--; //decrement
            }
        }

        var label = areaLabel;
        var labelColor = LABEL_NEUTRAL_COLOR;

        //if lurking disable arealabel
        if (nickName == "")
            label = "";

        //player and sprites label override areas
        if (rolledSprite != null) {
            if (rolledSprite.label != null && rolledSprite.label != "") {
                label = rolledSprite.label + ((rolledSprite.transparent) ? "[AFK]" : "");
            }

            if (rolledSprite.labelColor != null) {
                labelColor = rolledSprite.labelColor;
            }
        }

        //by no circumstance show you name as label
        if (me != null)
            if (label == me.nickName)
                label = "";

        //draw rollover label
        if (label != "" && longText == "") {
            textFont(font, FONT_SIZE);
            textAlign(LEFT, BASELINE);
            var lw = textWidth(label);
            var lx = mouseX;

            if ((mouseX + lw + TEXT_PADDING * 2) > width) {
                lx = width - lw - TEXT_PADDING * 2;
            }
            fill(UI_BG);
            noStroke();
            rect(floor(lx), floor(mouseY - TEXT_H - TEXT_PADDING * 2), lw + TEXT_PADDING * 2 + 1, TEXT_H + TEXT_PADDING * 2 + 1);
            fill(labelColor);
            text(label, floor(lx + TEXT_PADDING) + 1, floor(mouseY - TEXT_PADDING));
        }

        //long text above everything
        if (longText != "" && nickName != "") {

            noStroke();
            textFont(font, FONT_SIZE);
            textLeading(TEXT_LEADING);

            //dramatic text on black
            if (longTextLines == -1) {

                if (longTextAlign == "left")
                    textAlign(LEFT, CENTER);
                else
                    textAlign(CENTER, CENTER);

                fill(UI_BG);
                rect(0, 0, width, height);
                fill(LABEL_NEUTRAL_COLOR);
                //-1 to avoid blurry glitch
                text(longText, LONG_TEXT_PADDING, LONG_TEXT_PADDING, width - LONG_TEXT_PADDING * 2, height - LONG_TEXT_PADDING * 2 - 1);
            }
            else {

                if (longTextAlign == "left")
                    textAlign(LEFT, BASELINE);
                else
                    textAlign(CENTER, BASELINE);

                //measuring text height requires a PhD so we
                //require the user to do trial and error and counting the lines
                //and use some magic numbers

                var tw = LONG_TEXT_BOX_W - LONG_TEXT_PADDING * 2;
                var th = longTextLines * TEXT_LEADING;


                //single line centered text
                if (longTextAlign == "center" && longTextLines == 1)
                    tw = textWidth(longText + " ");

                var rw = tw + LONG_TEXT_PADDING * 2;
                var rh = th + LONG_TEXT_PADDING * 2;

                fill(UI_BG);

                rect(floor(width / 2 - rw / 2), floor(height / 2 - rh / 2), floor(rw), floor(rh));
                //rect(20, 20, 100, 50);

                fill(LABEL_NEUTRAL_COLOR);
                text(longText, floor(width / 2 - tw / 2 + LONG_TEXT_PADDING - 1), floor(height / 2 - th / 2) + TEXT_LEADING - 3, floor(tw));
            }
        }//end long text

        if (nickName == "" && (logoCounter < LOGO_STAY || LOGO_STAY == -1)) {
            logoCounter += deltaTime;
            animation(logo, floor(width / 2), floor(height / 2));
        }

    }//end game


}



function windowResized() {
    scaleCanvas();
}


function scaleCanvas() {
    //landscape scale to height
    if (windowWidth > windowHeight) {
        canvasScale = windowHeight / WIDTH; //scale to W because I want to leave room for chat and instructions (squareish)
        canvas.style("width", WIDTH * canvasScale + "px");
        canvas.style("height", HEIGHT * canvasScale + "px");
    }
    else {
        canvasScale = windowWidth / WIDTH;
        canvas.style("width", WIDTH * canvasScale + "px");
        canvas.style("height", HEIGHT * canvasScale + "px");
    }

    var container = document.getElementById("canvas-container");
    container.setAttribute("style", "width:" + WIDTH * canvasScale + "px; height: " + HEIGHT * canvasScale + "px");

    var form = document.getElementById("interface");
    form.setAttribute("style", "width:" + WIDTH * canvasScale + "px;");

}

//I could do this in DOM (regular html and javascript elements) 
//but I want to show a canvas with html overlay
function avatarSelection() {
    menuGroup = new Group();
    screen = "avatar";

    //buttons
    var previousBody, nextBody, previousColor, nextColor;

    var ss = loadSpriteSheet(arrowButton, 28, 28, 3);
    var animation = loadAnimation(ss);

    //the position is the bottom left
    previousBody = createSprite(8 * ASSET_SCALE + 14, 50 * ASSET_SCALE + 14);
    previousBody.addAnimation("default", animation);
    previousBody.animation.stop();
    previousBody.mirrorX(-1);
    menuGroup.add(previousBody);

    nextBody = createSprite(24 * ASSET_SCALE + 14, 50 * ASSET_SCALE + 14);
    nextBody.addAnimation("default", animation);
    nextBody.animation.stop();
    menuGroup.add(nextBody);

    previousColor = createSprite(90 * ASSET_SCALE + 14, 50 * ASSET_SCALE + 14);
    previousColor.addAnimation("default", animation);
    previousColor.animation.stop();
    previousColor.mirrorX(-1);
    menuGroup.add(previousColor);

    nextColor = createSprite(106 * ASSET_SCALE + 14, 50 * ASSET_SCALE + 14);
    nextColor.addAnimation("default", animation);
    nextColor.animation.stop();
    menuGroup.add(nextColor);

    previousBody.onMouseOver = nextBody.onMouseOver = previousColor.onMouseOver = nextColor.onMouseOver = function () {
        this.animation.changeFrame(1);
    }
    previousBody.onMouseOut = nextBody.onMouseOut = previousColor.onMouseOut = nextColor.onMouseOut = function () {
        this.animation.changeFrame(0);
    }

    previousBody.onMousePressed = nextBody.onMousePressed = previousColor.onMousePressed = nextColor.onMousePressed = function () {
        this.animation.changeFrame(2);
    }

    previousBody.onMouseReleased = function () {
        currentAvatar -= 1;
        if (currentAvatar < 0)
            currentAvatar = AVATARS - 1;

        previewAvatar();
        this.animation.changeFrame(1);
    }

    nextBody.onMouseReleased = function () {
        currentAvatar += 1;
        if (currentAvatar >= AVATARS)
            currentAvatar = 0;

        previewAvatar();
        this.animation.changeFrame(1);
    }

    previousColor.onMouseReleased = function () {
        paletteIndex--;

        if (paletteIndex >= 0)
            currentColors = generatedPalettes[paletteIndex];
        else {
            currentColors = randomizeAvatarColors();
            generatedPalettes.unshift(currentColors);
            paletteIndex = 0;
            //currentColors = randomizeAvatarColors();
        }


        previewAvatar();
        this.animation.changeFrame(1);
    }

    nextColor.onMouseReleased = function () {

        paletteIndex++;

        //if end of array generate and push a new one
        if (paletteIndex > generatedPalettes.length - 1) {

            currentColors = randomizeAvatarColors();

            generatedPalettes.push(currentColors);
        }
        else {
            currentColors = generatedPalettes[paletteIndex];
        }

        previewAvatar();
        this.animation.changeFrame(1);
    }

    //nextBody.onMouseReleased = previousColor.onMouseReleased = nextColor.onMouseReleased = function () {

    randomAvatar();
}



//copy the properties
function Player(p) {
    this.id = p.id;
    this.nickName = p.nickName;

    this.colors = p.colors;

    this.avatar = p.avatar;
    this.ignore = false;

    this.tint = color("#FFFFFF");


    if (ROOMS[p.room].tint != null) {
        this.tint = color(ROOMS[p.room].tint);
    }

    //tint the image
    this.avatarGraphics = paletteSwap(walkSheets[p.avatar], p.colors, this.tint);
    this.spriteSheet = loadSpriteSheet(this.avatarGraphics, AVATAR_W, AVATAR_H, round(walkSheets[p.avatar].width / AVATAR_W));
    this.walkAnimation = loadAnimation(this.spriteSheet);
    //emote
    this.emoteGraphics = paletteSwap(emoteSheets[p.avatar], p.colors, this.tint);
    this.emoteSheet = loadSpriteSheet(this.emoteGraphics, AVATAR_W, AVATAR_H, round(emoteSheets[p.avatar].width / AVATAR_W));
    this.emoteAnimation = loadAnimation(this.emoteSheet);
    this.emoteAnimation.frameDelay = 10;

    this.sprite = createSprite(100, 100);

    this.sprite.scale = ROOMS[p.room].avatarScale;

    this.sprite.addAnimation("walk", this.walkAnimation);
    this.sprite.addAnimation("emote", this.emoteAnimation);


    if (this.nickName == "")
        this.sprite.mouseActive = false;
    else
        this.sprite.mouseActive = true;

    //this.sprite.debug = true;
    this.sprite.setCollider("rectangle", 0, 0, 4, 10)


    //no parent in js? WHAAAAT?
    this.sprite.id = this.id;
    this.sprite.label = p.nickName;
    this.sprite.transparent = false;
    this.sprite.roomId = p.room; //sure anything goes
    this.sprite.avatar = true;
    this.sprite.depthOffset = AVATAR_H / 2;

    //save the dominant color for bubbles and rollover label
    var c1 = color(TOP_COLORS[p.colors[2]]);
    var c2 = color(BOTTOM_COLORS[p.colors[3]]);

    if (brightness(c1) > 30)
        this.sprite.labelColor = TOP_COLORS[p.colors[2]];
    else if (brightness(c2) > 30)
        this.sprite.labelColor = BOTTOM_COLORS[p.colors[3]];
    else
        this.sprite.labelColor = LABEL_NEUTRAL_COLOR;

    this.labelColor = this.sprite.labelColor;

    this.room = p.room;
    this.x = p.x;
    this.y = p.y;
    this.dir = 1;
    this.destinationX = p.destinationX;
    this.destinationY = p.destinationY;


    //lurkmode
    if (this.nickName == "")
        this.sprite.visible = false;

    this.stopWalkingAnimation = function () {

        if (this.sprite.getAnimationLabel() == "walk") {
            this.sprite.changeAnimation("emote");
            this.sprite.animation.changeFrame(0);
            this.sprite.animation.stop();
        }
    }

    this.playWalkingAnimation = function () {
        this.sprite.changeAnimation("walk");
        this.sprite.animation.play();
    }

    this.updatePosition = function () {
        this.sprite.position.x = round(this.x);
        this.sprite.position.y = round(this.y - AVATAR_H / 2 * this.sprite.scale);
    }

    if (this.nickName != "") {
        this.sprite.onMouseOver = function () {
            rolledSprite = this;
        };

        this.sprite.onMouseOut = function () {
            if (rolledSprite == this)
                rolledSprite = null;
        };

        this.sprite.onMousePressed = function () {

        };
    }
    //ugly as fuck but javascript made me do it
    this.sprite.originalDraw = this.sprite.draw;

    this.sprite.draw = function () {

        if (!this.ignore) {
            if (this.transparent)
                tint(255, 100);

            if (window[this.roomId + "DrawSprite"] != null) {
                window[this.roomId + "DrawSprite"](this.id, this, this.originalDraw);
            }
            else {
                this.originalDraw();
            }

            if (this.transparent)
                noTint();
        }
    }

    this.stopWalkingAnimation();
}


//they exist in a different container so kill them
function deleteAllSprites() {
    allSprites.removeSprites();
}

//speech bubble object
function Bubble(pid, message, col, x, y, oy) {
    //always starts at row zero

    this.row = 0;
    this.pid = pid;
    this.message = message;

    this.color = color(col);

    this.orphan = false;
    this.counter = BUBBLE_TIME;

    //to fix an inexplicable bug that mangles bitmap text on small textfields
    //I scale short messages
    this.fontScale = 1;
    if (message.length < 4) {
        this.fontScale = 2;
        this.message = this.message.toUpperCase();
    }


    textFont(font, FONT_SIZE * this.fontScale);
    textAlign(LEFT, BASELINE);
    this.tw = textWidth(this.message);
    //whole bubble with frame
    this.w = round(this.tw + TEXT_PADDING * 2);
    this.h = round(TEXT_H + TEXT_PADDING * 2 * this.fontScale);

    //save the original player position so I can render a line as long as they are not moving
    this.px = round(x);
    this.py = round(y);

    this.offY = oy;

    this.x = round(this.px - this.w / 2);
    if (this.x + this.w + BUBBLE_MARGIN > width) {
        this.x = width - this.w - BUBBLE_MARGIN
    }
    if (this.x < BUBBLE_MARGIN) {
        this.x = BUBBLE_MARGIN;
    }


    this.update = function () {
        this.counter -= deltaTime / 1000;

        noStroke();
        textFont(font, FONT_SIZE * this.fontScale);
        textAlign(LEFT, BASELINE);
        rectMode(CORNER);
        fill(UI_BG);
        this.y = this.offY - floor((TEXT_H + TEXT_PADDING * 2 + BUBBLE_MARGIN) * this.row);
        rect(this.x, this.y, this.w + 1, this.h);
        fill(this.color);
        text(this.message, floor(this.x + TEXT_PADDING) + 1, floor(this.h + this.y - TEXT_PADDING));

    }
}

//move bubbles up if necessary
function pushBubbles(b) {

    //go through bubbles
    for (var i = 0; i < bubbles.length; i++) {
        if (bubbles[i] != b && bubbles[i].row == b.row) {
            //this bubble is on the same row, will they overlap?
            if (b.x < (bubbles[i].x + bubbles[i].w) && (b.x + b.w) > bubbles[i].x) {
                bubbles[i].row++;
                pushBubbles(bubbles[i]);
                //if off screen mark for deletion
                if (bubbles[i].y - bubbles[i].h < 0)
                    bubbles[i].counter = -1;
            }

        }
    }
}



function isObstacle(x, y, room, a) {
    var obs = true;

    if (room != null && a != null) {

        //you know, at this point I"m not sure if you are using assets scaled by 2 for the areas
        //so I"m just gonna stretch the coordinates ok
        var px = floor(map(x, 0, WIDTH, 0, a.width));
        var py = floor(map(y, 0, HEIGHT, 0, a.height));

        var c1 = a.get(px, py);

        //if not white check if color is obstacle
        if (c1[0] != 255 || c1[1] != 255 || c1[2] != 255) {
            var cmd = getCommand(c1, room);

            if (cmd != null)
                if (cmd.obstacle != null)
                    obs = cmd.obstacle;
        }
        else
            obs = false; //if white

    }
    return obs;
}

//on mobile there is no rollover so allow a drag to count as mouse move
//the two functions SHOULD be mutually exclusive
//touchDown prevents duplicate event firings
var touchDown = false;

function mouseDragged() {
    mouseMoved();
}

function touchMoved() {
    mouseMoved();
    touchDown = true;
}

function touchEnded() {

    if (touchDown) {
        touchDown = false;
        canvasReleased();
    }
}

//rollover state
function mouseMoved() {

    if (walkIcon != null)
        walkIcon.visible = false;

    if (areas != null && me != null) {

        //you know, at this point I"m not sure if you are using assets scaled by 2 for the areas
        //so I"m just gonna stretch the coordinates ok
        var mx = floor(map(mouseX, 0, WIDTH, 0, areas.width));
        var my = floor(map(mouseY, 0, HEIGHT, 0, areas.height));

        var c = areas.get(mx, my);
        areaLabel = "";

        if (alpha(c) != 0 && me.room != null) {
            //walk icon?
            if (c[0] == 255 && c[1] == 255 && c[2] == 255) {
                if (walkIcon != null)
                    walkIcon.visible = true;
            }
            else {
                var command = getCommand(c, me.room);
                if (command != null)
                    if (command.label != null) {
                        areaLabel = command.label;
                    }
            }
        }

    }
}

//stop emoting
function canvasPressed() {

    //emote only if not walking
    if (nickName != "" && screen == "game" && mouseButton == RIGHT) {
        if (me.destinationX == me.x && me.destinationY == me.y)
            socket.emit("emote", { room: me.room, em: true });
    }
}


//when I click to move
function canvasReleased() {

    //print("CLICK " + mouseButton);

    if (screen == "error") {
    }
    else if (nickName != "" && screen == "game" && mouseButton == RIGHT) {
        if (me.destinationX == me.x && me.destinationY == me.y)
            socket.emit("emote", { room: me.room, em: false });
    }
    else if (nickName != "" && screen == "game" && mouseButton == LEFT) {
        //exit text
        if (longText != "" && longText != SETTINGS.INTRO_TEXT) {

            if (longTextLink != "")
                window.open(longTextLink, "_blank");

            longText = "";
            longTextLink = "";
        }
        else if (me != null) {

            longText = "";
            longTextLink = "";

            if (AFK) {
                AFK = false;
                if (socket != null)
                    socket.emit("focus", { room: me.room });
            }

            //clicked on person
            if (rolledSprite != null) {

                //click on player sprite attempt to move next to them
                if (rolledSprite.id != null) {
                    nextCommand = null;
                    var t = players[rolledSprite.id];
                    if (t != null && t != me) {
                        var d = (me.x < t.x) ? -(AVATAR_W * 2) : (AVATAR_W * 2);
                        socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: t.x + d, destinationY: t.y });
                    }
                }
            }
            //check the area info
            else if (areas != null && me.room != null) {

                //you know, at this point I'm not sure if you are using assets scaled by 2 for the areas
                //so I'm just gonna stretch the coordinates ok
                var mx = floor(map(mouseX, 0, WIDTH, 0, areas.width));
                var my = floor(map(mouseY, 0, HEIGHT, 0, areas.height));

                var c = areas.get(mx, my);

                //if transparent or semitransparent do nothing
                if (alpha(c) != 255) {
                    //cancel command
                    nextCommand = null;
                    //stop if moving
                    if (me.x != me.destinationX && me.y != me.destinationY)
                        socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: me.x, destinationY: me.y });
                }
                else if (c[0] == 255 && c[1] == 255 && c[2] == 255) {
                    //if white, generic walk stop command
                    nextCommand = null;

                    socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: mouseX, destinationY: mouseY });
                }
                else {
                    //if something else check the commands
                    var command = getCommand(c, me.room);

                    //walk and executed when you arrive or stop
                    if (command != null)
                        moveToCommand(command);
                }
            }


        }
    }

}

//queue a command, move to the point
function moveToCommand(command) {

    nextCommand = command;

    //I need to change my destination locally before the message bouces back

    if (command.point != null) {
        me.destinationX = command.point[0] * ASSET_SCALE;
        me.destinationY = command.point[1] * ASSET_SCALE;
        socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: command.point[0] * ASSET_SCALE, destinationY: command.point[1] * ASSET_SCALE });
    }
    else //just move where you clicked (area) 
    {
        me.destinationX = mouseX;
        me.destinationY = mouseY;
        socket.emit("move", { x: me.x, y: me.y, room: me.room, destinationX: mouseX, destinationY: mouseY });
    }

}

function getCommand(c, roomId) {
    try {
        //turn color into string
        var cString = color(c).toString("#rrggbb");//for com

        var areaColors = ROOMS[roomId].areaColors;
        var command;

        //go through properties
        for (var colorId in areaColors) {

            if (areaColors.hasOwnProperty(colorId)) {
                var aString = "#" + colorId.substr(1);

                if (aString == cString) {
                    //color found
                    command = areaColors[colorId];

                }
            }
        }
    }
    catch (e) {
        console.log("Get command error " + roomId + " color " + c);
        console.error(e);
    }

    return command;
}


function executeCommand(c) {
    areaLabel = "";
    //print("Executing command " + c.cmd);

    switch (c.cmd) {
        case "enter":
            var sx, sy;
            if (ROOMS[c.room] != null) {

                //stop music before you leave, if any
                if (ROOMS[me.room].musicLoop != null) {
                    ROOMS[me.room].musicLoop.stop();
                }

                if (c.enterPoint != null) {
                    sx = c.enterPoint[0] * ASSET_SCALE;
                    sy = c.enterPoint[1] * ASSET_SCALE;
                    socket.emit("changeRoom", { from: me.room, to: c.room, x: sx, y: sy });
                }
                else if (ROOMS[c.room].spawn != null) {
                    spawnZone = ROOMS[c.room].spawn;
                    sx = round(random(spawnZone[0] * ASSET_SCALE, spawnZone[2] * ASSET_SCALE));
                    sy = round(random(spawnZone[1] * ASSET_SCALE, spawnZone[3] * ASSET_SCALE));
                    socket.emit("changeRoom", { from: me.room, to: c.room, x: sx, y: sy });
                }
                else {
                    console.log("ERROR: No spawn point or area set for " + c.room);
                }



            }
            break;

        case "action":

            if (c.actionId != null) {
                socket.emit("action", c.actionId);
            }

            break;

        case "text":
            if (c.txt != null) {

                longText = c.txt;
                if (c.lines != null)
                    longTextLines = c.lines;
                else
                    longTextLines = 1;

                if (c.align != null)
                    longTextAlign = c.align;
                else
                    longTextAlign = "center";//or center

                if (c.url == null)
                    longTextLink = "";
                else
                    longTextLink = c.url;

            }
            else
                print("Warning for text: make sure to specify arg as text")
            break;


    }

}

//For better user experience I automatically focus on the chat textfield upon pressing a key
function keyPressed() {
    if (screen == "game") {
        var field = document.getElementById("chatField");
        field.focus();
    }
    if (screen == "user") {
        var field = document.getElementById("lobby-field");
        field.focus();
    }
}

//when I hits send
function talk(msg) {

    if (AFK) {
        AFK = false;
        if (socket != null && me != null)
            socket.emit("focus", { room: me.room });
    }

    if (msg.replace(/\s/g, "") != "" && nickName != "") {

        var command = commandLine(msg)

        if (!command)
            socket.emit("talk", { message: msg, color: me.labelColor, room: me.room, x: me.x, y: me.y });
    }
}

//client side command line
function commandLine(msg) {
    var found = false;

    switch (msg.toLowerCase()) {
        case "/sound off":
            SOUND = false;
            found = true;
            break;
        case "/sound on":
            SOUND = true;
            found = true;
            break;
        case "/afk":
            if (socket != null && me != null)
                socket.emit("blur", { room: me.room });

            AFK = true;
            found = true;
            break;

    }

    return found;
}

//called by the talk button in the html
function getTalkInput() {

    var time = new Date().getTime();

    if (time - lastMessage > SETTINGS.ANTI_SPAM) {

        // Selecting the input element and get its value 
        var inputVal = document.getElementById("chatField").value;
        //sending it to the talk function in sketch
        talk(inputVal);
        document.getElementById("chatField").value = "";
        //save time
        lastMessage = time;
        longText = "";
        longTextLink = "";
    }
    //prevent page from refreshing (default form behavior)
    return false;
}

//called by the continue button in the html
function nameOk() {
    var v = document.getElementById("lobby-field").value;

    if (v != "") {
        nickName = v;

        //if socket !null the connection has been established ie lurk mode
        if (socket != null) {
            socket.emit("sendName", v);
        }

        //prevent page from refreshing on enter (default form behavior)
        return false;
    }
}

function nameValidationCallBack(code) {
    if (socket.id) {

        if (code == 0) {
            console.log("Username already taken");
            var e = document.getElementById("lobby-error");

            if (e != null)
                e.innerHTML = "Username already taken";
        }
        else if (code == 3) {

            var e = document.getElementById("lobby-error");

            if (e != null)
                e.innerHTML = "Sorry, only standard western characters are allowed";
        }
        else {

            hideUser();
            showAvatar();
            avatarSelection();
        }
    }
}

//draws a random avatar body in the center of the canvas
//colors it a random color
function randomAvatar() {
    currentAvatar = floor(random(0, AVATARS));
    previewAvatar();
}

function previewAvatar() {

    if (avatarPreview != null)
        removeSprite(avatarPreview);


    var aGraphics = paletteSwap(emoteSheets[currentAvatar], currentColors);
    var aSS = loadSpriteSheet(aGraphics, AVATAR_W, AVATAR_H, round(emoteSheets[currentAvatar].width / AVATAR_W));
    var aAnim = loadAnimation(aSS);
    avatarPreview = createSprite(width / 2, height / 2);
    avatarPreview.scale = 4;
    avatarPreview.addAnimation("default", aAnim);
    avatarPreview.animation.frameDelay = 10;
    //avatarPreview.debug = true;
    //avatarPreview.animation.stop();
    menuGroup.add(avatarPreview);
}

function randomizeAvatarColors() {
    return [
        floor(random(0, HAIR_COLORS.length)),
        floor(random(0, SKIN_COLORS.length)),
        floor(random(0, TOP_COLORS.length)),
        floor(random(0, BOTTOM_COLORS.length))
    ]
}

function paletteSwap(ss, paletteNumbers, t) {

    var tint = [255, 255, 255];

    if (t != null)
        tint = [red(t), green(t), blue(t)];

    var img = createImage(ss.width, ss.height);
    img.copy(ss, 0, 0, ss.width, ss.height, 0, 0, ss.width, ss.height);
    img.loadPixels();

    var palette = [];

    palette[0] = HAIR_COLORS_RGB[paletteNumbers[0]];
    palette[1] = SKIN_COLORS_RGB[paletteNumbers[1]];
    palette[2] = TOP_COLORS_RGB[paletteNumbers[2]];
    palette[3] = BOTTOM_COLORS_RGB[paletteNumbers[3]];


    for (var i = 0; i < img.pixels.length; i += 4) {

        if (img.pixels[i + 3] == 255) {
            var found = false;

            //non transparent pix replace with palette
            for (var j = 0; j < REF_COLORS_RGB.length && !found; j++) {
                //print("Ref color " + j + " " + palette[j]);

                if (img.pixels[i] == REF_COLORS_RGB[j][0] && img.pixels[i + 1] == REF_COLORS_RGB[j][1] && img.pixels[i + 2] == REF_COLORS_RGB[j][2]) {
                    found = true;
                    img.pixels[i] = palette[j][0] * tint[0] / 255;
                    img.pixels[i + 1] = palette[j][1] * tint[1] / 255;
                    img.pixels[i + 2] = palette[j][2] * tint[2] / 255;
                }

            }
        }
    }
    img.updatePixels();

    return img;
}

function tintGraphics(img, colorString) {

    var c = color(colorString);
    let pg = createGraphics(img.width, img.height);
    pg.noSmooth();
    pg.tint(red(c), green(c), blue(c), 255);
    pg.image(img, 0, 0, img.width, img.height);
    //i need to convert it back to image in order to use it as spritesheet
    var img = createImage(pg.width, pg.height);
    img.copy(pg, 0, 0, pg.width, pg.height, 0, 0, pg.width, pg.height);

    return img;
}

//create a sprite from a "thing" object found in data
function createThing(thing, id) {

    var f = 1;

    if (thing.frames != null)
        f = thing.frames;

    var sw = floor(thing.spriteGraphics.width / f);
    var sh = thing.spriteGraphics.height;

    var ss = loadSpriteSheet(thing.spriteGraphics, sw, sh, f);
    var animation = loadAnimation(ss);

    if (thing.frameDelay != null)
        animation.frameDelay = thing.frameDelay;


    //the "real" position is the bottom left
    var ox = sw % 2;
    var oy = sh % 2;

    var newSprite = createSprite(floor(thing.position[0] + sw / 2) * ASSET_SCALE + ox, floor(thing.position[1] + sh / 2) * ASSET_SCALE + oy);
    newSprite.addAnimation("default", animation);

    newSprite.depthOffset = floor(sh / 2) - 4; //4 magic fucking number due to rounding

    newSprite.id = id;

    newSprite.scale = ASSET_SCALE;

    if (thing.visible != null) {
        newSprite.visible = thing.visible;
    }

    //if label make it rollover reactive
    newSprite.label = thing.label;
    if (thing.label != null) {

        newSprite.onMouseOver = function () {
            rolledSprite = this;
        };

        newSprite.onMouseOut = function () {
            if (rolledSprite == this)
                rolledSprite = null;
        };
    }
    //if command, make it interactive like an area
    if (thing.command != null) {
        newSprite.command = thing.command;

        newSprite.onMouseReleased = function () {
            if (rolledSprite == this)
                moveToCommand(this.command);
        };
    }

    return newSprite;
}


//removes it only from the client sprite list, not from the data
function removeThing(thingId, thingRoom) {
    if (me.room == thingRoom) {

        //getSprites is a p5 play function, returns an array
        var roomSprites = getSprites();
        //look for the sprite with that id and remove it
        for (var i = 0; i < roomSprites.length; i++) {
            if (roomSprites[i].id == thingId) {
                roomSprites[i].remove();
            }
        }
    }
}

//join from lurk mode
function joinGame() {

    deleteAllSprites();
    hideJoin();

    if (QUICK_LOGIN) {
        //assign random name and avatar and get to the game
        nickName = "user" + floor(random(0, 1000));
        currentAvatar = floor(random(0, emoteSheets.length));
        newGame();
    }
    else {
        screen = "user";
        showUser();
    }

}

function bodyOk() {

    newGame();
}

function keyTyped() {
    if (screen == "avatar") {
        if (keyCode === ENTER || keyCode === RETURN) {
            newGame();
        }
    }
}

function showUser() {
    var e = document.getElementById("user-form");
    if (e != null)
        e.style.display = "block";

    e = document.getElementById("lobby-container");
    if (e != null)
        e.style.display = "block";
}

function hideUser() {
    var e = document.getElementById("user-form");
    if (e != null)
        e.style.display = "none";

    e = document.getElementById("lobby-container");
    if (e != null)
        e.style.display = "none";
}

function showAvatar() {

    var e = document.getElementById("avatar-form");
    if (e != null) {
        e.style.display = "block";
    }

}

//don't show the link while the canvas loads
function showInfo() {

    var e = document.getElementById("info");
    if (e != null) {
        e.style.visibility = "visible";
    }

}

function hideAvatar() {

    var e = document.getElementById("avatar-form");
    if (e != null)
        e.style.display = "none";
}

function showJoin() {
    document.getElementById("join-form").style.display = "block";
}

function hideJoin() {
    document.getElementById("join-form").style.display = "none";
}

//enable the chat input when it's time
function showChat() {
    var e = document.getElementById("talk-form");

    if (e != null)
        e.style.display = "block";
}

function hideChat() {
    var e = document.getElementById("talk-form");
    if (e != null)
        e.style.display = "none";
}

function outOfCanvas() {
    areaLabel = "";
    rolledSprite = null;
}

//disable scroll on phone
function preventBehavior(e) {
    e.preventDefault();
};

document.addEventListener("touchmove", preventBehavior, { passive: false });

// Active
window.addEventListener("focus", function () {
    if (socket != null && me != null)
        socket.emit("focus", { room: me.room });
});

// Inactive
window.addEventListener("blur", function () {
    if (socket != null && me != null)
        socket.emit("blur", { room: me.room });
});

