Waifugame battle elements help

Instead of remembering all of the elemental advantages, this little script will display them where it's the most useful.

// ==UserScript==
// @name         Waifugame battle elements help
// @namespace    http://tampermonkey.net/
// @version      2025-04-23
// @description  Instead of remembering all of the elemental advantages, this little script will display them where it's the most useful.
// @author       Lulu5239
// @match        https://waifugame.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=waifugame.com
// @run-at       document-start
// @grant        none
// ==/UserScript==

(async ()=>{
  //'use strict';

  let path = document.location.pathname
  if(path.startsWith("/index.php/")){
    path = path.slice(10)
  }

  // Wait for scripts to exist
  let ok; let p = new Promise(f=>{ok=f})
  let observer = new MutationObserver((mutations, obs) => {
    if (typeof(startCountdown)!=="undefined") {
      obs.disconnect();
      ok()
    }else if(path==="/battle"){ // Edit class before scripts runs
      for(let card of document.querySelectorAll(".showCardInfo")){
        card.classList.remove("showCardInfo")
        card.addEventListener("click", ()=>{
          let hp = card.parentElement.querySelector("center").innerText.split("\n")[1].slice(0,-4)
          showWaifuMenu({
            name:card.parentElement.dataset["tippy-content"],
            id:card.parentElement.dataset.anniemay,
            cardID:card.dataset.cardid,
            xpText:card.parentElement.querySelector("span").innerText,
            relXP:0,
            hpText:hp+"%",
            relHP:hp,
          }, true)
        })
      }
    }
  });
  observer.observe(document, { childList: true, subtree: true });
  await p
  
  let party = localStorage["y_WG-party"] && JSON.parse(localStorage["y_WG-party"])
  if(!party){
    party = localStorage["y_WG-party"] = {}
  }
  if(path==="/home"){
    let nowHere = []
    for(let card of document.querySelectorAll(".card[data-amid]")){
      nowHere.push(card.dataset["amid"])
      let c = party[card.dataset["amid"]]
      if(!c){
        c = party[card.dataset["amid"]] = {
          cardid:card.dataset["cardid"],
          element:null,
          name:card.dataset["nameonly"],
        }
      }
      delete c.lastSeen
      let level = +card.querySelector(".levelBadge").innerText.slice(3)
      if(c.level!==level){
        c.level = level
        delete c.stats
      }
    }
    for(let k in party){
      if(!nowHere.includes(k)){
        if(!party[k].lastSeen){
          party[k].lastSeen = +new Date()
        continue}
        if(+new Date()-party[k].lastSeen>24*3600000){ // 24 hours
          delete party[k]
        }
      }
    }
    localStorage["y_WG-party"] = JSON.stringify(party)
  }
  window.battleHelpVars = {party}
  if(!path.startsWith("/battle")){return}
  
  var advantages = `normal >< normal;fight > normal;light < normal;wind >> fight;bug <> fight;tech > fight;dark < fight;light < fight;fight << wind;earth <> wind;bug << wind;grass << wind;electric >> wind;ice > wind;fight < poison;poison <> poison;earth >> poison;bug < poison;blood < poison;psychic > poison;dark > poison;light >< poison;normal < earth;wind <> earth;poison <<< earth;metal >< earth;grass >> earth;fire <<< earth;water > earth;electric <<< earth;ice > earth;music < earth;normal > bug;fight <> bug;wind >>> bug;earth < bug;tech << bug;grass << bug;fire >>> bug;ice > bug;normal < metal;fight > metal;wind < metal;poison < metal;earth >< metal;bug < metal;metal <> metal;grass <<< metal;fire >> metal;water > metal;electric >> metal;psychic < metal;ice <<< metal;music > metal;tech > blood;grass > blood;fire > blood;water << blood;normal > tech;wind < tech;bug >>> tech;tech >< tech;fire < tech;water >>> tech;electric > tech;psychic > tech;ice < tech;music << tech;wind >> grass;poison > grass;earth << grass;bug >> grass;metal >> grass;tech < grass;grass <> grass;fire >> grass;electric < grass;ice > grass;earth >> fire;bug << fire;metal << fire;grass << fire;fire <> fire;water >> fire;ice << fire;blood >> water;tech << water;grass > water;water <> water;fire << water;electric > water;ice <> water;wind << electric;earth >> electric;metal << electric;electric <> electric;fight < psychic;bug > psychic;blood > psychic;psychic <> psychic;dark >> psychic;light > psychic;fight > ice;metal >> ice;fire >> ice;water <> ice;ice <> ice;music < ice;tech >> music;electric < music;normal < dark;psychic <<< dark;music > dark;dark <> dark;light >< dark;poison >< light;blood < light;dark >< light;light <> light`.split(";").map(e=>e.split(" "))
  var advantagesSymbols = {
    ">":{text:"More damage", good:1},
    ">>":{text:"Good", good:2},
    ">>>":{text:"Perfect", good:3},
    "!>":{text:"Less defense", good:-0.9},
    "<":{text:"Less damage", good:-1},
    "<<":{text:"Bad", good:-2},
    "<<<":{text:"Very bad", good:-3},
    "!<":{text:"More defense", good:0.9},
    "><":{text:"Both damages more", good:0},
    "<>":{text:"Both damages less", good:0},
  }
  var magicElements = ["grass","fire","water","electric","psychic","ice","music","dark","light"]
   
  if(path==="/battle"){
    let list = []
    for(let card of document.querySelectorAll("img.battle-card")){
      let element = card.parentElement.querySelector("p").innerText.split(", ").slice(-1)[0].toLowerCase()
      card.parentElement.querySelector("p").style.marginBottom="0px"
      let text = document.createElement("p")
      card.parentElement.appendChild(text)
      let max = -9
      for(let id in party){
        if(!party[id].element || party[id].lastSeen){continue}
        party[id].effect = advantagesSymbols[advantages.find(e=>e[0]===party[id].element && e[2]===element)?.[1] || "!"+advantages.find(e=>e[2]===party[id].element && e[0]===element)?.[1]]
        if(party[id].effect?.good>max){
          max = party[id].effect.good
        }
      }
      if(max>0){
        let best = Object.values(party).filter(c=>c.effect?.good===max)
        text.innerHTML = (best.length===1 ? `<span>${best[0].name}</span> is` : best.slice(0,-1).map(c=>`<span>${c.name}</span>`).join(", ")+` and <span>${best.slice(-1)[0].name}</span> are`)+" good"
      }else if(max===0){
        text.innerText = "You don't have any good animu against that opponent."
      }else if(max>-9){
        text.innerText = "All of your animus are bad against that opponent!"
      }else{
        text.style.display = "none"
      }
      let button = card.parentElement.parentElement.querySelector("a.btn")
      let battleID = button.href.split("/").slice(-1)[0]
      list.push({
        id:battleID,
        element,
        level:+card.parentElement.querySelector("span.bg-highlight").innerText.slice(3)
      })
      if(localStorage["y_WG-autoBattle"]){ // Experimental, enable if you want
        text.innerHTML += ` <button class="btn autoBattleButton">Auto</button>`
        text.querySelector(".autoBattleButton").addEventListener("click",()=>{
          if(localStorage["y_WG-autoBattle"]!=="all"){
            localStorage["y_WG-autoBattle"] = battleID
          }
          button.click()
        })
      }
    }
    localStorage["y_WG-battles"] = JSON.stringify(list)
    document.querySelector("#partyView").querySelector("p.font-italic").innerText = "You can heal your Animus from this page."
  return}
  
  let previousParty = party
  party = window.battleHelpVars.party = {}
  for(let card of initialSwapData){
    let c = previousParty[card.id]
    if(!c){
      c = previousParty[card.id] = {
        name:card.name,
        level:card.lvl,
      }
    }
    c.element = card.element?.toLowerCase()
    if(card.lvl!==c.level){
      c.level = card.lvl
      delete c.stats
    }
    party[card.id] = c
  }
  localStorage["y_WG-party"] = JSON.stringify(previousParty)
  window.battleHelpVars.auto = localStorage["y_WG-autoBattle"]===battleID || localStorage["y_WG-autoBattle"]==="all"
  
  let handleSwapParty = (cards=[])=>{
    for(let card of cards){
      party[card.id].hp = card.currentHP
      party[card.id].level = card.lvl
      party[card.id].id = card.id
    }
    document.querySelector("#swapForXPoption").dataset.card = Object.values(party).find(c=>c.level<120 && !c.receivingXP && c.hp>0 && (!c.stats || c.stats.SPD>fullStats.p2?.stats.SPD || c.level>fullStats.p2?.level))?.id || ""
    document.querySelector("#swapForXPoption").style.display = document.querySelector("#swapForXPoption").dataset.card ? "block" : "none"
  }
  let currentCard = party[initialSwapData.find(c=>document.querySelector("#player_name").innerText.startsWith(c.name))?.id]
  battleHelpVars.getCurrentCard = ()=>currentCard

  let fullStats = window.battleHelpVars.fullStats = {}
  let winText
  let lastSequenceData = {}
  let originalPlaySequence = playSequence
  playSequence = (...args)=>{
    for(let e of args[0]){
      if(e.a==="playerwin" && e.t==="player1"){
        let battles = localStorage["y_WG-battles"] && JSON.parse(localStorage["y_WG-battles"])
        if(!battles){continue}
        let i = battles.findIndex(b=>b.id===battleID)
        if(i>=0){
          battles.splice(i,1)
          localStorage["y_WG-battles"] = JSON.stringify(battles)
        }
        let lowest = Object.values(party).reduce((p,c)=>(c.level<p ? c.level : p),999)
        battles.reverse()
        let battle = battles.find(b=>b.level<=lowest) || battles.reduce((p,b)=>(b.level<p.level ? b : p),{level:999})
        if(!battle?.element){continue}
        document.querySelector("#winner_block").insertAdjacentHTML("beforeend", `<button class="btn btn-secondary btn-block" id="btn_nextBattle"><i class="fas fa-sword"></i> Next ${window.battleHelpVars.auto ? "auto " : ""}battle<p style="margin-bottom:0px; color:#ccc; font-size:80%">${battle.element.slice(0,1).toUpperCase()+battle.element.slice(1)}, lv. ${battle.level}</p></button>`)
        document.querySelector("#btn_nextBattle").addEventListener("click", ()=>{
          if(window.battleHelpVars.auto){
            localStorage["y_WG-autoBattle"] = battle.id
          }
          document.location.href = "/battle/"+battle.id
        })
        if(localStorage["y_WG-autoBattle"]==="all" && winText?.includes("<br")){
          setTimeout(()=>{
            document.location.href = "/battle/"+battle.id
          }, 3000)
        }
      continue}
      if(e.a==="newhp" && e.t==="player1" && currentCard){
        currentCard.hp = e.p.abs
      continue}
      if(e.a==="faint"){
        window.battleHelpVars.usingBest = false
      continue}
      if(e.a==="narate" && winText===true){
        winText = e.p.text
      continue}
      if(e.a!=="debug"){continue}
      if(e.p.text.startsWith("DEBUG XP GAIN:")){
        for(let c of e.p.text.slice(e.p.text.indexOf("[")+1, e.p.text.indexOf("]")).split(";")){
          if(!party[c]){continue}
          party[c].receivingXP = true
        }
      }
      if(e.p.text==="Found next opponent: null"){
        winText = true
      }
      if(e.p.text.startsWith("p1{") || e.p.text.startsWith("p2 {") || e.p.text.startsWith("Found next opponent: {")){
        let stats = fullStats[e.p.text.startsWith("p1") ? "p1" : "p2"] = JSON.parse(e.p.text.startsWith("p1") ? e.p.text.slice(2) : e.p.text.startsWith("p2") ? e.p.text.slice(3).split("}").slice(0,-1).join("}")+"}" : e.p.text.slice(e.p.text.indexOf("{")))
        for(let p of ["moves", "special", "stats"]){
          stats[p] = JSON.parse(stats[p])
        }
        if(party[stats.id]){
          currentCard = party[stats.id]
          currentCard.receivingXP = true
          currentCard.stats = stats.stats
          // Store stats in party
          let previous = JSON.parse(localStorage["y_WG-party"])
          previous[stats.id].stats = stats.stats
          previous[stats.id].level = stats.level
          localStorage["y_WG-party"] = JSON.stringify(previous)
        }
        if(Object.keys(fullStats).length===2){
          showInventory({
            ...lastSequenceData,
            faked:true,
          })
        }
        handleSwapParty()
      }
    }
    setTimeout(()=>{
      if(fullStats.p2?.card.shard_sponsor_user_id==403880 && !localStorage["y_WG-autoBattle"] && !document.querySelectorAll("#unlockAutoBattle").length){
        narate("Oh, you are battling against one of <span>my developer's cards</span>;<br>I <i>(the user-script)</i> don't want to see that...<br>Please do it <span>fast</span>!")
        document.querySelector("#battle_view_player").insertAdjacentHTML("afterbegin", `<div style="width:100%; text-align:center; overflow:hidden; padding:10px;"><button id="unlockAutoBattle" class="btn" style="margin-top:-150px; background-color:#000; filter:drop-shadow(0 0 10px #ff0); display:inline-flex; align-items:center">Unlock auto-battle</button></div>`)
        setTimeout(()=>{
          let button = document.querySelector("#unlockAutoBattle")
          button.addEventListener("click", ()=>{
            if(localStorage["y_WG-autoBattle"]){
              button.style.marginTop = "-150px"
              window.battleHelpVars.auto = true
              showSuccessToast("Use a move to start the auto-battle.")
            }else{
              localStorage["y_WG-autoBattle"] = true
              button.innerText = "Auto-battle"
              showSuccessToast("Unlocked auto-battle!")
            }
          })
          button.style.marginTop = "10px"
        },5000)
      }
      if(busy){return}
      window.scrollTo(0, 185)
      if(!battleHelpVars.auto || document.querySelector("#action_block").style.display==="none"){return}
      if(document.querySelector("#swapForXPoption").dataset.card){
        document.querySelector("#btn_swapForXP").click()
      }else if(!window.battleHelpVars.usingBest || currentCard.hp<50 && currentCard.level<120){
        document.querySelector("#btn_swapToBest").click()
      }else{
        document.querySelector("#btn_bestMove").click()
      }
    },1000)
    return originalPlaySequence(...args)
  }
  let opponentElement = document.querySelector("#battle_view_opponent").style.backgroundImage.split("/").slice(-1)[0].split(".")[0]
  originalShowInventory = showInventory
  showInventory = (...args)=>{ // handleBattleAjax was a constant
    if(!args[0].faked){lastSequenceData = window.battleHelpVars.lastSequenceData = args[0]}
    if(fullStats.p1?.stats && fullStats.p1.level===currentCard.level){
      if(args[0].output){fullStats.p1.moves = currentCard.moves = args[0].output.move_data}
      if(!fullStats.p1.element && fullStats.p1.card?.element){fullStats.p1.element=fullStats.p1.card.element.toLowerCase()}
      let noPP = true
      for(let m in fullStats.p1.moves){
        let move = fullStats.p1.moves[m] = {...fullStats.p1.moves[m], ...args[0].output.moves_metadata[fullStats.p1.moves[m].m]}
        if(move.pp>0){noPP=false}
        let effect = advantages.find(a=>a[0]===move.elemental_type && a[2]===opponentElement)?.[1]
        move.estimatedDamage = move.power * fullStats.p1.stats[magicElements.includes(move.elemental_type) ? "SpATT" : "ATT"] / fullStats.p2.stats[magicElements.includes(move.elemental_type) ? "SpDEF" : "DEF"] * (move.elemental_type===fullStats.p1.element || move.elemental_type==="normal" ? 1.2 : 1) * (!effect ? 1 : effect.startsWith(">") ? 2 : 0.5) *0.52
      }
      if(noPP){currentCard.noPP = true}
    }
    if(args[0].faked){return}
    return originalShowInventory(...args)
  }
  
  let originalHandleSwapPlayer2 = handleSwapPlayer2
  handleSwapPlayer2 = (...args)=>{
    opponentElement = args[0].element?.toLowerCase()
    updateGoodness()
    return originalHandleSwapPlayer2(...args)
  }
  let originalHandleSwap = handleSwap
  handleSwap = (...args)=>{
    currentCard = Object.values(party).find(c=>c.name===args[0].name) // No better way...
    currentCard.receivingXP = true
    fullStats.p1 = {
      ...currentCard,
      moves:args[0].attacks,
    }
    currentCard.moves = args[0].attacks
    handleSwapParty(args[0].swap_party)
    showInventory({output:lastSequenceData.output, faked:true})
    let r = originalHandleSwap(...args)
    setTimeout(()=>{
      updateGoodness()
    },1000)
    return r
  }
  let actionSwapList = document.querySelector("#action_swap")
  //document.querySelector("#btn_swap").addEventListener("click", ()=>{
  var updateGoodness = window.battleHelpVars.updateGoodness = ()=>{
    for(let card of actionSwapList.children){
      let button = card.querySelector("button")
      let data = party[button.dataset.swapto]
      if(!data){continue}
      let text = button.querySelector(".elementInfo")
      if(!text){
        text = document.createElement("a")
        text.style = "display:block; corner-radius:2px"
        text.className = "elementInfo"
        button.appendChild(text)
      }
      let effect = advantagesSymbols[advantages.find(e=>e[0]===data.element && e[2]===opponentElement)?.[1] || "!"+advantages.find(e=>e[2]===data.element && e[0]===opponentElement)?.[1]]
      text.innerText = !effect ? "No advantage" : effect.text
      text.style.backgroundColor = !effect?.good ? "#0000" : effect.good>0 ? `#0${(5+Math.ceil(effect.good)*3).toString(16)}0${(5+Math.ceil(effect.good)*3).toString(16)}` : `#${(5+Math.floor(effect.good)*-3).toString(16)}00${(5+Math.floor(effect.good)*-3).toString(16)}`
      data.good = effect?.good || 0
    }
  }
  updateGoodness()
  let actionMenu = document.querySelector("#action_menu")
  actionMenu.insertAdjacentHTML("beforeend", `<div class="col-12 col-md-6 mb-2" id="swapForXPoption"><button id="btn_swapForXP" class="btn btn-block btn-secondary btn-sm"><i class="fas fa-exchange-alt"></i> Level up cards</button><div>`)
  actionMenu.querySelector("#btn_swapForXP").addEventListener("click", ()=>{
    let card = document.querySelector("#swapForXPoption").dataset.card
    if(!card){return}
    window.battleHelpVars.usingBest = false
    actionSwapList.querySelector(`button[data-swapto="${card}"]`).click()
  })
  actionMenu.insertAdjacentHTML("beforeend", `<div class="col-12 col-md-6 mb-2"><button id="btn_swapToBest" class="btn btn-block btn-secondary btn-sm"><i class="fas fa-exchange-alt"></i> Swap to best</button><div>`)
  actionMenu.querySelector("#btn_swapToBest").addEventListener("click", ()=>{
    let max
    for(let card of Object.values(party)){
      if(!card.hp || card.noPP){continue}
      card.goodATT = (card.good>0 ? card.good : 1/Math.abs(card.good-2)) * (card.stats?.[magicElements.includes(card.elemental) ? "SpATT" : "ATT"] || card.level*3 || 1) /(card.level<120 ? 2 : 1)
      if(max===undefined || card.goodATT>max){max=card.goodATT}
    }
    let card = Object.values(party).filter(card=>card.goodATT===max && !card.noPP).sort((c1,c2)=>c2.hp-c1.hp)[0]
    if(card===currentCard){ // Couldn't find better way to identify the current card
      if(window.battleHelpVars.auto){
        return document.querySelector("#btn_bestMove").click()
      }
      return showErrorToast("Already using best card!")
    }
    window.battleHelpVars.usingBest = true
    actionSwapList.querySelector(`button[data-swapto="${card.id}"]`).click()
  })

  actionMenu.insertAdjacentHTML("beforeend", `<div class="col-12 col-md-6 mb-2"><button id="btn_bestMove" class="btn btn-block btn-secondary btn-sm"><i class="fas fa-sword"></i> Use best attack</button><div>`)
  actionMenu.querySelector("#btn_bestMove").addEventListener("click", ()=>{
    if(!currentCard.stats){document.location.reload()}
    let best; let canEnd
    for(let move of currentCard.moves){
      if(!move.pp){continue}
      if(!best){best=move; continue}
      if(move.estimatedDamage*0.95>fullStats.p2.hp){ // Try to end battle with a single move (doesn't cost PP)
        if(move.accuracy>=best.accuracy){
          best = move
        }
        canEnd = true
        continue
      }
      if(canEnd){continue}
      if(move.estimatedDamage > best.estimatedDamage){
        best = move
      }
    }
    if(!best){
      currentCard.noPP = true
      return actionMenu.querySelector("#btn_swapToBest").click() // Out of PP: use other card
    }
    document.querySelector(`button[data-attack="${best.m}"]`).click()
  })
  
  handleSwapParty(initialSwapData)
})();