Battle macros

Use skills in a specific order by pressing less buttons.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Battle macros
// @version      2025-06-26
// @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
// ==/UserScript==

let click = (e, crect)=>{
  let rect = e.getBoundingClientRect()
  if(!["x","y","width","height"].find(k=>rect[k])){rect = crect}
  return $(e).trigger($.Event("tap",{
    target:e, currentTarget:e,
    x:rect && Math.floor(rect.x+rect.width*(0.5+(Math.random()*Math.random()*Math.sign(Math.random()-0.5)/2))),
    y:rect && Math.floor(rect.y+rect.height*(0.5+(Math.random()*Math.random()*Math.sign(Math.random()-0.5)/2))),
  }))
}
let recordFunction; let recordable
let cancel = 0
let farmingQuest
let lastHandledPage; let stageObserver
let waitingForSkillEnd = []
waitingForSkillEnd[0] = new Promise((ok, err)=>{
  waitingForSkillEnd[1] = ok
  waitingForSkillEnd[2] = err
})
let originalUnloader; let reloadables = {}

let onPage = async ()=>{
  if(document.location.hash===lastHandledPage){return}
  if(document.location.hash?.startsWith("#result") && farmingQuest){
    lastHandledPage = document.location.hash
    let autoQuests = GM_getValue("autoQuests")
    let settings = autoQuests[farmingQuest]
    if(settings.max===0){return}
    if(settings.max>0){
      settings.max--
      GM_setValue("autoQuests", autoQuests)
    }
    await new Promise(ok=>setTimeout(ok,5000))
    document.location.href = document.location.href.slice(0, document.location.href.indexOf("#")) + `#quest/supporter/${farmingQuest}/0`
  return}
  if(document.location.hash?.startsWith("#quest/supporter/"+farmingQuest) && farmingQuest){
    lastHandledPage = document.location.hash
    while(!document.querySelector(".se-quest-start")){await new Promise(ok=>setTimeout(ok,100))}
    click(document.querySelector(".se-quest-start"))
    let p = document.location.hash
    let button
    while(document.location.hash===p && !button){
      await new Promise(ok=>setTimeout(ok,100))
      button = document.querySelector(".btn-use-full.index-1")
    }
    if(button){
      let autoQuests = GM_getValue("autoQuests")
      let settings = autoQuests[farmingQuest]
      if(settings.maxHalfElixirs===0){return}
      if(settings.maxHalfElixirs>0){
        settings.maxHalfElixirs--
        GM_setValue("autoQuests", autoQuests)
      }
      click(button)
      button = null
      while(document.location.hash===p && !button){
        await new Promise(ok=>setTimeout(ok,100))
        button = document.querySelector(".common-item-recovery-pop .prt-popup-footer .btn-usual-ok")
      }
      click(button)
    }
  return}
  if(document.querySelector("#macros-list") || !document.location.hash?.startsWith("#battle") && !document.location.hash?.startsWith("#raid")){return}
  lastHandledPage = document.location.hash
  if(!originalUnloader){ // Don't reload some files every time
    let myCancel = cancel
    while(!requirejs.s.contexts._.defined["model/cjs-loader"]){await new Promise(ok=>setTimeout(ok,100)); if(cancel!==myCancel){return}}
    originalUnloader = requirejs.s.contexts._.defined["model/cjs-loader"].clear
    requirejs.s.contexts._.defined["model/cjs-loader"].clear = ()=>{
      for(let m in images){
        if(reloadables[m] || !lib[m]){continue}
        let script = Array.from(document.querySelectorAll(`body script[type="text/javascript"]`)).find(s=>s.innerHTML.includes(lib[m]+""))
        if(script){
          reloadables[m] = script.innerHTML
          script.remove()
        }
      }
    }
  }
  if(true){
    let remove = []
    for(let m in reloadables){
      if(!reloadables[m]){continue}
      let script = document.createElement("script")
      script.innerHTML = reloadables[m]
      document.body.appendChild(script)
      remove.push(script)
    }
    setTimeout(()=>{
      for(let script of remove){
        script.remove()
      }
    }, 2000)
  }
  if(true){
    let myCancel = cancel
    while(typeof(stage)=="undefined" || !stage?.pJsnData || !document.querySelector("#tpl-prt-total-damage")){
      if(cancel!==myCancel){return}
      await new Promise(ok=>setTimeout(ok,100))
    }
  }
  
  document.querySelector(".cnt-raid").style.paddingBottom = "0px"
  document.querySelector(".prt-raid-log").style.pointerEvents = "none"
  cancel++
  let view = Game.view.setupView//requirejs.s.contexts._.defined["view/raid/setup"].prototype

  let scenarioSpeed = 0; let scenarioEndTime = 0
  let originalPlayScenarios = view.playScenarios
  view.playScenarios = (...args)=>{
    //stage.lastScenario = [...args[0].scenario]
    let mergedDamage = []; let minimumTime = 0
    let newScenario = scenarioSpeed && !(stage.pJsnData.multi_raid_member_info?.length>1) ? [] : args[0].scenario
    for(let e of (newScenario.length ? [] : args[0].scenario)){
      if(["recast", "chain_burst_gauge"].includes(e.cmd)){
        newScenario.push(e)
        continue
      }
      if(e.cmd==="attack" && e.from==="player"){
        minimumTime += 800
        if(scenarioSpeed>=99){
          newScenario.push({cmd:"wait", fps:12})
          continue
        }else if(scenarioSpeed>=2){
          mergedDamage.splice(0, 0, ...e.damage.reduce((r,l)=>[...r, ...l], []))
        }else{
          e.damage = [e.damage.reduce((r,l)=>[...r, ...l],[])]
          newScenario.push(e)
        }
        continue
      }else if(e.cmd==="special" || e.cmd==="special_npc"){
        minimumTime += 2100
        if(scenarioSpeed>=99){
          newScenario.push({cmd:"wait", fps:12})
        continue}
        let lastDamage
        for(let a of e.list){
          if(a.damage){lastDamage=a.damage.slice(-1)[0]}
        }
        if(!lastDamage){continue}
        mergedDamage.splice(0, 0, ...e.total.map(t=>({
          pos:lastDamage.pos,
          num:1,
          value:+t.split.join(""),
          split:t.split,
          hp:lastDamage.hp,
          color:lastDamage.color || lastDamage.attr,
          critical:lastDamage.critical,
          miss:lastDamage.miss,
          guard:false,
          is_force_font_size:true,
          no_damage_motion:false,
        })))
        continue
      }else if(mergedDamage.length){
        let total = mergedDamage.reduce((p,o)=>p+o.value, 0)
        let color = mergedDamage.find(o=>o.color)?.color
        newScenario.push({
          cmd:"loop_damage",
          color,
          to:"boss",
          mode:"parallel",
          wait:1,
          is_rengeki:0,
          is_damage_sync_effect:false,
          is_activate_counter_damaged:"",
          is_bulk_display:false,
          list:[mergedDamage.map((a,i)=>{a.attack_num=i; a.size="m"; a.concurrent_attack_count=0; return a})],
          total:[{"pos":1,"split":(""+total).split(""),"attr":color,"count":0}]
        })
        mergedDamage = []
      }
      if(["modechange", "bg_change", "bgm"].includes(e.cmd)){
        newScenario.push(e)
        continue
      }
      if(["ability", "loop_damage", "windoweffect", "effect", "attack"].includes(e.cmd)){
        if(scenarioSpeed>=99 && e.cmd==="effect" && e.kind?.startsWith("burst")){minimumTime+=1000}
        else if(e.cmd==="ability" && e.to==="player" || e.cmd==="attack"){minimumTime+=1000}
        if(scenarioSpeed>=3){continue}
        if(e.wait){e.wait = 1}
      }
      if(["summon", "summon_simple", "chain_cutin"].includes(e.cmd)){
        minimumTime += e.cmd==="chain_cutin" ? 500 : 1000
        continue
      }
      if(scenarioSpeed>=99 && ["super", "message", "attack", "heal"].includes(e.cmd)){
        if(e.cmd==="super"){minimumTime+=2000}
        continue
      }
      newScenario.push(e)
    }
    args[0].scenario = newScenario
    scenarioEndTime = +new Date() + minimumTime
    return originalPlayScenarios.apply(view, args)
  };
  let originalPostProcessor = view.postprocessOnPlayScenarios
  let postProcessorDelayer = null
  view.postprocessOnPlayScenarios = (...args)=>{
    let o = args[2].timeline[0]
    let originalCall = o.call
    o.call = (...args2)=>{
      originalCall.apply(o, [()=>{
        if(postProcessorDelayer){
          postProcessorDelayer.push(args2[0])
          return;
        }
        postProcessorDelayer = [args2[0]]
        setTimeout(()=>{
          let nextF = waitingForSkillEnd[1]
          waitingForSkillEnd[0] = new Promise((ok, err)=>{
            waitingForSkillEnd[1] = ok
            waitingForSkillEnd[2] = err
          })
          let l = postProcessorDelayer
          postProcessorDelayer = null
          for(let f of l){
            f()
          }
          setTimeout(()=>{
            nextF()
          }, 10)
        }, scenarioEndTime - +new Date())
      }, ...args2.slice(1)])
    }
    let r = originalPostProcessor.apply(view, args)
    o.call = originalCall
    return r
  }
  
  let macros = GM_getValue("macros") || []
  document.querySelector(".contents").insertAdjacentHTML("beforeend",
    `<div id="macros-list"><div class="listed-macro" data-id="new">New...</div><div class="listed-macro" data-id="showAll">Show all</div><div class="listed-macro" data-id="cancel" style="display:none">Stop playing</div><div class="listed-macro" data-id="extra" style="background-color:#000; min-height:10px;"><div style="display:none"><button data-id="scenarioSpeed">Speed</button></div></div><div style="display:none" class="nothing"></div></div>
    <div id="macro-recording" style="display:none"><div class="listed-macro" data-id="stop"><button>End recording</button> <button>Cancel</button> <button>Add macro</button></div></div>
    <div id="macro-settings" style="display:none">
      <div class="listed-macro" style="background-color:#111">Back</div>
      <div class="listed-macro" style="text-align:center"></div>
      <div class="listed-macro">Rename</div>
      <div class="listed-macro"></div>
      <div class="listed-macro"></div>
      <div class="listed-macro"></div>
      <div class="listed-macro"></div>
      <div class="listed-macro">Move...</div>
      <div class="listed-macro">Speed: <select><option value="slow">Slow</option><option value="slower">Slower</option><option value="normal">Normal</option><option value="faster">Faster</option></select></div>
      <div class="listed-macro" style="background-color:#411">Delete</div>
    </div>
    <div id="macro-speed" style="display:none">
      <div class="listed-macro" style="background-color:#111" data-value="back">Back</div>
      ${[
        {value:0, name:"Default", description:"The default speed."},
        {value:1, name:"Not slow", description:"Skips long animations (like summons) and merges damage of attacks."},
        {value:2, name:"Faster", description:"Merges all attacks into a single animation."},
        {value:3, name:"Fast", description:"Skips more animations."},
        {value:99, name:"Skip all", description:"It would be sad to use that."},
        {value:100, name:"Auto farm", description:"Automatically farm this quest multiple times."},
      ].map(o=>`<div class="listed-macro" data-value="${o.value}" data-status="none"><a style="font-size:125%">${o.name}</a><br><a>${o.description}</a></div>`).join("")}
      <div style="display:none; color:#fff" class="autoSettings">
        <div>Auto farm settings:</div>
        <div>When starting, play macro <select data-key="macro" data-type="number" data-value=""></select> then enable <select data-key="autoGame"><option value="">nothing</option><option value="semi">semi auto</option><option value="full" selected>full auto</option></select>.</div>
        <div>Maximum <input data-type="number" data-key="max" placeholder="infinite"> battles and <input data-type="number" data-key="maxHalfElixirs" data-default="0" placeholder="infinite"> half elixirs.</div>
      </div>
    </div>
    <div id="pause-auto-farm" style="text-align:center; display:none; font-size:200%"><button>Pause auto farm</button></div>
    <style>
      .listed-macro {
        display:block;
        width:calc(100% - 10px);
        padding:5px;
        background-color:#222;
        color:#fff;
        margin-bottom:2px;
      }
      .listed-macro[data-playing="now"] {
        background-color:#922;
      }
      .listed-macro[data-playing="soon"] {
        background-color:#742;
      }
      .listed-macro[data-playing="original"] {
        background-color:#472;
      }
      .listed-macro[data-status="selected"]::after {
        content:"Selected for this opponent";
        color:#df9; display:block;
      }
      .listed-macro[data-status="selectedDefault"]::after {
        content:"Selected";
        color:#9f9; display:block;
      }
    </style>`
  )
  let list = document.querySelector("#macros-list")
  if(stageObserver){stageObserver.disconnect()}
  stageObserver = new MutationObserver(onPage)
  stageObserver.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 enemyHash = stage.pJsnData.boss.param.map(e=>e.enemy_id).join(",")
  
  let characterByImage = url=>url.split("/").slice(-1)[0].split("_")[0]
  let speeds = ["slow", "slower", "normal", "faster", "fast", "skip"]
  let pauseAutoFarm
  let playMacro = async id=>{
    let macro = macros[id]
    let line = list.querySelector(`[data-id="${id}"], .nothing`)
    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}"], .nothing`).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 speed = speeds.findIndex(s=>s===(macro.speed||"normal"))
    let wait = time=>new Promise(ok=>setTimeout(ok,time ? time : speed<=0 ? 2000 : 500))
    let myCancel = cancel
    while(actions.length){
      if(pauseAutoFarm){await pauseAutoFarm[0]}
      if(cancel>myCancel || document.querySelector(".prt-command-end").style.display){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]}"], .nothing`).dataset.playing = playing.slice(-1)[0]===id ? "original" : "soon"
        list.querySelector(`[data-id="${action.macro}"], .nothing`).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}"], .nothing`).dataset.playing = "soon"
        }else{
          list.querySelector(`[data-id="${last}"], .nothing`).removeAttribute("data-playing")
        }
        list.querySelector(`[data-id="${playing.slice(-1)[0]}"], .nothing`).dataset.playing = "now"
      continue}
      
      if(action.type==="skill"){
        let button = document.querySelector(`div[ability-id="${action.ability}"]`)
        if(button){
          let previousPos = null
          if(speed<=1 && 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()
          }else{
            previousPos = stage.gGameStatus.command_slide.now_pos
            stage.gGameStatus.command_slide.now_pos = +button.getAttribute("ability-character-num")
          }
          click(button, {x:44+69*+button.parentElement.dataset["ability-index"], y:468, width:40, height:42})
          if(action.character){
            if(speed<=1){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)
              if(speed<=1){await wait()}
            }
          }
          if(previousPos!==null){
            stage.gGameStatus.command_slide.now_pos = previousPos
          }
          if(speed<=2){await wait(200)}
        }
      }else if(action.type==="attack"){
        let button = document.querySelector(`.btn-attack-start.display-on`)
        if(button){
          let p = waitingForSkillEnd[0]
          click(button)
          await p
          while(!button.classList.contains("display-on") && !stage.gGameStatus.finish){
            p = waitingForSkillEnd[0]
            await p
          }
        }
      }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)+1}"]`)[0]
        if(!button){continue}
        click(button)
        while(document.querySelector(".pop-usual.pop-summon-detail").style.display!=="block"){await wait(100)}
        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}"], .nothing`).removeAttribute("data-playing")
    for(let i in next){
      list.querySelector(`[data-id="${i}"], .nothing`).removeAttribute("data-playing")
    }
    if(!list.querySelector("[data-playing]")){
      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", `<div class="listed-macro" data-id="${i}"><button style="padding:0px; font-size:8px; width:25px; display:inline-block">⚙️</button> <a>${macro.name}</a></div>`)
    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 show for all parties" : "Show for all parties"
      settings.children[5].innerText = macro.enemies?.includes(enemyHash) ? "Don't show for this opponent" : "Show for this opponent"
      settings.children[5].style.display = !macro.enemies ? "none" : null
      settings.children[6].innerText = !macro.enemies ? "Don't show for all opponents" : "Show for all opponents"
      settings.children[8].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) || macros[i].enemies && !macros[i].enemies.includes(enemyHash))){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", `<div class="listed-macro" style="background-color:#${extra.type==="skill" ? "141" : extra.type==="attack" ? "411" : extra.type==="summon" ? "441" : extra.type==="calock" ? "531" : "0000"}" ${Object.keys(extra).map(k=>`data-${k}="${extra[k]}"`).join(" ")}>${text}</div>`)
      }
    }
  })
  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", `<div class="listed-macro" style="background-color:#437" data-type="macro"><select class="new-select-thing"><option default>Select a macro...</option>${macros.map((m,i)=>`<option value="${i}">${m.name}</option>`)}</select></div>`)
    let select = recording.querySelector(".new-select-thing")
    select.className = null
    select.addEventListener("change", ()=>{
      select.parentElement.dataset.macro = select.value
      if(Array.from(select.parentElement.children).findIndex(e=>e===select)===select.parentElement.children.length-1){
        let f = recordFunction
        recordFunction = null
        playMacro(+select.value).then(()=>{
          recordFunction = f
        })
      }
      select.parentElement.innerText = macros[+select.value].name
    })
  })

  let autoQuests = GM_getValue("autoQuests") || {}
  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 show for all parties" : "Show for all parties"
  GM_setValue("macros", macros)})
  settings.children[5].addEventListener("click", ()=>{
    let macro = macros[+settings.dataset.macro]
    let i = macro.enemies.findIndex(p=>p===enemyHash)
    if(i===-1){
      macro.enemies.push(enemyHash)
    }else{
      macro.enemies.splice(i,1)
    }
    settings.children[5].innerText = i===-1 ? "Don't show for this opponent" : "Show for this opponent"
  GM_setValue("macros", macros)})
  settings.children[6].addEventListener("click", ()=>{
    let macro = macros[+settings.dataset.macro]
    if(macro.enemies){
      delete macro.enemies
    }else{
      macro.enemies = [enemyHash]
    }
    settings.children[5].innerText = "Don't show for this opponent"
    settings.children[5].style.display = !macro.enemies ? "none" : null
    settings.children[6].innerText = !macro.enemies ? "Don't show for all opponent" : "Show for all opponent"
  GM_setValue("macros", macros)})
  settings.children[7].addEventListener("click", ()=>{
    list.insertAdjacentHTML("afterbegin", `<div class="listed-macro" data-id="moveAfter">Move macro after...</div>`)
    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<before && a.macro>=after){
            a.macro++
          }else if(a.macro>before && a.macro<=after){
            a.macro--
          }
        }
      }
      for(let k in autoQuests){
        let o = autoQuests[k]
        if(!o.macro){continue}
        if(o.macro===before){
          o.macro = after
        }else if(o.macro<before && o.macro>=after){
          o.macro++
        }else if(o.macro>before && o.macro<=after){
          o.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[8].querySelector("select").addEventListener("change", ()=>{
    macros[+settings.dataset.macro].speed = settings.children[8].querySelector("select").value
  GM_setValue("macros", macros)})
  settings.children[9].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=null}else
        if(a.type==="macro" && a.macro>i){a.macro--}
      }
    }
    for(let k in autoQuests){
      let o = autoQuests[k]
      if(!o.macro){continue}
      if(o.macro===i){
        delete o.macro
      }else if(o.macro>i){
        o.macro--
      }
    }
    settings.style.display = "none"
    list.style.display = null
  GM_setValue("macros", macros)})

  if(GM_getValue("unlockedExtra")){
    let button = list.querySelector(`div.listed-macro[data-id="extra"]`)
    button.children[0].style.display = null
    button.style.backgroundColor = null
  }else{
    let unlocking
    list.querySelector(`div.listed-macro[data-id="extra"]`).addEventListener("click", async ()=>{
      if(unlocking){return}
      let button = list.querySelector(`div.listed-macro[data-id="extra"]`)
      if(!button.dataset.lastTry || +new Date()- +button.dataset.lastTry>5000){
        button.dataset.lastTry = +new Date()
        button.dataset.clicks = 0
      }
      let clicks = button.dataset.clicks = (+button.dataset.clicks||0) + 1
      if(clicks>=3){
        unlocking = true
        button.style.transition = "1s"
        button.style.backgroundColor = "#ff8"
        await new Promise(ok=>setTimeout(ok,1000))
        button.children[0].style.display = null
        button.style.backgroundColor = null
        GM_setValue("unlockedExtra", true)
      }
    })
  }
  let scenarioSpeeds = GM_getValue("scenarioSpeed") || {}
  let autoQuestSave
  scenarioSpeed = autoQuests[stage.pJsnData.quest_id] ? 100 : scenarioSpeeds[enemyHash] || scenarioSpeeds.default || 0
  let showMacroSpeeds = ()=>{
    document.querySelector("#macro-speed").style.display = null
    let list = document.querySelector(`#macro-speed div.autoSettings [data-key="macro"]`)
    list.innerHTML = `<option value="">none</option>`+macros.map((macro,i)=>(
      `<option value="${i}">${macro.name}</option>`
    ))
    list.value = list.dataset.value
  }
  list.querySelector(`button[data-id="scenarioSpeed"]`).addEventListener("click", ()=>{
    list.style.display = "none"
    showMacroSpeeds()
  })
  let autoFarming
  for(let speed of document.querySelectorAll(`#macro-speed div.listed-macro`)){
    speed.dataset.status = scenarioSpeeds.default==speed.dataset.value ? "selectedDefault" : scenarioSpeed==speed.dataset.value ? "selected" : "none"
    speed.addEventListener("click", ()=>{
      if(speed.dataset.value==="back"){
        (scenarioSpeed===100 && autoFarming ? document.querySelector("#pause-auto-farm") : list).style.display = null
        speed.parentElement.style.display = "none"
        if(pauseAutoFarm){
          pauseAutoFarm[1]()
          pauseAutoFarm = null
        }
      return}
      if(speed.dataset.status==="selectedDefault"){
        let enemy = speed.parentElement.querySelector(`[data-status="selected"]`)
        if(enemy){
          enemy.dataset.status = "none"
          delete scenarioSpeeds[enemyHash]
          scenarioSpeed = scenarioSpeeds.default
          if(enemy.dataset.value=="100"){
            autoQuestSave = autoQuests[stage.pJsnData.quest_id]
            delete autoQuests[stage.pJsnData.quest_id]
            GM_setValue("autoQuests", autoQuests)
            farmingQuest = undefined
            speed.parentElement.querySelector("div.autoSettings").style.display = "none"
            if(pauseAutoFarm){
              cancel++
              pauseAutoFarm[1]()
              pauseAutoFarm = null
            }
          }
        }
      }else if(speed.dataset.status==="selected"){
        if(speed.dataset.value=="100"){return}
        let d = speed.parentElement.querySelector(`[data-status="selectedDefault"]`)
        if(d){
          d.dataset.status = "none"
        }
        scenarioSpeeds.default = +speed.dataset.value
        speed.dataset.status = "selectedDefault"
      }else{
        let enemy = speed.parentElement.querySelector(`[data-status="selected"]`)
        if(enemy){
          enemy.dataset.status = "none"
          if(enemy.dataset.value=="100"){
            autoQuestSave = autoQuests[stage.pJsnData.quest_id]
            delete autoQuests[stage.pJsnData.quest_id]
            GM_setValue("autoQuests", autoQuests)
            farmingQuest = undefined
            speed.parentElement.querySelector("div.autoSettings").style.display = "none"
            if(pauseAutoFarm){
              cancel++
              pauseAutoFarm[1]()
              pauseAutoFarm = null
            }
          }
        }
        scenarioSpeeds[enemyHash] = scenarioSpeed = +speed.dataset.value
        speed.dataset.status = "selected"
        if(speed.dataset.value=="100"){
          autoQuests[stage.pJsnData.quest_id] = autoQuestSave || {maxHalfElixirs:0, autoGame:"full"}
          speed.parentElement.querySelector("div.autoSettings").style.display = null
          GM_setValue("autoQuests", autoQuests)
          farmingQuest = stage.pJsnData.quest_id
        }
      }
      GM_setValue("scenarioSpeed", scenarioSpeeds)
    })
  }
  for(let e of document.querySelectorAll("#macro-speed div.autoSettings select, #macro-speed div.autoSettings input")){
    let settings = autoQuests[stage.pJsnData.quest_id]
    if(!settings && e.dataset.default){
      e.value = e.dataset.default
    }else if(settings?.[e.dataset.key]!==undefined){
      if(e.dataset.key==="macro"){
        e.dataset.value = settings[e.dataset.key]
      }else{
        e.value = settings[e.dataset.key]
      }
    }
    e.addEventListener("change", ()=>{
      let settings = autoQuests[stage.pJsnData.quest_id]
      if(!settings){return}
      if(e.dataset.type==="number" && e.value && +e.value!==+e.value){
        e.value = ""
      return}
      settings[e.dataset.key] = e.dataset.type==="number" ? (e.value==="" ? undefined : +e.value) : e.value
      if(e.dataset.key==="macro"){
        e.dataset.value = e.value
      }
      GM_setValue("autoQuests", autoQuests)
    })
  }
  document.querySelector("#macro-speed div.autoSettings").style.display = autoQuests[stage.pJsnData.quest_id] ? null : "none"
  document.querySelector("#pause-auto-farm button").addEventListener("click", ()=>{
    pauseAutoFarm = []
    pauseAutoFarm[0] = new Promise(ok=>{pauseAutoFarm[1]=ok})
    document.querySelector("#pause-auto-farm").style.display = "none"
    showMacroSpeeds()
  })
  if(scenarioSpeed===100){
    document.querySelector("#pause-auto-farm").style.display = null
    list.style.display = "none"
    autoFarming = true
    farmingQuest = stage.pJsnData.quest_id
    let myCancel = cancel
    setTimeout(async ()=>{
      while(document.querySelector("#multi-btn-mask").style.display==="block" || stage.gGameStatus.ability_popup){await new Promise(ok=>setTimeout(ok,100))}
      await new Promise(ok=>setTimeout(ok,2000))
      if(pauseAutoFarm){await pauseAutoFarm[0]}
      if(cancel!==myCancel){return}
      let settings = autoQuests[stage.pJsnData.quest_id]
      if(scenarioSpeed!==100 || !settings){return}
      let end = document.querySelector(".prt-command-end")
      let observer = new MutationObserver(async ()=>{
        if(end.style.display && scenarioSpeed===100){
          observer.disconnect()
          if(pauseAutoFarm){await pauseAutoFarm[0]}
          if(cancel!==myCancel){return}
          click(end.children[0])
        }
      })
      observer.observe(end, {
        attributes:true,
      })
      if(settings.macro!==undefined){
        await playMacro(settings.macro)
        await new Promise(ok=>setTimeout(ok,200))
        if(cancel!==myCancel){return}
      }
      if(settings.autoGame){
        view.battleAutoType = settings.autoGame==="full" ? 2 : 1
        if(settings.autoGame==="semi"){
          click(document.querySelector(`.btn-attack-start.display-on`))
          await new Promise(ok=>setTimeout(ok,500))
          if(cancel!==myCancel){return}
          view._showAutoButton()
        }
        stage.gGameStatus.enable_auto_button = true
        let button = document.querySelector(".btn-auto")
        button.style.display = "block"
        click(button)
      }else{
        autoFarming = false
        document.querySelector("#macros-list").style.display = null
        document.querySelector("#pause-auto-farm").style.display = "none"
      }
    }, 10)
  }
}

window.addEventListener("hashchange", onPage)
onPage()

setTimeout(async ()=>{ // Don't refresh page when entering battle
  while(!requirejs.s.contexts._.defined["util/navigate"]){await new Promise(ok=>setTimeout(ok,500))}
  let original = requirejs.s.contexts._.defined["util/navigate"].hash
  requirejs.s.contexts._.defined["util/navigate"].hash = (...args)=>{
    cancel++; waitingForSkillEnd[2]()
    if(["quest/", "raid/", "mypage", "event/", "raid_multi/"].find(e=>args[0]?.replace("#","").startsWith(e))){
      if(args[1]?.refresh){delete args[1].refresh}
    }else{
      if(originalUnloader){originalUnloader.apply(requirejs.s.contexts._.defined["model/cjs-loader"], [])}
    }
    return original.apply(requirejs.s.contexts._.defined["util/navigate"], args)
  }
}, 1000)