You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2585 lines
78 KiB
JavaScript

//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)) {
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 });
});