// ==UserScript==
// @name TagPro Komacro
// @description Macro's // edit in-game // map-specific // no-script compatible // key combinations
// @author Ko
// @version 5.1
// @match *://*.koalabeast.com/*
// @match *://*.jukejuice.com/*
// @match *://*.newcompte.fr/*
// @require https://greasyfork.org/scripts/371240/code/TagPro%20Userscript%20Library.js
// @supportURL https://www.reddit.com/message/compose/?to=Wilcooo
// @icon https://raw.githubusercontent.com/wilcooo/TagPro-ScriptResources/master/MacroKey.png
// @website https://redd.it/9a7u4w
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect koalabeast.com
// @namespace https://greasyfork.org/users/152992
// ==/UserScript==
/* TODO
Disable input when composing a message
*/
(function(){
var chars={8:"⌫",9:"↹",13:"↩",16:"⇧",17:"✲",18:"⎇",19:"❚❚",20:"⇪",27:"⎋",32:"␣",33:"⇞",34:"⇟",35:"⇲",36:"⇱",37:"◀",38:"▲",39:"▶",40:"▼",45:"⎀",46:"⌦",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",91:navigator.platform.toLowerCase().includes("mac")?"⌘":"⊞",93:"≣",96:"0️⃣",97:"1️⃣",98:"2️⃣",99:"3️⃣",100:"4️⃣",101:"5️⃣",102:"6️⃣",103:"7️⃣",104:"8️⃣",105:"9️⃣",106:"✖️",107:"➕",109:"➖",110:"▣",111:"➗",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"⇭",145:"⇳",182:"💻",183:"🖩",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"};
// =====STYLE SECTION=====
// Create our own stylesheet to define the styles in:
var style = document.createElement('style');
document.head.appendChild(style);
style.id = 'komacro-style';
var styleSheet = style.sheet;
// The 'type' button next to a macro
styleSheet.insertRule(` .komacro-type { width: 100%; padding: 0; height: 37px; font-weight: bolder; color: black; }`);
styleSheet.insertRule(` .komacro-type:active, .komacro-type:focus, .komacro-type:hover { background: darkseagreen; color: black; animation: unset; }`);
// Put text on the type button
styleSheet.insertRule(` .komacro-type[data-type=all]::after { content: "all"; }`);
styleSheet.insertRule(` .komacro-type[data-type=team]::after { content: "team"; }`);
styleSheet.insertRule(` .komacro-type[data-type=group]::after { content: "group"; }`);
styleSheet.insertRule(` .komacro-type[data-type=mod]::after { content: "mod"; }`);
styleSheet.insertRule(` @keyframes teamcolors { from {background: #FFB5BD;} to {background: #CFCFFF;} }`);
styleSheet.insertRule(` .komacro-type[data-type=all] { background: white; }`);
styleSheet.insertRule(` .komacro-type[data-type=team] { background: linear-gradient(135deg, #FF7F7F, #7F7FFF); }`);
styleSheet.insertRule(` .komacro-type[data-type=group] { background: #E7E700; }`);
styleSheet.insertRule(` .komacro-type[data-type=mod] { background: #00B900; }`);
styleSheet.insertRule(` .komacro-combo::placeholder { font-size: small; font-style: italic; }`);
styleSheet.insertRule(` .komacro-del:hover, .komacro-del:active, .komacro-del:focus { background: darkred; }`);
styleSheet.insertRule(` #Komacro_wrapper [draggable=true] { cursor: grab; transition: padding .3s, background .3s, border .3s, box-shadow .3s; }`);
styleSheet.insertRule(` .komacro-dragging { cursor: grabbing !important; background: #CDDC39; border-radius: 3px; border: 1px solid #827717; box-shadow: 0 3px #827717; padding: 10px 0; }`);
// =====SOME FUNCTIONS=====
// Get all existing maps through the TagPro JSON api
var maps = new Promise(function(resolve,reject){
// Imediately resolve if we have cached the maps
if (GM_getValue('maps')) resolve(GM_getValue('maps'));
// .. But still always fetch the new maps, for the next time
GM_xmlhttpRequest({
method: "GET",
url: "http://" + location.hostname + "/maps.json",
onload: function(response) {
var maps = JSON.parse(response.responseText);
resolve(maps);
GM_setValue('maps',maps);
},
onerror: reject
});
});
// Update the maps in the selection boxes
// f.e. after changing the "distinguish mirrored" option
function update_maps() {
maps.then(function(maps){
[...settings.macrolist.getElementsByTagName("select")].forEach( function(select) {
select.innerHTML = '<option value="null">all maps</option>' });
for (var category of [maps.rotation, maps.retired]) {
for (var mapobj of category) {
if (!show_mirrored && mapobj.key.endsWith("_Mirrored")) continue;
let option = document.createElement("option");
option.innerText = mapobj.name;
option.value = mapobj.key;
[...settings.macrolist.getElementsByTagName("select")].forEach(function(select){
select.add(option.cloneNode(true));
select.value = select.parentElement.parentElement.macro.map || null;
});
}
}
});
}
// Save all macros in Tamper/Grease monkey
function save_macros(){GM_setValue("macros",macros);}
// Convert a macro to a few characters that we can display
function visual_macro(macro) {
var combo = [];
if (macro.ctrlKey) combo.push(chars[17]);
if (macro.shiftKey) combo.push(chars[16]);
if (macro.altKey) combo.push(chars[18]);
if (macro.metaKey) combo.push(chars[91]);
combo.push(chars[macro.keyCode]);
return combo.join(" ");
}
// Capture macro's that are typed into the box
function capture_macro(keyevent){
keyevent.preventDefault();
var macro = keyevent.target.parentElement.parentElement.macro;
macro.keyCode = keyevent.keyCode;
macro.shiftKey = keyevent.shiftKey && keyevent.keyCode != 16;
macro.ctrlKey = keyevent.ctrlKey && keyevent.keyCode != 17;
macro.altKey = keyevent.altKey && keyevent.keyCode != 18;
macro.metaKey = keyevent.metaKey && keyevent.keyCode != 91;
macro.location = keyevent.location;
save_macros();
keyevent.target.value = visual_macro(macro);
}
var dragging = null;
function ondragstart() {
dragging = this;
this.classList.add('komacro-dragging');
}
function ondragend() {
dragging = this;
this.classList.remove('komacro-dragging');
}
function ondragover() {
if (this.parentNode !== dragging.parentNode) return;
if (this == dragging) return;
const old_pos = [...dragging.parentNode.children].indexOf(dragging);
const new_pos = [...this.parentNode.children].indexOf(this);
if (new_pos < old_pos) this.parentNode.insertBefore(dragging, this);
else this.parentNode.insertBefore(dragging, this.nextSibling);
let old_i = macros.indexOf(dragging.macro);
let new_i = macros.indexOf(this.macro);
return macros.splice(new_i, 0, macros.splice(old_i,1)[0]);
}
// Create a new macro entry in the config panel.
// Either based on an existing macro, or a new line
function create_macro(macro){
// If it's a new macro
if (!macro) {
macro = {
type: 'team',
map: current_map,
};
if (!show_mirrored && current_map)
macro.map = current_map.replace('_Mirrored','');
macros.unshift(macro);
save_macros();
}
var entry = document.createElement('div');
entry.className = "form-group";
// Dragging
entry.draggable = true;
entry.ondragstart = ondragstart;
entry.ondragend = ondragend;
entry.ondragover = ondragover;
entry.macro = macro;
entry.innerHTML = `
<div class="col-xs-1"><button class="btn btn-default komacro-type"></div>
<div class="col-xs-2"><select class="form-control"><option value="null">all maps</option></select></div>
<div class="col-xs-2"><input type="text" readonly placeholder="type a macro..." autocomplete="off" class="form-control komacro-combo"></div>
<div class="col-xs-6"><input type="text" autocomplete="off" class="form-control"></div>
<div class="col-xs-1"><input type="button" value="X" class="btn btn-default komacro-del" style="width:100%;"></div>`;
var [type,map,combo,message,remove] = [...entry.children].map(a=>a.firstElementChild);
type.dataset.type = macro.type || 'all';
map.value = macro.map || null;
combo.value = visual_macro(macro);
message.value = macro.message || '';
type.onclick = function(){
var types = ["all","team","group","mod","all"];
this.dataset.type = types[types.indexOf(this.dataset.type) + 1];
this.parentElement.parentElement.macro.type = this.dataset.type;
save_macros();
this.blur();
}
map.onchange = function(){
this.parentElement.parentElement.macro.map = this.value;
save_macros();
}
combo.onkeydown = capture_macro;
combo.onfocus = function(){
this.value = '';
// Disable movement while composing a macro
if (!tpul.noscript) tagpro.disableControls = true;
}
combo.onblur = function(){
this.value = visual_macro(this.parentElement.parentElement.macro);
if (!tpul.noscript) tagpro.disableControls = false;
}
message.onchange = function(){
this.parentElement.parentElement.macro.message = this.value;
save_macros();
}
// Disable movement while composing a message
message.onfocus = function(){ if (!tpul.noscript) tagpro.disableControls = true; }
message.onblur = function(){ if (!tpul.noscript) tagpro.disableControls = false; }
remove.onclick = function(){
entry.remove();
macros.splice(macros.indexOf(this.parentElement.parentElement.macro),1);
save_macros();
}
return entry;
}
// =====SETTINGS SECTION=====
// I use tpul for this userscripts' options.
// see: https://github.com/wilcooo/TagPro-UserscriptLibrary
var settings = tpul.settings.addSettings({
id: 'Komacro',
title: "Configure Komacro",
tooltipText: "Komacro",
icon: "https://raw.githubusercontent.com/wilcooo/TagPro-ScriptResources/master/MacroKey.png",
buttons: ['close','reset'],
fields: {
show_mirrored: {
type: 'checkbox',
default: false,
section: ['',"You can drag your Komacro's to reorder them."],
label: "Distinguish mirrored maps",
},
neomacro: {
type: 'checkbox',
default: false,
label: 'Enable <a href="https://redd.it/32qqgr">numpad magic</a>',
title: 'Based on Neomacro'
}
},
events: {
open: function() {
// Add all saved macros to this config panel when it opens...
var panel = document.getElementById("Komacro_wrapper");
var macrolist = this.macrolist = document.createElement("div");
panel.appendChild(this.macrolist);
for (var macro of macros) macrolist.appendChild(create_macro(macro));
update_maps();
var new_btn = document.createElement("button");
new_btn.className = "btn btn-primary";
new_btn.style = "right: 30px; position: absolute;";
new_btn.innerText = "New Komacro";
document.getElementById('Komacro_show_mirrored_var').appendChild(new_btn);
new_btn.onclick = function(){
macrolist.insertBefore(create_macro(), macrolist.firstChild);
update_maps();
}
// Since we are not saving this GM_config, we need to save it ourselfs
// every time an input has changed
document.getElementById("Komacro_field_show_mirrored").onchange = function(){
settings.save();
show_mirrored = settings.get('show_mirrored');
update_maps();
}
document.getElementById("Komacro_field_neomacro").onchange = function(){
settings.save();
}
},
close: function(){
// By default, 'options canceled' is notified. We overwrite this notification directly after.
setTimeout(tpul.notify,0,'Your Komacro\'s are saved!','success');
},
reset: function(){
if (confirm("This will delete ALL your Komacro's!")) {
macros = []; save_macros();
this.macrolist.innerHTML = "";
setTimeout(tpul.notify,0,'All your Komacro\'s are deleted','warning');
} else {
setTimeout(tpul.notify,0,'Nothing happened :)','warning');
}
},
}
});
var show_mirrored = settings.get('show_mirrored');
// =====LOGIC SECTION=====
var macros = GM_getValue('macros', [
// The default komacros:
{keyCode: 71, message: "gg"},
{keyCode: 71, shiftKey: true, message: "GG"},
{keyCode: 72, type: "team", message: "Handing off!"},
{keyCode: 72, type: "team", shiftKey: true, message: "On regrab"},
{keyCode: 72, shiftKey: true, ctrlKey: true, altKey: true, map: "teamwork", message: "This map is awesome!"},
{keyCode: 72, shiftKey: true, ctrlKey: true, altKey: true, map: "bomber", message: "Oh no.. not this map..."},
{keyCode: 66, type: "group", message: "Back to group"},
{keyCode: 82, type: "team", message: "We need someone on regrab!"},
{keyCode: 77, type: "mod", message: "Please stop doing that."},
]);
// All possible options:
// keyCode, location, altKey, ctrlKey, shiftKey, metaKey, map, type, message
var current_map = null;
// When in a game:
if (tpul.playerLocation == 'game') {
// Find out what map is being played
if (!tpul.noscript && !tagpro.map) {
// Method 1: using the TagPro API
// Preferable since it's the quickest
tagpro.ready(function() {
tagpro.socket.on('map', function(map) {
maps.then(function(maps){
var mapobj = maps.rotation.find(m=> m.author == map.info.author && m.name == map.info.name);
if (mapobj) current_map = mapobj.key;
});
});
});
} else {
// Method 2: in case of no-script
// or if the 'map' event has been received before we could intercept it.
var mapInfo = document.getElementById('mapInfo');
(function getMapFromDom(i){
// This will be tried 2 times per second for max. 10 seconds
if (i > 20) throw 'Komacro couldn\'t find out what map is being played';
var map = mapInfo.innerText.match(/Map: (.*) by (.*)/);
if (!map) setTimeout(getMapFromDom, 500, ++i);
maps.then(function(maps){
var mapobj = maps.rotation.find(m=> m.author == map[2] && m.name == map[1]);
if (mapobj) current_map = mapobj.key;
});
})(0);
}
// Listening/handling macros:
var ignore_ctrlKey = false,
ignore_shiftKey = false,
ignore_altKey = false,
ignore_metaKey = false;
function handleMacro(keyevent) {
// If we're recording a macro: return;
if (keyevent.target.classList.contains('komacro-combo')) return;
// While the controls are disabled: return;
// (This is usually when typing a chat message or changing your name)
if (!tpul.noscript && tagpro.disableControls) return;
// When no-script is enabled, we can detect the chat box and name-change box like this:
if (["chat", "name"].includes( keyevent.target.id )) return;
var global_macro = null;
// Find a matching macro
for (var macro of macros) {
if (macro.keyCode == keyevent.keyCode &&
(!macro.location || macro.location == keyevent.location) &&
!!macro.ctrlKey == !!keyevent.ctrlKey &&
!!macro.shiftKey == !!keyevent.shiftKey &&
!!macro.altKey == !!keyevent.altKey &&
!!macro.metaKey == !!keyevent.metaKey) {
// Send a macro of which the map matches:
if (macro.map && (macro.map == current_map ||
!show_mirrored && macro.map.replace('_Mirrored','') == current_map.replace('_Mirrored','') )) {
keyevent.preventDefault();
tpul.chat.emit(macro.message, macro.type);
return;
}
// Save the global macro, but don't send it yet.
// because we could find another map-specific macro.
if (!macro.map || macro.map == "null") global_macro = macro;
}
}
// No map-specific macro has been found.
// We can send the global macro (if we found one)
if (global_macro) {
keyevent.preventDefault();
tpul.chat.emit(global_macro.message, global_macro.type);
return;
}
}
// Wait for any (non-modifier) key to be pressed
document.addEventListener('keydown', function(keydown) {
// Pressing down a modifier key is ignored,
// as they could be used to modify another key.
// Instead we look at their keyup event later.
if (['Control','Shift','Alt','Meta'].includes(keydown.key)) return;
// Remember what modifier keys are used during this event,
// so that we know to ignore their next keyup.
ignore_ctrlKey = keydown.ctrlKey;
ignore_shiftKey = keydown.shiftKey;
ignore_altKey = keydown.altKey;
ignore_metaKey = keydown.metaKey;
handleMacro(keydown);
});
// Wait for any modifier to be released:
document.addEventListener('keyup', function(keyup) {
// This time; ignore non-modifier keys
if ( ! ['Control','Shift','Alt','Meta'].includes(keyup.key)) return;
// Ignore modifiers that have been used to modify other keys
if (ignore_ctrlKey && keyup.key == 'Control' ||
ignore_shiftKey && keyup.key == 'Shift' ||
ignore_altKey && keyup.key == 'Alt' ||
ignore_metaKey && keyup.key == 'Meta') return;
// Remember what modifier keys are used during this event,
// so that we know to ignore their next keyup.
ignore_ctrlKey |= keyup.ctrlKey;
ignore_shiftKey |= keyup.shiftKey;
ignore_altKey |= keyup.altKey;
ignore_metaKey |= keyup.metaKey;
handleMacro(keyup);
});
// (Neomacro) Numpad Magic
(function neomacro(){
var directions = { 1: 'bottom left', 2: 'bottom', 3: 'bottom rigth', 4: 'left', 5: 'middle', 6: 'right', 7: 'top left', 8: 'top', 9: 'top right' }
var combo = [],
timeout
document.addEventListener('keydown', function(keydown) {
if (!settings.get('neomacro')) return
if (event.location != 3) return // the numpad
if (event.key == '.') return reset()
clearTimeout(timeout)
timeout = setTimeout(reset, 2e3)
combo.push(event.key)
parse()
})
function parse () {
switch (combo[0]) {
case '/':
if (combo[1] == '/') return send('kfc')
if (combo[1] <= 4) return send('past ' + combo[1])
if (1 in combo) return reset()
break
case '-':
if (combo[1] == '-') return send('base clear')
if (combo[1] <= 4) return send(combo[1] + ' enemies in base')
if (1 in combo) return reset()
break
case '*':
if (combo[1] == '*') return send('pups soon')
if (combo[1] in directions) return send(directions[combo[1]] + ' pup soon')
if (1 in combo) return reset()
break
case '+':
if (combo[1] in directions) {
var time = combo.slice(2).join('')
if (isNaN(time) || time < 0 || time > 60) return reset()
if (time.length == 2) return send(directions[combo[1]] + ' pup :' + time)
break
}
if (1 in combo) return reset()
break
default:
if (combo[0] in directions) return send(directions[combo[0]])
return reset()
}
}
function reset () {
combo.length = 0
clearTimeout(timeout)
tpul.notify("Try your macro again")
}
function send (message) {
combo.length = 0
clearTimeout(timeout)
tpul.chat.emit(message, 'team')
}
})()
// End of Numpad Magic
}
})();