//The 'V' is for 'VORE' but you can pretend it's for Vue.js if you really want. //Options for vchat var vchat_opts = { crush_messages: 3, //How many messages back should we try to combine if crushing is on pingThisOften: 10000, //ms pingDropsAllowed: 2, cookiePrefix: "vst-" //If you're another server, you can change this if you want. }; var DARKMODE_COLORS = { buttonBgColor: "#40628a", buttonTextColor: "#FFFFFF", windowBgColor: "#272727", highlightColor: "#009900", tabTextColor: "#FFFFFF", tabBackgroundColor: "#272727" }; var LIGHTMODE_COLORS = { buttonBgColor: "none", buttonTextColor: "#000000", windowBgColor: "none", highlightColor: "#007700", tabTextColor: "#000000", tabBackgroundColor: "none" }; /*********** * * Setup Methods * ************/ var set_storage = set_cookie; var get_storage = get_cookie; var domparser = new DOMParser(); //Upgrade to LS if (storageAvailable('localStorage')) { set_storage = set_localstorage; get_storage = get_localstorage; } //State-tracking variables var vchat_state = { ready: false, //Userinfo as reported by byond byond_ip: null, byond_cid: null, byond_ckey: null, //Ping status lastPingAttempt: 0, lastPingReply: 0, missedPings: 0, latency: 0, reconnecting: false } function start_vchat() { //Instantiate Vue.js start_vue(); //Inform byond we're done vchat_state.ready = true; push_Topic('done_loading'); //I'll do my own winsets doWinset("htmloutput", {"is-visible": true}); doWinset("oldoutput", {"is-visible": false}); doWinset("chatloadlabel", {"is-visible": false}); //Commence the pingening send_ping(); setInterval(send_ping, vchat_opts.pingThisOften); } //Loads vue for chat usage var vueapp; function start_vue() { vueapp = new Vue({ el: '#app', data: { messages: [], //List o messages from byond tabs: [ //Our tabs {name: "Main", classes: ["vc_showall"], immutable: true, active: true} ], always_show: ["vc_looc", "vc_system"], //Classes to always display on every tab unread_messages: {}, //Message categories that haven't been looked at since we got one of them editing: false, //If we're in settings edit mode paused: false, //Autoscrolling latency: 0, //Not necessarily network latency, since the game server has to align the responses into ticks ext_styles: "", //Styles for chat downloaded files is_admin: false, //Settings inverted: false, //Dark mode crushing: true, //Combine similar messages showingnum: 200, //How many messages to show animated: true, //Small CSS animations for new messages fontsize: "zoom_normal", //Font size nudging //The table to map game css classes to our vchat classes type_table: [ { matches: ".say, .emote", becomes: "vc_localchat", pretty: "Local Chat", required: false, admin: false }, { matches: ".alert, .syndradio, .centradio, .airadio, .entradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .supradio, .srvradio, .expradio, .radio, .deptradio, .newscaster", becomes: "vc_radio", pretty: "Radio Comms", required: false, admin: false }, { matches: ".notice, .adminnotice, .info, .sinister, .cult", becomes: "vc_info", pretty: "Notices", required: false, admin: false }, { matches: ".critical, .danger, .userdanger, .warning, .italics", becomes: "vc_warnings", pretty: "Warnings", required: false, admin: false }, { matches: ".deadsay", becomes: "vc_deadchat", pretty: "Deadchat", required: false, admin: false }, { matches: ".ooc:not(.looc)", becomes: "vc_globalooc", pretty: "Global OOC", required: false, admin: false }, { matches: ".pm", becomes: "vc_adminpm", pretty: "Admin PMs", required: false, admin: false }, { matches: ".admin_channel", becomes: "vc_adminchat", pretty: "Admin Chat", required: false, admin: true }, { matches: ".mod_channel", becomes: "vc_modchat", pretty: "Mod Chat", required: false, admin: true }, { matches: ".event_channel", becomes: "vc_eventchat", pretty: "Event Chat", required: false, admin: true }, { matches: ".ooc.looc, .ooc .looc", //Dumb game becomes: "vc_looc", pretty: "Local OOC", required: true }, { matches: ".boldannounce", becomes: "vc_system", pretty: "System Messages", required: true } ], }, created: function() { /*Dog mode setTimeout(function(){ document.body.className += " woof"; },5000); */ /* Stress test var varthis = this; setInterval( function() { if(varthis.messages.length > 10000) { return; } var stringymessages = JSON.stringify(varthis.messages); var unstringy = JSON.parse(stringymessages); unstringy.forEach( function(message) { message.id = (varthis.messages.length + 1); varthis.messages.push(message); }); varthis.internal_message("Now have " + varthis.messages.length + " messages in array."); }, 10000); */ }, mounted: function() { //Load our settings this.load_settings(); var xhr = new XMLHttpRequest(); xhr.open('GET', 'ss13styles.css'); xhr.onreadystatechange = (function() { this.ext_styles = xhr.responseText; }).bind(this); xhr.send(); }, updated: function() { if(!this.editing && !this.paused) { window.scrollTo(0,document.getElementById("messagebox").scrollHeight); } }, watch: { //Save the inverted setting to LS inverted: function (newSetting) { set_storage("darkmode",newSetting); if(newSetting) { //Special treatment for
which is outside Vue's scope and has custom css document.body.classList.add("inverted"); switch_ui_mode(DARKMODE_COLORS); } else { document.body.classList.remove("inverted"); switch_ui_mode(LIGHTMODE_COLORS); } }, crushing: function (newSetting) { set_storage("crushing",newSetting); }, animated: function (newSetting) { set_storage("animated",newSetting); }, fontsize: function (newSetting) { set_storage("fontsize",newSetting); }, showingnum: function (newSetting, oldSetting) { if(!isFinite(newSetting)) { this.showingnum = oldSetting; return; } newSetting = Math.floor(newSetting); if(newSetting <= 50) { this.showingnum = 50; } else if(newSetting > 2000) { this.showingnum = 2000; } set_storage("showingnum",this.showingnum); } }, computed: { //Which tab is active? active_tab: function() { //Had to polyfill this stupid .find since IE doesn't have EC6 let tab = this.tabs.find( function(tab) { return tab.active; }); return tab; }, //Which classes does the active tab need? active_classes: function() { let classarray = this.active_tab.classes; let classtext = classarray.toString(); //Convert to a string let classproper = classtext.replace(/,/g," "); if(this.inverted) classproper += " inverted"; return classproper; //Swap commas for spaces }, //What color does the latency pip get? ping_classes: function() { if(this.latency === 0) { return "grey"; } else if(this.latency < 0 ) {return "red"; } else if(this.latency <= 200) { return "green"; } else if(this.latency <= 400) { return "yellow"; } else { return "red"; } }, shown_messages: function() { if(this.messages.length <= this.showingnum) { return this.messages; } else { return this.messages.slice(-1*this.showingnum); } } }, methods: { //Load the chat settings load_settings: function() { this.inverted = get_storage("darkmode", false); this.crushing = get_storage("crushing", true); this.showingnum = get_storage("showingnum", 200); this.animated = get_storage("animated", true); this.fontsize = get_storage("fontsize", 'zoom_normal'); }, //Change to another tab switchtab: function(tab) { if(tab == this.active_tab) return; this.active_tab.active = false; tab.active = true; tab.classes.forEach( function(cls) { this.unread_messages[cls] = 0; }, this); }, //Toggle edit mode editmode: function() { this.editing = !this.editing; }, //Toggle autoscroll pause: function() { this.paused = !this.paused; }, //Create a new tab (stupid lack of classes in ES5...) newtab: function() { this.tabs.push({ name: "New Tab", classes: this.always_show, immutable: false, active: false }); this.switchtab(this.tabs[this.tabs.length - 1]); }, //Rename an existing tab renametab: function() { if(this.active_tab.immutable) { return; } var tabtorename = this.active_tab; var newname = window.prompt("Type the desired tab name:", tabtorename.name); if(newname === null || newname === "" || tabtorename === null) { return; } tabtorename.name = newname; }, //Delete the currently active tab deltab: function(tab) { if(!tab) { tab = this.active_tab; } if(tab.immutable) { return; } this.switchtab(this.tabs[0]); this.tabs.splice(this.tabs.indexOf(tab), 1); }, movetab: function(tab, shift) { if(!tab || tab.immutable) { return; } var at = this.tabs.indexOf(tab); var to = at + shift; this.tabs.splice(to, 0, this.tabs.splice(at, 1)[0]); }, tab_unread_count: function(tab) { var unreads = 0; var thisum = this.unread_messages; tab.classes.find( function(cls){ if(thisum[cls]) { unreads += thisum[cls]; } }); return unreads; }, tab_unread_classes: function(tab) { var unreads = false; var thisum = this.unread_messages; tab.classes.find( function(cls){ if(thisum[cls]) { unreads = true; return true; } }); return { red: unreads, grey: !unreads}; }, //Push a new message into our array add_message: function(message) { //IE doesn't support the 'class' syntactic sugar so we're left making our own object. let newmessage = { time: message.time, category: "error", content: message.message, repeats: 1 }; //Get a category newmessage.category = this.get_category(newmessage.content); if(!this.active_tab.classes.some(function(cls) { return (cls == newmessage.category || cls == "vc_showall"); })) { if (isNaN(this.unread_messages[newmessage.category])) { this.unread_messages[newmessage.category] = 0; } this.unread_messages[newmessage.category] += 1; } //Try to crush it with one of the last few if(this.crushing) { let crushwith = this.messages.slice(-(vchat_opts.crush_messages)); for (let i = crushwith.length - 1; i >= 0; i--) { let oldmessage = crushwith[i]; if(oldmessage.content == newmessage.content) { newmessage.repeats += oldmessage.repeats; this.messages.splice(this.messages.indexOf(oldmessage), 1); } } } //Append to vue's messages newmessage.id = (this.messages.length + 1); this.messages.push(newmessage); }, //Push an internally generated message into our array internal_message: function(message) { let newmessage = { time: this.messages.length ? this.messages.slice(-1).time+1 : 0, category: "vc_system", content: "[VChat Internal] " + message + "" }; newmessage.id = (this.messages.length + 1); this.messages.push(newmessage); }, on_mouseup: function(event) { // Focus map window on mouseup so hotkeys work. Exception for if they highlighted text or clicked an input. let ele = event.target; let textSelected = ('getSelection' in window) && window.getSelection().isCollapsed === false; if (!textSelected && !(ele && (ele.tagName === 'INPUT' || ele.tagName === 'TEXTAREA'))) { focusMapWindow(); // Okay focusing map window appears to prevent click event from being fired. So lets do it ourselves. event.preventDefault(); event.target.click(); } }, click_message: function(event) { let ele = event.target; if(ele.tagName === "A") { event.stopPropagation(); event.preventDefault ? event.preventDefault() : (event.returnValue = false); //The second one is the weird IE method. var href = ele.getAttribute('href'); // Gets actual href without transformation into fully qualified URL if (href[0] == '?' || (href.length >= 8 && href.substring(0,8) == "byond://")) { window.location = href; //Internal byond link } else { //It's an external link window.location = "byond://?action=openLink&link="+encodeURIComponent(href); } } }, //Derive a vchat category based on css classes get_category: function(message) { if(!vchat_state.ready) { push_Topic('not_ready'); return; } let doc = domparser.parseFromString(message, 'text/html'); let evaluating = doc.querySelector('span'); let category = "nomatch"; //What we use if the classes aren't anything we know. if(!evaluating) return category; this.type_table.find( function(type) { if(evaluating.msMatchesSelector(type.matches)) { category = type.becomes; return true; } }); return category; }, save_chatlog: function() { var textToSave = ""; this.messages.forEach( function(message) { textToSave += message.content; if(message.repeats > 1) { textToSave += "(x"+message.repeats+")"; } textToSave += "