//check README.md for more information //VS Code intellisense /// /* 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)) { console.log("YES"); players[p.id].destinationX = p.destinationX; players[p.id].destinationY = p.destinationY; } else { console.log("NO"); } } 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 }); });