// ==UserScript== // @name Battle macros // @version 2025-04-22 // @description Use skills in a specific order by pressing less buttons. // @author Lulu5239 // @match *://game.granbluefantasy.jp/* // @match *://gbf.game.mbga.jp/* // @grant GM_getValue // @grant GM_setValue // @namespace https://greasyfork.org/users/1449836 // @downloadURL https://update.greasyfork.cloud/scripts/533272/Battle%20macros.user.js // @updateURL https://update.greasyfork.cloud/scripts/533272/Battle%20macros.meta.js // ==/UserScript== var click = e=>e.dispatchEvent(new Event("tap",{bubbles:true, cancelable:true})) var recordFunction; let recordable let cancel = 0 var onPage = async ()=>{ if(document.querySelectorAll("#macros-list").length || !document.location.hash?.startsWith("#battle") && !document.location.hash?.startsWith("#raid")){return} while(typeof(stage)=="undefined" || !stage?.pJsnData || !document.querySelectorAll("#tpl-prt-total-damage").length){await new Promise(ok=>setTimeout(ok,100))} document.querySelector(".cnt-raid").style.paddingBottom = "0px" document.querySelector(".prt-raid-log").style.pointerEvents = "none" cancel++ let macros = GM_getValue("macros") || [] document.querySelector(".contents").insertAdjacentHTML("beforeend", `
New...
Show all
` ) let list = document.querySelector("#macros-list") let observer = new MutationObserver(onPage) observer.observe(list.parentElement, { childList:true, }) let recording = document.querySelector("#macro-recording") let settings = document.querySelector("#macro-settings") let partyHash = [stage.pJsnData.player.param.map(e=>e.pid).join(","), stage.pJsnData.summon.map(s=>s.id).join(",")].join(";") let characterByImage = url=>url.split("/").slice(-1)[0].split("_")[0] let playMacro = async id=>{ let macro = macros[id] let line = list.querySelector(`[data-id="${id}"]`) line.dataset.playing = "now" list.querySelector(`.listed-macro[data-id="cancel"]`).style.display = null let actions = [...macro.actions] let next = {} let check; check = (n,rec)=>{ if(rec && !next[n]){ list.querySelector(`[data-id="${n}"]`).dataset.playing = "soon" next[n] = 1 }else{next[n]++} if(rec>10){return} for(let action of macros[n].actions){ if(action.type!=="macro"){continue} check(action.macro, rec+1) } } next[id] = 1 check(id,0) let playing = [id] let wait = time=>new Promise(ok=>setTimeout(ok,time ? time : macro.speed==="slow" ? 2000 : 500)) let myCancel = cancel while(actions.length){ if(cancel>myCancel){break} let action = actions.splice(0,1)[0] if(action.type==="macro"){ if(!macros[action.macro]){continue} next[action.macro]-- list.querySelector(`[data-id="${playing.slice(-1)[0]}"]`).dataset.playing = playing.slice(-1)[0]===id ? "original" : "soon" list.querySelector(`[data-id="${action.macro}"]`).dataset.playing = "now" playing.push(action.macro) actions.splice(0, 0, ...macros[action.macro].actions, {type:"leaveMacro"}) continue} if(action.type==="leaveMacro"){ let last = playing.splice(-1, 1)[0] if(next[last]>0){ list.querySelector(`[data-id="${last}"]`).dataset.playing = "soon" }else{ list.querySelector(`[data-id="${last}"]`).removeAttribute("data-playing") } list.querySelector(`[data-id="${playing.slice(-1)[0]}"]`).dataset.playing = "now" continue} if(action.type==="skill"){ let button = document.querySelectorAll(`div[ability-id="${action.ability}"]`)[0] if(button){ if(document.querySelector(`.prt-command-chara[pos="${+button.getAttribute("ability-character-num")+1}"]`).style.display!=="block"){ let back = document.querySelector(`.btn-command-back`) if(back.classList.contains("display-on")){ click(back) await wait() } click(document.querySelector(`.btn-command-character[pos="${+button.getAttribute("ability-character-num")}"]`)) await wait() } click(button) if(action.character){ await wait() let character for(let c of document.querySelectorAll(`.pop-select-member .prt-character .btn-command-character img`)){ if(characterByImage(c.src)===action.character){ character = c } } if(character){ click(character) await wait() } } await wait(200) } }else if(action.type==="attack"){ let button = document.querySelectorAll(`.btn-attack-start.display-on`)[0] if(button){ click(button) await new Promise((ok,err)=>{ let observer = new MutationObserver(()=>{ if(cancel>myCancel || button.classList.contains("display-on")){ok()} }) observer.observe(button, { attributes:true, }) }) } }else if(action.type==="summon"){ let back = document.querySelector(`.btn-command-back`) if(back.classList.contains("display-on")){ click(back) await wait() } let button = document.querySelectorAll(".btn-command-summon.summon-on")[0] if(!button){continue} click(button) await wait() button = document.querySelectorAll(`.btn-summon-available.on[summon-id="${action.summon==="support" ? "supporter" : stage.pJsnData.summon.findIndex(s=>s.id===action.summon)}"]`)[0] if(!button){continue} click(button) await wait(200) click(document.querySelector(".btn-summon-use")) await wait() }else if(action.type==="calock"){ let button = document.querySelector(".btn-lock") let n = action.lock!="false" ? 1 : 0 if(button.classList.contains("lock"+(1-n))){continue} if(button.parentElement.style.display==="none"){ click(document.querySelector(`.btn-command-back`)) await wait() } click(button) if(macro.speed==="slow"){await wait()} } } list.querySelector(`[data-id="${id}"]`).removeAttribute("data-playing") for(let i in next){ list.querySelector(`[data-id="${i}"]`).removeAttribute("data-playing") } if(!list.querySelectorAll("[data-playing]").length){ list.querySelector(`.listed-macro[data-id="cancel"]`).style.backgroundColor = null list.querySelector(`.listed-macro[data-id="cancel"]`).style.display = "none" } } let moveMode; let showAll let createListedMacro = i=>{ let macro = macros[i] list.querySelector(`.listed-macro[data-id="new"]`).insertAdjacentHTML("beforebegin", `
${macro.name}
`) let line = list.querySelector(`.listed-macro[data-id="${i}"]`) line.addEventListener("click", async ()=>{ if(line.dataset.playing){return} if(moveMode!==undefined){return moveMode(line)} await playMacro(line.dataset.id) }) line.querySelector(`button`).addEventListener("click", ev=>{ if(moveMode!==undefined){return} ev.stopPropagation() list.style.display = "none" settings.style.display = null settings.dataset.macro = line.dataset.id settings.children[1].innerText = macro.name settings.children[3].innerText = macro.parties?.includes(partyHash) ? "Don't show for this party" : "Show for this party" settings.children[3].style.display = !macro.parties ? "none" : null settings.children[4].innerText = !macro.parties ? "Don't always show" : "Always show" settings.children[6].querySelector("select").value = macro.speed || "normal" window.scrollTo(0, window.innerHeight) }) } let listMacros = ()=>{ for(let i in macros){ if(!showAll && macros[i].parties && !macros[i].parties.includes(partyHash)){continue} createListedMacro(i) } } listMacros() let skillByImage = url=>document.querySelector(`.prt-ability-list img[src="${url}"]`).parentElement if(!recordable){ $(document.body).on("tap", ev=>{ if(recordFunction){recordFunction(ev.target)} }) recordable = true } list.querySelector(`.listed-macro[data-id="new"]`).addEventListener("click", ()=>{ list.style.display = "none" recording.style.display = null recordFunction = original=>{ let usefulParent = original let character while(usefulParent && !["lis-ability","prt-popup-body","btn-attack-start","btn-summon-use","btn-quick-summon","btn-lock"].find(c=>usefulParent.classList.contains(c))){ if(usefulParent.classList.contains("btn-command-character")){character = usefulParent} usefulParent = usefulParent.parentElement } if(!usefulParent){return} let extra = {} let text if(usefulParent.classList.contains("btn-attack-start")){ extra.type = "attack" text = "Attack" }else if(usefulParent.classList.contains("btn-summon-use") || usefulParent.classList.contains("btn-quick-summon")){ extra.type = "summon" if(usefulParent.classList.contains("btn-quick-summon")){ usefulParent = document.querySelector(".lis-summon.is-quick") }else if(usefulParent.getAttribute("summon-id")==="supporter"){ text = "Support summon" extra.summon = "support" }else{ usefulParent = document.querySelector(`.lis-summon[pos="${usefulParent.getAttribute("summon-id")}"]`) } if(!extra.summon){ let summon = stage.pJsnData.summon[+usefulParent.getAttribute("pos") -1] text = summon.name extra.summon = summon.id } }else if(usefulParent.classList.contains("btn-lock")){ extra.type = "calock" extra.lock = usefulParent.classList.contains("lock1") text = (extra.lock ? "No" : "Auto")+" charge attack" }else{ extra.type = "skill" if(usefulParent.parentElement.classList.contains("pop-usual") && character){ extra.character = characterByImage(character.querySelector("img.img-chara-command").src) usefulParent = skillByImage(usefulParent.querySelector("img.img-ability-icon").src) }else{ usefulParent = usefulParent.querySelector("[ability-id]") } extra.ability = usefulParent.getAttribute("ability-id") text = usefulParent.getAttribute("ability-name") } let last; let p = extra.type==="skill" ? "ability" : extra.type for(let e of recording.querySelectorAll(`[data-type]`)){last = e} if(last && extra.type!=="attack" && last.dataset.type===extra.type && extra[p]==last.dataset[p]){ for(let k in last.dataset){last.removeAttribute("data-"+k)} for(let k in extra){last.dataset[k] = extra[k]} last.innerText = text }else{ recording.insertAdjacentHTML("beforeend", `
`data-${k}="${extra[k]}"`).join(" ")}>${text}
`) } } }) list.querySelector(`.listed-macro[data-id="showAll"]`).addEventListener("click", ()=>{ showAll = true list.querySelector(`.listed-macro[data-id="showAll"]`).style.display = "none" for(let e of list.querySelectorAll(`.listed-macro[data-id]`)){ if(+e.dataset.id>=0){e.remove()} } listMacros() }) list.querySelector(`.listed-macro[data-id="cancel"]`).addEventListener("click", ()=>{ cancel++ list.querySelector(`.listed-macro[data-id="cancel"]`).style.backgroundColor = "#422" }) recording.querySelector(`.listed-macro[data-id="stop"]`).children[0].addEventListener("click", ()=>{ let name = prompt("Macro name?") if(!name){return} let macro = { name, actions:[], parties:[partyHash], } for(let action of recording.querySelectorAll(".listed-macro[data-type]")){ action.remove() if(action.dataset.type==="macro" && action.dataset.macro===undefined){continue} macro.actions.push({ name:action.innerText, ...action.dataset, }) } for(let a of macro.actions){ if(a.type==="macro"){a.macro = +a.macro} } macros.push(macro) createListedMacro(macros.length-1) list.style.display = null recording.style.display = "none" GM_setValue("macros", macros) recordFunction = null }) recording.querySelector(`.listed-macro[data-id="stop"]`).children[1].addEventListener("click", ()=>{ for(let action of recording.querySelectorAll(".listed-macro[data-type]")){ action.remove() } list.style.display = null recording.style.display = "none" recordFunction = null }) recording.querySelector(`.listed-macro[data-id="stop"]`).children[2].addEventListener("click", ()=>{ recording.insertAdjacentHTML("beforeend", `
`) let select = recording.querySelector(".new-select-thing") select.className = null select.addEventListener("change", ()=>{ select.parentElement.dataset.macro = select.value select.parentElement.innerText = macros[+select.value].name }) }) settings.children[0].addEventListener("click", ()=>{ settings.style.display = "none" list.style.display = null GM_setValue("macros", macros)}) settings.children[2].addEventListener("click", ()=>{ let name = prompt("New macro name") if(!name){return} macros[+settings.dataset.macro].name = name settings.children[1].innerText = name list.querySelector(`[data-id="${+settings.dataset.macro}"] a`).innerText = name GM_setValue("macros", macros)}) settings.children[3].addEventListener("click", ()=>{ let macro = macros[+settings.dataset.macro] let i = macro.parties.findIndex(p=>p===partyHash) if(i===-1){ macro.parties.push(partyHash) }else{ macro.parties.splice(i,1) } settings.children[3].innerText = i===-1 ? "Don't show for this party" : "Show for this party" GM_setValue("macros", macros)}) settings.children[4].addEventListener("click", ()=>{ let macro = macros[+settings.dataset.macro] if(macro.parties){ delete macro.parties }else{ macro.parties = [partyHash] } settings.children[3].innerText = "Don't show for this party" settings.children[3].style.display = !macro.parties ? "none" : null settings.children[4].innerText = !macro.parties ? "Don't always show" : "Always show" GM_setValue("macros", macros)}) settings.children[5].addEventListener("click", ()=>{ list.insertAdjacentHTML("afterbegin", `
Move macro after...
`) let line = list.querySelector(`.listed-macro[data-id="moveAfter"]`) moveMode = element=>{ let before = +settings.dataset.macro; let after = +element.dataset.id || 0 for(let m of macros){ for(let a of m.actions){ if(a.type!=="macro"){continue} if(a.macro===before){ a.macro = after }else if(a.macro=after){ a.macro++ }else if(a.macro>before && a.macro<=after){ a.macro-- } } } let macro = macros[before] macros[before] = null macros.splice(after>=0 ? after +1 : 0, 0, macro) macros.splice(macros.findIndex(m=>!m), 1) for(let e of list.querySelectorAll(`.listed-macro[data-id]`)){ if(+e.dataset.id>=0){e.remove()} } listMacros() moveMode = undefined settings.style.display = null list.style.display = "none" list.querySelector(`.listed-macro[data-id="moveAfter"]`).remove() if(!showAll){ list.querySelector(`.listed-macro[data-id="showAll"]`).style.display = null } GM_setValue("macros", macros)} list.querySelector(`.listed-macro[data-id="showAll"]`).style.display = "none" line.addEventListener("click", ()=>{ moveMode(line) }) settings.style.display = "none" list.style.display = null }) settings.children[6].querySelector("select").addEventListener("change", ()=>{ macros[+settings.dataset.macro].speed = settings.children[6].querySelector("select").value GM_setValue("macros", macros)}) settings.children[7].addEventListener("click", ()=>{ if(!confirm("Delete the macro?")){return} let i = +settings.dataset.macro list.querySelector(`[data-id="${i}"]`).remove() macros.splice(+settings.dataset.macro, 1) for(let e of list.querySelectorAll("[data-id]")){ if(e.dataset.id>i){ e.dataset.id = +e.dataset.id -1 } } for(let m of macros){ for(let a of m.actions){ if(a.type==="macro" && a.macro>i){a.macro--} } } settings.style.display = "none" list.style.display = null GM_setValue("macros", macros)}) } window.addEventListener("hashchange", onPage) onPage()