NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name FlatChat
// @namespace com.dounford.flatmmo.flatChat
// @version 2.3.1
// @description Better chat for FlatMMO
// @author Dounford
// @license MIT
// @match *://flatmmo.com/play.php*
// @grant none
// @require https://openuserjs.org/install/Dounford/FlatMMOPlus.user.js
// @require https://cdnjs.cloudflare.com/ajax/libs/anchorme/2.1.2/anchorme.min.js
// ==/UserScript==
(function() {
'use strict';
//Which css variable corresponds to each color
const messageColors = {
white: "messagesColor",//local message
grey: "messagesColor", //global message
server: "serverMessages",
pink: "milestoneMessages", //server messages
red: "warningMessages", //errors (trade declines, no energy)
lime: "restMessages", //rest message
green: "lvlUpMessages", //level up
cyan: "areaChangeMessages", //Leaving/Entering town
pmReceived: "pmReceivedMessages", //private messages
pmSent: "pmSentMessages",
gold: "pingMessages",
};
const ding = new Audio("https://github.com/Dounford-Felipe/DHM-Idle-Again/raw/refs/heads/main/ding.wav");
const IPSigils = new Set([
'images/ui/basket_egg_sigil.png', 'images/ui/basket_sigil.png', 'images/ui/bat_sigil.png', 'images/ui/bell_sigil.png', 'images/ui/blue_party_hat_sigil.png', 'images/ui/broken_bell_sigil.png', 'images/ui/bronze_event_2_sigil.png', 'images/ui/bronze_event_sigil.png', 'images/ui/bunny_sigil.png', 'images/ui/candy_cane_sigil.png', 'images/ui/carrot_sigil.png', 'images/ui/cat_sigil.png', 'images/ui/chocolate_sigil.png', 'images/ui/dh1_max_sigil.png', 'images/ui/easter_egg_sigil.png', 'images/ui/event_2_sigil.png', 'images/ui/event_sigil.png', 'images/ui/fake_bell_sigil.png', 'images/ui/fancy_bell_sigil.png', 'images/ui/ghost_sigil.png', 'images/ui/gift_sigil.png', 'images/ui/gold_event_2_sigil.png', 'images/ui/gold_event_sigil.png', 'images/ui/green_party_hat_sigil.png', 'images/ui/hatching_chicken_sigil.png', 'images/ui/mad_bunny_sigil.png', 'images/ui/mummy_head_sigil.png', 'images/ui/mummy_sigil.png', 'images/ui/pink_party_hat_sigil.png', 'images/ui/pumpkin_sigil.png', 'images/ui/red_party_hat_sigil.png', 'images/ui/reindeer_sigil.png', 'images/ui/santa_hat_sigil.png', 'images/ui/silver_event_2_sigil.png', 'images/ui/silver_event_sigil.png', 'images/ui/skull_sigil.png', 'images/ui/snowflake_sigil.png', 'images/ui/snowman_sigil.png', 'images/ui/spider_sigil.png', 'images/ui/tree_sigil.png', 'images/ui/white_party_hat_sigil.png', 'images/ui/yellow_party_hat_sigil.png', 'images/ui/zombie_sigil.png'
])
const defaultThemes = {
dark: {
chatBackground: "#191b24",
/*Top bar*/
topBarBackground: "#131c37",
tabsBackground: "#393a5b",
activeTabBackground: "#4357af",
hoverTabBackground: "#D3D3D3",
tabsTextColor: "#e7e7e7",
unreadMessagesBackground: "#000090",
unreadMessagesTextColor: "#e7e7e7",
/*Channels*/
oddMessageBackground: "#191b24",
evenMessageBackground: "#191b24",
messageTimeColor: "#ffffff",
messageSenderUsernameColor: "#ffffff",
regularMessageColor: "#E1E1E1",
serverMessageColor: "#6495ED",
milestoneMessageColor: "#FF1493",
warningMessageColor: "#FF0000",
restMessageColor: "#00FF00",
lvlUpMessageColor: "#008000",
areaChangeMessageColor: "#00FFFF",
pmReceivedMessageColor: "#ffdea1",
pmSentMessageColor: "#78ffb5",
pingBackground: "#3F51B5",
pingTextColor: "#ffffff",
tooltipBorder: "#808080",
tooltipBackground: "#aaaaaa",
tooltipTextColor: "#000000",
/*Bottom bar*/
chatBarBackground: "#131419",
usernameBottomBar: "#C0C0C0",
chatBarTextColor: "#ffffff",
buttonsBackground: "#000090",
buttonsTextColor: "#C0C0C0",
/*Context Menu*/
contextMenuBackground: "#191b24",
contextMenuUsernameColor: "#C0C0C0",
contextMenuButtonBackground: "#000090",
contextMenuWarningButtonBackground: "#ff0000",
contextMenuTextColor: "#ffffff",
/*Misc*/
hyperlinkTextColor: "#00FFFF",
visitedHyperlinkTextColor: "#00FFFF",
},
light: {
chatBackground: "#F5F7FA",
/*Top bar*/
topBarBackground: "#E3E8F0",
tabsBackground: "#D1DAE6",
activeTabBackground: "#4A90E2",
hoverTabBackground: "#B8C7D9",
tabsTextColor: "#2C3E50",
unreadMessagesBackground: "#FF6B6B",
unreadMessagesTextColor: "#FFFFFF",
/*Channels*/
oddMessageBackground: "#F5F7FA",
evenMessageBackground: "#FFFFFF",
messageTimeColor: "#7F8C8D",
messageSenderUsernameColor: "#2C3E50",
regularMessageColor: "#34495E",
serverMessageColor: "#3498DB",
milestoneMessageColor: "#9B59B6",
warningMessageColor: "#E74C3C",
restMessageColor: "#27AE60",
lvlUpMessageColor: "#2ECC71",
areaChangeMessageColor: "#1ABC9C",
pmReceivedMessageColor: "#E67E22",
pmSentMessageColor: "#D35400",
pingBackground: "#F1C40F",
pingTextColor: "#2C3E50",
tooltipBorder: "#BDC3C7",
tooltipBackground: "#ECF0F1",
tooltipTextColor: "#2C3E50",
/*Bottom bar*/
chatBarBackground: "#E3E8F0",
usernameBottomBar: "#7F8C8D",
chatBarTextColor: "#2C3E50",
buttonsBackground: "#4A90E2",
buttonsTextColor: "#FFFFFF",
/*Context Menu*/
contextMenuBackground: "#FFFFFF",
contextMenuUsernameColor: "#34495E",
contextMenuButtonBackground: "#4A90E2",
contextMenuWarningButtonBackground: "#E74C3C",
contextMenuTextColor: "#2C3E50",
/*Misc*/
hyperlinkTextColor: "#2980B9",
visitedHyperlinkTextColor: "#8E44AD",
},
mocha: {
chatBackground: "#1e1e2e",
/*Top bar*/
topBarBackground: "#181825",
tabsBackground: "#1e1e2e",
activeTabBackground: "#45475a",
hoverTabBackground: "#313244",
tabsTextColor: "#cdd6f4",
unreadMessagesBackground: "#313244",
unreadMessagesTextColor: "#cdd6f4",
/*Channels*/
oddMessageBackground: "#1e1e2e",
evenMessageBackground: "#1e1e2e",
messageTimeColor: "#cdd6f4",
messageSenderUsernameColor: "#cdd6f4",
regularMessageColor: "#cdd6f4",
serverMessageColor: "#b4befe",
milestoneMessageColor: "#f5c2e7",
warningMessageColor: "#f38ba8",
restMessageColor: "#a6e3a1",
lvlUpMessageColor: "#94e2d5",
areaChangeMessageColor: "#89dceb",
pmReceivedMessageColor: "#fab387",
pmSentMessageColor: "#f9e2af",
pingBackground: "#313244",
pingTextColor: "#cdd6f4",
tooltipBorder: "#11111b",
tooltipBackground: "#313244",
tooltipTextColor: "#cdd6f4",
/*Bottom bar*/
chatBarBackground: "#11111b",
usernameBottomBar: "#cdd6f4",
chatBarTextColor: "#cdd6f4",
buttonsBackground: "#1e1e2e",
buttonsTextColor: "#c0c0c0",
/*Context Menu*/
contextMenuBackground: "#191b24",
contextMenuUsernameColor: "#cdd6f4",
contextMenuButtonBackground: "#191b24",
contextMenuWarningButtonBackground: "#f38ba8",
contextMenuTextColor: "#cdd6f4",
/*Misc*/
hyperlinkTextColor: "#cba6f7",
visitedHyperlinkTextColor: "#cba6f7"
}
}
const themesText = {
chatBackground: "Chat Background",
/*Top bar*/
topBarBackground: "Top Bar Background",
tabsBackground: "Tabs Background",
activeTabBackground: "Active Tab Background",
hoverTabBackground: "Hover Tab Background",
tabsTextColor: "Tabs Text",
unreadMessagesBackground: "Unread Background",
unreadMessagesTextColor: "Unread Text",
/*Channels*/
oddMessageBackground: "Odd Messages Background",
evenMessageBackground: "Even Messages Background",
messageTimeColor: "Message Time Color (#fff = unset)",
messageSenderUsernameColor: "Sender Color (#fff = unset)",
regularMessageColor: "Regular Message Color",
serverMessageColor: "Server Message",
milestoneMessageColor: "Milestone Message",
warningMessageColor: "Error/Warning Message",
restMessageColor: "Rest Messages",
lvlUpMessageColor: "LVL UP Messages",
areaChangeMessageColor: "Entering/Leaving Town",
pmReceivedMessageColor: "Private Messages Received",
pmSentMessageColor: "Private Messages Sent",
pingBackground: "Ping Messages Background",
pingTextColor: "Ping Messages Text",
tooltipBorder: "!lvl Tooltip Border",
tooltipBackground: "!lvl Tooltip Background",
tooltipTextColor: "!lvl Tooltip Text",
/*Bottom bar*/
chatBarBackground: "Chat Bar Background",
usernameBottomBar: "Username Color",
chatBarTextColor: "Chat Text",
buttonsBackground: "Buttons Background",
buttonsTextColor: "Buttons Text",
/*Context Menu*/
contextMenuBackground: "Context Menu Background",
contextMenuUsernameColor: "Context Menu Username",
contextMenuButtonBackground: "Context Menu Button Background",
contextMenuWarningButtonBackground: "Context Menu Warning Button Background",
contextMenuTextColor: "Context Menu Text",
/*Misc*/
hyperlinkTextColor: "Hyperlink Color",
visitedHyperlinkTextColor: "Visited Hyperlink Color",
};
const chatInset = {
bottomRight: "auto 0 0 auto",
bottomLeft: "auto auto 0 auto",
topRight: "0 0 auto auto",
topLeft: "0 auto auto"
}
const textToNotification = {
nessieTime: {
blocked: " seconds left.",
name: "nessieTime",
image: "https://flatmmo.com/images/npcs/lochness_monster_stand1.png",
title: "TIMER",
text: "0 seconds left",
ticks: 3600,
color: "white"
},
cannonFixed: {
blocked: "has fixed the cannon",
name: "cannonFix",
image: "https://flatmmo.com/images/objects/cannon1_lower.png",
title: "CANNON",
text: "FIXED",
ticks: 900,
color: "white"
},
cannonBroken: {
blocked: "The cannon broke",
name: "cannonFix",
image: "https://flatmmo.com/images/objects/broken_cannon1_lower.png",
title: "CANNON",
text: "BROKEN",
ticks: 900,
color: "white"
},
fireFish: {
blocked: "fires fish",
name: "fishFired",
image: "https://flatmmo.com/images/items/yellow_fish.png",
title: "FISH FIRED",
text: "0/0",
ticks: 900,
color: "white"
},
bondfirePoints: {
blocked: "1 bondfire",
name: "bondfirePoint",
image: "https://flatmmo.com/images/objects/bondfire1_lower.png",
title: "BONDFIRE",
text: "0 points",
ticks: 900,
color: "white"
},
prepareFire: {
blocked: "prepare to light",
name: "ignore",
image: "https://flatmmo.com/images/items/none.png",
title: "",
text: "",
ticks: 0,
color: "white"
},
lightFire: {
blocked: "successfully light",
name: "lightFire",
image: "https://flatmmo.com/images/objects/fire1_lower.png",
title: "FIRE",
text: "0 seconds",
ticks: 600,
color: "white"
},
lowEnergy: {
blocked: "too tired",
name: "lowEnergy",
image: "https://flatmmo.com/images/ui/sleep.png",
title: "TIRED",
text: "0 energy",
ticks: 600,
color: "white"
},
dehydration: {
blocked: "due to dehydration",
name: "dehydration",
image: "https://flatmmo.com/images/items/water_bucket.png",
title: "DEHYDRATION",
text: "-10 hp",
ticks: 600,
color: "white"
},
sipOfWater: {
blocked: "sip of water",
name: "sipOfWater",
image: "https://flatmmo.com/images/items/water_bucket.png",
title: "-1 BUCKET",
text: "",
ticks: 600,
color: "white"
},
orbDig: {
blocked: "start digging",
name: "orbDig",
image: "https://flatmmo.com/images/items/none.png",
title: "",
text: "",
ticks: 0,
color: "white"
},
noRunning: {
blocked: "combat has interrupetd",
name: "noRunning",
image: "https://flatmmo.com/images/worship/run.png",
title: "COMBAT",
text: "No longer Running",
ticks: 600,
color: "white"
},
fastBite: {
blocked: "between bites!",
name: "fastBite",
image: "https://flatmmo.com/images/items/raw_tuna.png",
title: "FAST BITE",
text: "Wait 3 seconds",
ticks: 600,
color: "white"
},
alreadyRunning: {
blocked: "already running",
name: "alreadyRunning",
image: "https://flatmmo.com/images/worship/run.png",
title: "",
text: "",
ticks: 0,
color: "white"
},
lavaFishing: {
blocked: "lava to catch",
name: "lavaFishing",
image: "https://flatmmo.com/images/worship/run.png",
title: "",
text: "",
ticks: 0,
color: "white"
},
}
class flatChatPlugin extends FlatMMOPlusPlugin {
constructor() {
super("flatChat", {
about: {
name: GM_info.script.name,
version: GM_info.script.version,
author: GM_info.script.author,
description: GM_info.script.description
},
config: [
{
id: "ignorePings",
label: "Silence Pings",
type: "boolean",
default: false
},
{
id: "pingVolume",
label: "Ping Volume",
type: "range",
min: 0,
max: 100,
step: 1,
default: 100,
},
{
id: "showTime",
label: "Mssage Received Time",
type: "boolean",
default: true
},
{
id: "showSpam",
label: "Show Spam Messages",
type: "boolean",
default: false
},
{
id: "hideUnwanted",
label: "Mark Ignored Words as spoiler",
type: "boolean",
default: true
},
{
id: "fontSize",
label: "Message Font Size",
type: "number",
min: 0,
max: 10,
step: 0.1,
default: 1.5
},
{
id: "theme",
label: "Theme",
type: "select",
options: [
{value: "light", label: "Light"},
{value: "dark", label: "Dark"},
{value: "mocha", label: "Mocha by Mae"}
],
default: "dark"
},
{
id: "chatWidth",
label: "Chat Width",
type: "range",
min: 0,
max: 100,
step: 1,
default: 50,
},
{
id: "chatHeight",
label: "Chat Height",
type: "number",
step: 5,
default: 190
},
{
id: "chatOpacity",
label: "Chat Opacity (%)",
type: "range",
min: 10,
max: 100,
step: 5,
default: 100,
},
{
id: "maxMessages",
label: "Max Messages Per Channel (0 = unlimited)",
type: "number",
max: 5000,
step: 50,
default: 0
},
{
id: "chatPosition",
label: "Chat Position",
type: "select",
options: [
{value: "detached", label: "Own Window (Detached)"},
{value: "bottomRight", label: "Bottom Right"},
{value: "bottomLeft", label: "Bottom Left"},
{value: "topRight", label: "Top Right"},
{value: "topLeft", label: "Top Left"},
{value: "outside", label: "Outside Canvas"},
{value: "outsideSideTab", label: "Outside Canvas (Side Tabs)"},
],
default: "bottomRight"
},
{
id: "profilePage",
label: "Default Profile",
type: "select",
options: [
{value: "ingame", label: "In-game Profile"},
{value: "page", label: "Profile Page"},
{value: "stats", label: "FlatMMO Stats"},
],
default: "ingame"
},
{
id: "alwaysOnFocus",
label: "Keep chat on focus all the time",
type: "boolean",
default: false
},
{
id: "pingChat",
label: "Copy all ping messages on the ping chat",
type: "boolean",
default: true
},
{
id: "alwaysTabsPM",
label: "Always create tabs for PMs",
type: "boolean",
default: false
},
{
id: "hideCloseBtn",
label: "Hide Close Button",
type: "boolean",
default: false
},
{
id: "defaultPmChat",
label: "Default PM Chat Tab",
type: "select",
options: [
{value: "whisper", label: "Whisper"},
{value: "local", label: "Local"},
{value: "global", label: "Global"},
],
default: "whisper"
},
{
id: "shortcuts",
label: "Shortcuts List (Use the key between [])",
type: "object",
default: {gz: "gratz"},
key: "Shortcut",
value: "Message"
},
{
id: "nicknames",
label: "Nicknames List",
type: "object",
default: {dounbot2: "dounbot", felipewolf: "Liam"},
key: "Username",
value: "Nickname"
},
{
id: "ignoredPlayers",
label: "Players Ignored",
type: "list",
default: []
},
{
id: "ignoredWords",
label: "Blocked Words",
type: "list",
default: []
},
{
id: "watchedPlayers",
label: "Watched Players",
type: "list",
default: []
},
{
id: "watchedWords",
label: "Watched Words",
type: "list",
default: []
},
{
id: "scriptsToLoad",
label: "Scripts to Load",
type: "list",
unique: true,
default: []
},
{
id: "themeEditor",
label: "THEME EDITOR",
panel: "flatChat-ThemeEditor",
type: "panel"
}
]
});
this.fcElement = null;
this.detachedChat = null;
this.defaultChannels = new Set(["channel_local","channel_global","channel_pings","channel_whisper"])
this.channels = {};
this.currentChannel = "channel_local";
//This is for messages received before the chat loads
this.messagesWaiting = [];
//This is for the up and down arrows feature
this.chatHistory = [];
this.historyPosition = -1;
this.ignoreClick = false;
this.lastPM = ""; //Used for /r
this.themes = {};
//This is for the /load feature
this.loadedScripts = new Set();
this.loadedDependencies = new Set();
//Versions after 1.0.8 will always auto update
if(FlatMMOPlus.version < "1.0.8") {
this.loadScript("fmp");
}
}
async loadScript(id) {
try {
if(this.loadedScripts.has(id)) {
this.showWarning(id + " is already loaded", "red");
return false;
}
let script;
await fetch('https://scripts.dounford.tech/scripts/' + id).then(async (response) => {
script = await response.text()
script = JSON.parse(script);
})
for (let dependency in script.dependencies) {
//Don't load the same dependency more than once
if(this.loadedDependencies.has(dependency)) {
break;
}
this.createScript(script.dependencies[dependency]);
this.loadedDependencies.add(dependency);
}
this.createScript(script.code);
this.loadedScripts.add(id);
return true;
} catch(err) {
console.error(id + " was not loaded");
return false;
}
}
createScript(code) {
const script = document.createElement("script");
script.textContent = code;
document.head.appendChild(script);
}
onLogin() {
this.removeOriginalChat();
this.addStyle();
this.addUI();
this.loadChannels();
this.switchChannel("local", false);
this.messagesWaiting.forEach((message)=>{
if(message.username === "") {
this.showServerMessage(message);
return
}
this.showMessage(message);
});
this.defineCommands();
ding.volume = this.config.pingVolume / 100;
this.addThemeEditor();
this.changeThemeEditor();
this.showWarning("Welcome to flatmmo.com", "orange");
this.showWarning(document.querySelectorAll("#chat span")[1].innerHTML, "white");
this.showWarning(`<span><strong style="color:cyan">FYI: </strong> Use the /help command to see information on available chat commands.</span>`, "white");
this.configurePosition();
this.config.scriptsToLoad.forEach(async script => {
await this.loadScript(script)
})
}
onChat(data) {
if (data.yell) {
data.channel = "channel_global";
} else {
data.channel = "channel_local";
}
if (data.username === "" && data.color === "white") {
data.color = "server"
};
if (data.color === "pink" && data.message.startsWith("[PM")) {
let match = data.message.match(/\[PM (?:to|from) (.*?)\](.*)/);
if(match) {
if(data.message.startsWith("[PM to")) {
data.color = "pmSent";
} else {
data.color = "pmReceived";
}
data.username = match[1].trim().replaceAll(" ", "_");
data.message = match[2].trim();
if(this.config.ignoredPlayers.includes(data.username)) {
return
}
if(this.config["alwaysTabsPM"] && !this.channels["private_" + data.username]) {
this.newChannel(data.username, true)
}
data.channel = this.channels["private_" + data.username] ? "private_" + data.username : "channel_" + this.config.defaultPmChat;
this.lastPM = data.username;
} else {
console.warn("There was something wrong with this pm:", data.message)
}
};
if(FlatMMOPlus.loggedIn) {
if(data.username === "") {
this.showServerMessage(data);
return
}
this.showMessage(data);
} else {
this.messagesWaiting.push(data);
}
}
onConfigsChanged() {
this.changedConfigs.forEach(config => {
switch (config) {
case "pingVolume": {
ding.volume = this.config.pingVolume / 100;
} break;
case "fontSize": {
this.fcElement.querySelector("#flatChatChannels").style.setProperty("--fc-messageFontSize", this.config.fontSize + "rem");
} break;
case "theme": {
this.fcElement.classList.forEach(className => {
if(className.startsWith("flatChatTheme-")) {
this.fcElement.classList.remove(className);
}
})
this.fcElement.classList.add("flatChatTheme-" + this.config.theme);
} break;
case "pingChat": {
if(this.config.pingChat) {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']").classList.remove("displaynone")
} else {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']").classList.add("displaynone")
}
} break;
case "defaultPmChat": {
if(this.config.defaultPmChat === "whisper") {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']").classList.remove("displaynone")
} else {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']").classList.add("displaynone")
}
} break;
case "chatWidth": {
this.fcElement.style.setProperty("--fc-chatWidth", this.config.chatWidth + "%");
} break;
case "chatHeight": {
this.fcElement.style.setProperty("--fc-chatHeight", this.config.chatHeight + "px");
} break;
case "chatPosition": {
this.configurePosition()
} break;
case "hideCloseBtn": {
const closeBtn = this.fcElement.querySelector("#flatChatCloseBtn");
closeBtn.style.display = this.config.hideCloseBtn ? "none" : "";
} break;
case "chatOpacity": {
this.fcElement.style.setProperty("--fc-chatOpacity", this.config.chatOpacity / 100);
if(this.config.chatOpacity < 100) {
this.fcElement.classList.add("flatChatDimmed");
} else {
this.fcElement.classList.remove("flatChatDimmed");
}
} break;
}
})
}
saveConfig() {
localStorage.setItem("flatmmoplus.flatChat.config", JSON.stringify(this.config));
}
removeOriginalChat() {
add_to_chat = function(){};
refresh_chat_div = function(){};
paint_chat = function(){};
document.getElementById("chat").style.display = "none";
document.querySelector(".chat-input").style.display = "none"
window.FlatMMOPlus.registerCustomChatCommand(["clear","clean"], (command, data='') => {
this.fcElement.querySelector(`#flatChatChannels [data-channel=${FlatMMOPlus.plugins.flatChat.currentChannel}]`).innerHTML = "";
}, `Clears all messages in chat.`);
}
addStyle() {
document.querySelector(".td-ui").style.height = "auto"
const style = document.createElement("style");
style.innerHTML = `#game>table>tbody>tr:nth-child(2) td {
position: relative;
}
#flatChat {
position: absolute;
width: var(--fc-chatWidth, 50%);
background-color: var(--fc-chatBackground);
border-radius: 0 0 3% 3%;
user-select: text;
}
#flatChatExpandBtn{
width: 2rem;
cursor: pointer;
}
#flatChatTopBar {
background-color: var(--fc-topBarBackground);
display: flex;
flex-direction: row;
justify-content: space-between;
color: var(--fc-tabsTextColor);
fill: var(--fc-tabsTextColor);
}
#flatChatTabs {
display: flex;
flex-direction: row;
font-size: var(--fc-tabFontSize, 1rem);
}
.flatChatTab {
background-color: var(--fc-tabsBackground);
position: relative;
border-radius: 15% 15% 0 0;
margin-right: 1px;
padding: 0.375rem;
&:hover {
background-color: var(--fc-hoverTabBackground);
cursor: pointer;
}
}
.flatChatTabActive {
background-color: var(--fc-activeTabBackground);
}
.flatChatUnread {
position: absolute;
right: 10%;
top: 0;
background-color: var(--fc-unreadMessagesBackground);
color: var(--fc-unreadMessagesTextColor);
padding: 0 0.3rem;
border-radius: 0.5rem;
}
.flatChatTopBarCollapsed {
transform: rotate(180deg);
}
#flatChatMainArea[closed] {
display: none;
}
#flatChatChannels {
height: var(--fc-chatHeight, 150px);
font-size: var(--fc-messageFontSize, 1rem);
}
.flatChatChannel {
width: 100%;
height: 100%;
overflow-y: auto;
scrollbar-width: thin;
}
/*messages*/
.flatChatChannel div {
overflow-wrap: anywhere;
color: var(--fc-regularMessageColor);
span {
margin-left: 5px;
}
a {
text-decoration: none;
color: var(--fc-hyperlinkTextColor) !important;
&:visited {
color: var(--fc-visitedHyperlinkTextColor) !important;
}
}
}
.flatChatChannel div:nth-child(even) {
background-color: var(--fc-evenMessageBackground, var(--fc-chatBackground));
}
.flatChatChannel div:nth-child(odd) {
background-color: var(--fc-oddMessageBackground, var(--fc-chatBackground));
}
.fc-timestamp {
color: var(--fc-messageTimeColor);
}
.fc-playerName {
color: var(--fc-messageSenderUsernameColor);
}
.fc-serverMessages {
color: var(--fc-serverMessageColor) !important;
}
.fc-milestoneMessages {
color: var(--fc-milestoneMessageColor) !important;
}
.fc-warningMessages {
color: var(--fc-warningMessageColor) !important;
}
.fc-restMessages {
color: var(--fc-restMessageColor) !important;
}
.fc-lvlUpMessages {
color: var(--fc-lvlUpMessageColor) !important;
}
.fc-areaChangeMessages {
color: var(--fc-areaChangeMessageColor) !important;
}
.fc-pmReceivedMessages {
color: var(--fc-pmReceivedMessageColor) !important;
}
.fc-pmSentMessages {
color: var(--fc-pmSentMessageColor) !important;
}
.fc-pingMessages {
background-color: var(--fc-pingBackground) !important;
color: var(--fc-pingTextColor) !important;
}
#flatChatBottomBar {
display: flex;
}
#flatChatInputDiv {
flex: auto;
display: flex;
align-items: center;
border-radius: 0 0 3% 3%;
background-color: var(--fc-chatBarBackground);
color: var(--fc-chatBarTextColor);
}
#flatChatInput {
flex: auto;
border: 0;
padding: 0;
margin: 5px;
font-size: 18px;
background-color: transparent;
color: var(--fc-chatBarTextColor);
outline: transparent;
&::placeholder {
color: var(--fc-usernameBottomBar);
}
&::-webkit-search-cancel-button {
display: none;
}
}
.flatChatBtn {
background-color: var(--fc-buttonsBackground) !important;
color: var(--fc-buttonsTextColor) !important;
border: 0 !important;
padding: 5px;
height: 100%;
display: flex;
align-self: center;
align-items: center;
margin-left: 3px;
}
.chatSigil {
width: var(--fc-messageFontSize, 1rem);
vertical-align: bottom;
margin-right: 5px;
}
.chatName {
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.flatChatHidden {
background-color: black;
color: transparent;
a {
color: transparent;
}
}
.flatChatSideTabs {
display: flex;
#flatChatTopBar {
flex-direction: column;
}
#flatChatTabs {
flex-direction: column;
}
#flatChatMainArea {
width: var(--fc-chatWidth, 50%);
}
}
.flatChatPopup {
border-radius: 0;
#flatChatChannels {
height: calc(100vh - 86px) !important;
}
}
.flatChatLevelTooltip {
background-color: var(--fc-tooltipBorder, gray) !important;
div {
background-color: var(--fc-tooltipBackground, gray) !important;
color: var(--fc-tooltipTextColor, black) !important;
}
}
#flatChatContextMenu {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 5px;
position: absolute;
padding: 5px;
background-color: var(--fc-contextMenuBackground) !important;
border-radius: 5%;
bottom: 0;
}
#flatChatContextMenu button {
width: 80%;
color: var(--fc-contextMenuTextColor) !important;
cursor: pointer;
font-size: var(--fc-messageFontSize);
&:hover {
filter: brightness(0.8);
}
}
#flatChatContextUsername {
color: var(--fc-contextMenuUsernameColor);
cursor: text;
font-size: var(--fc-messageFontSize);
}
.flatChatContextBtn {
background-color: var(--fc-contextMenuButtonBackground) !important;
}
.flatChatContextWarningBtn {
background-color: var(--fc-contextMenuWarningButtonBackground) !important;
}
#flatChatCopyUsername {
visibility: hidden;
position: absolute;
top: 0;
padding: 3px;
border-radius: 5px;
background-color: var(--fc-contextMenuBackground);
color: var(--fc-contextMenuTextColor);
font-size: var(--fc-messageFontSize);
}
#flatChat.flatChatDimmed {
opacity: var(--fc-chatOpacity, 1);
transition: opacity 0.2s ease;
}
#flatChat.flatChatDimmed:hover,
#flatChat.flatChatDimmed:focus-within {
opacity: 1 !important;
}
#flatChatResizeHandle {
height: 6px;
cursor: ns-resize;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.2s ease;
background-color: var(--fc-topBarBackground);
}
#flatChatResizeHandle:hover {
opacity: 1;
}
#flatChatResizeHandle::after {
content: "ยทยทยท";
font-size: 10px;
color: var(--fc-tabsTextColor, #aaa);
letter-spacing: 2px;
}`
document.head.append(style);
//Get saved themes
if(localStorage.getItem("flatChat2-themes")) {
this.themes = JSON.parse(localStorage.getItem("flatChat2-themes"));
}
//Default themes can't be removed
for (let theme in defaultThemes) {
if (!this.themes.hasOwnProperty(theme)) {
this.themes[theme] = defaultThemes[theme];
}
}
for (let theme in this.themes) {
this.addTheme(theme)
this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== theme)
this.opts.config[6].options.push({value: theme, label: this.toTitleCase(theme)})
}
}
addUI() {
this.fcElement = document.createElement("div");
this.fcElement.innerHTML = `<div id="flatChatTopBar">
<div id="flatChatTabs"></div>
<div id="flatChatExpandBtn" style="fill:var(--fc-tabsTextColor, white)">
<svg id="Layer_1" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="
height: 30px;
">
<path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9 c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3 z"></path>
</svg>
</div>
</div>
<div id="flatChatMainArea">
<div id="flatChatResizeHandle"></div>
<div id="flatChatChannels" style="height: var(--fc-chatHeight, 190px);"></div>
<div id="flatChatBottomBar">
<button type="text" id="flatChatCloseBtn" class="flatChatBtn">
<img src="https://cdn.idle-pixel.com/images/x.png">
</button>
<div id="flatChatInputDiv">
<input type="search" id="flatChatInput" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" placeholder="">
<button type="text" id="flatChatSendBtn" class="flatChatBtn">Send</button>
</div>
<button type="button" id="flatChatAutoScrollBtn" class="flatChatBtn">
<img src="https://cdn.idle-pixel.com/images/x.png" id="flatChatAutoScrollfalse" alt="" class="displaynone">
<img src="https://cdn.idle-pixel.com/images/check.png" id="flatChatAutoScrolltrue" alt="">
</button>
</div>
<div id="flatChatContextMenu" style="visibility:hidden">
<span id="flatChatContextUsername" data-user="" class="flatChatContextUsername"></span>
<button data-action="message" class="flatChatContextBtn">Message</button>
<button data-action="tabMessage" class="flatChatContextBtn">Message (Tab)</button>
<button data-action="profile" class="flatChatContextBtn">Profile</button>
<button data-action="trade" class="flatChatContextBtn">Trade</button>
<button data-action="stalk" class="flatChatContextWarningBtn">Stalk</button>
<button data-action="ignore" class="flatChatContextWarningBtn">Ignore</button>
</div>
<div id="flatChatCopyUsername" style="visibility:hidden">USERNAME COPIED</div>`
this.fcElement.id = "flatChat";
this.fcElement.style.inset = "auto 0 0 auto";
this.fcElement.style.setProperty("--fc-messageFontSize", this.config.fontSize + "rem")
this.fcElement.style.setProperty("--fc-chatWidth", this.config.chatWidth + "%")
this.fcElement.style.setProperty("--fc-chatHeight", this.config.chatHeight + "px")
this.fcElement.style.setProperty("--fc-chatOpacity", this.config.chatOpacity / 100)
let currentTheme = this.config.theme
if(this.themes[currentTheme]) {
this.fcElement.classList = "flatChat flatChatTheme-" + this.config.theme;
} else {
this.config.theme = "dark";
this.fcElement.classList = "flatChat flatChatTheme-dark";
this.saveConfig();
}
if(this.config.chatOpacity < 100) {
this.fcElement.classList.add("flatChatDimmed");
}
document.getElementById("chat").insertAdjacentElement("beforebegin", this.fcElement);
this.fcElement.querySelector("#flatChatExpandBtn").addEventListener("click", ()=>{
this.fcElement.querySelector("#flatChatExpandBtn").classList.toggle("flatChatTopBarCollapsed");
this.fcElement.querySelector("#flatChatMainArea").toggleAttribute("closed");
})
this.fcElement.querySelector("#flatChatInput").placeholder = Globals.local_username;
const closeBtn = this.fcElement.querySelector("#flatChatCloseBtn");
closeBtn.style.display = this.config.hideCloseBtn ? "none" : "";
closeBtn.onclick = () => this.closeChannel();
const sendBtn = this.fcElement.querySelector("#flatChatSendBtn");
sendBtn.onclick = () => this.sendMessage();
this.fcElement.querySelector("#flatChatAutoScrollBtn").onclick = () => this.toggleAutoScroll();
// Resize handle drag logic
const resizeHandle = this.fcElement.querySelector("#flatChatResizeHandle");
resizeHandle.addEventListener("mousedown", (e) => {
e.preventDefault();
const startY = e.clientY;
const startHeight = this.fcElement.querySelector("#flatChatChannels").offsetHeight;
const onMouseMove = (e) => {
const delta = startY - e.clientY;
const newHeight = Math.max(50, startHeight + delta);
this.fcElement.style.setProperty("--fc-chatHeight", newHeight + "px");
};
const onMouseUp = () => {
this.fcElement.removeEventListener("mousemove", onMouseMove);
this.fcElement.removeEventListener("mouseup", onMouseUp);
const finalHeight = this.fcElement.querySelector("#flatChatChannels").offsetHeight;
this.config.chatHeight = finalHeight;
this.saveConfig();
};
this.fcElement.addEventListener("mousemove", onMouseMove);
this.fcElement.addEventListener("mouseup", onMouseUp);
});
const channelsDiv = this.fcElement.querySelector("#flatChatChannels");
channelsDiv.onwheel = (event)=>{
this.scrollChannel(event);
}
//Shows custom context menu on right click
channelsDiv.addEventListener("contextmenu", (e) => {
const sender = e.target.closest("[data-sender]");
if (sender) {
e.preventDefault()
const username = sender.dataset.sender;
const contestUser = this.fcElement.querySelector("#flatChatContextUsername");
contestUser.setAttribute("data-user", username);
contestUser.innerText = username;
const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
contextMenu.style.visibility = "visible";
}
});
this.fcElement.querySelector("#flatChatContextMenu").addEventListener("click", (e) => {this.contextMenu(e)})
this.addDocumentEvents(document);
this.fcElement.querySelector("#flatChatInput").addEventListener("blur", function(){
setTimeout(function() {
if(FlatMMOPlus.plugins.flatChat.config["alwaysOnFocus"] && document.activeElement.tagName !== "INPUT" && document.activeElement.contentEditable !== "true"){
this.fcElement.querySelector("#flatChatInput").focus({
preventScroll: true
})
}
}, 1)
})
}
addDocumentEvents(documentElement) {
//Context menu should close if you click outside
documentElement.addEventListener("click", (e) => {
const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
if (!contextMenu.contains(e.target)) {
contextMenu.style.visibility = "hidden";
}
});
documentElement.addEventListener("keydown", (e) => {
if (e.key === "F4") {
e.preventDefault();
this.ignoreClick = !this.ignoreClick;
if(this.ignoreClick) {
this.fcElement.style.opacity = "0.2";
this.fcElement.style.pointerEvents = "none";
} else {
this.fcElement.style.opacity = "1";
this.fcElement.style.pointerEvents = "unset";
}
}
//Switch back and forth between channels with tab and shift+tab
if (e.key === "Tab" && e.target.closest('#flatChat')) {
e.preventDefault();
let sibling = null;
if(e.shiftKey) {
sibling = this.fcElement.querySelector(".flatChatTabActive").previousElementSibling || this.fcElement.querySelector("#flatChatTabs").lastElementChild;
} else {
sibling = this.fcElement.querySelector(".flatChatTabActive").nextElementSibling || this.fcElement.querySelector("#flatChatTabs").firstElementChild;
}
const channel = sibling.dataset.channel
const match = channel.match(/(.*?)_(.*)/);
if (match) {
const type = match[1];
const channel = match[2];
this.switchChannel(channel, type === "private")
}
return;
}
const input = this.fcElement.querySelector("#flatChatInput");
if(documentElement.activeElement === input) {
if(e.key === "Enter") {
if(has_modal_open()) return;
this.sendMessage();
}
//Navigate between sent messages
if (e.key === "ArrowUp" && input.selectionStart === 0) {
if(this.historyPosition + 1 === this.chatHistory.length) {return}
input.value = this.chatHistory[++this.historyPosition] || "";
input.selectionStart = input.value.length
} else if (e.key === "ArrowDown" && input.selectionStart === input.value.length) {
if(this.historyPosition - 1 < -1) {return}
input.value = this.chatHistory[--this.historyPosition] || "";
input.selectionStart = input.value.length
}
} else if(e.key === "Enter") {
if(has_modal_open()) return;
input.focus({
preventScroll: true
})
}
}, true)
documentElement.querySelector("#flatChatChannels").addEventListener("click", async (e) => {
const sender = e.target.closest("[data-sender]");
if (sender) {
const username = sender.dataset.sender;
//It will try to copy with navigator.clipboard, but it doesn't work if the window is not focused
try {
await documentElement.defaultView.navigator.clipboard.writeText(username.replaceAll("_", " "));
} catch (error) {
}
const copyMessage = this.fcElement.querySelector("#flatChatCopyUsername");
copyMessage.style.visibility = "visible"
setTimeout(()=>{copyMessage.style.visibility = "hidden"}, 1000);
}
});
}
configurePosition() {
if(this.config.chatPosition === "detached") {
this.detach();
return;
} else if(this.detachedChat) {
this.reattach();
}
if(this.config.chatPosition === "outsideSideTab") {
flatChat.classList.add("flatChatSideTabs");
flatChat.style.position = "unset";
} else {
flatChat.classList.remove("flatChatSideTabs");
if(this.config.chatPosition === "outside") {
flatChat.style.position = "unset";
} else {
flatChat.style.position = "absolute";
flatChat.style.inset = chatInset[this.config.chatPosition];
}
}
}
detach() {
if(this.detachedChat && !this.detachedChat.closed) {
this.detachedChat.focus();
return;
}
this.detachedChat = window.open(
"",
"chatPopup",
"width=600,height=800,resizable=yes"
);
this.fcElement.classList.add("flatChatPopup");
this.detachedChat.document.head.insertAdjacentHTML("beforeend","<title>Flat Chat</title>")
document.head.querySelectorAll("style").forEach(style => {
this.detachedChat.document.head.appendChild(style.cloneNode(true));
})
document.head.querySelectorAll("link").forEach(link => {
this.detachedChat.document.head.appendChild(link.cloneNode(true));
})
this.detachedChat.document.body.appendChild(this.fcElement);
this.addDocumentEvents(this.detachedChat.document);
this.detachedChat.onbeforeunload = () => {
this.config.chatPosition = "bottomRight";
this.saveConfig();
this.reattach();
}
window.onbeforeunload = () => {
if(this.detachedChat && !this.detachedChat.closed) {
this.detachedChat.close();
this.config.chatPosition = "detached";
this.saveConfig();
}
}
}
reattach() {
this.detachedChat?.close();
this.detachedChat = null;
document.getElementById("chat").insertAdjacentElement("beforebegin", this.fcElement)
this.fcElement.classList.remove("flatChatPopup");
}
addTheme(theme) {
if(!this.themes[theme]) {return};
let themeSyle;
if(document.querySelector("#fc-themeStyle-" + theme)) {
themeSyle = document.querySelector("#fc-themeStyle-" + theme)
themeSyle.innerHTML = "";
} else {
themeSyle = document.createElement("style");
themeSyle.id = "fc-themeStyle-" + theme;
document.head.appendChild(themeSyle);
}
themeSyle.innerHTML = `.flatChatTheme-${theme} {`
for (let option in this.themes[theme]) {
themeSyle.innerHTML += `\n\t--fc-${option}: ${this.themes[theme][option]};`
}
themeSyle.innerHTML += "\n}"
}
addThemeEditor() {
FlatMMOPlus.addPanel("flatChat-ThemeEditor", "Theme Editor", ()=>{
let content = `
<style>
#ui-panel-flatChat-ThemeEditor-content {
display: grid;
grid-template-columns: auto auto;
font-size: larger;
height: 550px;
overflow-y: scroll;
* {
border-top: solid 1px black;
margin-bottom: 5px;
padding: 5px;
}
select {
grid-column: 1 / span 2;
text-align: center;
font-size: large;
}
input[type="color"] {
width: 50px;
height: 50px;
}
}
</style>
<select id="flatChat-ThemeEditor-theme" onchange="FlatMMOPlus.plugins.flatChat.changeThemeEditor()">`
FlatMMOPlus.plugins.flatChat.opts.config[6].options.forEach(theme=>{
content += `<option value="${theme.value}">${theme.label}</option>`
})
content += "</select>"
for (let option in themesText) {
content += `<label for="fc-${option}-editor">${themesText[option]}</label>
<input type="color" id="fc-${option}-editor">`
}
content += `<div style="display: grid;grid-template-columns: auto auto;grid-column: 1 / span 2;">
<input type="text" id="fc-themeName-editor" placeholder="Theme Name" style="grid-column: 1 / span 2;">
<button type="button" onclick="FlatMMOPlus.plugins.flatChat.saveTheme()">Save</button>
<button type="button" onclick="FlatMMOPlus.plugins.flatChat.deleteTheme()">Delete Theme</button>
<input type="text" id="fc-import-editor" placeholder="Import/Export" style="grid-column: 1 / span 2;">
<button type="button" onclick="FlatMMOPlus.plugins.flatChat.importTheme()">Import</button>
<button type="button" onclick="FlatMMOPlus.plugins.flatChat.exportTheme()">Export</button>
</div>
`
return content;
})
}
changeThemeEditor() {
const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
document.getElementById("fc-themeName-editor").value = document.querySelector(`#flatChat-ThemeEditor-theme option[value=${theme}]`).innerText;
for (let option in this.themes[theme]) {
document.getElementById("fc-" + option + "-editor").value = this.themes[theme][option]
}
}
saveTheme() {
const theme = document.getElementById("fc-themeName-editor").value;
if(!theme) {return};
const themeName = this.toCamelCase(theme);
//Make sure it doesn't duplicate
if(this.themes[themeName]) {
this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== themeName)
document.querySelector(`#flatChat-ThemeEditor-theme option[value=${themeName}]`).remove();
} else {
this.themes[themeName] = {};
}
for (let option in this.themes.dark) {
const value = document.getElementById("fc-" + option + "-editor").value;
this.themes[themeName][option] = value;
if((option === "messageTimeColor" || option === "messageSenderUsernameColor") && value == "#ffffff") {
this.themes[themeName][option] = "unset";
}
}
this.opts.config[6].options.push({value: themeName, label: theme})
document.getElementById("flatChat-ThemeEditor-theme").innerHTML += `<option value="${themeName}">${theme}</option>`
document.getElementById("flatChat-ThemeEditor-theme").value = themeName;
//Change to new theme
this.config.theme = themeName;
const flatChat = document.getElementById("flatChat");
flatChat.classList = "flatChat flatChatTheme-" + this.config.theme;
this.saveConfig();
localStorage.setItem("flatChat2-themes", JSON.stringify(this.themes));
this.addTheme(themeName);
}
deleteTheme() {
const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
console.log(theme)
//Return if it doesn't exist
if(!this.themes[theme]) {return};
//Default themes can't be removed, they will be go back to default instead
if(theme in defaultThemes) {
this.themes[theme] = structuredClone(defaultThemes[theme]);
this.changeThemeEditor();
this.saveTheme();
return;
};
//remove from themes
delete this.themes[theme];
//remove from fm+ config
this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== theme);
//Remove the option on theme editor
document.querySelector(`#flatChat-ThemeEditor-theme option[value=${theme}]`).remove();
//save themes on localstorage
localStorage.setItem("flatChat2-themes", JSON.stringify(this.themes));
//If there is a theme style (it should exist) remove it
if (document.getElementById("fc-themeStyle-" + theme)) {
document.getElementById("fc-themeStyle-" + theme).remove();
}
this.config.theme = "dark";
const flatChat = document.getElementById("flatChat");
flatChat.classList = "flatChat flatChatTheme-dark";
}
importTheme() {
const themeString = document.getElementById("fc-import-editor").value;
try {
const themeObj = JSON.parse(themeString);
if(!themeObj.name) {return};
document.getElementById("fc-themeName-editor").value = this.toTitleCase(themeObj.name);
for (let option in themeObj.theme) {
document.getElementById("fc-" + option + "-editor").value = themeObj.theme[option]
}
this.saveTheme()
} catch (error) {
console.error("What you are trying to import is not a valid theme");
}
}
exportTheme() {
const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
const themeString = JSON.stringify({name: theme, "theme": this.themes[theme]});
document.getElementById("fc-import-editor").value = themeString;
}
defineCommands() {
window.FlatMMOPlus.registerCustomChatCommand("ohelp", (command, data='') => {
Globals.websocket.send(`CHAT=/help`);
}, `Shows the original vanilla /help`);
window.FlatMMOPlus.registerCustomChatCommand(["players","who"], (command, data='') => {
if (this.currentChannel === "channel_global") {
Globals.websocket.send('CHAT=/players');
} else if (this.currentChannel === "channel_local") {
this.showWarning(Object.keys(players).join(", "), "white");
} else if (this.currentChannel.startsWith("private_")) {
this.showWarning(`${this.currentChannel.slice(8)} & ${Globals.local_username}`, "white");
}
}, `Show all players in room or global.`);
window.FlatMMOPlus.registerCustomChatCommand("detach", (command, data='') => {
if(this.config.chatPosition === "detached") {
this.config.chatPosition = "bottomRight";
this.saveConfig();
this.reattach();
} else {
this.config.chatPosition = "detached";
this.saveConfig();
this.detach();
}
}, "Detach/Reattach the chat from the window");
//Pm will only open a tab if a message is not specified
window.FlatMMOPlus.registerCustomChatCommand("pm", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
const space = data.indexOf(" ");
if (space <= 0) {
this.newChannel(data, true);
this.switchChannel(data, true);
} else {
const receiver = data.substring(0, space);
const message = data.substring(space + 1);
Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
}
}, `Send a private message to someone.<br><strong>Usage:</strong> /pm [username] [message]`);
window.FlatMMOPlus.registerCustomChatCommand("r", (command, data='') => {
if (this.lastPM === "") {
return
}
if (data === "") {
this.newChannel(this.lastPM, true);
this.switchChannel(this.lastPM, true);
return;
}
const receiver = this.lastPM;
const message = data;
Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
}, `Auto respond the last pm.`);
//pm* will always open a new tab
window.FlatMMOPlus.registerCustomChatCommand("pm*", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
const space = data.indexOf(" ");
if (space <= 0) {
this.newChannel(data, true);
this.switchChannel(data, true);
} else {
const receiver = data.substring(0, space);
const message = data.substring(space + 1);
this.newChannel(receiver, true);
this.switchChannel(receiver, true);
Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
}
}, `Opens a private channel in a new tab.<br><b>Usage:</b>/pm* [username] <message (optional)>`);
window.FlatMMOPlus.registerCustomChatCommand("page", (command, data='') => {
if (data === "") {
window.open(`https://flatstats.ravenwoodsoftware.org/player/${Globals.local_username}`,"_blank")
return;
}
window.open(`https://flatstats.ravenwoodsoftware.org/player/${data}`,"_blank");
}, `Opens the Player Profile Page in a new tab.<br><b>Usage:</b>/page [username]`);
window.FlatMMOPlus.registerCustomChatCommand("profile", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + data.replaceAll("_", " "));
}, `Opens the player profile.<br><b>Usage:</b>/profile [username]`);
window.FlatMMOPlus.registerCustomChatCommand("trade", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
Globals.websocket.send("SEND_TRADE_REQUEST=" + data.replaceAll("_", " "));
}, `Send a trade request if the player is in the same map.<br><b>Usage:</b>/trade [username]`);
window.FlatMMOPlus.registerCustomChatCommand("leave", (command, data='') => {
if (data === "") {
this.closeChannel();
return;
};
if(data in this.channels) {
this.closeChannel(data);
};
}, `Closes a chat tab.<br><b>Usage:</b>/leave <channel (optional)>`);
window.FlatMMOPlus.registerCustomChatCommand("ignore", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
this.watchIgnorePlayersWords("ignoredPlayers", data)
}, `Ignores all messages from user.<br><b>Usage:</b>/ignore [username] (use _ for names with space)`);
window.FlatMMOPlus.registerCustomChatCommand("unignore", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
this.config.ignoredPlayers = this.config.ignoredPlayers.filter(player => player !== data);
this.saveConfig();
this.showWarning(data + " removed from Ignored List");
}, `Removes someone from the Ignored List.<br><b>Usage:</b>/unignore [username] (use _ for names with space)`);
window.FlatMMOPlus.registerCustomChatCommand("watch", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
this.watchIgnorePlayersWords("watchedPlayers",data);
}, `Highlights all messages from user.<br><b>Usage:</b>/watch [username] (use _ for names with space)`);
window.FlatMMOPlus.registerCustomChatCommand("unwatch", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the username", "red");
return;
}
this.config.watchedPlayers = this.config.watchedPlayers.filter(player => player !== data);
this.saveConfig();
this.showWarning(data + " removed from Watched List");
}, `Removes someone from the Watched List.<br><b>Usage:</b>/unwatch [username] (use _ for names with space)`);
window.FlatMMOPlus.registerCustomChatCommand("ignoreword", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify one term", "red");
return;
}
this.watchIgnorePlayersWords("ignoredWords",data);
}, `Ignores all messages that contains this term.<br><b>Usage:</b>/ignoreword [term]`);
window.FlatMMOPlus.registerCustomChatCommand("unignoreword", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the word", "red");
return;
}
this.config.ignoredWords = this.config.ignoredWords.filter(word => word !== data);
this.saveConfig();
this.showWarning(data + " removed from Ignored List");
}, `Removes a term from the Ignored List.<br><b>Usage:</b>/unignoreword [term]`);
window.FlatMMOPlus.registerCustomChatCommand("watchword", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify at least one word", "red");
return;
}
this.watchIgnorePlayersWords("watchedWords",data);
}, `Ping you every time this word is sent.<br><b>Usage:</b>/watchword [word] (use _ for words with space)`);
window.FlatMMOPlus.registerCustomChatCommand("unwatchword", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the word", "red");
return;
}
this.config.watchedWords = this.config.watchedWords.filter(word => word !== data);
this.saveConfig();
this.showWarning(data + " removed from Watched List");
}, `Removes a term from the Watched List.<br><b>Usage:</b>/unwatchword [term]`);
window.FlatMMOPlus.registerCustomChatCommand("tick", (command, data='') => {
this.showWarning(`The current action takes ${progress_bar_target + 1} ticks (${(progress_bar_target + 1) / 2} seconds)`);
}, `Shows the time needed to complete the current action`);
window.FlatMMOPlus.registerCustomChatCommand("scripts", async (command, data='') => {
let scriptList;
await fetch('https://scripts.dounford.tech/scripts').then(async (response) => {
scriptList = await response.text()
scriptList = scriptList.slice(0,-1).split(";");
})
scriptList.forEach(item => this.showWarning(item, "cyan"));
this.showWarning("Use /load [name]", "cyan");
}, "List all scripts that can be loaded with /load [name]");
window.FlatMMOPlus.registerCustomChatCommand("load", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the script you want to load", "red");
return;
}
//Only adds to auto load if it exists
if (this.loadScript(data)) {
this.config.scriptsToLoad.push(data);
this.saveConfig();
}
}, "Loads a script");
window.FlatMMOPlus.registerCustomChatCommand("unload", (command, data='') => {
if (data === "") {
this.showWarning("You need to specify the script you want to unload", "red");
return;
}
this.config.scriptsToLoad = this.config.scriptsToLoad.filter(script=> script !== data);
this.saveConfig();
this.showWarning(data + " will not auto load anymore");
}, "Remove a script from auto load (it doesn't unload, you need to refresh the page)");
}
newChannel(channel, isPrivate) {
const channelName = (isPrivate ? "private_" : "channel_") + channel;
if(this.channels[channelName]) {return};
this.channels[channelName] = {
name: channel,
isPrivate,
autoScroll: true,
unreadMessages: 0,
inputText: "",
lastMessage: "",
lastSender: "",
}
const tabBtn = document.createElement("div");
tabBtn.className = "flatChatTab";
tabBtn.setAttribute("data-channel", channelName);
tabBtn.innerHTML = `<span class="flatChatTabName">${channel.replaceAll("_", " ")}</span>
<div class="flatChatUnread"></div>`
this.fcElement.querySelector("#flatChatTabs").appendChild(tabBtn);
tabBtn.onclick = () => {
this.switchChannel(channel, isPrivate);
}
this.fcElement.querySelector("#flatChatChannels").insertAdjacentHTML("beforeend",`<div class="flatChatChannel" data-channel="${channelName}" style="display:none"></div>`);
if(isPrivate) {
const privateChannel = this.fcElement.querySelector(`#flatChatChannels [data-channel=${channelName}]`);
privateChannel.insertAdjacentHTML("beforeend",`
<div class="fc-lvlUpMessages"><strong>${this.getDateStr()}</strong><span>New chat with ${channel}</span></div>`)
this.fcElement.querySelectorAll(`[data-channel=${this.config.defaultPmChat}] .fc-pmReceivedMessages,.fc-pmSentMessages`).forEach(el => {
const match = el.innerText.match(/(?:>|<) (?:\[[0-9][0-9]:[0-9][0-9]])?(.*?):/)
if(match && match[1] === channel) {
privateChannel.appendChild(el);
}
})
}
this.saveChannels();
}
closeChannel(channel) {
const oldChannel = channel || this.currentChannel;
if(this.defaultChannels.has(oldChannel)) {
return;
}
this.switchChannel("local", false);
delete this.channels[oldChannel];
this.fcElement.querySelector(`#flatChatTabs [data-channel=${oldChannel}]`).remove();
this.fcElement.querySelector(`#flatChatChannels [data-channel=${oldChannel}]`).remove();
this.saveChannels();
}
saveChannels() {
const channels = Object.keys(this.channels);
localStorage.setItem("flatChat-channels",JSON.stringify(channels))
}
loadChannels() {
const channels = JSON.parse(localStorage.getItem("flatChat-channels") || '["channel_local","channel_global","channel_pings","channel_whisper"]');
//It should not be possible to remove default channels, but just in case
this.defaultChannels.forEach(channel => {
if(!channels.includes(channel)) {
channels.push(channel)
}
})
channels.forEach(channel => {
const match = channel.match(/(.*?)_(.*)/);
if (match) {
const type = match[1];
const name = match[2];
this.newChannel(name, type === "private");
}
})
if(this.config.pingChat) {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']")?.classList.remove("displaynone")
} else {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']")?.classList.add("displaynone")
}
if(this.config.defaultPmChat === "whisper") {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']")?.classList.remove("displaynone")
} else {
this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']")?.classList.add("displaynone")
}
this.fcElement.querySelector(`.flatChatChannel[data-channel=channel_pings]`).addEventListener("click", async (e) => {
const message = e.target.closest("[data-mid]");
if (message) {
const id = message.dataset.mid;
const channelName = message.dataset.channelname;
const isPrivate = channelName.slice(0,7) === "private";
const channel = channelName.slice(8);
this.switchChannel(channel, isPrivate)
this.fcElement.querySelector(`[data-omid="${id}"]`)?.scrollIntoView();
}
});
}
switchChannel(channel, isPrivate) {
const input = this.fcElement.querySelector("#flatChatInput");
//remove old
this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}]`).classList.remove("flatChatTabActive");
this.fcElement.querySelector(`.flatChatChannel[data-channel=${this.currentChannel}]`).style.display = "none";
this.channels[this.currentChannel].inputText = input.value;
//update current chat
const newChannel = (isPrivate ? "private_" : "channel_") + channel
//Makes sure the channel exists
if (!this.channels[newChannel]) {
newChannel = "channel_local"
};
this.currentChannel = newChannel;
//Removes unreadMessages number
this.channels[this.currentChannel].unreadMessages = 0;
const unreadDiv = this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}] .flatChatUnread`);
unreadDiv.style.visibility = "hidden";
//Change auto scroll icon
const autoScroll = this.channels[this.currentChannel].autoScroll;
this.fcElement.querySelector("#flatChatAutoScroll" + autoScroll).className = "";
this.fcElement.querySelector("#flatChatAutoScroll" + !autoScroll).className = "displaynone";
//shows the new chat
this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}]`).classList.add("flatChatTabActive");
const messageArea = this.fcElement.querySelector(`.flatChatChannel[data-channel=${this.currentChannel}]`);
messageArea.style.display = "block";
input.value = this.channels[this.currentChannel].inputText;
//Auto scrolls if needed
if (this.channels[this.currentChannel].autoScroll) {
messageArea.scrollTop = messageArea.scrollHeight;
}
}
scrollChannel(e) {
//Zoom in/out chat messages
if (e.shiftKey) {
let size = this.config.fontSize || 1
if (e.deltaY < 0) {
size = parseFloat((size + 0.1).toFixed(1));
} else {
size = parseFloat((size - 0.1).toFixed(1));
}
this.fcElement.style.setProperty("--fc-messageFontSize", size + "rem")
this.config.fontSize = size;
this.saveConfig();
return;
}
}
toggleAutoScroll() {
this.channels[this.currentChannel].autoScroll = !this.channels[this.currentChannel].autoScroll;
this.fcElement.querySelector("#flatChatAutoScrolltrue").classList.toggle("displaynone");
this.fcElement.querySelector("#flatChatAutoScrollfalse").classList.toggle("displaynone");
}
watchIgnorePlayersWords(type, word) {
//type can be watchedWords, ignoredWords, watchedPlayers, ignoredPlayers
//Warning first because of the ignoredWord list
this.showWarning(word + " added to " + type + " list");
this.config[type].push(word)
this.saveConfig();
}
contextMenu(e) {
const data = e.target.closest("[data-action]");
if (data) {
const username = this.fcElement.querySelector("#flatChatContextUsername").dataset.user;
const action = data.dataset.action;
const input = this.fcElement.querySelector("#flatChatInput");
switch (action) {
case "message": {
input.value = "/pm " + username + " ";
input.focus();
} break;
case "tabMessage": {
this.newChannel(username, true);
this.switchChannel(username, true);
} break;
case "profile": {
switch(this.config.profilePage) {
case "ingame": {
Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + username.replaceAll("_", " "));
} break;
case "page": {
window.open(`https://flatmmo.com/profile/?user=${username.replaceAll("_", " ")}`, '_blank');
} break;
case "stats": {
window.open(`https://flatstats.ravenwoodsoftware.org/player/${username.replaceAll("_", " ")}`, '_blank');
} break;
}
} break;
case "trade": {
Globals.websocket.send("SEND_TRADE_REQUEST=" + username.replaceAll("_", " "));
} break;
case "stalk": {
this.watchIgnorePlayersWords("watchedPlayers", username);
} break;
case "ignore": {
this.watchIgnorePlayersWords("ignoredPlayers", username);
} break;
}
const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
contextMenu.style.visibility = "hidden";
}
}
updateUnread(channel) {
if(channel !== this.currentChannel) {
const unreadMessages = ++this.channels[channel].unreadMessages;
const unreadDiv = this.fcElement.querySelector(`.flatChatTab[data-channel=${channel}] .flatChatUnread`);
unreadDiv.innerText = unreadMessages
unreadDiv.style.visibility = "visible";
}
}
//Server messages don't require some of the checks, most is the same as user messages, but I think it is better to have them as different functions now
showServerMessage(data, html = false) {
// data = {
// username: "",
// tag: "none",
// sigil: "none",
// color: "white",
// message: "oi gente",
// yell: false,
// channel: "channel_local"
// channel: "private_dounford"
// usernameColor: "red"
// }
//If for some reason the channel does not exist
if(!data.channel in this.channels) {
data.channel = "channel_local"
}
let message = data.message;
for (const not in textToNotification) {
if(message.includes(textToNotification[not].blocked)) {
this.updateNotification(message, not);
this.showNotification(not);
return;
}
}
let messageContainer = document.createElement('div');
if (messageColors[data.color]) {
messageContainer.className += " fc-" + messageColors[data.color]
} else {
//In case a color that doesn't has a variable yet is used
messageContainer.style.color = data.color || "white";
}
//Message reiceived time [12:43]
if (this.config.showTime) {
const timeStrong = document.createElement("strong");
timeStrong.className = "fc-timestamp"
timeStrong.innerHTML = this.getDateStr();
messageContainer.appendChild(timeStrong);
}
//For now it will never use html, but maybe Smitty changes something in the future
const messageSpan = document.createElement('span');
if (html) {
messageSpan.innerHTML = message;
} else {
messageSpan.innerText = message;
}
//I find it hard to believe that the server will ever send links, but just in case...
messageSpan.innerHTML = anchorme({
input: messageSpan.innerHTML,
options: {
attributes: {
target: "_blank",
class: "detected"
}
},
});
//If the message contains any ignored word it will ignore the message or mark as spoiler
if(this.config.ignoredWords.some(word => message.includes(word))) {
if(this.config["hideUnwanted"]) {
messageSpan.style.cursor = "pointer";
messageSpan.classList.add("flatChatHidden");
messageSpan.onclick = ()=>{
messageSpan.classList.toggle("flatChatHidden")
}
} else {
return;
}
}
messageContainer.appendChild(messageSpan);
const messageArea = this.fcElement.querySelector(`#flatChatChannels [data-channel=${data.channel}]`);
messageArea.appendChild(messageContainer);
//Trim old messages if over the limit
if(this.config.maxMessages > 0) {
console.log(this.config.maxMessages)
while(messageArea.children.length > this.config.maxMessages) {
messageArea.removeChild(messageArea.firstElementChild);
}
}
//Update the unread messages number if needed
this.updateUnread(data.channel);
if (this.channels[data.channel].autoScroll) {
messageArea.scrollTop = messageArea.scrollHeight;
}
}
showMessage(data, html = false) {
// data = {
// username: "dounford",
// tag: "none",
// sigil: "none",
// color: "white",
// message: "oi gente",
// yell: false,
// channel: "channel_local"
// channel: "private_dounford"
// usernameColor: "red"
// }
//If for some reason the channel does not exist
if(!data.channel in this.channels) {
data.channel = "channel_local"
}
let message = data.message;
//This should prevent some spam to show
const lastSender = this.channels[data.channel]?.lastSender || "";
const lastMessage = this.channels[data.channel]?.lastMessage || "";
if (lastSender === data.username && lastMessage === data.message && !this.getConfig("showSpam")) {
return;
}
//If the message sender is blocked the message will be ignored
if(this.config.ignoredPlayers.includes(data.username)) {
return;
}
let messageContainer = document.createElement('div');
//Ping if any watched word is present or if the message contains the username
//Doesn't ping if it is yours message
const isMention = message.includes("@" + Globals.local_username);
const hasWatchedWord = this.config.watchedWords.some(word => message.includes(word));
let isPing = false;
if (data.username !== Globals.local_username && (isMention || hasWatchedWord)) {
isPing = this.config.pingChat;
messageContainer.className = "fc-pingMessages";
//Ignore ping is just about the sound
if(!this.config.ignorePings) {
ding.play();
}
}
if (data.color && data.color !== "white" && data.color !== "grey") {
if (messageColors[data.color]) {
messageContainer.className += " fc-" + messageColors[data.color]
} else {
//In case a color that doesn't has a variable yet is used
messageContainer.style.color = data.color;
}
}
//Message reiceived time [12:43]
if (this.config.showTime) {
const timeStrong = document.createElement("strong");
timeStrong.className = "fc-timestamp"
timeStrong.innerHTML = this.getDateStr();
messageContainer.appendChild(timeStrong);
}
if (data.tag && data.tag !== "none") {
let tag = document.createElement("span");
tag.innerText = data.tag === "investor-plus" ? "INVESTOR" : data.tag === "moderator" ? "MOD" : data.tag.toUpperCase();
tag.classList.add("chat-tag-" + data.tag);
messageContainer.appendChild(tag);
}
if (data.sigil && data.sigil !== "none") {
const sigilImg = new Image();
sigilImg.className = "chatSigil"
if (IPSigils.has(data.sigil)) {
//I'm using IP sigils for now, FMMO sigils have terrible resolution
sigilImg.src = "https://cdn.idle-pixel.com/images/" + data.sigil.slice(10);
} else {
sigilImg.src = "https://flatmmo.com/" + data.sigil;
}
messageContainer.appendChild(sigilImg);
}
let isPm = data.channel.startsWith("private_");
if (data.username) {
const senderStrong = document.createElement("strong");
let username = data.username.replaceAll("_", " ");
if(isPm && data.color === "pmSent") {
username = Globals.local_username;
}
username = this.config.nicknames[username] || username;
senderStrong.innerText = username + ":";
senderStrong.className = "fc-playerName";
senderStrong.setAttribute("data-sender", data.username.replaceAll(" ", "_"));
messageContainer.appendChild(senderStrong);
if(this.config.watchedPlayers.includes(data.username) && !isPm) {
messageContainer.className = "fc-pingMessages";
}
} else {
for (const not in textToNotification) {
if(message.includes(textToNotification[not].blocked)) {
this.updateNotification(message, not);
this.showNotification(not);
return;
}
}
}
const messageSpan = document.createElement('span');
const match = message.match(/My (.*?) level is: (.*?) \((.*?) xp\)/);
if(match) {
const [_, skill, level, xp] = match;
this.createLevelTooltip(messageSpan, this.toTitleCase(skill), level, xp);
} else if (html) {
messageSpan.innerHTML = message;
} else {
messageSpan.innerText = message;
}
messageSpan.innerHTML = anchorme({
input: messageSpan.innerHTML,
options: {
attributes: {
target: "_blank",
class: "detected"
}
},
});
//If the message contains any ignored word it will ignore the message or mark as spoiler
if(this.config.ignoredWords.some(word => message.includes(word))) {
if(this.config["hideUnwanted"]) {
messageSpan.style.cursor = "pointer";
messageSpan.classList.add("flatChatHidden");
messageSpan.onclick = ()=>{
messageSpan.classList.toggle("flatChatHidden")
}
} else {
return;
}
}
messageContainer.appendChild(messageSpan);
const messageArea = this.fcElement.querySelector(`#flatChatChannels [data-channel=${data.channel}]`);
messageArea.appendChild(messageContainer);
//Clones the message on the ping channel
if(isPing) {
const pingElement = messageContainer.cloneNode(true);
pingElement.className = ""; //Every message is a ping, there is no reason to highlight them
const mid = Date.now();
messageContainer.setAttribute("data-omid", mid);
pingElement.setAttribute("data-mid", mid);
pingElement.setAttribute("data-channelname", data.channel);
const pingsArea = this.fcElement.querySelector(`.flatChatChannel[data-channel=channel_pings]`);
pingsArea.appendChild(pingElement);
if(this.config.maxMessages > 0) {
console.log(this.config.maxMessages)
while(pingsArea.children.length > this.config.maxMessages) {
pingsArea.removeChild(pingsArea.firstElementChild);
}
}
//Update the unread messages number if needed
if(this.currentChannel !== "channel_pings") {
this.updateUnread("channel_pings");
}
}
//Trim old messages if over the limit
if(this.config.maxMessages > 0) {
while(messageArea.children.length > this.config.maxMessages) {
messageArea.removeChild(messageArea.firstElementChild);
}
}
if(data.channel !== this.currentChannel) {
//Update the unread messages number if needed
this.updateUnread(data.channel);
}
if (this.channels[data.channel].autoScroll) {
messageArea.scrollTop = messageArea.scrollHeight;
}
this.channels[data.channel].lastSender = data.username;
this.channels[data.channel].lastMessage = data.message;
}
createLevelTooltip(element, skill, level, xp) {
const ixp = parseInt(xp.replaceAll(",",""));
level = parseInt(level);
let nextLvl = "";
if(skill !== "Global" && level !== 100) {
const value = format_number(FlatMMOPlus.level[level + 1] - ixp);
nextLvl = `<div>XP to Level Up: ${value}</div>`
}
element.innerHTML = `<span class="flatChatLevelSpan">Lvl ${level} ${skill}</span>
<div class="tooltiptext flatChatLevelTooltip">
<div>${skill}</div>
<div>Level: ${level}</div>
<div>XP: ${xp}</div>
${nextLvl}
</div>`;
element.classList.add("tooltip");
}
updateNotification(message, blocked) {
switch (blocked) {
case "nessieTime": {
textToNotification[blocked].text = message.slice(0,-1);
} break;
case "fireFish": {
const match = message.match(/\((.*)\)/);
if (match) {
textToNotification[blocked].text = match[1];
} else {
textToNotification[blocked].text = "0/0";
}
} break;
case "bondfirePoints": {
const match = message.match(/\((.*) /);
if (match) {
textToNotification[blocked].text = match[1] + " points";
} else {
textToNotification[blocked].text = "0 points";
}
} break;
case "lightFire": {
const match = message.match(/last (.*) /)
if (match) {
textToNotification[blocked].text = match[1] + " seconds";
} else {
textToNotification[blocked].text = "0 seconds";
}
} break;
}
}
showNotification(blocked) {
const not = textToNotification[blocked];
FlatMMOPlus.addNotification(not.name, not.image, not.title, not.text, not.ticks, not.color);
}
showWarning(message, color = "aquamarine") {
const data = {
username: "",
tag: "none",
sigil: "none",
color: color,
message: message,
yell: false,
channel: this.currentChannel
}
this.showMessage(data, true);
}
sendMessage() {
const input = this.fcElement.querySelector("#flatChatInput");
let message = input.value.trim();
if (!message) {return};
//[here] can't be modified, it will always use the current_map id
message = message.replace("[here]", current_map);
//This replaces all [shortcuts]
Object.keys(this.config.shortcuts).forEach(shortcut => message = message.replaceAll(`[${shortcut}]`, this.config.shortcuts[shortcut]));
input.value = "";
this.channels[this.currentChannel].inputText = "";
if(message !== this.chatHistory[0]) {
this.chatHistory.unshift(message);
}
this.historyPosition = -1;
if(message.startsWith("/")) {
const space = message.indexOf(" ");
let command;
let data;
if (space <= 0) {
command = message.substring(1);
data = "";
} else {
command = message.substring(1, space);
data = message.substring(space + 1);
}
if (window.FlatMMOPlus.handleCustomChatCommand(command, data)) {
return
} else {
Globals.websocket.send('CHAT=' + message);
return;
}
}
const maxlength = this.currentChannel.startsWith("channel") ? 95 : 85;
const messageArray = this.divideStringByLength(message, maxlength);
const isPM = message.startsWith("/pm");
for (let i = 0; i < messageArray.length; i++) {
if(this.currentChannel === "channel_whisper" || (isPM && i > 0)) {
FlatMMOPlus.customChatCommands.r("r", messageArray[i])
} else if(this.currentChannel === "channel_local") {
Globals.websocket.send('CHAT=' + messageArray[i]);
} else if(this.currentChannel === "channel_global") {
Globals.websocket.send('CHAT=/yell ' + messageArray[i]);
} else if (this.currentChannel.startsWith("private_")) {
const username = this.currentChannel.slice(8);
Globals.websocket.send(`CHAT=/pm ${username} ${messageArray[i]}`);
}
}
}
//Utilities functions
getDateStr(timestamp) {
const date = timestamp ? new Date(timestamp) : new Date();
const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
const min = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
const dataStr = '[' + hour + ':' + min + ']'
return dataStr;
}
toCamelCase(str) {
if (!str || typeof str !== 'string') {
return '';
}
const words = str.split(/[\s_-]+/);
const camelCaseWords = words.map((word, index) => {
if (index === 0) {
return word.toLowerCase();
} else {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
});
return camelCaseWords.join('');
}
toTitleCase(str) {
const result = str.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1)
}
divideStringByLength(str, l, delimiterChat = " "){
const strs = [];
while(str.length > l){
let pos = str.substring(0, l).lastIndexOf(delimiterChat);
pos = pos <= 0 ? l : pos;
strs.push(str.substring(0, pos));
let i = str.indexOf(delimiterChat, pos)+1;
if(i < pos || i > pos+l)
i = pos;
str = str.substring(i);
}
strs.push(str);
return strs;
}
}
const plugin = new flatChatPlugin();
FlatMMOPlus.registerPlugin(plugin);
})();